@preventive/triage 1.0.0-alpha.15 → 1.0.0-alpha.16
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/common/scan-server.ts +13 -0
- package/common/server-info.ts +1 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/out/client-managed.js +83 -1
- package/out/client-sync.js +14 -14
- package/out/graph.js +11 -11
- package/out/index.html +3 -2
- package/out/provider-icons/claude.svg +1 -0
- package/out/provider-icons/deepseek.svg +1 -0
- package/out/provider-icons/google.svg +1 -0
- package/out/provider-icons/grok.svg +1 -0
- package/out/provider-icons/moonshot.svg +1 -0
- package/out/provider-icons/nvidia.svg +1 -0
- package/out/provider-icons/openai.svg +1 -0
- package/out/provider-icons/openrouter.svg +1 -0
- package/out/provider-icons/qwen.svg +1 -0
- package/out/provider-icons/zai.svg +1 -0
- package/out/terminal.js +202 -191
- package/out/view.css +1 -1
- package/out/view.js +138 -135
- package/package.json +6 -3
- package/report/index.js +1 -1
- package/report/src/md-structure.js +1 -1
- package/report/src/meta.js +25 -0
- package/report/src/parse-md.js +71 -2
- package/server-common/scan-config.ts +19 -0
- package/server-e2e/config.ts +6 -0
- package/server-e2e/http.ts +1 -1
- package/server-e2e/index.ts +1 -2
- package/server-e2e/static.ts +19 -11
- package/out/client-admin.js +0 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/triage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.16",
|
|
4
4
|
"description": "Client & relay server for triaging of automated reports",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"server-e2e/ws-server.ts",
|
|
82
82
|
"server-e2e/config.example.json",
|
|
83
83
|
"server-common/origin.ts",
|
|
84
|
+
"server-common/scan-config.ts",
|
|
84
85
|
"report/index.js",
|
|
85
86
|
"report/src/finding-id.js",
|
|
86
87
|
"report/src/finding.js",
|
|
@@ -103,10 +104,10 @@
|
|
|
103
104
|
"report/src/write-md.js",
|
|
104
105
|
"common/save-error-reason.ts",
|
|
105
106
|
"common/server-info.ts",
|
|
107
|
+
"common/scan-server.ts",
|
|
106
108
|
"common/utf8.d.ts",
|
|
107
109
|
"common/utf8.js",
|
|
108
110
|
"out/brotli-fallback.js",
|
|
109
|
-
"out/client-admin.js",
|
|
110
111
|
"out/client-managed.js",
|
|
111
112
|
"out/client-sync.js",
|
|
112
113
|
"out/graph.js",
|
|
@@ -115,6 +116,7 @@
|
|
|
115
116
|
"out/index.html",
|
|
116
117
|
"out/manifest.webmanifest",
|
|
117
118
|
"out/prism.js",
|
|
119
|
+
"out/provider-icons",
|
|
118
120
|
"out/stasis.svg",
|
|
119
121
|
"out/terminal.js",
|
|
120
122
|
"out/view.css",
|
|
@@ -132,6 +134,7 @@
|
|
|
132
134
|
"serve": "node build.js serve",
|
|
133
135
|
"server": "node server-e2e/index.ts",
|
|
134
136
|
"server-managed": "node server-managed/index.ts",
|
|
137
|
+
"server-managed-test": "node server-managed/test-server.ts",
|
|
135
138
|
"test": "node --js-base-64 --experimental-test-module-mocks --test-timeout=120000 --test ./tests/*.test.js ./report/tests/*.test.js"
|
|
136
139
|
},
|
|
137
140
|
"devDependencies": {
|
|
@@ -139,7 +142,7 @@
|
|
|
139
142
|
"@exodus/stasis-core": "^1.0.0-beta.4",
|
|
140
143
|
"@noble/ciphers": "^2.3.0",
|
|
141
144
|
"@preventive/diff": "^1.0.0",
|
|
142
|
-
"@preventive/terminal": "
|
|
145
|
+
"@preventive/terminal": "2.0.0-beta.1",
|
|
143
146
|
"@rray/frontend": "^1.0.0",
|
|
144
147
|
"@stylistic/stylelint-plugin": "^5.3.0",
|
|
145
148
|
"@types/node": "^26.2.0",
|
package/report/index.js
CHANGED
|
@@ -88,7 +88,7 @@ import { deriveFindingId } from './src/finding-id.js'
|
|
|
88
88
|
// run-meta projection a caller applies to the findings it loads.
|
|
89
89
|
export { parseCodexCsvToScans } from './src/parse-codex.js'
|
|
90
90
|
export { computeFileHash, deriveFindingId, findingId } from './src/finding-id.js'
|
|
91
|
-
export { META_FIELDS, inheritReportMeta, reportRepoGithub } from './src/meta.js'
|
|
91
|
+
export { META_FIELDS, inheritReportMeta, repoDirectory, reportRepoGithub } from './src/meta.js'
|
|
92
92
|
// The writing side: the document writer, and the label tables it
|
|
93
93
|
// spells the app's enumerations with, for the viewer's surfaces that
|
|
94
94
|
// describe the same things in prose.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
const FENCE_RE = /^( *)(```|~~~)/u
|
|
22
22
|
// A list marker and the gap to its text; `m[0].length` is the column
|
|
23
23
|
// the item's continuation lines are indented to.
|
|
24
|
-
const LIST_MARKER_RE = /^( *)(?:[-*+]|\d{1,9}[.)]) +(?=\S)/u
|
|
24
|
+
export const LIST_MARKER_RE = /^( *)(?:[-*+]|\d{1,9}[.)]) +(?=\S)/u
|
|
25
25
|
|
|
26
26
|
export function fenceRanges(text) {
|
|
27
27
|
const ranges = []
|
package/report/src/meta.js
CHANGED
|
@@ -49,3 +49,28 @@ export function reportRepoGithub(data) {
|
|
|
49
49
|
const slug = (url ? `${url[1]}/${url[2]}` : trimmed).replace(/\.git$/u, '')
|
|
50
50
|
return isRepoSlug(slug) ? slug : null
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
// `"directory": "packages/babel-core"` beside that `github` — where
|
|
54
|
+
// inside the repository the tree the report describes sits, the same
|
|
55
|
+
// field npm's `repository` object carries for a package in a monorepo
|
|
56
|
+
// (`{ "github": "babel/babel", "directory": "packages/babel-core" }`).
|
|
57
|
+
// The paths a report writes are relative to that tree, so a link is
|
|
58
|
+
// the repo, then this, then the path: `babel/babel` +
|
|
59
|
+
// `packages/babel-core` + `src/index.js`. Declared in both the places
|
|
60
|
+
// `repo` is — the report header and a finding's own — and read off
|
|
61
|
+
// whichever of the two answered for the repo, never mixed.
|
|
62
|
+
//
|
|
63
|
+
// Normalised to a bare relative path: no leading `./` or `/`, no
|
|
64
|
+
// trailing one, no repeated separators, and `''` — same as declaring
|
|
65
|
+
// none — for the repo root. `''` too for anything a link builder
|
|
66
|
+
// would splice into a broken URL, the rule `reportRepoGithub` follows
|
|
67
|
+
// above: a non-string, a `?` or `#` (either cuts the path short of the
|
|
68
|
+
// file, and swallows the `#L42` anchor a line link puts after it), or
|
|
69
|
+
// a `..` segment climbing out of the repository.
|
|
70
|
+
export function repoDirectory(repo) {
|
|
71
|
+
const raw = typeof repo?.directory === 'string' ? repo.directory.trim() : ''
|
|
72
|
+
const path = raw.replace(/^(?:\.\/)+/u, '').replaceAll(/^\/+|\/+$/gu, '').replaceAll(/\/{2,}/gu, '/')
|
|
73
|
+
if (!path || path === '.') return ''
|
|
74
|
+
if (/[?#]/u.test(path) || path.split('/').includes('..')) return ''
|
|
75
|
+
return path
|
|
76
|
+
}
|
package/report/src/parse-md.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// carry anything mandatory.
|
|
31
31
|
|
|
32
32
|
import { frozenIdBasis } from './parse-md-id.js'
|
|
33
|
-
import { findMdLink, normalizeNewlines, splitHeadingLine, unescapeMd } from './md-structure.js'
|
|
33
|
+
import { LIST_MARKER_RE, findMdLink, normalizeNewlines, splitHeadingLine, unescapeMd } from './md-structure.js'
|
|
34
34
|
|
|
35
35
|
const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'high_bug', 'bug', 'informational'])
|
|
36
36
|
|
|
@@ -93,7 +93,9 @@ function parseBlock(block) {
|
|
|
93
93
|
// the description is an always-open block. They survive a round trip
|
|
94
94
|
// through this finding's own export, which writes them as sections
|
|
95
95
|
// that parse-deepview-md.js narrativeSplit reads back as fields.
|
|
96
|
-
if (sections['reproduction steps'])
|
|
96
|
+
if (sections['reproduction steps']) {
|
|
97
|
+
finding.reproduction = normalizeStepList(sections['reproduction steps'])
|
|
98
|
+
}
|
|
97
99
|
if (sections['recommended fix']) finding.recommendation = sections['recommended fix']
|
|
98
100
|
if (meta.repository) finding.repo = { github: meta.repository }
|
|
99
101
|
// Auxiliary metadata, kept as plain strings: nothing renders these
|
|
@@ -193,6 +195,73 @@ function parseLocation(loc) {
|
|
|
193
195
|
return { file, line, locationLink, linked: link !== null }
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// The `## Reproduction steps` section as a reader can follow it. This
|
|
199
|
+
// report sometimes writes a whole sequence as ONE list item, in two
|
|
200
|
+
// shapes, and neither reads as a list:
|
|
201
|
+
//
|
|
202
|
+
// * a RUN-IN enumeration — `1. 1) Save 2) Restart 3) Watch`, which
|
|
203
|
+
// markdown reads as one step whose text holds all the others —
|
|
204
|
+
// becomes a line per step;
|
|
205
|
+
// * a list of ONE step stops being a list, its marker numbering the
|
|
206
|
+
// single thing the section says.
|
|
207
|
+
//
|
|
208
|
+
// The steps keep the numbers the report gave them, gaps and all: this
|
|
209
|
+
// text is printed as written, so a `6)` behind a `4)` is the report's
|
|
210
|
+
// own count rather than something to renumber.
|
|
211
|
+
//
|
|
212
|
+
// Only a section that IS one item is touched — no other line may open a
|
|
213
|
+
// list of its own — and the run-in reading is tried behind the outer
|
|
214
|
+
// marker (`1. 1) …`) and at the line's own start (`1) … 2) …`), since
|
|
215
|
+
// either can carry the enumeration. The id comes from the RAW block
|
|
216
|
+
// (parse-md-id.js), so reading the section better moves nothing.
|
|
217
|
+
function normalizeStepList(text) {
|
|
218
|
+
const lines = text.split('\n')
|
|
219
|
+
const at = lines.findIndex((line) => line.trim())
|
|
220
|
+
if (at === -1 || !LIST_MARKER_RE.test(lines[at])) return text
|
|
221
|
+
if (lines.some((line, i) => i !== at && LIST_MARKER_RE.test(line))) return text
|
|
222
|
+
const item = lines[at].replace(/^ */u, '')
|
|
223
|
+
const body = item.replace(LIST_MARKER_RE, '')
|
|
224
|
+
if (!body.trim()) return text
|
|
225
|
+
const steps = runInSteps(body) ?? runInSteps(item)
|
|
226
|
+
// An enumeration neither reading could take apart stays as it
|
|
227
|
+
// arrived. Both halves matter: a BODY opening on a marker is the
|
|
228
|
+
// sequence behind an outer one (`1. 3) Later 2) Earlier`), and
|
|
229
|
+
// unwrapping would leave that marker leading the section; an ITEM
|
|
230
|
+
// opening on one is the sequence itself (`3) Later 2) Earlier`),
|
|
231
|
+
// where the marker shed as the item's own is a step number, and
|
|
232
|
+
// unwrapping would drop it and leave the rest of the count behind.
|
|
233
|
+
if (steps === null && (LIST_MARKER_RE.test(body) || RUN_IN_HEAD_RE.test(item))) return text
|
|
234
|
+
const read = steps === null ? [body]
|
|
235
|
+
: steps.length === 1 ? [steps[0].step]
|
|
236
|
+
: steps.map(({ number, step }) => `${number}. ${step}`)
|
|
237
|
+
lines.splice(at, 1, ...read)
|
|
238
|
+
return lines.join('\n')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// `1) Save 2) Restart` → a step per marker, or null when the text is no
|
|
242
|
+
// run-in list: the first marker has to open it and the numbers have to
|
|
243
|
+
// ascend, or a step that merely cites `RFC 2616) …` would split the
|
|
244
|
+
// prose around it. A number in parens — `curl(1)`, `(2) results` — is
|
|
245
|
+
// not a marker, and a marker with NOTHING behind it — a truncated
|
|
246
|
+
// `1) Save 2)` — is a sequence this can't read, not a step of its own.
|
|
247
|
+
const RUN_IN_STEP_RE = /(?:^|[ \t])(\d{1,9})\)(?=[ \t]|$)/gu
|
|
248
|
+
// The same marker, asked of a text's own start.
|
|
249
|
+
const RUN_IN_HEAD_RE = /^\d{1,9}\)(?=[ \t]|$)/u
|
|
250
|
+
|
|
251
|
+
function runInSteps(text) {
|
|
252
|
+
const marks = [...text.matchAll(RUN_IN_STEP_RE)]
|
|
253
|
+
if (marks.length === 0 || marks[0].index !== 0) return null
|
|
254
|
+
const steps = []
|
|
255
|
+
for (const [i, mark] of marks.entries()) {
|
|
256
|
+
const number = Number(mark[1])
|
|
257
|
+
if (i > 0 && number <= steps[i - 1].number) return null
|
|
258
|
+
const step = text.slice(mark.index + mark[0].length, marks[i + 1]?.index).trim()
|
|
259
|
+
if (!step) return null
|
|
260
|
+
steps.push({ number, step })
|
|
261
|
+
}
|
|
262
|
+
return steps
|
|
263
|
+
}
|
|
264
|
+
|
|
196
265
|
// Rows of an `## Evidence` section, in document order:
|
|
197
266
|
//
|
|
198
267
|
// 1. [libs/a.ts:10–20](https://github.com/o/r/blob/<sha>/libs/a.ts#L10-L20)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { normalizeScanServer } from '../common/scan-server.ts'
|
|
2
|
+
|
|
3
|
+
export function configuredScanServer(value: string | undefined): string | null {
|
|
4
|
+
if (!value?.trim()) return null
|
|
5
|
+
const server = normalizeScanServer(value)
|
|
6
|
+
if (!server) throw new Error('DEEPVIEW_SCAN_SERVER must be an absolute HTTP(S) URL without credentials, query, or fragment')
|
|
7
|
+
return server
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Apply at serve time, before compression and ETag generation. Packaged HTML
|
|
11
|
+
// stays strict and does not bake in the build machine's configuration.
|
|
12
|
+
export function scanServerHtml(html: string, server: string | null, { advertise = false } = {}): string {
|
|
13
|
+
if (!server) return html
|
|
14
|
+
const origin = new URL(server).origin
|
|
15
|
+
const result = html.replace(/(connect-src 'self')(?=[;"])/u, `$1 ${origin}`)
|
|
16
|
+
if (!advertise) return result
|
|
17
|
+
const content = server.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<')
|
|
18
|
+
return result.replace('</head>', `<meta name="deepview-scan-server" content="${content}">\n</head>`)
|
|
19
|
+
}
|
package/server-e2e/config.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { readFileSync } from 'node:fs'
|
|
|
11
11
|
import { argv, env } from 'node:process'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
import { dirname, join } from 'node:path'
|
|
14
|
+
import { configuredScanServer } from '../server-common/scan-config.ts'
|
|
14
15
|
|
|
15
16
|
export type Config = {
|
|
16
17
|
port: number
|
|
@@ -26,6 +27,7 @@ export type Config = {
|
|
|
26
27
|
tokenSecret: Uint8Array<ArrayBuffer> | null
|
|
27
28
|
password: string | null
|
|
28
29
|
trustProxyEnv: string | undefined
|
|
30
|
+
deepviewScanServer: string | null
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// Parse + range-validate an integer env var, exiting with a clear
|
|
@@ -49,6 +51,9 @@ const HELP = `Usage: node server-e2e/index.ts
|
|
|
49
51
|
Environment:
|
|
50
52
|
PORT listen port (default 8765)
|
|
51
53
|
HOST bind host (default 127.0.0.1)
|
|
54
|
+
DEEPVIEW_SCAN_SERVER optional HTTP(S) scan-service URL; advertised
|
|
55
|
+
in /api/config and allowed by the UI CSP.
|
|
56
|
+
Unset by default (no external scan service).
|
|
52
57
|
DB_PATH sqlite file (default: server-e2e/data/data.db);
|
|
53
58
|
ignored when DATABASE_URL is set
|
|
54
59
|
DATABASE_URL Neon Postgres connection string; if set,
|
|
@@ -205,5 +210,6 @@ export function loadConfig(): Config {
|
|
|
205
210
|
port, host, dbPath, objstoreDir, reapIntervalMs, reapDisabled, maxInflightPerSocket,
|
|
206
211
|
debug, neonUrl, blobToken, tokenSecret, password,
|
|
207
212
|
trustProxyEnv: env['TRUST_PROXY'],
|
|
213
|
+
deepviewScanServer: configuredScanServer(env['DEEPVIEW_SCAN_SERVER']),
|
|
208
214
|
}
|
|
209
215
|
}
|
package/server-e2e/http.ts
CHANGED
|
@@ -119,7 +119,7 @@ export function createHttpServer(deps: HttpServerDeps): Server {
|
|
|
119
119
|
// `build.js build` output sibling to this file; the loader handles
|
|
120
120
|
// enumeration, pre-compression, and ETag derivation. Plugged in after
|
|
121
121
|
// the `/api/objstore/...` REST branch.
|
|
122
|
-
const handleStatic = loadStatic(fileURLToPath(new URL('../out', import.meta.url)))
|
|
122
|
+
const handleStatic = loadStatic(fileURLToPath(new URL('../out', import.meta.url)), serverInfo.deepviewScanServer ?? null)
|
|
123
123
|
|
|
124
124
|
const httpServer = createServer((req: HttpRequest, res: ServerResponse) => {
|
|
125
125
|
// Static mode probe: GET /api/config → this server's `server-info` as JSON.
|
package/server-e2e/index.ts
CHANGED
|
@@ -346,7 +346,7 @@ const { track, isShuttingDown, install: installLifecycle } = createLifecycle()
|
|
|
346
346
|
// The sync protocol this build advertises — emitted as a `server-info` frame
|
|
347
347
|
// right after the challenge on every connection. This is the e2e boot, so it
|
|
348
348
|
// always advertises e2e.
|
|
349
|
-
const SERVER_INFO: ServerInfo = { mode: 'e2e', managed: null }
|
|
349
|
+
const SERVER_INFO: ServerInfo = { mode: 'e2e', managed: null, ...(config.deepviewScanServer ? { deepviewScanServer: config.deepviewScanServer } : {}) }
|
|
350
350
|
|
|
351
351
|
// Shared per-connection dispatch surface. Both the WS plane
|
|
352
352
|
// (installWsServer below) and the SSE+POST fallback (installSseServer
|
|
@@ -484,4 +484,3 @@ export { httpServer, wss }
|
|
|
484
484
|
if (import.meta.main) {
|
|
485
485
|
start()
|
|
486
486
|
}
|
|
487
|
-
|
package/server-e2e/static.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// `join(staticDir, dirent.name)` from `readdirSync` at boot — the
|
|
7
7
|
// request URL is only used as a Map key, never joined with the
|
|
8
8
|
// filesystem — so the handler has no path-traversal surface at all:
|
|
9
|
-
// `..`, percent-encoded slashes
|
|
10
|
-
//
|
|
9
|
+
// `..`, percent-encoded slashes and absolute-form URIs all just
|
|
10
|
+
// produce a key that isn't in the map and 404. Provider SVGs are
|
|
11
|
+
// the one explicitly allowed asset subdirectory.
|
|
11
12
|
//
|
|
12
13
|
// Compression: every entry whose extension is in COMPRESSIBLE gets
|
|
13
14
|
// pre-computed brotli + gzip at boot. The handler picks brotli over
|
|
@@ -38,6 +39,7 @@ import { readFileSync, readdirSync } from 'node:fs'
|
|
|
38
39
|
import { createHash } from 'node:crypto'
|
|
39
40
|
import { brotliCompressSync, gzipSync, constants as zlibConstants } from 'node:zlib'
|
|
40
41
|
import { extname, join } from 'node:path'
|
|
42
|
+
import { scanServerHtml } from '../server-common/scan-config.ts'
|
|
41
43
|
|
|
42
44
|
// MIME types for extensions the production UI bundle currently
|
|
43
45
|
// emits. Anything else falls through to `application/octet-stream`
|
|
@@ -102,8 +104,8 @@ export type StaticHandler = (req: HttpRequest, res: ServerResponse) => boolean
|
|
|
102
104
|
// caller falls through to its next route. Missing `staticDir`
|
|
103
105
|
// (pre-build case) logs a warning and returns a handler that always
|
|
104
106
|
// answers false; the API/WS planes are unaffected.
|
|
105
|
-
export function loadStatic(staticDir: string): StaticHandler {
|
|
106
|
-
const files = readStaticFiles(staticDir)
|
|
107
|
+
export function loadStatic(staticDir: string, deepviewScanServer: string | null = null): StaticHandler {
|
|
108
|
+
const files = readStaticFiles(staticDir, deepviewScanServer)
|
|
107
109
|
return function handleStatic(req: HttpRequest, res: ServerResponse): boolean {
|
|
108
110
|
if (req.method !== 'GET' && req.method !== 'HEAD') return false
|
|
109
111
|
if (typeof req.url !== 'string') return false
|
|
@@ -151,7 +153,7 @@ export function loadStatic(staticDir: string): StaticHandler {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
function readStaticFiles(staticDir: string): ReadonlyMap<string, StaticEntry> {
|
|
156
|
+
function readStaticFiles(staticDir: string, deepviewScanServer: string | null): ReadonlyMap<string, StaticEntry> {
|
|
155
157
|
const files = new Map<string, StaticEntry>()
|
|
156
158
|
let entries
|
|
157
159
|
try { entries = readdirSync(staticDir, { withFileTypes: true }) } catch (err) {
|
|
@@ -165,18 +167,24 @@ function readStaticFiles(staticDir: string): ReadonlyMap<string, StaticEntry> {
|
|
|
165
167
|
throw err
|
|
166
168
|
}
|
|
167
169
|
for (const entry of entries) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
if (entry.isDirectory() && entry.name === 'provider-icons') {
|
|
171
|
+
for (const icon of readdirSync(join(staticDir, entry.name), { withFileTypes: true })) {
|
|
172
|
+
if (!icon.isFile() || extname(icon.name) !== '.svg') continue
|
|
173
|
+
const name = `${entry.name}/${icon.name}`
|
|
174
|
+
files.set(name, buildEntry(staticDir, name))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Exclude other subdirectories, symlinks and non-file entries.
|
|
171
178
|
if (!entry.isFile()) continue
|
|
172
|
-
files.set(entry.name, buildEntry(staticDir, entry.name))
|
|
179
|
+
files.set(entry.name, buildEntry(staticDir, entry.name, deepviewScanServer))
|
|
173
180
|
}
|
|
174
181
|
return files
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
function buildEntry(staticDir: string, name: string): StaticEntry {
|
|
184
|
+
function buildEntry(staticDir: string, name: string, deepviewScanServer: string | null = null): StaticEntry {
|
|
178
185
|
const ext = extname(name)
|
|
179
|
-
const
|
|
186
|
+
const source = readFileSync(join(staticDir, name))
|
|
187
|
+
const raw = name === 'index.html' ? Buffer.from(scanServerHtml(source.toString('utf8'), deepviewScanServer)) : source
|
|
180
188
|
const type = CONTENT_TYPE[ext] ?? 'application/octet-stream'
|
|
181
189
|
// HTML: lift `<link rel="(module)preload" …>` into a Link header and
|
|
182
190
|
// drop the tags from the served body. ETag + compression run against
|
package/out/client-admin.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
var z=globalThis,L=z.ShadowRoot&&(z.ShadyCSS===void 0||z.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,D=Symbol(),ie=new WeakMap,E=class{constructor(e,t,r){if(this._$cssResult$=!0,r!==D)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(L&&e===void 0){let r=t!==void 0&&t.length===1;r&&(e=ie.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&ie.set(t,e))}return e}toString(){return this.cssText}},oe=i=>new E(typeof i=="string"?i:i+"",void 0,D),y=(i,...e)=>{let t=i.length===1?i[0]:e.reduce((r,s,o)=>r+(a=>{if(a._$cssResult$===!0)return a.cssText;if(typeof a=="number")return a;throw Error("Value passed to 'css' function must be a 'css' function result: "+a+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+i[o+1],i[0]);return new E(t,i,D)},ne=(i,e)=>{if(L)i.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let r=document.createElement("style"),s=z.litNonce;s!==void 0&&r.setAttribute("nonce",s),r.textContent=t.cssText,i.appendChild(r)}},j=L?i=>i:i=>i instanceof CSSStyleSheet?(e=>{let t="";for(let r of e.cssRules)t+=r.cssText;return oe(t)})(i):i;var{is:Re,defineProperty:Te,getOwnPropertyDescriptor:Ie,getOwnPropertyNames:Ue,getOwnPropertySymbols:Oe,getPrototypeOf:Ne}=Object,H=globalThis,ae=H.trustedTypes,ze=ae?ae.emptyScript:"",Le=H.reactiveElementPolyfillSupport,C=(i,e)=>i,B={toAttribute(i,e){switch(e){case Boolean:i=i?ze:null;break;case Object:case Array:i=i==null?i:JSON.stringify(i)}return i},fromAttribute(i,e){let t=i;switch(e){case Boolean:t=i!==null;break;case Number:t=i===null?null:Number(i);break;case Object:case Array:try{t=JSON.parse(i)}catch{t=null}}return t}},de=(i,e)=>!Re(i,e),le={attribute:!0,type:String,converter:B,reflect:!1,useDefault:!1,hasChanged:de};Symbol.metadata??=Symbol("metadata"),H.litPropertyMetadata??=new WeakMap;var g=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=le){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let r=Symbol(),s=this.getPropertyDescriptor(e,r,t);s!==void 0&&Te(this.prototype,e,s)}}static getPropertyDescriptor(e,t,r){let{get:s,set:o}=Ie(this.prototype,e)??{get(){return this[t]},set(a){this[t]=a}};return{get:s,set(a){let h=s?.call(this);o?.call(this,a),this.requestUpdate(e,h,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??le}static _$Ei(){if(this.hasOwnProperty(C("elementProperties")))return;let e=Ne(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(C("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(C("properties"))){let t=this.properties,r=[...Ue(t),...Oe(t)];for(let s of r)this.createProperty(s,t[s])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[r,s]of t)this.elementProperties.set(r,s)}this._$Eh=new Map;for(let[t,r]of this.elementProperties){let s=this._$Eu(t,r);s!==void 0&&this._$Eh.set(s,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let r=new Set(e.flat(1/0).reverse());for(let s of r)t.unshift(j(s))}else e!==void 0&&t.push(j(e));return t}static _$Eu(e,t){let r=t.attribute;return r===!1?void 0:typeof r=="string"?r:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return ne(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$ET(e,t){let r=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,r);if(s!==void 0&&r.reflect===!0){let o=(r.converter?.toAttribute!==void 0?r.converter:B).toAttribute(t,r.type);this._$Em=e,o==null?this.removeAttribute(s):this.setAttribute(s,o),this._$Em=null}}_$AK(e,t){let r=this.constructor,s=r._$Eh.get(e);if(s!==void 0&&this._$Em!==s){let o=r.getPropertyOptions(s),a=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:B;this._$Em=s;let h=a.fromAttribute(t,o.type);this[s]=h??this._$Ej?.get(s)??h,this._$Em=null}}requestUpdate(e,t,r,s=!1,o){if(e!==void 0){let a=this.constructor;if(s===!1&&(o=this[e]),r??=a.getPropertyOptions(e),!((r.hasChanged??de)(o,t)||r.useDefault&&r.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(a._$Eu(e,r))))return;this.C(e,t,r)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:r,reflect:s,wrapped:o},a){r&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,a??t??this[e]),o!==!0||a!==void 0)||(this._$AL.has(e)||(this.hasUpdated||r||(t=void 0),this._$AL.set(e,t)),s===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[s,o]of this._$Ep)this[s]=o;this._$Ep=void 0}let r=this.constructor.elementProperties;if(r.size>0)for(let[s,o]of r){let{wrapped:a}=o,h=this[s];a!==!0||this._$AL.has(s)||h===void 0||this.C(s,void 0,o,h)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(r=>r.hostUpdate?.()),this.update(t)):this._$EM()}catch(r){throw e=!1,this._$EM(),r}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};g.elementStyles=[],g.shadowRootOptions={mode:"open"},g[C("elementProperties")]=new Map,g[C("finalized")]=new Map,Le?.({ReactiveElement:g}),(H.reactiveElementVersions??=[]).push("2.1.2");var K=globalThis,ce=i=>i,M=K.trustedTypes,pe=M?M.createPolicy("lit-html",{createHTML:i=>i}):void 0,_e="$lit$",b=`lit$${Math.random().toFixed(9).slice(2)}$`,be="?"+b,He=`<${be}>`,x=document,R=()=>x.createComment(""),T=i=>i===null||typeof i!="object"&&typeof i!="function",G=Array.isArray,Me=i=>G(i)||typeof i?.[Symbol.iterator]=="function",V=`[
|
|
2
|
-
\f\r]`,P=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,he=/-->/g,ue=/>/g,$=RegExp(`>|${V}(?:([^\\s"'>=/]+)(${V}*=${V}*(?:[^
|
|
3
|
-
\f\r"'\`<>=]|("|')|))|$)`,"g"),me=/'/g,fe=/"/g,ve=/^(?:script|style|textarea|title)$/i,Y=i=>(e,...t)=>({_$litType$:i,strings:e,values:t}),n=Y(1),dt=Y(2),ct=Y(3),A=Symbol.for("lit-noChange"),c=Symbol.for("lit-nothing"),ge=new WeakMap,w=x.createTreeWalker(x,129);function ye(i,e){if(!G(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return pe!==void 0?pe.createHTML(e):e}var De=(i,e)=>{let t=i.length-1,r=[],s,o=e===2?"<svg>":e===3?"<math>":"",a=P;for(let h=0;h<t;h++){let l=i[h],d,u,p=-1,f=0;for(;f<l.length&&(a.lastIndex=f,u=a.exec(l),u!==null);)f=a.lastIndex,a===P?u[1]==="!--"?a=he:u[1]!==void 0?a=ue:u[2]!==void 0?(ve.test(u[2])&&(s=RegExp("</"+u[2],"g")),a=$):u[3]!==void 0&&(a=$):a===$?u[0]===">"?(a=s??P,p=-1):u[1]===void 0?p=-2:(p=a.lastIndex-u[2].length,d=u[1],a=u[3]===void 0?$:u[3]==='"'?fe:me):a===fe||a===me?a=$:a===he||a===ue?a=P:(a=$,s=void 0);let _=a===$&&i[h+1].startsWith("/>")?" ":"";o+=a===P?l+He:p>=0?(r.push(d),l.slice(0,p)+_e+l.slice(p)+b+_):l+b+(p===-2?h:_)}return[ye(i,o+(i[t]||"<?>")+(e===2?"</svg>":e===3?"</math>":"")),r]},I=class i{constructor({strings:e,_$litType$:t},r){let s;this.parts=[];let o=0,a=0,h=e.length-1,l=this.parts,[d,u]=De(e,t);if(this.el=i.createElement(d,r),w.currentNode=this.el.content,t===2||t===3){let p=this.el.content.firstChild;p.replaceWith(...p.childNodes)}for(;(s=w.nextNode())!==null&&l.length<h;){if(s.nodeType===1){if(s.hasAttributes())for(let p of s.getAttributeNames())if(p.endsWith(_e)){let f=u[a++],_=s.getAttribute(p).split(b),N=/([.?@])?(.*)/.exec(f);l.push({type:1,index:o,name:N[2],strings:_,ctor:N[1]==="."?F:N[1]==="?"?Z:N[1]==="@"?W:S}),s.removeAttribute(p)}else p.startsWith(b)&&(l.push({type:6,index:o}),s.removeAttribute(p));if(ve.test(s.tagName)){let p=s.textContent.split(b),f=p.length-1;if(f>0){s.textContent=M?M.emptyScript:"";for(let _=0;_<f;_++)s.append(p[_],R()),w.nextNode(),l.push({type:2,index:++o});s.append(p[f],R())}}}else if(s.nodeType===8)if(s.data===be)l.push({type:2,index:o});else{let p=-1;for(;(p=s.data.indexOf(b,p+1))!==-1;)l.push({type:7,index:o}),p+=b.length-1}o++}}static createElement(e,t){let r=x.createElement("template");return r.innerHTML=e,r}};function k(i,e,t=i,r){if(e===A)return e;let s=r!==void 0?t._$Co?.[r]:t._$Cl,o=T(e)?void 0:e._$litDirective$;return s?.constructor!==o&&(s?._$AO?.(!1),o===void 0?s=void 0:(s=new o(i),s._$AT(i,t,r)),r!==void 0?(t._$Co??=[])[r]=s:t._$Cl=s),s!==void 0&&(e=k(i,s._$AS(i,e.values),s,r)),e}var q=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:r}=this._$AD,s=(e?.creationScope??x).importNode(t,!0);w.currentNode=s;let o=w.nextNode(),a=0,h=0,l=r[0];for(;l!==void 0;){if(a===l.index){let d;l.type===2?d=new U(o,o.nextSibling,this,e):l.type===1?d=new l.ctor(o,l.name,l.strings,this,e):l.type===6&&(d=new J(o,this,e)),this._$AV.push(d),l=r[++h]}a!==l?.index&&(o=w.nextNode(),a++)}return w.currentNode=x,s}p(e){let t=0;for(let r of this._$AV)r!==void 0&&(r.strings!==void 0?(r._$AI(e,r,t),t+=r.strings.length-2):r._$AI(e[t])),t++}},U=class i{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,r,s){this.type=2,this._$AH=c,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=r,this.options=s,this._$Cv=s?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=k(this,e,t),T(e)?e===c||e==null||e===""?(this._$AH!==c&&this._$AR(),this._$AH=c):e!==this._$AH&&e!==A&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):Me(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==c&&T(this._$AH)?this._$AA.nextSibling.data=e:this.T(x.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:r}=e,s=typeof r=="number"?this._$AC(e):(r.el===void 0&&(r.el=I.createElement(ye(r.h,r.h[0]),this.options)),r);if(this._$AH?._$AD===s)this._$AH.p(t);else{let o=new q(s,this),a=o.u(this.options);o.p(t),this.T(a),this._$AH=o}}_$AC(e){let t=ge.get(e.strings);return t===void 0&&ge.set(e.strings,t=new I(e)),t}k(e){G(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,r,s=0;for(let o of e)s===t.length?t.push(r=new i(this.O(R()),this.O(R()),this,this.options)):r=t[s],r._$AI(o),s++;s<t.length&&(this._$AR(r&&r._$AB.nextSibling,s),t.length=s)}_$AR(e=this._$AA.nextSibling,t){for(this._$AP?.(!1,!0,t);e!==this._$AB;){let r=ce(e).nextSibling;ce(e).remove(),e=r}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e))}},S=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,r,s,o){this.type=1,this._$AH=c,this._$AN=void 0,this.element=e,this.name=t,this._$AM=s,this.options=o,r.length>2||r[0]!==""||r[1]!==""?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=c}_$AI(e,t=this,r,s){let o=this.strings,a=!1;if(o===void 0)e=k(this,e,t,0),a=!T(e)||e!==this._$AH&&e!==A,a&&(this._$AH=e);else{let h=e,l,d;for(e=o[0],l=0;l<o.length-1;l++)d=k(this,h[r+l],t,l),d===A&&(d=this._$AH[l]),a||=!T(d)||d!==this._$AH[l],d===c?e=c:e!==c&&(e+=(d??"")+o[l+1]),this._$AH[l]=d}a&&!s&&this.j(e)}j(e){e===c?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??"")}},F=class extends S{constructor(){super(...arguments),this.type=3}j(e){this.element[this.name]=e===c?void 0:e}},Z=class extends S{constructor(){super(...arguments),this.type=4}j(e){this.element.toggleAttribute(this.name,!!e&&e!==c)}},W=class extends S{constructor(e,t,r,s,o){super(e,t,r,s,o),this.type=5}_$AI(e,t=this){if((e=k(this,e,t,0)??c)===A)return;let r=this._$AH,s=e===c&&r!==c||e.capture!==r.capture||e.once!==r.once||e.passive!==r.passive,o=e!==c&&(r===c||s);s&&this.element.removeEventListener(this.name,this,r),o&&this.element.addEventListener(this.name,this,e),this._$AH=e}handleEvent(e){typeof this._$AH=="function"?this._$AH.call(this.options?.host??this.element,e):this._$AH.handleEvent(e)}},J=class{constructor(e,t,r){this.element=e,this.type=6,this._$AN=void 0,this._$AM=t,this.options=r}get _$AU(){return this._$AM._$AU}_$AI(e){k(this,e)}};var je=K.litHtmlPolyfillSupport;je?.(I,U),(K.litHtmlVersions??=[]).push("3.3.3");var $e=(i,e,t)=>{let r=t?.renderBefore??e,s=r._$litPart$;if(s===void 0){let o=t?.renderBefore??null;r._$litPart$=s=new U(e.insertBefore(R(),o),o,void 0,t??{})}return s._$AI(i),s};var Q=globalThis,m=class extends g{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=$e(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return A}};m._$litElement$=!0,m.finalized=!0,Q.litElementHydrateSupport?.({LitElement:m});var Be=Q.litElementPolyfillSupport;Be?.({LitElement:m});(Q.litElementVersions??=[]).push("4.2.2");var we=["admin","manage","triage","view","none"];var xe={dependencies:"Dependencies",security:"Security"};async function O(){let i=await fetch("/api/auth/session",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)return null;let e=await i.json();return{id:e?.user?.id??null,csrfToken:typeof e?.csrfToken=="string"?e.csrfToken:null}}async function Ve(){let i=await fetch("/api/admin/users",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);let e=await i.json();return Array.isArray(e?.users)?e.users:[]}async function qe(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/set-role",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({userId:i,role:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}var X=class extends m{static properties={_users:{state:!0},_error:{state:!0}};static styles=y`:host{display:block;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}h1{font-size:1.15rem;font-weight:600;margin:0 0 1rem;user-select:none}.users{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.users li{display:flex;align-items:center;gap:.7rem;padding:.55rem .85rem}.users li+li{border-top:1px solid var(--border)}.avatar{position:relative;flex-shrink:0;width:28px;height:28px;border-radius:50%;overflow:hidden;background:var(--accent);display:inline-grid;place-items:center;user-select:none}.avatar>span{font-size:.8rem;font-weight:600;color:var(--bg);text-transform:uppercase}.avatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.avatar img.broken{display:none}.who{display:flex;flex-direction:column;min-width:0}.login{font-weight:600;font-size:.9rem}.name{color:var(--muted);font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.role{margin-left:auto;flex-shrink:0;font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.2rem .4rem}.role:disabled{opacity:.55}.msg{color:var(--muted);font-size:.9rem}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._users=null,this._error=null,this._me=null,this._csrf=null}connectedCallback(){super.connectedCallback(),this._load()}async _load(){this._error=null,this._users=null;try{let[e,t]=await Promise.all([O(),Ve()]);this._me=e?.id??null,this._csrf=e?.csrfToken??null,this._users=t}catch(e){this._error=String(e?.message??e)}}render(){return n`<div class="wrap"><h1>Users</h1>${this._error==null?this._users==null?n`<p class="msg">Loading…</p>`:this._list():n`<p class="msg error">Couldn't load users: ${this._error}</p>`}</div>`}_list(){return this._users.length===0?n`<p class="msg">No users yet.</p>`:n`<ul class="users">${this._users.map(e=>this._row(e))}</ul>`}_row(e){let t=(e.login?.[0]??"?").toUpperCase(),r=e.id===this._me;return n`<li><span class="avatar"><span>${t}</span> <img alt="" src="${`/api/avatar/${encodeURIComponent(e.id)}`}" @error="${s=>s.currentTarget.classList.add("broken")}"> </span><span class="who"><span class="login">${e.login}</span> ${e.name?n`<span class="name">${e.name}</span>`:c} </span><select class="role" ?disabled="${r}" aria-label="${`Change ${e.login}\u2019s role`}" data-tooltip="${r?"You can\u2019t change your own role":"Change role"}" @change="${s=>this._changeRole(e,s.target.value,s.target)}">${we.map(s=>n`<option value="${s}" ?selected="${s===e.role}">${s}</option>`)}</select></li>`}async _changeRole(e,t,r){let s=e.role;if(t!==s){try{await qe(e.id,t,this._csrf),e.role=t}catch(o){console.warn("admin: set role failed:",o),r.value=s}this.requestUpdate()}}};customElements.define("managed-admin-users",X);async function Fe(){let i=await fetch("/api/admin/repositories",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function Ze(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/repositories/select",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({repoId:i,selected:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}var We=n`<svg class="repo-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M2 2.75A2.75 2.75 0 0 1 4.75 0h7.5a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75H4.5a1 1 0 0 0 0 2h8a.75.75 0 0 1 0 1.5h-8A2.5 2.5 0 0 1 2 13V2.75Zm2.75-.25a1.25 1.25 0 0 0-1.25 1.25v7.32c.317-.114.66-.07 1 .18V2.5h-.5a.25.25 0 0 0 .75 0Zm6.75 0H5.5v8.5h6V2.5Z"/></svg>`,ee=class extends m{static properties={_data:{state:!0},_error:{state:!0},_tab:{state:!0}};static styles=y`:host{display:block;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.head{display:flex;align-items:center;gap:1rem;margin:0 0 .75rem}h1{font-size:1.15rem;font-weight:600;margin:0;user-select:none}.connect{margin-left:auto;flex-shrink:0;font-size:.85rem;font-weight:600;color:var(--bg);background:var(--accent);text-decoration:none;border-radius:6px;padding:.35rem .7rem;white-space:nowrap}.connect:hover{opacity:.9}.tabs{display:flex;gap:.25rem;margin:0 0 1rem;border-bottom:1px solid var(--border)}.tab{font:inherit;font-size:.85rem;font-weight:600;color:var(--muted);background:0 0;border:none;border-bottom:2px solid transparent;margin-bottom:-1px;padding:.4rem .55rem;cursor:pointer;user-select:none}.tab:hover{color:var(--text)}.tab.active{color:var(--text);border-bottom-color:var(--accent)}.hint{color:var(--muted);font-size:.82rem;margin:0 0 .8rem}.repos{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.repos li{display:flex;align-items:center;gap:.6rem;padding:.55rem .85rem}.repos li+li{border-top:1px solid var(--border)}.select{flex-shrink:0;width:16px;height:16px;accent-color:var(--accent);cursor:pointer}.repo-icon{flex-shrink:0;color:var(--muted)}.full-name{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{flex-shrink:0;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:.05rem .4rem}.open{margin-left:auto;flex-shrink:0;font-size:.82rem;color:var(--accent);text-decoration:none}.open:hover{text-decoration:underline}.msg{color:var(--muted);font-size:.9rem;line-height:1.5}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._tab="manage"}connectedCallback(){super.connectedCallback(),this._load()}async _load(){this._error=null,this._data=null;try{let[e,t]=await Promise.all([O(),Fe()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}render(){let e=this._data?.installUrl;return n`<div class="wrap"><div class="head"><h1>Repositories</h1>${e?n`<a class="connect" href="${e}" target="_blank" rel="noopener noreferrer">Connect a repository</a>`:c}</div><div class="tabs" role="tablist"><button type="button" role="tab" class="tab ${this._tab==="manage"?"active":""}" aria-selected="${this._tab==="manage"}" @click="${()=>{this._tab="manage"}}">Manage repositories</button> <button type="button" role="tab" class="tab ${this._tab==="select"?"active":""}" aria-selected="${this._tab==="select"}" @click="${()=>{this._tab="select"}}">Select repositories</button></div>${this._body()}</div>`}_body(){if(this._error!=null)return n`<p class="msg error">Couldn't load repositories: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.repositories)?this._data.repositories:[];return e.length===0&&this._data.tokenMissing?n`<p class="msg">We don't have current GitHub access for your account. Please <strong>log out and log back in</strong> to grant repository access.</p>`:this._tab==="select"?this._selectView(e):this._manageView(e)}_manageView(e){let t=e.filter(r=>r.installed);return t.length===0?n`<p class="msg">No repositories connected yet. ${this._data.installUrl?n`Use “Connect a repository” to install the GitHub App on the repositories you want to read.`:n`Install the GitHub App on the repositories you want to read.`}</p>`:n`<p class="hint">Connected through the GitHub App — we can read these repositories.</p><ul class="repos">${t.map(r=>this._row(r,!1))}</ul>`}_selectView(e){return e.length===0?n`<p class="msg">No repositories found. ${this._data.installUrl?n`Use “Connect a repository” to install the GitHub App on the private repositories you want.`:n`Public repositories you own appear here.`}</p>`:n`${this._data.tokenMissing?n`<p class="hint">Some public repositories may be hidden — log out and back in to refresh GitHub access.</p>`:c}<p class="hint">Check a repository to operate on it — we'll read its contents.</p><ul class="repos">${e.map(t=>this._row(t,!0))}</ul>`}_row(e,t){return n`<li>${t?n`<input type="checkbox" class="select" .checked="${e.selected===!0}" aria-label="${`Select ${e.fullName} to operate on`}" data-tooltip="${e.selected?"Selected \u2014 remove from the operate-on set":"Select this repository to operate on"}" @change="${r=>this._toggle(e,r.target)}">`:c} ${We} <span class="full-name">${e.fullName}</span> ${e.private?n`<span class="badge">private</span>`:c} ${e.htmlUrl?n`<a class="open" href="${e.htmlUrl}" target="_blank" rel="noopener noreferrer">open</a>`:c}</li>`}async _toggle(e,t){let r=t.checked;try{await Ze(e.id,r,this._csrf),e.selected=r}catch(s){console.warn("admin: repo selection failed:",s),t.checked=!r}this.requestUpdate()}};customElements.define("managed-admin-repos",ee);async function Je(){let i=await fetch("/api/admin/reports",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function Ke(i,e,t){let r={"content-type":i.type||"application/json","x-report-filename":encodeURIComponent(i.name)};e&&(r["x-csrf-token"]=e),t!=null&&(r["x-repo-id"]=String(t));let s=await fetch("/api/admin/reports",{method:"POST",credentials:"same-origin",headers:r,body:i});if(!s.ok)throw new Error(s.status===413?"too large":`HTTP ${s.status}`);return s.json()}async function Ge(i,e){let t=e?{"x-csrf-token":e}:{},r=await fetch(`/api/admin/reports/${encodeURIComponent(i)}`,{method:"DELETE",credentials:"same-origin",headers:t});if(!r.ok)throw new Error(`HTTP ${r.status}`)}async function Ye(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/reports/set-repo",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({reportId:i,repoId:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}function Ae(i){return typeof i!="number"||!Number.isFinite(i)?"":i<1024?`${i} B`:i<1048576?`${(i/1024).toFixed(1)} KB`:`${(i/1048576).toFixed(1)} MB`}function ke(i,e){return n`<option value="" ?selected="${e==null||e===""}">No repository</option>${i.map(t=>n`<option value="${t.repoId}" ?selected="${Number(e)===t.repoId}">${t.fullName}</option>`)}`}function Se(i,e,t){return!Array.isArray(i)||i.length===0?c:n`<label class="repo-picker"><span class="repo-picker-label">Attach uploads to</span> <select class="repo-select" @change="${r=>t(r.target.value===""?null:Number(r.target.value))}">${ke(i,e)}</select></label>`}function Ee(i,e,t){return!Array.isArray(i)||i.length===0?c:n`<select class="repo-attach" aria-label="Attach to a repository" @change="${r=>t(r.target.value===""?null:Number(r.target.value))}">${ke(i,e)}</select>`}function Ce(i,e=!0){let t=document.createElement("input");t.type="file",t.multiple=e,t.addEventListener("change",()=>{i([...t.files])},{once:!0}),t.click()}function Pe(i,e,t){let r=0,s=d=>Array.from(d.dataTransfer?.types??[]).includes("Files"),o=d=>{s(d)&&(d.preventDefault(),r+=1,t(!0))},a=d=>{s(d)&&d.preventDefault()},h=d=>{s(d)&&(r=Math.max(0,r-1),r===0&&t(!1))},l=d=>{s(d)&&(d.preventDefault(),d.stopPropagation(),r=0,t(!1),e([...d.dataTransfer.files]))};return i.addEventListener("dragenter",o),i.addEventListener("dragover",a),i.addEventListener("dragleave",h),i.addEventListener("drop",l),()=>{i.removeEventListener("dragenter",o),i.removeEventListener("dragover",a),i.removeEventListener("dragleave",h),i.removeEventListener("drop",l)}}var Qe=n`<svg class="report-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 10 4.25V1.5Zm7.75.689V4.25c0 .138.112.25.25.25h2.061ZM4.5 8.75A.75.75 0 0 1 5.25 8h5.5a.75.75 0 0 1 0 1.5h-5.5A.75.75 0 0 1 4.5 8.75Zm0 2.5a.75.75 0 0 1 .75-.75h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1-.75-.75Z"/></svg>`,te=class extends m{static properties={_data:{state:!0},_error:{state:!0},_busy:{state:!0},_dragOver:{state:!0}};static styles=y`:host{display:block;position:relative;flex:1 1 auto;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.dropzone{position:absolute;inset:.6rem;z-index:5;display:grid;place-items:center;border:2px dashed var(--accent);border-radius:10px;background:rgb(from var(--bg) r g b / .9);color:var(--accent);font-size:1rem;font-weight:600;pointer-events:none}.head{display:flex;align-items:center;gap:1rem;margin:0 0 .75rem}h1{font-size:1.15rem;font-weight:600;margin:0;user-select:none}.head-actions{margin-left:auto;flex-shrink:0;display:flex;align-items:center;gap:.5rem}.repo-picker{display:inline-flex;align-items:center;gap:.4rem}.repo-picker-label{font-size:.8rem;color:var(--muted);white-space:nowrap}.repo-select{font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.2rem .4rem;max-width:14rem}.repo-attach{flex-shrink:0;font:inherit;font-size:.78rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.1rem .3rem;max-width:11rem}.upload{flex-shrink:0;font:inherit;font-size:.85rem;font-weight:600;color:var(--bg);background:var(--accent);border:none;cursor:pointer;border-radius:6px;padding:.35rem .7rem;white-space:nowrap}.upload:hover{opacity:.9}.upload:disabled{opacity:.55;cursor:default}.hint{color:var(--muted);font-size:.82rem;margin:0 0 .8rem}.reports{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.reports li{display:flex;align-items:center;gap:.6rem;padding:.55rem .85rem}.reports li+li{border-top:1px solid var(--border)}.report-icon{flex-shrink:0;color:var(--muted)}.who{display:flex;flex-direction:column;min-width:0}.filename{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta{color:var(--muted);font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions{margin-left:auto;flex-shrink:0;display:flex;gap:.7rem;align-items:center}.download{font-size:.82rem;color:var(--accent);text-decoration:none}.download:hover{text-decoration:underline}.delete{font:inherit;font-size:.82rem;color:var(--critical,#c00);background:0 0;border:none;cursor:pointer;padding:0}.delete:hover{text-decoration:underline}.msg{color:var(--muted);font-size:.9rem;line-height:1.5}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._busy=!1,this._repoId=null,this._dragOver=!1,this._teardownDrop=null,this._queue=[]}connectedCallback(){super.connectedCallback(),this._load(),this._teardownDrop=Pe(this,e=>{this._upload(e)},e=>{this._dragOver=e})}disconnectedCallback(){super.disconnectedCallback(),this._teardownDrop?.()}async _load(){this._error=null,this._data=null;try{let[e,t]=await Promise.all([O(),Je()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}render(){return n`${this._dragOver?n`<div class="dropzone">Drop reports to upload</div>`:c}<div class="wrap"><div class="head"><h1>Reports</h1><div class="head-actions">${Se(this._data?.repos,this._repoId,e=>{this._repoId=e})} <button type="button" class="upload" ?disabled="${this._busy}" @click="${()=>Ce(e=>{this._upload(e)})}">${this._busy?"Uploading\u2026":"Upload report"}</button></div></div>${this._body()}</div>`}_body(){if(this._error!=null)return n`<p class="msg error">Couldn't load reports: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.reports)?this._data.reports:[];return e.length===0?n`<p class="msg">No reports uploaded yet. Drag & drop files here, or use “Upload report”, to add a findings report (JSON, markdown, or CSV) for the server to operate on.</p>`:n`<p class="hint">Reports stored on the server.</p><ul class="reports">${e.map(t=>this._row(t))}</ul>`}_row(e){let t=e.uploadedByLogin?`by ${e.uploadedByLogin}`:"uploader removed",r=Number.isFinite(e.uploadedAt)?new Date(e.uploadedAt).toLocaleString():"",s=e.bundleFilename?`bundle: ${e.bundleFilename}`:e.bundleIntegrity?"bundle: (not uploaded)":null,o=[t,r,Ae(e.byteSize),s].filter(Boolean).join(" \xB7 ");return n`<li>${Qe} <span class="who"><span class="filename">${e.filename}</span> <span class="meta">${o}</span> </span><span class="actions">${Ee(this._data?.repos,e.repoId,a=>this._setRepo(e,a))} <a class="download" href="${`/api/admin/reports/${encodeURIComponent(e.id)}`}">download</a> <button type="button" class="delete" @click="${()=>this._delete(e)}">delete</button></span></li>`}async _setRepo(e,t){try{await Ye(e.id,t,this._csrf)}catch(r){this._error=`Couldn't change repo: ${String(r?.message??r)}`}await this._load()}async _upload(e){if(e.length!==0&&(this._queue.push(...e),!this._busy)){this._busy=!0,this._error=null;try{for(;this._queue.length>0;){let t=this._queue.shift();await Ke(t,this._csrf,this._repoId)}}catch(t){this._queue=[],this._error=`Upload failed: ${String(t?.message??t)}`}finally{this._busy=!1,await this._load()}}}async _delete(e){if(globalThis.confirm?.(`Delete \u201C${e.filename}\u201D? This can't be undone.`)){try{await Ge(e.id,this._csrf)}catch(t){this._error=`Delete failed: ${String(t?.message??t)}`}await this._load()}}};customElements.define("managed-admin-reports",te);async function Xe(){let i=await fetch("/api/admin/bundles",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function et(i,e,t){let r={"content-type":"application/octet-stream","x-bundle-filename":encodeURIComponent(i.name)};e&&(r["x-csrf-token"]=e),t!=null&&(r["x-repo-id"]=String(t));let s=await fetch("/api/admin/bundles",{method:"POST",credentials:"same-origin",headers:r,body:i});if(!s.ok)throw new Error(s.status===413?"too large":`HTTP ${s.status}`);return s.json()}async function tt(i,e){let t=e?{"x-csrf-token":e}:{},r=await fetch(`/api/admin/bundles/${encodeURIComponent(i)}`,{method:"DELETE",credentials:"same-origin",headers:t});if(!r.ok)throw new Error(`HTTP ${r.status}`)}async function rt(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/bundles/set-repo",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({bundleId:i,repoId:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}var st=n`<svg class="report-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M8.878.392a1.75 1.75 0 0 0-1.756 0l-5.25 3.045A1.75 1.75 0 0 0 1 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 0 0 1.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514ZM7.875 1.69a.25.25 0 0 1 .25 0l4.63 2.685L8 7.133 3.245 4.375Zm-5.125 4.4 4.5 2.61v5.317l-4.375-2.537a.25.25 0 0 1-.125-.216Zm6 7.927V8.7l4.5-2.61v3.96a.25.25 0 0 1-.125.216Z"/></svg>`,re=class extends m{static properties={_data:{state:!0},_error:{state:!0},_busy:{state:!0},_dragOver:{state:!0}};static styles=y`:host{display:block;position:relative;flex:1 1 auto;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.dropzone{position:absolute;inset:.6rem;z-index:5;display:grid;place-items:center;border:2px dashed var(--accent);border-radius:10px;background:rgb(from var(--bg) r g b / .9);color:var(--accent);font-size:1rem;font-weight:600;pointer-events:none}.head{display:flex;align-items:center;gap:1rem;margin:0 0 .75rem}h1{font-size:1.15rem;font-weight:600;margin:0;user-select:none}.head-actions{margin-left:auto;flex-shrink:0;display:flex;align-items:center;gap:.5rem}.repo-picker{display:inline-flex;align-items:center;gap:.4rem}.repo-picker-label{font-size:.8rem;color:var(--muted);white-space:nowrap}.repo-select{font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.2rem .4rem;max-width:14rem}.repo-attach{flex-shrink:0;font:inherit;font-size:.78rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.1rem .3rem;max-width:11rem}.upload{flex-shrink:0;font:inherit;font-size:.85rem;font-weight:600;color:var(--bg);background:var(--accent);border:none;cursor:pointer;border-radius:6px;padding:.35rem .7rem;white-space:nowrap}.upload:hover{opacity:.9}.upload:disabled{opacity:.55;cursor:default}.hint{color:var(--muted);font-size:.82rem;margin:0 0 .8rem}.reports{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.reports li{display:flex;align-items:center;gap:.6rem;padding:.55rem .85rem}.reports li+li{border-top:1px solid var(--border)}.report-icon{flex-shrink:0;color:var(--muted)}.who{display:flex;flex-direction:column;min-width:0}.filename{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta{color:var(--muted);font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{flex-shrink:0;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:.05rem .4rem}.actions{margin-left:auto;flex-shrink:0;display:flex;gap:.7rem;align-items:center}.download{font-size:.82rem;color:var(--accent);text-decoration:none}.download:hover{text-decoration:underline}.delete{font:inherit;font-size:.82rem;color:var(--critical,#c00);background:0 0;border:none;cursor:pointer;padding:0}.delete:hover{text-decoration:underline}.msg{color:var(--muted);font-size:.9rem;line-height:1.5}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._busy=!1,this._repoId=null,this._dragOver=!1,this._teardownDrop=null,this._queue=[]}connectedCallback(){super.connectedCallback(),this._load(),this._teardownDrop=Pe(this,e=>{this._upload(e)},e=>{this._dragOver=e})}disconnectedCallback(){super.disconnectedCallback(),this._teardownDrop?.()}async _load(){this._error=null,this._data=null;try{let[e,t]=await Promise.all([O(),Xe()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}render(){return n`${this._dragOver?n`<div class="dropzone">Drop bundles to upload</div>`:c}<div class="wrap"><div class="head"><h1>Bundles</h1><div class="head-actions">${Se(this._data?.repos,this._repoId,e=>{this._repoId=e})} <button type="button" class="upload" ?disabled="${this._busy}" @click="${()=>Ce(e=>{this._upload(e)})}">${this._busy?"Uploading\u2026":"Upload bundle"}</button></div></div>${this._body()}</div>`}_body(){if(this._error!=null)return n`<p class="msg error">Couldn't load bundles: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.bundles)?this._data.bundles:[];return e.length===0?n`<p class="msg">No bundles uploaded yet. Drag & drop files here, or use “Upload bundle”, to add a sourcemap or stasis archive for the server to operate on; reports auto-link by content hash.</p>`:n`<p class="hint">Bundles stored on the server.</p><ul class="reports">${e.map(t=>this._row(t))}</ul>`}_row(e){let t=e.uploadedByLogin?`by ${e.uploadedByLogin}`:"uploader removed",r=Number.isFinite(e.uploadedAt)?new Date(e.uploadedAt).toLocaleString():"",s=[t,r,Ae(e.byteSize)].filter(Boolean).join(" \xB7 ");return n`<li title="${e.integrity??""}">${st} <span class="who"><span class="filename">${e.filename}</span> <span class="meta">${s}</span> </span>${e.kind?n`<span class="badge">${e.kind}</span>`:c} <span class="actions">${Ee(this._data?.repos,e.repoId,o=>this._setRepo(e,o))} <a class="download" href="${`/api/admin/bundles/${encodeURIComponent(e.id)}`}">download</a> <button type="button" class="delete" @click="${()=>this._delete(e)}">delete</button></span></li>`}async _setRepo(e,t){try{await rt(e.id,t,this._csrf)}catch(r){this._error=`Couldn't change repo: ${String(r?.message??r)}`}await this._load()}async _upload(e){if(e.length!==0&&(this._queue.push(...e),!this._busy)){this._busy=!0,this._error=null;try{for(;this._queue.length>0;){let t=this._queue.shift();await et(t,this._csrf,this._repoId)}}catch(t){this._queue=[],this._error=`Upload failed: ${String(t?.message??t)}`}finally{this._busy=!1,await this._load()}}}async _delete(e){if(globalThis.confirm?.(`Delete \u201C${e.filename}\u201D? Linked reports will keep their pending link.`)){try{await tt(e.id,this._csrf)}catch(t){this._error=`Delete failed: ${String(t?.message??t)}`}await this._load()}}};customElements.define("managed-admin-bundles",re);async function it(){let i=await fetch("/api/admin/teams",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function v(i,e,t){let r={"content-type":"application/json"};e&&(r["x-csrf-token"]=e);let s=await fetch(i,{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify(t)});if(!s.ok)throw new Error(s.status===409?"name already taken":`HTTP ${s.status}`)}var se=class extends m{static properties={_data:{state:!0},_error:{state:!0},_busy:{state:!0},_renamingId:{state:!0}};static styles=y`:host{display:block;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.head{display:flex;align-items:center;gap:.5rem;margin:0 0 1rem}h1{font-size:1.15rem;font-weight:600;margin:0 auto 0 0;user-select:none}input,select{font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.25rem .4rem}.new-name{width:12rem}.btn{font:inherit;font-size:.82rem;font-weight:600;color:var(--bg);background:var(--accent);border:none;border-radius:6px;padding:.3rem .6rem;cursor:pointer;white-space:nowrap}.btn:hover{opacity:.9}.btn:disabled{opacity:.55;cursor:default}.team{border:1px solid var(--border);border-radius:8px;padding:.85rem;margin:0 0 1rem}.team-head{display:flex;align-items:center;gap:.6rem;margin:0 0 .6rem}.team-name{font-weight:600;font-size:1rem}.rename-input{width:12rem}.sub{margin:.5rem 0 0}.sub-title{font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 .3rem}.links{list-style:none;margin:0 0 .4rem;padding:0}.links li{display:flex;align-items:center;gap:.6rem;padding:.25rem 0}.ln{font-size:.88rem}.path{color:var(--muted);font-size:.82rem}.perms{margin-left:auto;display:flex;gap:.8rem}.perm{display:inline-flex;align-items:center;gap:.25rem;font-size:.8rem;color:var(--muted);user-select:none}.add-row{display:flex;align-items:center;gap:.4rem;margin:.3rem 0 0;flex-wrap:wrap}.add-row select{max-width:16rem}.x{font:inherit;font-size:.8rem;color:var(--critical,#c00);background:0 0;border:none;cursor:pointer;padding:0}.x:hover{text-decoration:underline}.delete{margin-left:auto}.muted{color:var(--muted);font-size:.82rem;margin:0 0 .3rem}.msg{color:var(--muted);font-size:.9rem}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._busy=!1,this._renamingId=null}connectedCallback(){super.connectedCallback(),this._load()}async _load(){this._error=null;try{let[e,t]=await Promise.all([O(),it()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}async _do(e){if(!this._busy){this._busy=!0,this._error=null;try{await e()}catch(t){this._error=String(t?.message??t)}finally{this._busy=!1,await this._load()}}}render(){return n`<div class="wrap"><div class="head"><h1>Teams</h1><input class="new-name" type="text" placeholder="New team name" maxlength="100" @keydown="${e=>{e.key==="Enter"&&this._create()}}"> <button class="btn" ?disabled="${this._busy}" @click="${()=>this._create()}">Create</button></div>${this._body()}</div>`}_body(){if(this._error!=null&&this._data==null)return n`<p class="msg error">Couldn't load teams: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.teams)?this._data.teams:[];return n`${this._error==null?c:n`<p class="msg error">${this._error}</p>`} ${e.length===0?n`<p class="msg">No teams yet. Create one above.</p>`:e.map(t=>this._team(t))}`}_team(e){return n`<div class="team"><div class="team-head">${this._renamingId===e.id?n`<input class="rename-input" type="text" .value="${e.name}" maxlength="100" @keydown="${t=>this._renameKey(t,e)}"> <button class="btn" ?disabled="${this._busy}" @click="${t=>this._saveRename(e,t)}">save</button> <button class="x" @click="${()=>{this._renamingId=null}}">cancel</button>`:n`<span class="team-name">${e.name}</span> <button class="x" @click="${()=>{this._renamingId=e.id}}">rename</button> <button class="x delete" @click="${()=>this._deleteTeam(e)}">delete team</button>`}</div><div class="sub"><div class="sub-title">Repositories</div>${e.repos.length===0?n`<p class="muted">No repositories linked.</p>`:n`<ul class="links">${e.repos.map(t=>this._repoRow(e,t))}</ul>`} ${this._addRepoRow(e)}</div><div class="sub"><div class="sub-title">Members</div>${e.members.length===0?n`<p class="muted">No members.</p>`:n`<ul class="links">${e.members.map(t=>this._memberRow(e,t))}</ul>`} ${this._addMemberRow(e)}</div></div>`}_repoRow(e,t){return n`<li><span class="ln">${t.fullName}${t.path?n`<span class="path">/${t.path}</span>`:c}</span> <button class="x" @click="${()=>this._do(()=>v("/api/admin/teams/remove-repo",this._csrf,{teamId:e.id,repoId:t.repoId}))}">remove</button></li>`}_addRepoRow(e){let t=Array.isArray(this._data.repos)?this._data.repos:[];if(t.length===0)return n`<p class="muted">No selected repositories to link — pick some on “Manage repositories”.</p>`;let r=new Set(e.repos.map(s=>s.repoId));return n`<div class="add-row"><select class="add-repo-sel"><option value="">Add repository…</option>${t.map(s=>n`<option value="${s.repoId}">${s.fullName}${r.has(s.repoId)?" \u2014 update path":""}</option>`)}</select> <input class="add-repo-path" type="text" placeholder="subpath (optional)" maxlength="500"> <button class="btn" @click="${s=>this._addRepo(e,s)}">add</button></div>`}_memberRow(e,t){let r=Array.isArray(this._data.permissions)?this._data.permissions:[];return n`<li><span class="ln">${t.login}</span> <span class="perms">${r.map(s=>n`<label class="perm"><input type="checkbox" .checked="${t[s]===!0}" @change="${o=>this._togglePerm(e,t,s,o.target.checked)}"> ${xe[s]??s}</label>`)} </span><button class="x" @click="${()=>this._do(()=>v("/api/admin/teams/remove-member",this._csrf,{teamId:e.id,userId:t.userId}))}">remove</button></li>`}_addMemberRow(e){let t=Array.isArray(this._data.users)?this._data.users:[],r=new Set(e.members.map(s=>s.userId));return n`<div class="add-row"><select class="add-member-sel"><option value="">Add member…</option>${t.map(s=>n`<option value="${s.id}" ?disabled="${r.has(s.id)}">${s.login}${r.has(s.id)?" (member)":""}</option>`)}</select> <button class="btn" @click="${s=>this._addMember(e,s)}">add</button></div>`}_create(){let e=this.renderRoot.querySelector(".new-name"),t=e?.value.trim();t&&this._do(async()=>{await v("/api/admin/teams",this._csrf,{name:t}),e&&(e.value="")})}_deleteTeam(e){globalThis.confirm?.(`Delete team \u201C${e.name}\u201D? Its repo + member links are removed.`)&&this._do(()=>v("/api/admin/teams/delete",this._csrf,{teamId:e.id}))}_renameKey(e,t){e.key==="Enter"?this._saveRename(t,e):e.key==="Escape"&&(this._renamingId=null)}_saveRename(e,t){if(this._busy)return;let r=t.target.closest(".team-head")?.querySelector(".rename-input")?.value.trim()??"";this._renamingId=null,!(r===""||r===e.name)&&this._do(()=>v("/api/admin/teams/rename",this._csrf,{teamId:e.id,name:r}))}_addRepo(e,t){let r=t.target.closest(".add-row"),s=Number(r?.querySelector(".add-repo-sel")?.value);if(!Number.isSafeInteger(s)||s<=0)return;let o=r?.querySelector(".add-repo-path")?.value??"";this._do(()=>v("/api/admin/teams/set-repo",this._csrf,{teamId:e.id,repoId:s,path:o}))}_addMember(e,t){let r=t.target.closest(".add-row")?.querySelector(".add-member-sel")?.value;r&&this._do(()=>v("/api/admin/teams/set-member",this._csrf,{teamId:e.id,userId:r}))}_togglePerm(e,t,r,s){let o={};for(let a of this._data.permissions??[])o[a]=t[a]===!0;o[r]=s,this._do(()=>v("/api/admin/teams/set-member",this._csrf,{teamId:e.id,userId:t.userId,...o}))}};customElements.define("managed-admin-teams",se);export{re as ManagedAdminBundles,te as ManagedAdminReports,ee as ManagedAdminRepos,se as ManagedAdminTeams,X as ManagedAdminUsers};
|
|
4
|
-
/*! Bundled license information:
|
|
5
|
-
|
|
6
|
-
@lit/reactive-element/css-tag.js:
|
|
7
|
-
(**
|
|
8
|
-
* @license
|
|
9
|
-
* Copyright 2019 Google LLC
|
|
10
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
11
|
-
*)
|
|
12
|
-
|
|
13
|
-
@lit/reactive-element/reactive-element.js:
|
|
14
|
-
lit-html/lit-html.js:
|
|
15
|
-
lit-element/lit-element.js:
|
|
16
|
-
(**
|
|
17
|
-
* @license
|
|
18
|
-
* Copyright 2017 Google LLC
|
|
19
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
20
|
-
*)
|
|
21
|
-
|
|
22
|
-
lit-html/is-server.js:
|
|
23
|
-
(**
|
|
24
|
-
* @license
|
|
25
|
-
* Copyright 2022 Google LLC
|
|
26
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
27
|
-
*)
|
|
28
|
-
*/
|