@justdanielndev/status-page 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc.cjs +1 -0
- package/.upptimerc.yml +42 -0
- package/CHANGELOG.md +791 -0
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/cypress/fixtures/example.json +5 -0
- package/cypress/integration/incident.spec.js +9 -0
- package/cypress/integration/live-status.spec.js +12 -0
- package/cypress/plugins/index.js +17 -0
- package/cypress/support/commands.js +25 -0
- package/cypress/support/index.js +20 -0
- package/cypress.json +4 -0
- package/i18n.yml +61 -0
- package/init-tests.ts +12 -0
- package/jest.config.js +4 -0
- package/package.json +78 -0
- package/post-process.ts +61 -0
- package/pre-process.ts +32 -0
- package/release.config.js +1 -0
- package/rollup.config.js +116 -0
- package/src/client.js +18 -0
- package/src/components/ActiveIncidents.svelte +79 -0
- package/src/components/ActiveScheduled.svelte +96 -0
- package/src/components/Graph.svelte +76 -0
- package/src/components/History.svelte +84 -0
- package/src/components/Incident.svelte +161 -0
- package/src/components/Incidents.svelte +83 -0
- package/src/components/LiveStatus.svelte +190 -0
- package/src/components/Loading.svelte +37 -0
- package/src/components/Nav.svelte +88 -0
- package/src/components/Scheduled.svelte +72 -0
- package/src/components/Summary.svelte +54 -0
- package/src/routes/_error.svelte +41 -0
- package/src/routes/_layout.svelte +110 -0
- package/src/routes/error.svelte +27 -0
- package/src/routes/history/[number].svelte +17 -0
- package/src/routes/incident/[number].svelte +13 -0
- package/src/routes/index.svelte +48 -0
- package/src/routes/rate-limit-exceeded.svelte +88 -0
- package/src/server.js +25 -0
- package/src/service-worker.js +15 -0
- package/src/template.html +34 -0
- package/src/utils/createOctokit.js +67 -0
- package/static/global.css +203 -0
- package/static/logo-192.png +0 -0
- package/static/logo-512.png +0 -0
- package/static/manifest.json +20 -0
- package/static/themes/dark.css +26 -0
- package/static/themes/light.css +26 -0
- package/static/themes/night.css +26 -0
- package/static/themes/ocean.css +26 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Loading from "../components/Loading.svelte";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import config from "../data/config.json";
|
|
5
|
+
import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
|
|
6
|
+
|
|
7
|
+
let loading = true;
|
|
8
|
+
const octokit = createOctokit();
|
|
9
|
+
const owner = config.owner;
|
|
10
|
+
const repo = config.repo;
|
|
11
|
+
let incidents = [];
|
|
12
|
+
|
|
13
|
+
onMount(async () => {
|
|
14
|
+
try {
|
|
15
|
+
incidents = (
|
|
16
|
+
await cachedResponse(`scheduled-current-${owner}-${repo}`, () =>
|
|
17
|
+
octokit.issues.listForRepo({
|
|
18
|
+
owner,
|
|
19
|
+
repo,
|
|
20
|
+
state: "open",
|
|
21
|
+
filter: "all",
|
|
22
|
+
sort: "created",
|
|
23
|
+
direction: "desc",
|
|
24
|
+
labels: "maintenance",
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
).data;
|
|
28
|
+
incidents = incidents.map((incident, index) => {
|
|
29
|
+
incident.showHeading =
|
|
30
|
+
index === 0 ||
|
|
31
|
+
new Date(incidents[index - 1].created_at).toLocaleDateString() !==
|
|
32
|
+
new Date(incident.created_at).toLocaleDateString();
|
|
33
|
+
incident.metadata = {};
|
|
34
|
+
if (incident.body.includes("<!--")) {
|
|
35
|
+
const summary = incident.body.split("<!--")[1].split("-->")[0];
|
|
36
|
+
const lines = summary
|
|
37
|
+
.split("\n")
|
|
38
|
+
.filter((i) => i.trim())
|
|
39
|
+
.filter((i) => i.includes(":"));
|
|
40
|
+
lines.forEach((i) => {
|
|
41
|
+
incident.metadata[i.split(/:(.+)/)[0].trim()] = i.split(/:(.+)/)[1].trim();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return incident;
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
handleError(error);
|
|
48
|
+
}
|
|
49
|
+
loading = false;
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<section>
|
|
54
|
+
{#if loading}
|
|
55
|
+
<Loading />
|
|
56
|
+
{:else if incidents.length}
|
|
57
|
+
<h2>{config.i18n.scheduledMaintenance}</h2>
|
|
58
|
+
{#each incidents as incident}
|
|
59
|
+
<article class="degraded degraded-active link">
|
|
60
|
+
<div class="f">
|
|
61
|
+
<div>
|
|
62
|
+
<h4>{incident.title.replace("🛑", "").replace("⚠️", "").trim()}</h4>
|
|
63
|
+
{#if incident.metadata.start && incident.metadata.end}
|
|
64
|
+
<div>
|
|
65
|
+
{(new Date(incident.metadata.start).getTime() < new Date().getTime()
|
|
66
|
+
? config.i18n.scheduledMaintenanceSummaryStarted
|
|
67
|
+
: config.i18n.scheduledMaintenanceSummaryStarts
|
|
68
|
+
)
|
|
69
|
+
.replace(/\$DATE/g, new Date(incident.metadata.start).toLocaleString(config.i18n.locale))
|
|
70
|
+
.replace(
|
|
71
|
+
/\$DURATION/g,
|
|
72
|
+
Math.floor(
|
|
73
|
+
(new Date(incident.metadata.end).getTime() -
|
|
74
|
+
new Date(incident.metadata.start).getTime()) /
|
|
75
|
+
60000
|
|
76
|
+
)
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
81
|
+
<div class="f r">
|
|
82
|
+
<a href={`${config.path}/incident/${incident.number}`}>
|
|
83
|
+
{config.i18n.incidentReport.replace(/\$NUMBER/g, incident.number)}
|
|
84
|
+
</a>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</article>
|
|
88
|
+
{/each}
|
|
89
|
+
{/if}
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
section {
|
|
94
|
+
margin-bottom: 2rem;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Loading from "../components/Loading.svelte";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import config from "../data/config.json";
|
|
5
|
+
import Line from "svelte-chartjs/src/Line.svelte";
|
|
6
|
+
import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
|
|
7
|
+
|
|
8
|
+
export let slug;
|
|
9
|
+
let loading = true;
|
|
10
|
+
const octokit = createOctokit();
|
|
11
|
+
const owner = config.owner;
|
|
12
|
+
const repo = config.repo;
|
|
13
|
+
let commits = [];
|
|
14
|
+
let labels = [];
|
|
15
|
+
let data = [];
|
|
16
|
+
let width = 800;
|
|
17
|
+
|
|
18
|
+
onMount(async () => {
|
|
19
|
+
try {
|
|
20
|
+
commits = (
|
|
21
|
+
await cachedResponse(`commits-${owner}-${repo}-${slug}`, () =>
|
|
22
|
+
octokit.repos.listCommits({
|
|
23
|
+
owner,
|
|
24
|
+
repo,
|
|
25
|
+
path: `history/${slug}.yml`,
|
|
26
|
+
per_page: 28,
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
).data.reverse();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
handleError(error);
|
|
32
|
+
}
|
|
33
|
+
commits = commits.map((commit, index) => {
|
|
34
|
+
commit.showHeading =
|
|
35
|
+
index === 0 ||
|
|
36
|
+
new Date(commits[index - 1].created_at).toLocaleDateString() !==
|
|
37
|
+
new Date(commit.created_at).toLocaleDateString();
|
|
38
|
+
return commit;
|
|
39
|
+
});
|
|
40
|
+
data = commits
|
|
41
|
+
.filter((commit) => commit.commit.message.includes("ms) [skip ci]"))
|
|
42
|
+
.map((commit) => parseInt(commit.commit.message.split(" in ")[1].split("ms")[0]));
|
|
43
|
+
labels = commits
|
|
44
|
+
.filter((commit) => commit.commit.message.includes("ms) [skip ci]"))
|
|
45
|
+
.map((commit) => new Date(commit.commit.committer.date).toLocaleString(config.i18n.locale));
|
|
46
|
+
loading = false;
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<section bind:clientWidth={width}>
|
|
51
|
+
{#if loading}
|
|
52
|
+
<Loading />
|
|
53
|
+
{:else if data.length}
|
|
54
|
+
<h2>{config.i18n.sevelDayResponseTime}</h2>
|
|
55
|
+
<Line
|
|
56
|
+
data={{
|
|
57
|
+
labels,
|
|
58
|
+
datasets: [
|
|
59
|
+
{
|
|
60
|
+
label: config.i18n.responseTimeMs,
|
|
61
|
+
backgroundColor: config.graphBackgroundColor || "#89e0cf",
|
|
62
|
+
borderColor: config.graphBorderColor || "#1abc9c",
|
|
63
|
+
data,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}}
|
|
67
|
+
{width}
|
|
68
|
+
height={400}
|
|
69
|
+
options={{
|
|
70
|
+
responsive: true,
|
|
71
|
+
maintainAspectRatio: true,
|
|
72
|
+
scales: { xAxes: [{ display: false, gridLines: { display: false } }] },
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
{/if}
|
|
76
|
+
</section>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Loading from "../components/Loading.svelte";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import config from "../data/config.json";
|
|
5
|
+
import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
|
|
6
|
+
|
|
7
|
+
export let slug;
|
|
8
|
+
let loading = true;
|
|
9
|
+
const octokit = createOctokit();
|
|
10
|
+
const owner = config.owner;
|
|
11
|
+
const repo = config.repo;
|
|
12
|
+
let incidents = [];
|
|
13
|
+
|
|
14
|
+
onMount(async () => {
|
|
15
|
+
try {
|
|
16
|
+
incidents = (
|
|
17
|
+
await cachedResponse(`closed-issues-${owner}-${repo}-${slug}`, () =>
|
|
18
|
+
octokit.issues.listForRepo({
|
|
19
|
+
owner,
|
|
20
|
+
repo,
|
|
21
|
+
state: "closed",
|
|
22
|
+
filter: "all",
|
|
23
|
+
sort: "created",
|
|
24
|
+
direction: "desc",
|
|
25
|
+
labels: `status,${slug}`,
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
).data;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
handleError(error);
|
|
31
|
+
}
|
|
32
|
+
incidents = incidents.map((incident, index) => {
|
|
33
|
+
incident.showHeading =
|
|
34
|
+
index === 0 ||
|
|
35
|
+
new Date(incidents[index - 1].created_at).toLocaleDateString() !==
|
|
36
|
+
new Date(incident.created_at).toLocaleDateString();
|
|
37
|
+
return incident;
|
|
38
|
+
});
|
|
39
|
+
loading = false;
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
h2 {
|
|
45
|
+
margin-top: 2rem;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
|
|
49
|
+
<section>
|
|
50
|
+
{#if loading}
|
|
51
|
+
<Loading />
|
|
52
|
+
{:else if incidents.length}
|
|
53
|
+
<h2>{config.i18n.pastIncidents}</h2>
|
|
54
|
+
{#each incidents as incident}
|
|
55
|
+
{#if incident.showHeading}
|
|
56
|
+
<h3>{new Date(incident.created_at).toLocaleDateString(config.i18n.locale)}</h3>
|
|
57
|
+
{/if}
|
|
58
|
+
<article class="down link {incident.title.includes('degraded') ? 'degraded' : ''}">
|
|
59
|
+
<div class="f">
|
|
60
|
+
<div>
|
|
61
|
+
<h4>{incident.title.replace('🛑', '').replace('⚠️', '').trim()}</h4>
|
|
62
|
+
<div>
|
|
63
|
+
{@html config.i18n.pastIncidentsResolved
|
|
64
|
+
.replace(
|
|
65
|
+
/\$MINUTES/g,
|
|
66
|
+
(
|
|
67
|
+
(new Date(incident.closed_at).getTime() -
|
|
68
|
+
new Date(incident.created_at).getTime()) /
|
|
69
|
+
60000
|
|
70
|
+
).toFixed(0)
|
|
71
|
+
)
|
|
72
|
+
.replace(/\$POSTS/g, incident.comments)}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="f r">
|
|
76
|
+
<a href={`${config.path}/incident/${incident.number}`}>
|
|
77
|
+
{config.i18n.incidentReport.replace(/\$NUMBER/g, incident.number)}
|
|
78
|
+
</a>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</article>
|
|
82
|
+
{/each}
|
|
83
|
+
{/if}
|
|
84
|
+
</section>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Loading from "../components/Loading.svelte";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import snarkdown from "snarkdown";
|
|
5
|
+
import config from "../data/config.json";
|
|
6
|
+
import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
|
|
7
|
+
|
|
8
|
+
export let number;
|
|
9
|
+
|
|
10
|
+
let md = snarkdown;
|
|
11
|
+
let loading = true;
|
|
12
|
+
let loadingIncident = true;
|
|
13
|
+
|
|
14
|
+
const octokit = createOctokit();
|
|
15
|
+
const owner = config.owner;
|
|
16
|
+
const repo = config.repo;
|
|
17
|
+
let comments = [];
|
|
18
|
+
let incident = {};
|
|
19
|
+
|
|
20
|
+
onMount(async () => {
|
|
21
|
+
try {
|
|
22
|
+
incident = (
|
|
23
|
+
await cachedResponse(`issue-${owner}-${repo}-${number}`, () =>
|
|
24
|
+
octokit.issues.get({
|
|
25
|
+
owner,
|
|
26
|
+
repo,
|
|
27
|
+
issue_number: number,
|
|
28
|
+
sort: "created",
|
|
29
|
+
direction: "desc",
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
).data;
|
|
33
|
+
incident.metadata = {};
|
|
34
|
+
if (incident.body.includes("<!--")) {
|
|
35
|
+
const summary = incident.body.split("<!--")[1].split("-->")[0];
|
|
36
|
+
const lines = summary
|
|
37
|
+
.split("\n")
|
|
38
|
+
.filter((i) => i.trim())
|
|
39
|
+
.filter((i) => i.includes(":"));
|
|
40
|
+
lines.forEach((i) => {
|
|
41
|
+
incident.metadata[i.split(/:(.+)/)[0].trim()] = i.split(/:(.+)/)[1].trim();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
handleError(error);
|
|
46
|
+
}
|
|
47
|
+
loadingIncident = false;
|
|
48
|
+
try {
|
|
49
|
+
comments = (
|
|
50
|
+
await cachedResponse(`issue-comments-${owner}-${repo}-${number}`, () =>
|
|
51
|
+
octokit.issues.listComments({
|
|
52
|
+
owner,
|
|
53
|
+
repo,
|
|
54
|
+
issue_number: number,
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
).data.reverse();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
handleError(error);
|
|
60
|
+
}
|
|
61
|
+
loading = false;
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<svelte:head>
|
|
66
|
+
<title>{config.i18n.incidentTitle.replace("$NUMBER", number)}</title>
|
|
67
|
+
</svelte:head>
|
|
68
|
+
|
|
69
|
+
<h2>
|
|
70
|
+
{#if loadingIncident}
|
|
71
|
+
{config.i18n.incidentDetails}
|
|
72
|
+
{:else}
|
|
73
|
+
{incident.title}
|
|
74
|
+
<span class={`tag ${incident.state}`}>
|
|
75
|
+
{incident.state === "closed"
|
|
76
|
+
? incident.metadata.start
|
|
77
|
+
? config.i18n.incidentCompleted
|
|
78
|
+
: config.i18n.incidentFixed
|
|
79
|
+
: incident.metadata.start
|
|
80
|
+
? config.i18n.incidentScheduled
|
|
81
|
+
: config.i18n.incidentOngoing}
|
|
82
|
+
</span>
|
|
83
|
+
{/if}
|
|
84
|
+
</h2>
|
|
85
|
+
|
|
86
|
+
<section>
|
|
87
|
+
{#if loading}
|
|
88
|
+
<Loading />
|
|
89
|
+
{:else}
|
|
90
|
+
<div class="f">
|
|
91
|
+
<dl>
|
|
92
|
+
{#if incident.metadata.start}
|
|
93
|
+
<dt>
|
|
94
|
+
{new Date(incident.metadata.start).getTime() < new Date().getTime()
|
|
95
|
+
? config.i18n.startedAt
|
|
96
|
+
: config.i18n.startsAt}
|
|
97
|
+
</dt>
|
|
98
|
+
<dd>{new Date(incident.metadata.start).toLocaleString(config.i18n.locale)}</dd>
|
|
99
|
+
{:else}
|
|
100
|
+
<dt>{config.i18n.incidentOpenedAt}</dt>
|
|
101
|
+
<dd>{new Date(incident.created_at).toLocaleString(config.i18n.locale)}</dd>
|
|
102
|
+
{/if}
|
|
103
|
+
{#if incident.metadata.start && incident.metadata.end}
|
|
104
|
+
<dt>{config.i18n.duration}</dt>
|
|
105
|
+
<dd>
|
|
106
|
+
{config.i18n.durationMin.replace(
|
|
107
|
+
/\$DURATION/g,
|
|
108
|
+
Math.floor(
|
|
109
|
+
(new Date(incident.metadata.end).getTime() -
|
|
110
|
+
new Date(incident.metadata.start).getTime()) /
|
|
111
|
+
60000
|
|
112
|
+
)
|
|
113
|
+
)}
|
|
114
|
+
</dd>
|
|
115
|
+
{:else if incident.closed_at}
|
|
116
|
+
<dt>{config.i18n.incidentClosedAt}</dt>
|
|
117
|
+
<dd>{new Date(incident.closed_at).toLocaleString(config.i18n.locale)}</dd>
|
|
118
|
+
{/if}
|
|
119
|
+
</dl>
|
|
120
|
+
<div class="r">
|
|
121
|
+
<p>
|
|
122
|
+
<a href={`https://github.com/${config.owner}/${config.repo}/issues/${number}`}>
|
|
123
|
+
{config.i18n.incidentViewOnGitHub}
|
|
124
|
+
</a>
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
{#each comments as comment}
|
|
129
|
+
<article>
|
|
130
|
+
<p>
|
|
131
|
+
{@html md(comment.body)}
|
|
132
|
+
</p>
|
|
133
|
+
<div>
|
|
134
|
+
{@html config.i18n.incidentCommentSummary
|
|
135
|
+
.replace(
|
|
136
|
+
/\$DATE/g,
|
|
137
|
+
`<a href=${comment.html_url}>${new Date(comment.created_at).toLocaleString(config.i18n.locale)}</a>`
|
|
138
|
+
)
|
|
139
|
+
.replace(/\$AUTHOR/g, `<a href=${comment.user.html_url}>@${comment.user.login}</a>`)}
|
|
140
|
+
</div>
|
|
141
|
+
</article>
|
|
142
|
+
{/each}
|
|
143
|
+
{/if}
|
|
144
|
+
</section>
|
|
145
|
+
|
|
146
|
+
<footer><a href={config.path}>{config.i18n.incidentBack}</a></footer>
|
|
147
|
+
|
|
148
|
+
<style>
|
|
149
|
+
footer {
|
|
150
|
+
margin-top: 2rem;
|
|
151
|
+
}
|
|
152
|
+
p {
|
|
153
|
+
margin-top: 0;
|
|
154
|
+
}
|
|
155
|
+
h2 {
|
|
156
|
+
line-height: 1;
|
|
157
|
+
}
|
|
158
|
+
.r {
|
|
159
|
+
text-align: right;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Loading from "../components/Loading.svelte";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import config from "../data/config.json";
|
|
5
|
+
import { cachedResponse, createOctokit, handleError } from "../utils/createOctokit";
|
|
6
|
+
|
|
7
|
+
let loading = true;
|
|
8
|
+
const octokit = createOctokit();
|
|
9
|
+
const owner = config.owner;
|
|
10
|
+
const repo = config.repo;
|
|
11
|
+
let incidents = [];
|
|
12
|
+
|
|
13
|
+
onMount(async () => {
|
|
14
|
+
try {
|
|
15
|
+
incidents = (
|
|
16
|
+
await cachedResponse(`closed-issues-${owner}-${repo}`, () =>
|
|
17
|
+
octokit.issues.listForRepo({
|
|
18
|
+
owner,
|
|
19
|
+
repo,
|
|
20
|
+
state: "closed",
|
|
21
|
+
filter: "all",
|
|
22
|
+
sort: "created",
|
|
23
|
+
direction: "desc",
|
|
24
|
+
labels: "status",
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
).data;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
handleError(error);
|
|
30
|
+
}
|
|
31
|
+
incidents = incidents.map((incident, index) => {
|
|
32
|
+
incident.showHeading =
|
|
33
|
+
index === 0 ||
|
|
34
|
+
new Date(incidents[index - 1].created_at).toLocaleDateString() !==
|
|
35
|
+
new Date(incident.created_at).toLocaleDateString();
|
|
36
|
+
return incident;
|
|
37
|
+
});
|
|
38
|
+
loading = false;
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
h2 {
|
|
44
|
+
margin-top: 2rem;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
47
|
+
|
|
48
|
+
<section>
|
|
49
|
+
{#if loading}
|
|
50
|
+
<Loading />
|
|
51
|
+
{:else if incidents.length}
|
|
52
|
+
<h2>{config.i18n.pastIncidents}</h2>
|
|
53
|
+
{#each incidents as incident}
|
|
54
|
+
{#if incident.showHeading}
|
|
55
|
+
<h3>{new Date(incident.created_at).toLocaleDateString(config.i18n.locale)}</h3>
|
|
56
|
+
{/if}
|
|
57
|
+
<article class="down link {incident.title.includes('degraded') ? 'degraded' : ''}">
|
|
58
|
+
<div class="f">
|
|
59
|
+
<div>
|
|
60
|
+
<h4>{incident.title.replace('🛑', '').replace('⚠️', '').trim()}</h4>
|
|
61
|
+
<div>
|
|
62
|
+
{@html config.i18n.pastIncidentsResolved
|
|
63
|
+
.replace(
|
|
64
|
+
/\$MINUTES/g,
|
|
65
|
+
(
|
|
66
|
+
(new Date(incident.closed_at).getTime() -
|
|
67
|
+
new Date(incident.created_at).getTime()) /
|
|
68
|
+
60000
|
|
69
|
+
).toFixed(0)
|
|
70
|
+
)
|
|
71
|
+
.replace(/\$POSTS/g, incident.comments)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="f r">
|
|
75
|
+
<a href={`${config.path}/incident/${incident.number}`}>
|
|
76
|
+
{config.i18n.incidentReport.replace(/\$NUMBER/g, incident.number)}
|
|
77
|
+
</a>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</article>
|
|
81
|
+
{/each}
|
|
82
|
+
{/if}
|
|
83
|
+
</section>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Loading from "../components/Loading.svelte";
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import config from "../data/config.json";
|
|
5
|
+
import { createOctokit, handleError } from "../utils/createOctokit";
|
|
6
|
+
|
|
7
|
+
let loading = true;
|
|
8
|
+
const octokit = createOctokit();
|
|
9
|
+
const owner = config.owner;
|
|
10
|
+
const repo = config.repo;
|
|
11
|
+
let sites = [];
|
|
12
|
+
|
|
13
|
+
let { apiBaseUrl, userContentBaseUrl } = config["status-website"] || {};
|
|
14
|
+
if (!apiBaseUrl) apiBaseUrl = "https://api.github.com";
|
|
15
|
+
if (!userContentBaseUrl) userContentBaseUrl = "https://raw.githubusercontent.com";
|
|
16
|
+
|
|
17
|
+
const graphsBaseUrl = `${userContentBaseUrl}/${owner}/${repo}/master/graphs`;
|
|
18
|
+
let form = null;
|
|
19
|
+
|
|
20
|
+
let selected = "week";
|
|
21
|
+
|
|
22
|
+
onMount(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${userContentBaseUrl}/${owner}/${repo}/master/history/summary.json`);
|
|
25
|
+
sites = await res.json();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
handleError(error);
|
|
28
|
+
}
|
|
29
|
+
loading = false;
|
|
30
|
+
if (form) form.classList.remove("changed");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const changed = () => {
|
|
34
|
+
if (form) {
|
|
35
|
+
form.classList.add("changed");
|
|
36
|
+
setTimeout(() => form.classList.remove("changed"), 500);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div class="f changed" bind:this={form}>
|
|
42
|
+
<h2>{config.i18n.liveStatus}</h2>
|
|
43
|
+
<form class="f r">
|
|
44
|
+
<div>
|
|
45
|
+
<input
|
|
46
|
+
value="day"
|
|
47
|
+
bind:group={selected}
|
|
48
|
+
name="d"
|
|
49
|
+
type="radio"
|
|
50
|
+
on:change={changed}
|
|
51
|
+
id="data_day"
|
|
52
|
+
/><label for="data_day">{config.i18n.duration24H}</label>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<input
|
|
56
|
+
value="week"
|
|
57
|
+
bind:group={selected}
|
|
58
|
+
name="d"
|
|
59
|
+
type="radio"
|
|
60
|
+
on:change={changed}
|
|
61
|
+
id="data_week"
|
|
62
|
+
/><label for="data_week">{config.i18n.duration7D}</label>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<input
|
|
66
|
+
value="month"
|
|
67
|
+
bind:group={selected}
|
|
68
|
+
name="d"
|
|
69
|
+
type="radio"
|
|
70
|
+
on:change={changed}
|
|
71
|
+
id="data_month"
|
|
72
|
+
/><label for="data_month">{config.i18n.duration30D}</label>
|
|
73
|
+
</div>
|
|
74
|
+
<div>
|
|
75
|
+
<input
|
|
76
|
+
value="year"
|
|
77
|
+
bind:group={selected}
|
|
78
|
+
name="d"
|
|
79
|
+
type="radio"
|
|
80
|
+
on:change={changed}
|
|
81
|
+
id="data_year"
|
|
82
|
+
/><label for="data_year">{config.i18n.duration1Y}</label>
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<input
|
|
86
|
+
value="all"
|
|
87
|
+
bind:group={selected}
|
|
88
|
+
name="d"
|
|
89
|
+
type="radio"
|
|
90
|
+
on:change={changed}
|
|
91
|
+
id="data_all"
|
|
92
|
+
/><label for="data_all">{config.i18n.durationAll}</label>
|
|
93
|
+
</div>
|
|
94
|
+
</form>
|
|
95
|
+
</div>
|
|
96
|
+
<section class="live-status">
|
|
97
|
+
{#if loading}
|
|
98
|
+
<Loading />
|
|
99
|
+
{:else if sites.length}
|
|
100
|
+
{#each sites as site}
|
|
101
|
+
<article
|
|
102
|
+
class={`${site.status} link graph`}
|
|
103
|
+
style="--background: url('{`${graphsBaseUrl}/${site.slug}/response-time${
|
|
104
|
+
selected === "day"
|
|
105
|
+
? "-day"
|
|
106
|
+
: selected === "week"
|
|
107
|
+
? "-week"
|
|
108
|
+
: selected === "month"
|
|
109
|
+
? "-month"
|
|
110
|
+
: selected === "year"
|
|
111
|
+
? "-year"
|
|
112
|
+
: ""
|
|
113
|
+
}.png`}')"
|
|
114
|
+
><h4>
|
|
115
|
+
<img class="icon" alt="" src={site.icon} />
|
|
116
|
+
<a href={`${config.path}/history/${site.slug}`}>{site.name}</a>
|
|
117
|
+
</h4>
|
|
118
|
+
<div>
|
|
119
|
+
{@html config.i18n.overallUptime.split("$UPTIME")[0]}
|
|
120
|
+
<span class="data"
|
|
121
|
+
>{selected === "day"
|
|
122
|
+
? site.uptimeDay
|
|
123
|
+
: selected === "week"
|
|
124
|
+
? site.uptimeWeek
|
|
125
|
+
: selected === "month"
|
|
126
|
+
? site.uptimeMonth
|
|
127
|
+
: selected === "year"
|
|
128
|
+
? site.uptimeYear
|
|
129
|
+
: site.uptime}
|
|
130
|
+
{@html config.i18n.overallUptime.split("$UPTIME")[1]}</span
|
|
131
|
+
>
|
|
132
|
+
</div>
|
|
133
|
+
{#if site.showAverageResponseTime === undefined || site.showAverageResponseTime}
|
|
134
|
+
<div>
|
|
135
|
+
{@html config.i18n.averageResponseTime.split("$TIME")[0]}
|
|
136
|
+
<span class="data"
|
|
137
|
+
>{selected === "day"
|
|
138
|
+
? site.timeDay
|
|
139
|
+
: selected === "week"
|
|
140
|
+
? site.timeWeek
|
|
141
|
+
: selected === "month"
|
|
142
|
+
? site.timeMonth
|
|
143
|
+
: selected === "year"
|
|
144
|
+
? site.timeYear
|
|
145
|
+
: site.time}
|
|
146
|
+
{@html config.i18n.averageResponseTime.split("$TIME")[1]}</span
|
|
147
|
+
>
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
</article>
|
|
151
|
+
{/each}
|
|
152
|
+
{/if}
|
|
153
|
+
</section>
|
|
154
|
+
|
|
155
|
+
<style>
|
|
156
|
+
article.graph {
|
|
157
|
+
background-image: var(--background);
|
|
158
|
+
background-size: contain;
|
|
159
|
+
background-repeat: no-repeat;
|
|
160
|
+
background-position: center right;
|
|
161
|
+
}
|
|
162
|
+
.icon {
|
|
163
|
+
height: 1rem;
|
|
164
|
+
margin-right: 0.33rem;
|
|
165
|
+
vertical-align: middle;
|
|
166
|
+
transform: scale(1.1) translateY(-0.1rem);
|
|
167
|
+
}
|
|
168
|
+
a {
|
|
169
|
+
text-decoration: none;
|
|
170
|
+
}
|
|
171
|
+
.r input:checked + label {
|
|
172
|
+
font-weight: bold;
|
|
173
|
+
}
|
|
174
|
+
.r input {
|
|
175
|
+
display: none;
|
|
176
|
+
}
|
|
177
|
+
.r label {
|
|
178
|
+
margin-left: 1rem;
|
|
179
|
+
}
|
|
180
|
+
.data {
|
|
181
|
+
transition: 0.3s;
|
|
182
|
+
}
|
|
183
|
+
.changed + section {
|
|
184
|
+
background-color: transparent;
|
|
185
|
+
}
|
|
186
|
+
.data {
|
|
187
|
+
padding: 0.15rem 0.25rem;
|
|
188
|
+
border-radius: 0.2rem;
|
|
189
|
+
}
|
|
190
|
+
</style>
|