@scp-dev/dashboard 0.0.0 → 0.2.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/README.md +57 -0
- package/dist/public/app.css +187 -0
- package/dist/public/app.js +80 -0
- package/dist/public/graph.css +85 -0
- package/dist/public/graph.js +742 -0
- package/dist/public/index.html +1 -0
- package/dist/public/selfhost.js +1315 -0
- package/dist/server.js +460 -0
- package/package.json +18 -3
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @scp-dev/dashboard
|
|
2
|
+
|
|
3
|
+
Optional dashboard server for the SCP relay. Requires Node 18 or later and has no runtime dependencies.
|
|
4
|
+
The browser UI uses the hosted relay's renderers. Build from the repository root with `bun install`
|
|
5
|
+
and `bun run --filter @scp-dev/dashboard build`.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @scp-dev/dashboard --relay https://relay.example.com
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Open `http://127.0.0.1:8788` and sign in with a relay token or device code. Approve device codes with
|
|
12
|
+
`scp-relay device approve <code> --account <handle> --member <name>` on the relay host.
|
|
13
|
+
Members and tokens are managed with the relay admin CLI.
|
|
14
|
+
|
|
15
|
+
| Option | Environment | Default |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `--relay <url>` | `SCP_DASHBOARD_RELAY` | Required relay origin |
|
|
18
|
+
| `--listen <host:port>` | `SCP_DASHBOARD_LISTEN` | `127.0.0.1:8788` |
|
|
19
|
+
| `--public-url <url>` | `SCP_DASHBOARD_PUBLIC_URL` | Request origin |
|
|
20
|
+
| `--forward-header <name>` (repeatable) | `SCP_DASHBOARD_FORWARD_HEADERS` (comma separated) | None |
|
|
21
|
+
|
|
22
|
+
`--help` shows usage, `--version` prints the relay release version, and `--health` checks the local
|
|
23
|
+
listener's `/health` route without contacting the relay.
|
|
24
|
+
|
|
25
|
+
Credentials stay server side in memory. The browser receives an opaque HttpOnly, SameSite=Strict cookie
|
|
26
|
+
and a session-bound CSRF token. Every authenticated request checks the relay session; a relay 401 signs
|
|
27
|
+
the browser out. Event WebSockets use the session's bearer and revalidate every 30 seconds. A dashboard
|
|
28
|
+
restart signs everyone out. POST requests require JSON, a matching Origin and a valid CSRF token.
|
|
29
|
+
|
|
30
|
+
Use an HTTPS reverse proxy and set `--public-url https://dashboard.example.com` for Secure host cookies
|
|
31
|
+
and external Origin checks. The proxy must support WebSocket upgrades.
|
|
32
|
+
|
|
33
|
+
## Container
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
docker run --rm -p 8788:8788 \
|
|
37
|
+
-e SCP_DASHBOARD_RELAY=https://relay.example.com \
|
|
38
|
+
-e SCP_DASHBOARD_PUBLIC_URL=https://dashboard.example.com \
|
|
39
|
+
ghcr.io/scphq/scp-dashboard:latest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The image runs as uid 65532, listens on `0.0.0.0:8788`, and health checks the dashboard independently
|
|
43
|
+
of relay availability. See the [self-hosting guide](https://scp.dev/docs/self-host) for Compose with
|
|
44
|
+
Postgres, relay and dashboard. Build locally from the repository root with
|
|
45
|
+
`docker build -f packages/dashboard/Dockerfile -t scp-dashboard:local .`.
|
|
46
|
+
|
|
47
|
+
## SSO
|
|
48
|
+
|
|
49
|
+
Put the dashboard itself behind an authenticating SSO proxy. Set `--forward-header` for each identity
|
|
50
|
+
header expected by the relay. The proxy must overwrite these headers and block direct dashboard access.
|
|
51
|
+
Run the relay with `--auth trusted-header` and include the dashboard host's IP in `--trusted-proxy`.
|
|
52
|
+
Only explicitly named identity headers are forwarded on every request, including event upgrades.
|
|
53
|
+
The relay accepts or rejects that identity; no bearer is stored or sent by the browser in this mode.
|
|
54
|
+
Signing out clears the cookie, but the SSO proxy may authenticate the next request again.
|
|
55
|
+
|
|
56
|
+
To build a separate UI, see [docs/openapi/relay-dashboard.yaml](https://github.com/scphq/scp/blob/main/docs/openapi/relay-dashboard.yaml).
|
|
57
|
+
The relay's `--cors-origin` lets a UI on another origin call that API directly with a bearer.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
|
|
2
|
+
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
3
|
+
:root{--bg:#fff;--surface:#f7f7f6;--fg:#0b0b0b;--ink:#555;--dim:#6b6b6b;--faint:#a8a8a8;--line:rgb(0 0 0/.12);--line-soft:rgb(0 0 0/.065);--acc:#4746d4;--acc-soft:rgb(71 70 212/.09);--warn:#b4321f;--head:rgb(255 255 255/.82);
|
|
4
|
+
--sans:"Geist",-apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;--mono:"Geist Mono",ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;--gutter:clamp(1rem,4vw,3rem);--headh:4rem}
|
|
5
|
+
@media (prefers-color-scheme:dark){:root{--bg:#090909;--surface:#111;--fg:#f2f2f0;--ink:#a6a6a6;--dim:#8c8c8c;--faint:#575757;--line:rgb(255 255 255/.13);--line-soft:rgb(255 255 255/.07);--acc:#9d9cff;--acc-soft:rgb(157 156 255/.1);--warn:#ff8a78;--head:rgb(9 9 9/.78)}}
|
|
6
|
+
html{color-scheme:light dark;-webkit-text-size-adjust:100%}
|
|
7
|
+
body{display:flex;flex-direction:column;min-height:100dvh;background:var(--bg);color:var(--fg);font-family:var(--sans);font-size:.9375rem;line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
8
|
+
main{flex:1}
|
|
9
|
+
a{color:inherit}
|
|
10
|
+
::selection{background:var(--fg);color:var(--bg)}
|
|
11
|
+
:focus-visible{outline:1px solid var(--fg);outline-offset:3px}
|
|
12
|
+
code,pre,kbd{font-family:var(--mono)}
|
|
13
|
+
.wrap{position:relative;width:100%;max-width:56rem;margin-inline:auto;padding-inline:var(--gutter);border-inline:1px solid var(--line-soft)}
|
|
14
|
+
.rule{border-top:1px solid var(--line-soft)}
|
|
15
|
+
@media (max-width:48rem){.wrap{border-inline:0}}
|
|
16
|
+
.wide .wrap{max-width:100rem}
|
|
17
|
+
.head{position:sticky;top:0;z-index:20;border-bottom:1px solid var(--line-soft);background:var(--head);-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}
|
|
18
|
+
.head-in{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;height:var(--headh)}
|
|
19
|
+
.brand{display:inline-flex;align-items:center;gap:.625rem;font-weight:600;letter-spacing:.02em;text-decoration:none}
|
|
20
|
+
.brand-dim{color:var(--dim);font-weight:500}
|
|
21
|
+
.mark{width:16px;height:16px;flex:none;color:var(--fg)}
|
|
22
|
+
.nav{display:flex;align-items:center;gap:1.5rem}
|
|
23
|
+
.nav a,.linkish{font-family:var(--mono);font-size:.75rem;color:var(--ink);padding-block:.25rem;border:0;border-bottom:1px solid transparent;background:none;text-decoration:none;cursor:pointer}
|
|
24
|
+
.nav a:hover,.linkish:hover,.nav a[aria-current]{color:var(--fg)}
|
|
25
|
+
.nav a[aria-current]{border-bottom-color:var(--fg)}
|
|
26
|
+
.nav form{display:inline}
|
|
27
|
+
.page{padding-block:clamp(2.5rem,6vw,4.5rem) 5rem;min-height:calc(100dvh - var(--headh) - 7rem)}
|
|
28
|
+
.label{font-family:var(--mono);font-size:.6875rem;letter-spacing:.14em;text-transform:uppercase;color:var(--dim)}
|
|
29
|
+
h1{margin-top:1rem;font-size:clamp(2rem,4.6vw,3rem);line-height:1;font-weight:580;letter-spacing:-.045em;text-wrap:balance}
|
|
30
|
+
h2{font-size:1.125rem;font-weight:560;letter-spacing:-.015em}
|
|
31
|
+
.lead{margin-top:1rem;max-width:36rem;color:var(--ink);font-size:1rem}
|
|
32
|
+
.small{font-size:.875rem;color:var(--ink)}
|
|
33
|
+
.mini{font-family:var(--mono);font-size:.6875rem;color:var(--dim)}
|
|
34
|
+
.mini-link{font-family:var(--mono);font-size:.6875rem;color:var(--dim);text-decoration:none}
|
|
35
|
+
.mini-link:hover{color:var(--fg)}
|
|
36
|
+
.block{margin-top:3rem;padding-top:1.75rem;border-top:1px solid var(--line-soft)}
|
|
37
|
+
.block>h2+*{margin-top:1rem}
|
|
38
|
+
.form{display:grid;gap:1rem;max-width:26rem;margin-top:2rem}
|
|
39
|
+
.form.inline{display:flex;flex-wrap:wrap;align-items:end;max-width:none}
|
|
40
|
+
.field{display:grid;gap:.375rem}
|
|
41
|
+
.field span{font-family:var(--mono);font-size:.6875rem;letter-spacing:.08em;text-transform:uppercase;color:var(--dim)}
|
|
42
|
+
input[type=email],input[type=text],select{height:2.75rem;padding-inline:.875rem;border:1px solid var(--line);border-radius:0;background:var(--bg);color:var(--fg);font:inherit;font-size:.9375rem}
|
|
43
|
+
input:focus,select:focus{outline:none;border-color:var(--fg)}
|
|
44
|
+
input.code{font-family:var(--mono);font-size:1.25rem;letter-spacing:.2em;text-transform:uppercase}
|
|
45
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;height:2.75rem;padding-inline:1.25rem;border:1px solid;font-family:var(--mono);font-size:.75rem;white-space:nowrap;cursor:pointer;text-decoration:none;transition:background-color .2s,color .2s,border-color .2s}
|
|
46
|
+
.btn-primary{border-color:var(--fg);background:var(--fg);color:var(--bg)}
|
|
47
|
+
.btn-primary:hover{background:transparent;color:var(--fg)}
|
|
48
|
+
.btn-ghost{border-color:var(--line);background:transparent;color:var(--fg)}
|
|
49
|
+
.btn-ghost:hover{border-color:var(--fg)}
|
|
50
|
+
.btn-small{height:2rem;padding-inline:.75rem;font-size:.6875rem}
|
|
51
|
+
.acts{display:flex;flex-wrap:wrap;gap:.75rem}
|
|
52
|
+
.table{margin-top:1rem;overflow-x:auto;border:1px solid var(--line-soft)}
|
|
53
|
+
table{width:100%;border-collapse:collapse;font-size:.875rem}
|
|
54
|
+
th,td{padding:.75rem 1rem;text-align:left;vertical-align:middle;border-bottom:1px solid var(--line-soft)}
|
|
55
|
+
tr:last-child td{border-bottom:0}
|
|
56
|
+
th{background:var(--surface);font-family:var(--mono);font-size:.6875rem;font-weight:400;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);white-space:nowrap}
|
|
57
|
+
td.mono,.mono{font-family:var(--mono);font-size:.75rem}
|
|
58
|
+
td.end,th.end{text-align:right}
|
|
59
|
+
td form{display:inline}
|
|
60
|
+
.empty{margin-top:1rem;padding:1.25rem;border:1px dashed var(--line);color:var(--dim);font-size:.875rem}
|
|
61
|
+
.notice{margin-top:1.5rem;padding:.875rem 1rem;border:1px solid var(--line-soft);border-left:1px solid var(--acc);background:var(--acc-soft);font-size:.875rem}
|
|
62
|
+
.notice.err{border-left-color:var(--warn);background:transparent;color:var(--warn)}
|
|
63
|
+
.secret{margin-top:1rem;padding:1rem 1.125rem;border:1px solid var(--line);background:var(--surface);font-family:var(--mono);font-size:.8125rem;overflow-wrap:anywhere;user-select:all}
|
|
64
|
+
.usercode{margin-top:1.5rem;font-family:var(--mono);font-size:clamp(2rem,6vw,3rem);letter-spacing:.12em}
|
|
65
|
+
.role{font-family:var(--mono);font-size:.6875rem;padding:.125rem .375rem;border:1px solid var(--line);color:var(--ink)}
|
|
66
|
+
h3{font-size:.9375rem;font-weight:560;letter-spacing:-.01em}
|
|
67
|
+
.nowrap{white-space:nowrap}
|
|
68
|
+
.push{margin-left:auto}
|
|
69
|
+
.block-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem 1rem}
|
|
70
|
+
.dot{display:inline-block;width:6px;height:6px;margin-right:.5rem;vertical-align:middle;border:1px solid var(--faint);background:transparent}
|
|
71
|
+
.dot.online{border-color:var(--acc);background:var(--acc)}
|
|
72
|
+
.dot.idle{border-color:var(--dim);background:var(--dim)}
|
|
73
|
+
.pulse{animation:pulse 2.4s ease-in-out infinite}
|
|
74
|
+
@keyframes pulse{50%{opacity:.25}}
|
|
75
|
+
.tag{display:inline-block;font-family:var(--mono);font-size:.6875rem;padding:.0625rem .375rem;background:var(--surface);border:1px solid var(--line-soft);color:var(--ink);white-space:nowrap}
|
|
76
|
+
.machine{margin-top:1.5rem}
|
|
77
|
+
table.agents,table.activity{table-layout:fixed;min-width:56rem}
|
|
78
|
+
table.agents th,table.agents td,table.activity th,table.activity td{height:2.125rem;padding:.25rem .75rem;line-height:1.25rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
79
|
+
table.agents .end,table.activity .end{text-align:right}
|
|
80
|
+
table.agents .tag+.tag{margin-left:.25rem}
|
|
81
|
+
.c-agent{width:30%}.c-title{width:18%}.c-presence{width:8.5rem}.c-status{width:20%}.c-labels{width:16%}.c-seen{width:6.5rem}
|
|
82
|
+
.c-when{width:7rem}.c-who{width:30%}
|
|
83
|
+
tr.sub td.agent{padding-left:1.75rem}
|
|
84
|
+
tr.sub.d2 td.agent{padding-left:2.75rem}
|
|
85
|
+
tr.sub.d3 td.agent{padding-left:3.75rem}
|
|
86
|
+
tr.sub.d4 td.agent{padding-left:4.75rem}
|
|
87
|
+
.caret{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;margin-right:.125rem;padding:0;border:0;background:none;color:var(--dim);vertical-align:-2px;cursor:pointer}
|
|
88
|
+
.caret svg{transition:transform .15s}
|
|
89
|
+
.caret[aria-expanded=true] svg{transform:rotate(90deg)}
|
|
90
|
+
.caret:hover{color:var(--fg)}
|
|
91
|
+
:root{--h-claude:#c15f3c;--h-codex:#0f8a6c;--h-opencode:#b56a00;--h-gemini:#1a73e8;--h-agy:#4285f4;--h-grok:#3a3a3a;--h-muse:#0866ff;--h-pi:#c03d86;--h-t3code:#4f46e5}
|
|
92
|
+
@media (prefers-color-scheme:dark){:root{--h-claude:#e08b6b;--h-codex:#3fc6a0;--h-opencode:#e6a43f;--h-gemini:#7aa9ff;--h-agy:#8ab4f8;--h-grok:#e6e6e6;--h-muse:#3d8bff;--h-pi:#ec7bb8;--h-t3code:#a5a1ff}}
|
|
93
|
+
.hicon{display:inline-block;overflow:visible;width:12px;height:12px;margin-right:.375rem;vertical-align:-1px;color:var(--dim)}
|
|
94
|
+
.h-claude{color:var(--h-claude)}.h-codex{color:var(--h-codex)}.h-opencode{color:var(--h-opencode)}.h-gemini{color:var(--h-gemini)}.h-agy{color:var(--h-agy)}.h-grok{color:var(--h-grok)}.h-muse{color:var(--h-muse)}.h-pi{color:var(--h-pi)}.h-t3code{color:var(--h-t3code)}
|
|
95
|
+
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
|
|
96
|
+
.wake-badge{display:inline-flex;vertical-align:middle;margin-left:.25rem;color:var(--acc)}
|
|
97
|
+
.bolt{width:11px;height:11px}
|
|
98
|
+
.dot.idle.wake,.dot.hibernated{border-color:var(--acc);background:var(--acc-soft)}
|
|
99
|
+
details.offline{margin-top:.5rem}
|
|
100
|
+
details.offline>summary{display:inline-block;padding:.125rem 0;cursor:pointer;list-style:none}
|
|
101
|
+
details.offline>summary::-webkit-details-marker{display:none}
|
|
102
|
+
details.offline>summary::before{content:"+ ";color:var(--faint)}
|
|
103
|
+
details.offline[open]>summary::before{content:"- "}
|
|
104
|
+
details.offline .when-open,details.offline[open] .when-shut{display:none}
|
|
105
|
+
details.offline[open] .when-open{display:inline}
|
|
106
|
+
details.offline>summary:hover{color:var(--fg)}
|
|
107
|
+
.machine .table{margin-top:.625rem}
|
|
108
|
+
.machine-head{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem .75rem}
|
|
109
|
+
.machine-head .dot{margin-right:0}
|
|
110
|
+
.machine-head h3{font-family:var(--mono);font-size:.875rem;font-weight:500}
|
|
111
|
+
.steps{list-style:none;display:grid;gap:1.25rem;margin-top:1.5rem;counter-reset:step}
|
|
112
|
+
.steps li{counter-increment:step}
|
|
113
|
+
.steps .label::before{content:counter(step,decimal-leading-zero) " ";color:var(--faint)}
|
|
114
|
+
.cmd{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-top:.5rem;padding:.5rem .5rem .5rem 1rem;border:1px solid var(--line);background:var(--surface)}
|
|
115
|
+
.cmd code{font-size:.8125rem;overflow-wrap:anywhere;user-select:all}
|
|
116
|
+
.cmd .prompt{color:var(--faint);user-select:none}
|
|
117
|
+
.sub-block{margin-top:2.25rem;padding-top:1.5rem;border-top:1px solid var(--line-soft)}
|
|
118
|
+
.sub-block>*+*{margin-top:.75rem}
|
|
119
|
+
.org-head h1{margin-top:.75rem;font-size:clamp(1.75rem,3.4vw,2.25rem)}
|
|
120
|
+
.org-title{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem 1rem}
|
|
121
|
+
.tabs{display:flex;gap:1.75rem;margin-top:1.75rem;border-bottom:1px solid var(--line-soft);overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
|
|
122
|
+
.tabs::-webkit-scrollbar{display:none}
|
|
123
|
+
.tabs a{flex:none;margin-bottom:-1px;padding:.75rem 0 .6875rem;border-bottom:1px solid transparent;font-family:var(--mono);font-size:.75rem;color:var(--ink);text-decoration:none;white-space:nowrap;transition:color .15s,border-color .15s}
|
|
124
|
+
.tabs a:hover{color:var(--fg)}
|
|
125
|
+
.tabs a[aria-current]{color:var(--fg);border-bottom-color:var(--fg)}
|
|
126
|
+
@media (max-width:40rem){.tabs{gap:1.25rem;margin-inline:calc(-1 * var(--gutter));padding-inline:var(--gutter);-webkit-mask-image:linear-gradient(90deg,#000 80%,transparent);mask-image:linear-gradient(90deg,#000 80%,transparent)}
|
|
127
|
+
table.activity{min-width:0}table.activity colgroup,table.activity thead{display:none}
|
|
128
|
+
table.activity tr{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--line-soft)}
|
|
129
|
+
table.activity tr:last-child{border-bottom:0}
|
|
130
|
+
table.activity td{height:auto!important;padding:0!important;border:0}
|
|
131
|
+
table.activity td:last-child{grid-column:1/-1;white-space:normal!important}}
|
|
132
|
+
.tab-body>.block:first-child,.tab-body>.notice+.block,.tab-body>[role=alert]+.block{margin-top:2rem;padding-top:0;border-top:0}
|
|
133
|
+
.tab-body>.block~.block{margin-top:2.5rem}
|
|
134
|
+
.live-stamp .dot{animation:none;border-color:var(--faint);background:transparent}
|
|
135
|
+
[data-feed-state=open] .live-stamp .dot{border-color:var(--acc);background:var(--acc);animation:pulse 2.4s ease-in-out infinite}
|
|
136
|
+
.summary-head{margin-top:2rem}
|
|
137
|
+
.tiles{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:1px;margin-top:1rem;border:1px solid var(--line-soft);background:var(--line-soft)}
|
|
138
|
+
.tile{padding:1rem 1.25rem 1.125rem;background:var(--bg)}
|
|
139
|
+
.tile-label{font-family:var(--mono);font-size:.6875rem;letter-spacing:.08em;text-transform:uppercase;color:var(--dim)}
|
|
140
|
+
.tile-value{margin-top:.375rem;font-family:var(--mono);font-size:2rem;line-height:1.1;font-weight:500;letter-spacing:-.03em;color:var(--fg)}
|
|
141
|
+
.tile.on .tile-value{color:var(--acc)}
|
|
142
|
+
.tile .mini{margin-top:.25rem}
|
|
143
|
+
@media (max-width:56rem){.tiles{grid-template-columns:repeat(6,minmax(0,1fr))}.tile{grid-column:span 2}.tile:nth-child(n+4){grid-column:span 3}}
|
|
144
|
+
@media (max-width:30rem){.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}.tile,.tile:nth-child(n+4){grid-column:auto}.tile:last-child{grid-column:span 2}.tile{padding:.875rem 1rem}.tile-value{font-size:1.625rem}}
|
|
145
|
+
.panels{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);align-items:start;gap:1.25rem;margin-top:1.25rem}
|
|
146
|
+
@media (max-width:56rem){.panels{grid-template-columns:minmax(0,1fr)}}
|
|
147
|
+
.panel{display:flex;flex-direction:column;padding:1.125rem 1.25rem 1.25rem;border:1px solid var(--line-soft)}
|
|
148
|
+
.panel-head{display:flex;align-items:baseline;gap:1rem}
|
|
149
|
+
.panel h2{font-size:.9375rem}
|
|
150
|
+
.now-list{list-style:none;margin-top:.75rem}
|
|
151
|
+
.now-list li{padding:.625rem 0;border-top:1px solid var(--line-soft)}
|
|
152
|
+
.now-list li:first-child{border-top:0}
|
|
153
|
+
.now-who{display:flex;align-items:center;gap:.5rem;min-width:0}
|
|
154
|
+
.now-who .hicon{margin-right:0}
|
|
155
|
+
.now-who .mono{font-weight:500;color:var(--fg)}
|
|
156
|
+
.now-who .dot{margin-right:.375rem}
|
|
157
|
+
.now-what{margin-top:.125rem;padding-left:1.25rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
158
|
+
.now-what .role{margin-right:.125rem}
|
|
159
|
+
.more-line,.last-line{margin-top:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
160
|
+
.last-line{display:flex;gap:.375rem;padding-top:.75rem;border-top:1px solid var(--line-soft)}
|
|
161
|
+
.last-line .what{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
162
|
+
.last-line .mini-link{flex:none;margin-left:auto;padding-left:1rem}
|
|
163
|
+
.panel .empty{margin-top:.75rem}
|
|
164
|
+
.strip{display:grid;gap:.5rem;margin-top:1rem}
|
|
165
|
+
.strip-row{display:flex;align-items:center;gap:.625rem;min-width:0}
|
|
166
|
+
.strip-who{display:flex;flex:none;align-items:baseline;gap:.375rem;width:11rem;min-width:0;overflow:hidden;white-space:nowrap}
|
|
167
|
+
.strip-who .hicon{flex:none;margin-right:0}
|
|
168
|
+
.strip-who .mini{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
169
|
+
.strip-name{flex:0 0 auto;max-width:7.5rem;overflow:hidden;text-overflow:ellipsis}
|
|
170
|
+
.cells{display:flex;flex-wrap:wrap;gap:3px;min-width:0}
|
|
171
|
+
.cell{display:inline-block;width:10px;height:10px;border:1px solid var(--faint);background:transparent}
|
|
172
|
+
.cell.online{border-color:var(--acc);background:var(--acc)}
|
|
173
|
+
.cell.idle{border-color:var(--dim);background:var(--dim)}
|
|
174
|
+
.cell.idle.wake,.cell.hibernated{border-color:var(--acc);background:var(--acc-soft)}
|
|
175
|
+
.strip-key{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;margin:1rem 0 1.25rem}
|
|
176
|
+
.strip-key .cell{width:8px;height:8px;margin-left:.375rem}
|
|
177
|
+
.strip-key .cell:first-child{margin-left:0}
|
|
178
|
+
.fleet-line{margin-top:auto;padding-top:.875rem;border-top:1px solid var(--line-soft)}
|
|
179
|
+
.notice.held{margin-top:1rem}
|
|
180
|
+
.notice a{color:var(--fg)}
|
|
181
|
+
table.orgs td.end,table.orgs th.end{text-align:right}
|
|
182
|
+
.agents-cell{white-space:nowrap}
|
|
183
|
+
.harnesses{display:inline-flex;align-items:center;gap:.25rem;margin-right:.5rem;vertical-align:-1px}
|
|
184
|
+
.harnesses .hicon{margin-right:0}
|
|
185
|
+
.harnesses .more{line-height:1}
|
|
186
|
+
.foot-in{display:flex;flex-wrap:wrap;justify-content:space-between;gap:1rem;padding-block:2rem}
|
|
187
|
+
@media (prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
const chosen = new Map();
|
|
3
|
+
const fold = (root) => {
|
|
4
|
+
for (const b of root.querySelectorAll("[data-tree]")) {
|
|
5
|
+
const v = chosen.get(b.getAttribute("data-tree"));
|
|
6
|
+
if (v) b.setAttribute("aria-expanded", v);
|
|
7
|
+
}
|
|
8
|
+
const shut = new Set([...root.querySelectorAll('[data-tree][aria-expanded="false"]')].map((b) => b.getAttribute("data-tree")));
|
|
9
|
+
for (const r of root.querySelectorAll("tr[data-anc]")) r.hidden = r.getAttribute("data-anc").split(" ").some((a) => shut.has(a));
|
|
10
|
+
};
|
|
11
|
+
document.addEventListener("click", (e) => {
|
|
12
|
+
const t = e.target instanceof Element ? e.target.closest("[data-tree]") : null;
|
|
13
|
+
if (!t) return;
|
|
14
|
+
chosen.set(t.getAttribute("data-tree"), t.getAttribute("aria-expanded") === "true" ? "false" : "true");
|
|
15
|
+
fold(document);
|
|
16
|
+
});
|
|
17
|
+
document.addEventListener("click", async (e) => {
|
|
18
|
+
const b = e.target instanceof Element ? e.target.closest("[data-copy]") : null;
|
|
19
|
+
if (!b) return;
|
|
20
|
+
try {
|
|
21
|
+
await navigator.clipboard.writeText(b.getAttribute("data-copy"));
|
|
22
|
+
b.textContent = "Copied";
|
|
23
|
+
setTimeout(() => { b.textContent = "Copy"; }, 1500);
|
|
24
|
+
} catch {}
|
|
25
|
+
});
|
|
26
|
+
const live = document.querySelector("[data-live]");
|
|
27
|
+
if (live) {
|
|
28
|
+
const every = Number(live.getAttribute("data-live")) || 8000;
|
|
29
|
+
let timer = 0;
|
|
30
|
+
const later = (ms) => { clearTimeout(timer); timer = setTimeout(tick, ms); };
|
|
31
|
+
let soon = false;
|
|
32
|
+
const tick = async () => {
|
|
33
|
+
soon = false;
|
|
34
|
+
const selecting = String(window.getSelection() || "").length > 0;
|
|
35
|
+
if (!document.hidden && !selecting) {
|
|
36
|
+
try {
|
|
37
|
+
let next;
|
|
38
|
+
if (typeof window.scpRegion === "function") next = await window.scpRegion();
|
|
39
|
+
else {
|
|
40
|
+
const res = await fetch(location.href, { credentials: "same-origin", headers: { accept: "text/html" } });
|
|
41
|
+
next = res.ok ? new DOMParser().parseFromString(await res.text(), "text/html").querySelector("[data-live]") : null;
|
|
42
|
+
}
|
|
43
|
+
if (next) {
|
|
44
|
+
const open = new Set([...live.querySelectorAll("details[open][data-key]")].map((d) => d.getAttribute("data-key")));
|
|
45
|
+
for (const d of next.querySelectorAll("details[data-key]")) if (open.has(d.getAttribute("data-key"))) d.open = true;
|
|
46
|
+
fold(next);
|
|
47
|
+
live.replaceChildren(...next.childNodes);
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
later(document.documentElement.getAttribute("data-stream") === "open" ? every * 4 : every);
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("scp:refresh", () => { if (!soon) { soon = true; later(250); } });
|
|
54
|
+
later(every);
|
|
55
|
+
const path = live.getAttribute("data-events");
|
|
56
|
+
if (path && !document.querySelector("[data-graph]") && typeof WebSocket === "function") {
|
|
57
|
+
const state = (open) => {
|
|
58
|
+
document.documentElement.setAttribute("data-feed-state", open ? "open" : "poll");
|
|
59
|
+
for (const s of document.querySelectorAll("[data-feed]")) s.textContent = open ? "live" : "polling";
|
|
60
|
+
};
|
|
61
|
+
let backoff = 1000;
|
|
62
|
+
const connect = () => {
|
|
63
|
+
let ws, ping = 0;
|
|
64
|
+
try { ws = new WebSocket((location.protocol === "https:" ? "wss://" : "ws://") + location.host + path); } catch { return; }
|
|
65
|
+
ws.onopen = () => { backoff = 1000; state(true); ping = setInterval(() => { try { ws.send("ping"); } catch {} }, 30000); };
|
|
66
|
+
ws.onmessage = (m) => { if (typeof m.data === "string" && m.data !== "pong" && !m.data.includes('"hello"')) document.dispatchEvent(new CustomEvent("scp:refresh")); };
|
|
67
|
+
ws.onclose = (c) => {
|
|
68
|
+
clearInterval(ping);
|
|
69
|
+
state(false);
|
|
70
|
+
if (c.code === 4003) return;
|
|
71
|
+
setTimeout(connect, backoff);
|
|
72
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
new MutationObserver(() => state(document.documentElement.getAttribute("data-feed-state") === "open")).observe(live, { childList: true });
|
|
76
|
+
connect();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const tab = document.querySelector('.tabs [aria-current="page"]');
|
|
80
|
+
if (tab) tab.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
:root{--id-0:#2f6fdb;--id-1:#c2410c;--id-2:#0f8a6c;--id-3:#a3369e;--id-4:#a16207;--id-5:#6d4fd6;--id-6:#be123c;--id-7:#3f7d20}
|
|
3
|
+
@media (prefers-color-scheme:dark){:root{--id-0:#7aa9ff;--id-1:#f59a63;--id-2:#3fc6a0;--id-3:#e27ad9;--id-4:#e3b341;--id-5:#a991ff;--id-6:#fb7185;--id-7:#8fd16a}}
|
|
4
|
+
.g-over label{display:inline-flex;align-items:center;gap:.375rem;cursor:pointer}
|
|
5
|
+
.trails{display:inline-flex;align-items:center;gap:.375rem}
|
|
6
|
+
.trails .seg{display:inline-flex;border:1px solid var(--line)}
|
|
7
|
+
.trails .seg button{height:1.375rem;padding-inline:.4375rem;border:0;background:transparent;color:var(--ink);font-family:var(--mono);font-size:.625rem;cursor:pointer}
|
|
8
|
+
.trails .seg button+button{border-left:1px solid var(--line)}
|
|
9
|
+
.trails .seg button[aria-pressed=true]{background:var(--fg);color:var(--bg)}
|
|
10
|
+
.graph{margin-top:1.25rem}
|
|
11
|
+
.graph-legend{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem}
|
|
12
|
+
.graph-legend .key{display:inline-block;width:1.25rem;height:0;margin-left:.5rem;border-top:1.5px solid var(--faint)}
|
|
13
|
+
.graph-legend .key:first-child{margin-left:0}
|
|
14
|
+
.graph-legend .key.msg{border-top-color:var(--acc)}
|
|
15
|
+
[data-graph-stream]:empty{display:none}
|
|
16
|
+
[data-graph-stream]::before{content:"";display:inline-block;width:6px;height:6px;margin-right:.375rem;vertical-align:1px;border:1px solid var(--faint)}
|
|
17
|
+
[data-stream=open] [data-graph-stream]::before{border-color:var(--acc);background:var(--acc);animation:pulse 2.4s ease-in-out infinite}
|
|
18
|
+
.graph-canvas{position:relative;margin-top:.75rem;overflow:hidden;border:1px solid var(--line-soft);background:var(--bg)}
|
|
19
|
+
.graph-svg{display:block;width:100%;height:420px;font-family:var(--mono);user-select:none;-webkit-user-select:none}
|
|
20
|
+
.graph-svg .hull{fill:var(--surface);stroke:var(--surface);stroke-linejoin:round;stroke-linecap:round}
|
|
21
|
+
.graph-svg .hull-rim{fill:none;stroke:var(--line-soft);stroke-linejoin:round;stroke-linecap:round}
|
|
22
|
+
.graph-svg .region.ided .hull-rim{stroke:color-mix(in srgb,var(--id) 30%,var(--line-soft))}
|
|
23
|
+
.graph-svg .region.offline,.graph-svg .region-head.offline{opacity:.55}
|
|
24
|
+
.graph-svg .mlabel{font-size:11px;font-weight:500;fill:var(--fg);paint-order:stroke;stroke:var(--surface);stroke-width:5px;stroke-linejoin:round}
|
|
25
|
+
.graph-svg .mlabel .n{fill:var(--dim);font-weight:400}
|
|
26
|
+
.graph-svg .mid{font-size:10px;fill:color-mix(in srgb,var(--id) 40%,var(--dim));paint-order:stroke;stroke:var(--surface);stroke-width:5px;stroke-linejoin:round}
|
|
27
|
+
.graph-svg .mdot{fill:var(--bg);stroke:var(--faint)}
|
|
28
|
+
.graph-svg .mdot.online{fill:var(--acc);stroke:var(--acc)}
|
|
29
|
+
.graph-svg .empty-note{font-size:10.5px;fill:var(--faint)}
|
|
30
|
+
.graph-svg .hub circle{fill:var(--bg);stroke:var(--line)}
|
|
31
|
+
.graph-svg .hub svg{color:var(--dim)}
|
|
32
|
+
.graph-svg .hub text{font-size:10.5px;fill:var(--dim);text-anchor:middle;paint-order:stroke;stroke:var(--bg);stroke-width:5px;stroke-linejoin:round}
|
|
33
|
+
.graph-svg .edge{fill:none;stroke-linecap:round;transition:opacity calc(.25s / var(--trail,1))}
|
|
34
|
+
.graph-svg .edge.spawn{stroke:var(--faint);stroke-width:1.25}
|
|
35
|
+
.graph-svg .edge.msg{stroke:var(--acc)}
|
|
36
|
+
.graph-svg .edge.msg.live{opacity:1!important}
|
|
37
|
+
.graph-svg .arrow{fill:var(--acc)}
|
|
38
|
+
.graph-svg .pulse{fill:var(--acc)}
|
|
39
|
+
.graph-svg .pulse-halo{fill:var(--acc);opacity:.25}
|
|
40
|
+
.graph-svg .pulse.reply,.graph-svg .pulse-halo.reply{fill:var(--fg)}
|
|
41
|
+
.graph-svg .node{cursor:pointer;outline:none;transition:opacity .3s}
|
|
42
|
+
.graph-svg .node .disc{fill:var(--bg);stroke:var(--line);stroke-width:1;transition:stroke .2s}
|
|
43
|
+
.graph-svg .node.online .disc{stroke:color-mix(in srgb,var(--fg) 34%,transparent)}
|
|
44
|
+
.graph-svg .node.human .disc{stroke-dasharray:2.5 2}
|
|
45
|
+
.graph-svg .node.offline{opacity:.5}
|
|
46
|
+
.graph-svg .node.historic{opacity:.3}
|
|
47
|
+
.graph-svg .node.offline .disc{stroke-dasharray:2 2}
|
|
48
|
+
.graph-svg .node.enter{opacity:0}
|
|
49
|
+
.graph-svg .node:hover .disc,.graph-svg .node:focus-visible .disc,.graph-svg .node.on .disc{stroke:var(--fg);stroke-width:1.5}
|
|
50
|
+
.graph-svg .ring{fill:none;stroke:var(--acc);stroke-width:2;opacity:0;transform-box:fill-box;transform-origin:center}
|
|
51
|
+
.graph-svg .node.hit .ring{animation:graph-hit .7s ease-out}
|
|
52
|
+
@keyframes graph-hit{0%{opacity:.9;transform:scale(1)}100%{opacity:0;transform:scale(1.55)}}
|
|
53
|
+
.graph-svg .glyph{color:var(--ink);overflow:visible}
|
|
54
|
+
.graph-svg .pdot{stroke:var(--bg);stroke-width:2;fill:var(--faint)}
|
|
55
|
+
.graph-svg .pdot.online{fill:var(--acc)}
|
|
56
|
+
.graph-svg .pdot.idle{fill:var(--dim)}
|
|
57
|
+
.graph-svg .bolt-g{color:var(--acc)}
|
|
58
|
+
.graph-svg .bolt-g circle{fill:var(--bg)}
|
|
59
|
+
.graph-svg .nlabel{font-size:11px;fill:var(--fg);text-anchor:middle;paint-order:stroke;stroke:var(--surface);stroke-width:5px;stroke-linejoin:round}
|
|
60
|
+
.graph-svg .node.offline .nlabel{fill:var(--dim)}
|
|
61
|
+
.graph-svg.focus .edge:not(.hl),.graph-svg.focus .node:not(.hl){opacity:.12!important}
|
|
62
|
+
.graph-svg.focus .edge.hl.spawn{stroke:var(--ink)}
|
|
63
|
+
.graph-tip{position:absolute;z-index:3;width:max-content;max-width:20rem;padding:.625rem .75rem;border:1px solid var(--line);background:var(--bg);font-size:.75rem;line-height:1.45;pointer-events:none;box-shadow:0 6px 24px rgb(0 0 0/.1)}
|
|
64
|
+
.graph-tip .t-head{display:flex;align-items:center;gap:.4rem;font-family:var(--mono);font-size:.8125rem;font-weight:500;color:var(--fg)}
|
|
65
|
+
.graph-tip .t-head svg{width:13px;height:13px;flex:none;overflow:visible;color:var(--ink)}
|
|
66
|
+
.graph-tip .t-addr{margin-top:.125rem;font-family:var(--mono);font-size:.6875rem;color:var(--dim);overflow-wrap:anywhere}
|
|
67
|
+
.graph-tip .t-row{margin-top:.375rem;color:var(--ink)}
|
|
68
|
+
.graph-tip .t-kv{display:grid;grid-template-columns:auto 1fr;gap:.125rem .75rem;margin-top:.5rem;font-family:var(--mono);font-size:.6875rem}
|
|
69
|
+
.graph-tip .t-kv dt{color:var(--dim)}
|
|
70
|
+
.graph-tip .t-kv dd{color:var(--fg);overflow-wrap:anywhere}
|
|
71
|
+
body.bare{overflow:hidden;height:100dvh;min-height:0}
|
|
72
|
+
.gfull{position:fixed;inset:0;margin:0}
|
|
73
|
+
.gfull.gtab{position:relative;inset:auto;height:max(26rem,calc(100dvh - 19rem));margin-top:1.5rem;border:1px solid var(--line-soft)}
|
|
74
|
+
.gtab .g-left{top:.75rem;left:.75rem}
|
|
75
|
+
.gtab .g-right{top:.75rem;right:.75rem}
|
|
76
|
+
.g-acts{display:inline-flex;align-items:center;gap:1rem}
|
|
77
|
+
@media (max-width:40rem){.gtab .g-right{top:auto;bottom:.75rem;left:.75rem;justify-items:start}.gtab{height:max(26rem,calc(100dvh - 16rem))}}
|
|
78
|
+
.gfull .graph-canvas{position:absolute;inset:0;margin:0;border:0}
|
|
79
|
+
.gfull .graph-svg{height:100%}
|
|
80
|
+
.g-over{position:absolute;z-index:2;display:grid;gap:.25rem;padding:.875rem 1rem;border:1px solid var(--line-soft);background:var(--head);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}
|
|
81
|
+
.g-left{top:1rem;left:1rem;max-width:min(24rem,calc(100vw - 2rem))}
|
|
82
|
+
.g-left h1{font-size:1.25rem;line-height:1.25;letter-spacing:-.02em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
83
|
+
.g-right{top:1rem;right:1rem;justify-items:end}
|
|
84
|
+
.g-empty{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}
|
|
85
|
+
@media (max-width:40rem){.g-right{top:auto;bottom:1rem}}
|