@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preventive/triage",
3
- "version": "1.0.0-alpha.15",
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": "^1.12.0",
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 = []
@@ -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
+ }
@@ -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']) finding.reproduction = 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('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;')
18
+ return result.replace('</head>', `<meta name="deepview-scan-server" content="${content}">\n</head>`)
19
+ }
@@ -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
  }
@@ -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.
@@ -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
-
@@ -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, nested subpaths and absolute-form
10
- // URIs all just produce a key that isn't in the map and 404.
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
- // `isFile()` filters subdirectories, symlinks, FIFOs etc. The
169
- // build emits a flat tree; dropping anything else keeps the
170
- // surface minimal even if a stray non-file sneaks in.
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 raw = readFileSync(join(staticDir, name))
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
@@ -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
- */