@huaqiu/dsh-tool-plugin-update 0.6.7 → 0.6.8
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/lib/client.js +1060 -324
- package/lib/client.js.map +1 -1
- package/package.json +3 -3
- package/src/client/copy.ts +93 -0
- package/src/client/index.ts +640 -1
- package/src/client/update-view.ts +502 -329
|
@@ -162,122 +162,142 @@ function blockedReason(item: PluginUpdateItem, copy: UpdateCopy): string | undef
|
|
|
162
162
|
return by.length > 0 ? by.join(', ') : undefined
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
/**
|
|
165
|
+
/** Two-letter avatar initials for a card (CJK-safe). */
|
|
166
|
+
function avatarInitials(item: PluginUpdateItem): string {
|
|
167
|
+
const src =
|
|
168
|
+
typeof item.displayName === 'string' && item.displayName.trim().length > 0
|
|
169
|
+
? item.displayName.trim()
|
|
170
|
+
: shortName(item.name)
|
|
171
|
+
if (/[⺀-鿿]/.test(src)) return src.slice(0, 2)
|
|
172
|
+
const words = src.split(/[\s_\-./@]+/).filter(Boolean)
|
|
173
|
+
if (words.length >= 2) return (words[0]![0]! + words[1]![0]!).toUpperCase()
|
|
174
|
+
return src.replace(/^@/, '').slice(0, 2).toUpperCase()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Shared per-card changelog toggle drawer (the per-plugin changes[] list). */
|
|
178
|
+
function changelogDrawer(item: PluginUpdateItem, copy: UpdateCopy, open: boolean): HTMLElement {
|
|
179
|
+
const drawer = el('div', 'hqpu-changelog-drawer')
|
|
180
|
+
drawer.style.display = open ? '' : 'none'
|
|
181
|
+
drawer.append(el('div', 'hqpu-changelog-drawer-title', copy.ui.changelogTitle))
|
|
182
|
+
const ul = el('ul')
|
|
183
|
+
for (const change of item.changes ?? []) {
|
|
184
|
+
if (typeof change === 'string' && change.length > 0) ul.append(el('li', '', change))
|
|
185
|
+
}
|
|
186
|
+
drawer.append(ul)
|
|
187
|
+
return drawer
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** One package card per the mockup: avatar initials, display name + package
|
|
191
|
+
* subtitle, old→new version row (new in emerald), a 日志 toggle expanding the
|
|
192
|
+
* inline changelog. Blocked/incompatible cards are dimmed with a rose
|
|
193
|
+
* 无法安装 badge and a reason line, and carry NO install button. */
|
|
166
194
|
function packageCard(
|
|
167
195
|
item: PluginUpdateItem,
|
|
168
196
|
copy: UpdateCopy,
|
|
169
|
-
|
|
170
|
-
|
|
197
|
+
open: boolean,
|
|
198
|
+
onToggle: () => void,
|
|
171
199
|
): HTMLLIElement {
|
|
172
|
-
const
|
|
200
|
+
const blocked = item.state === 'blocked'
|
|
201
|
+
const card = el('li', blocked ? 'hqpu-card hqpu-card--blocked' : 'hqpu-card')
|
|
173
202
|
card.dataset.pluginName = item.name
|
|
174
203
|
card.dataset.state = item.state
|
|
175
|
-
// The full package name is the card's tooltip; the row shows the display name.
|
|
176
|
-
card.title = `${item.name} · ${versionLine(item)}`
|
|
177
204
|
|
|
178
205
|
const head = el('div', 'hqpu-card-head')
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
glyph.innerHTML = PLUGIN_ICON(24)
|
|
206
|
+
const left = el('div')
|
|
207
|
+
const avatar = el('span', 'hqpu-avatar', avatarInitials(item))
|
|
208
|
+
avatar.setAttribute('aria-hidden', 'true')
|
|
183
209
|
|
|
184
210
|
const main = el('div', 'hqpu-card-main')
|
|
185
|
-
const titleRow = el('div', 'hqpu-title-row')
|
|
186
|
-
// Prefer the user-facing display name from a v2 manifest; fall back to the
|
|
187
|
-
// shortened package id (the shell's own naming rule).
|
|
188
211
|
const titleText =
|
|
189
212
|
typeof item.displayName === 'string' && item.displayName.trim().length > 0
|
|
190
213
|
? item.displayName.trim()
|
|
191
214
|
: shortName(item.name)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const descText = size.length > 0 ? `${versionLine(item)} · ${size}` : versionLine(item)
|
|
201
|
-
const desc = el('span', 'hqpu-card-desc', descText)
|
|
202
|
-
main.append(titleRow, desc)
|
|
203
|
-
|
|
204
|
-
// Optional one-line description from a v2 manifest.
|
|
205
|
-
if (typeof item.description === 'string' && item.description.trim().length > 0) {
|
|
206
|
-
main.append(el('span', 'hqpu-card-desc', item.description.trim()))
|
|
215
|
+
main.append(el('span', 'hqpu-card-title', titleText))
|
|
216
|
+
main.append(el('span', 'hqpu-card-sub', shortName(item.name)))
|
|
217
|
+
|
|
218
|
+
// old → new version row; the NEW version renders in emerald.
|
|
219
|
+
const ver = el('div', 'hqpu-version-row')
|
|
220
|
+
if (item.currentVersion !== null && item.currentVersion !== undefined) {
|
|
221
|
+
ver.append(el('span', 'hqpu-ver-old', item.currentVersion))
|
|
222
|
+
ver.append(el('span', 'hqpu-ver-arrow', '→'))
|
|
207
223
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
const size = formatMb(item.sizeBytes ?? 0)
|
|
225
|
+
ver.append(
|
|
226
|
+
el('span', 'hqpu-ver-new', size.length > 0 ? `${item.availableVersion} (${size})` : item.availableVersion),
|
|
227
|
+
)
|
|
228
|
+
main.append(ver)
|
|
229
|
+
left.append(avatar, main)
|
|
230
|
+
|
|
231
|
+
const right = el('div', 'hqpu-card-end')
|
|
232
|
+
if (blocked) {
|
|
233
|
+
right.append(el('span', 'hqpu-blocked-badge', copy.ui.blockedBadge))
|
|
234
|
+
} else if (Array.isArray(item.changes) && item.changes.length > 0) {
|
|
235
|
+
const toggle = el('button', 'hqpu-log-toggle')
|
|
236
|
+
toggle.type = 'button'
|
|
237
|
+
toggle.dataset.open = open ? 'true' : 'false'
|
|
238
|
+
toggle.innerHTML = `${copy.ui.changelogBtn}<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6"/></svg>`
|
|
239
|
+
toggle.addEventListener('click', onToggle)
|
|
240
|
+
right.append(toggle)
|
|
223
241
|
}
|
|
224
|
-
head.append(
|
|
242
|
+
head.append(left, right)
|
|
225
243
|
card.append(head)
|
|
226
244
|
|
|
227
|
-
//
|
|
228
|
-
if (Array.isArray(item.changes) && item.changes.length > 0) {
|
|
229
|
-
|
|
230
|
-
const toggle = el('button', 'hqpu-btn hqpu-btn--ghost hqpu-btn--sm', copy.release.changesTitle)
|
|
231
|
-
toggle.type = 'button'
|
|
232
|
-
const list = el('ul', 'hqpu-changes-list')
|
|
233
|
-
list.style.display = 'none'
|
|
234
|
-
for (const change of item.changes) {
|
|
235
|
-
if (typeof change === 'string' && change.length > 0) {
|
|
236
|
-
list.append(el('li', '', change))
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
toggle.addEventListener('click', () => {
|
|
240
|
-
list.style.display = list.style.display === 'none' ? '' : 'none'
|
|
241
|
-
})
|
|
242
|
-
expander.append(toggle, list)
|
|
243
|
-
card.append(expander)
|
|
245
|
+
// Inline changelog drawer (actionable items only).
|
|
246
|
+
if (!blocked && Array.isArray(item.changes) && item.changes.length > 0) {
|
|
247
|
+
card.append(changelogDrawer(item, copy, open))
|
|
244
248
|
}
|
|
245
249
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
// now, never shown).
|
|
249
|
-
if (item.state === 'blocked') {
|
|
250
|
+
// Blocked reason line(s): mapped copy plus the server's full reasons/notes.
|
|
251
|
+
if (blocked) {
|
|
250
252
|
const reason = blockedReason(item, copy)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
const entries = [
|
|
254
|
+
...(reason !== undefined ? [reason] : []),
|
|
255
|
+
...[...(item.reasons ?? []), ...(item.notes ?? [])].filter(
|
|
256
|
+
(e): e is string => typeof e === 'string' && e.length > 0,
|
|
257
|
+
),
|
|
258
|
+
]
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
const line = el('p', 'hqpu-reason-line')
|
|
261
|
+
line.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5V8.5M8 11.25h.01M14.5 8a6.5 6.5 0 11-13 0 6.5 6.5 0 0113 0Z"/></svg>`
|
|
262
|
+
line.append(document.createTextNode(entry))
|
|
263
|
+
card.append(line)
|
|
258
264
|
}
|
|
259
265
|
}
|
|
260
266
|
return card
|
|
261
267
|
}
|
|
262
268
|
|
|
263
|
-
/** A section:
|
|
269
|
+
/** A section: head (title + count pill + expand-all toggle) then its cards. */
|
|
264
270
|
function group(
|
|
265
271
|
heading: string,
|
|
266
272
|
items: PluginUpdateItem[],
|
|
267
273
|
copy: UpdateCopy,
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
changelogOpen: Set<string>,
|
|
275
|
+
allOpen: boolean,
|
|
276
|
+
onToggle: (name: string) => void,
|
|
277
|
+
onToggleAll: () => void,
|
|
270
278
|
): HTMLElement | null {
|
|
271
279
|
if (items.length === 0) return null
|
|
272
280
|
const section = el('section', 'hqpu-group')
|
|
273
281
|
section.dataset.group = heading
|
|
274
282
|
|
|
275
|
-
const head = el('div', 'hqpu-
|
|
276
|
-
|
|
277
|
-
|
|
283
|
+
const head = el('div', 'hqpu-section-head')
|
|
284
|
+
const titleWrap = el('div', 'hqpu-section-title')
|
|
285
|
+
titleWrap.append(el('span', '', heading))
|
|
286
|
+
titleWrap.append(el('span', 'hqpu-count-pill', String(items.length)))
|
|
287
|
+
head.append(titleWrap)
|
|
288
|
+
// Expand-all only makes sense over actionable, changelog-bearing cards.
|
|
289
|
+
const actionable = items.some((i) => i.state !== 'blocked' && Array.isArray(i.changes) && i.changes.length > 0)
|
|
290
|
+
if (actionable) {
|
|
291
|
+
const all = el('button', 'hqpu-all-changelog', copy.ui.allChangelog)
|
|
292
|
+
all.type = 'button'
|
|
293
|
+
all.addEventListener('click', onToggleAll)
|
|
294
|
+
head.append(all)
|
|
295
|
+
}
|
|
278
296
|
|
|
279
297
|
const list = el('ul', 'hqpu-cards')
|
|
280
|
-
for (const item of items)
|
|
298
|
+
for (const item of items) {
|
|
299
|
+
list.append(packageCard(item, copy, allOpen || changelogOpen.has(item.name), () => onToggle(item.name)))
|
|
300
|
+
}
|
|
281
301
|
|
|
282
302
|
section.append(head, list)
|
|
283
303
|
return section
|
|
@@ -342,6 +362,19 @@ function formatEta(seconds: number | undefined): string | null {
|
|
|
342
362
|
return `${Math.floor(minutes / 60)} h ${minutes % 60} min`
|
|
343
363
|
}
|
|
344
364
|
|
|
365
|
+
/** "上次检查: N 分钟前" — relative time from an ISO timestamp. */
|
|
366
|
+
function relativeAgo(iso: string | null | undefined, copy: UpdateCopy): string | null {
|
|
367
|
+
if (typeof iso !== 'string' || iso.length === 0) return null
|
|
368
|
+
const then = new Date(iso).getTime()
|
|
369
|
+
if (!Number.isFinite(then)) return null
|
|
370
|
+
const diffMin = Math.round((Date.now() - then) / 60_000)
|
|
371
|
+
if (diffMin < 1) return copy.ui.agoJust
|
|
372
|
+
if (diffMin < 60) return copy.ui.agoMinutes(diffMin)
|
|
373
|
+
const hours = Math.round(diffMin / 60)
|
|
374
|
+
if (hours < 24) return copy.ui.agoHours(hours)
|
|
375
|
+
return copy.ui.agoDays(Math.round(hours / 24))
|
|
376
|
+
}
|
|
377
|
+
|
|
345
378
|
function jobKindLabel(job: UpdateJob, copy: UpdateCopy): string {
|
|
346
379
|
return job.kind === 'runtime' ? copy.job.kindRuntime : copy.job.kindPluginSet
|
|
347
380
|
}
|
|
@@ -377,41 +410,57 @@ function stepsList(job: UpdateJob, copy: UpdateCopy): HTMLElement | null {
|
|
|
377
410
|
return wrap
|
|
378
411
|
}
|
|
379
412
|
|
|
413
|
+
/** Map a job phase onto the 4-step indicator index (下载/校验/替换/重启). */
|
|
414
|
+
function stepIndex(phase: UpdateJob['phase']): number {
|
|
415
|
+
switch (phase) {
|
|
416
|
+
case 'downloading':
|
|
417
|
+
return 0
|
|
418
|
+
case 'verifying':
|
|
419
|
+
case 'resolving':
|
|
420
|
+
return 1
|
|
421
|
+
case 'staging':
|
|
422
|
+
case 'activating':
|
|
423
|
+
case 'awaiting-host':
|
|
424
|
+
return 2
|
|
425
|
+
case 'restarting':
|
|
426
|
+
case 'verifying-after-restart':
|
|
427
|
+
return 3
|
|
428
|
+
case 'queued':
|
|
429
|
+
return -1
|
|
430
|
+
default:
|
|
431
|
+
return -1
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
380
435
|
/**
|
|
381
|
-
* The ACTIVE-JOB
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
436
|
+
* The ACTIVE-JOB banner per the mockup: pulsing brand dot + job title with the
|
|
437
|
+
* target version, mono percentage, a gradient bar only when `percent != null`,
|
|
438
|
+
* phase + speed line ("3.1 MB/s (286MB / 450MB)"), and a 4-step indicator row
|
|
439
|
+
* (done=emerald, active=pulsing brand, upcoming=muted).
|
|
385
440
|
*/
|
|
386
441
|
function activeJobView(job: UpdateJob, copy: UpdateCopy): HTMLElement {
|
|
387
442
|
const section = el('section', 'hqpu-group')
|
|
388
443
|
section.dataset.group = 'active-job'
|
|
389
444
|
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
phaseLine.append(el('span', '', transition))
|
|
406
|
-
card.append(phaseLine)
|
|
407
|
-
|
|
408
|
-
if (typeof job.message === 'string' && job.message.length > 0) {
|
|
409
|
-
card.append(el('p', 'hqpu-status', job.message))
|
|
445
|
+
const card = el('div', 'hqpu-active')
|
|
446
|
+
|
|
447
|
+
// Top row: pulse dot + title, mono percent.
|
|
448
|
+
const top = el('div', 'hqpu-active-top')
|
|
449
|
+
const titleWrap = el('div', 'hqpu-active-title')
|
|
450
|
+
const pulse = el('span', 'hqpu-pulse-dot')
|
|
451
|
+
pulse.append(el('span', 'hqpu-pulse-ring'))
|
|
452
|
+
const title =
|
|
453
|
+
job.kind === 'runtime'
|
|
454
|
+
? copy.ui.jobRuntimeTitle(job.targetVersion)
|
|
455
|
+
: copy.ui.jobPluginTitle(job.targetVersion)
|
|
456
|
+
titleWrap.append(pulse, el('span', '', title))
|
|
457
|
+
const pct = job.progress.percent
|
|
458
|
+
if (pct !== null) {
|
|
459
|
+
top.append(el('span', 'hqpu-active-percent', `${Math.round(pct)}%`))
|
|
410
460
|
}
|
|
461
|
+
card.append(top)
|
|
411
462
|
|
|
412
|
-
//
|
|
413
|
-
// 100% before the job is terminal.
|
|
414
|
-
const pct = job.progress.percent
|
|
463
|
+
// Gradient bar — only when the server publishes a real percentage.
|
|
415
464
|
if (pct !== null) {
|
|
416
465
|
const bar = el('div', 'hqpu-progress')
|
|
417
466
|
const fill = el('div', 'hqpu-progress-fill')
|
|
@@ -420,23 +469,59 @@ function activeJobView(job: UpdateJob, copy: UpdateCopy): HTMLElement {
|
|
|
420
469
|
card.append(bar)
|
|
421
470
|
}
|
|
422
471
|
|
|
423
|
-
|
|
424
|
-
|
|
472
|
+
// Phase text + telemetry line: "3.1 MB/s (286MB / 450MB)".
|
|
473
|
+
const meta = el('div', 'hqpu-active-meta')
|
|
474
|
+
const phaseText =
|
|
475
|
+
typeof job.message === 'string' && job.message.length > 0 ? job.message : phaseLabel(job, copy)
|
|
476
|
+
meta.append(el('span', '', phaseText))
|
|
477
|
+
const speed = formatBytes(job.progress.bytesPerSecond)
|
|
425
478
|
const done = formatBytes(job.progress.bytesDone)
|
|
426
479
|
const total = formatBytes(job.progress.bytesTotal)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (
|
|
480
|
+
const telemetry =
|
|
481
|
+
speed !== null && done !== null && total !== null
|
|
482
|
+
? `${speed}/s (${done} / ${total})`
|
|
483
|
+
: speed !== null
|
|
484
|
+
? `${speed}/s`
|
|
485
|
+
: done !== null && total !== null
|
|
486
|
+
? copy.job.bytes(done, total)
|
|
487
|
+
: ''
|
|
488
|
+
if (telemetry.length > 0) meta.append(el('span', '', telemetry))
|
|
489
|
+
card.append(meta)
|
|
490
|
+
|
|
491
|
+
// previous → target transition + what happens next (restart / elevation),
|
|
492
|
+
// and the honest "closing does not cancel" note.
|
|
493
|
+
const transition = el('div', 'hqpu-active-meta')
|
|
494
|
+
transition.append(
|
|
495
|
+
el(
|
|
496
|
+
'span',
|
|
497
|
+
'',
|
|
498
|
+
job.previousVersion !== null && job.previousVersion.length > 0
|
|
499
|
+
? `${job.previousVersion} → ${job.targetVersion}`
|
|
500
|
+
: job.targetVersion,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
transition.append(
|
|
504
|
+
el('span', '', job.kind === 'runtime' ? copy.job.willSwapRuntime : copy.job.willRestartDsh),
|
|
505
|
+
)
|
|
506
|
+
card.append(transition)
|
|
507
|
+
card.append(el('p', 'hqpu-job-meta', copy.job.closeNote))
|
|
508
|
+
|
|
509
|
+
// 4-step indicator: done=emerald, active=pulsing brand, upcoming=muted.
|
|
510
|
+
const current = stepIndex(job.phase)
|
|
511
|
+
const stepsRow = el('div', 'hqpu-job-steps4')
|
|
512
|
+
copy.ui.jobSteps.forEach((label: string, index: number) => {
|
|
513
|
+
const cls =
|
|
514
|
+
current === -1
|
|
515
|
+
? 'hqpu-job-step--todo'
|
|
516
|
+
: index < current
|
|
517
|
+
? 'hqpu-job-step--done'
|
|
518
|
+
: index === current
|
|
519
|
+
? 'hqpu-job-step--active'
|
|
520
|
+
: 'hqpu-job-step--todo'
|
|
521
|
+
stepsRow.append(el('div', cls, label))
|
|
522
|
+
})
|
|
523
|
+
card.append(stepsRow)
|
|
436
524
|
|
|
437
|
-
// What happens next — restart / runtime swap + elevation implications.
|
|
438
|
-
card.append(el('p', 'hqpu-status', job.kind === 'runtime' ? copy.job.willSwapRuntime : copy.job.willRestartDsh))
|
|
439
|
-
card.append(el('p', 'hqpu-status', copy.job.closeNote))
|
|
440
525
|
section.append(card)
|
|
441
526
|
return section
|
|
442
527
|
}
|
|
@@ -785,6 +870,11 @@ function sourceEditor(
|
|
|
785
870
|
const labels = sourceLabels(copy, kind)
|
|
786
871
|
const block = el('div', `hqpu-source-editor hqpu-source-editor--${kind}`)
|
|
787
872
|
if (!actions.sourceEditing) {
|
|
873
|
+
// Read mode: show the URL currently in effect (mono, muted), then edit.
|
|
874
|
+
const urlLine = el('p', 'hqpu-source-current-url', source.url)
|
|
875
|
+
urlLine.style.cssText =
|
|
876
|
+
'margin:2px 0 6px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#a1a1aa;word-break:break-all;'
|
|
877
|
+
block.append(urlLine)
|
|
788
878
|
block.append(el('p', 'hqpu-status hqpu-source-hint', labels.hint))
|
|
789
879
|
const edit = el('button', 'hqpu-btn hqpu-btn--outline hqpu-btn--sm', labels.edit)
|
|
790
880
|
edit.type = 'button'
|
|
@@ -870,172 +960,164 @@ function pluginSourceGroup(
|
|
|
870
960
|
return section
|
|
871
961
|
}
|
|
872
962
|
|
|
873
|
-
/**
|
|
874
|
-
|
|
963
|
+
/**
|
|
964
|
+
* The host's own report of the last runtime swap. Rolled-back / failed renders
|
|
965
|
+
* as an AMBER alert card (title, old→new body, errorCode chip, 忽略 dismiss);
|
|
966
|
+
* succeeded renders as a subtle green inline line — not a loud banner. The
|
|
967
|
+
* plugin has NO log-open affordance, so "查看诊断日志" is omitted (never faked).
|
|
968
|
+
*/
|
|
969
|
+
function runtimeResultAlert(
|
|
875
970
|
report: PluginUpdateReport,
|
|
876
971
|
copy: UpdateCopy,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
972
|
+
dismissed: boolean,
|
|
973
|
+
onDismiss: () => void,
|
|
974
|
+
): HTMLElement | null {
|
|
975
|
+
const lastResult = report.runtime.lastResult
|
|
976
|
+
if (lastResult === null || lastResult === undefined) return null
|
|
977
|
+
|
|
978
|
+
// Green inline confirmation — quiet by design.
|
|
979
|
+
if (lastResult.state === 'succeeded') {
|
|
980
|
+
const line = el('p', 'hqpu-alert-green')
|
|
981
|
+
line.dataset.role = 'runtime-success'
|
|
982
|
+
line.textContent = copy.ui.successInline
|
|
983
|
+
return line
|
|
984
|
+
}
|
|
985
|
+
if (dismissed) return null
|
|
890
986
|
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
987
|
+
const section = el('section', 'hqpu-group')
|
|
988
|
+
section.dataset.group = 'runtime-alert'
|
|
989
|
+
const card = el('div', 'hqpu-alert hqpu-alert--amber')
|
|
990
|
+
card.innerHTML = WARNING_ICON(14)
|
|
991
|
+
|
|
992
|
+
const body = el('div')
|
|
993
|
+
const title = el('div', 'hqpu-alert-title')
|
|
994
|
+
title.textContent = lastResult.state === 'failed' ? copy.ui.failedTitle : copy.ui.rollbackTitle
|
|
995
|
+
body.append(title)
|
|
996
|
+
|
|
997
|
+
const oldVer = lastResult.installedVersion ?? lastResult.targetVersion ?? '—'
|
|
998
|
+
const code = typeof lastResult.errorCode === 'string' && lastResult.errorCode.length > 0
|
|
999
|
+
? lastResult.errorCode
|
|
1000
|
+
: '—'
|
|
1001
|
+
const text =
|
|
1002
|
+
lastResult.state === 'failed'
|
|
1003
|
+
? copy.ui.failedBody(oldVer, code)
|
|
1004
|
+
: copy.ui.rollbackBody(oldVer, code)
|
|
1005
|
+
// Build the body as text + an inline <code> chip for the error code.
|
|
1006
|
+
const bodyP = el('p', 'hqpu-alert-body')
|
|
1007
|
+
const textOnly =
|
|
1008
|
+
lastResult.state === 'failed'
|
|
1009
|
+
? copy.ui.failedBody(oldVer, '')
|
|
1010
|
+
: copy.ui.rollbackBody(oldVer, '')
|
|
1011
|
+
// Strip the trailing "code" placeholder we appended; re-add as a chip.
|
|
1012
|
+
bodyP.append(document.createTextNode(textOnly.replace(/\s*$/, '')))
|
|
1013
|
+
if (typeof lastResult.errorCode === 'string' && lastResult.errorCode.length > 0) {
|
|
1014
|
+
bodyP.append(document.createTextNode(' '))
|
|
1015
|
+
const chip = el('code', '', lastResult.errorCode)
|
|
1016
|
+
bodyP.append(chip)
|
|
1017
|
+
}
|
|
1018
|
+
if (typeof lastResult.message === 'string' && lastResult.message.length > 0) {
|
|
1019
|
+
bodyP.append(el('br'), document.createTextNode(lastResult.message))
|
|
903
1020
|
}
|
|
1021
|
+
body.append(bodyP)
|
|
904
1022
|
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
const
|
|
1023
|
+
// Dismiss action (no log button — the plugin has no log-open affordance).
|
|
1024
|
+
const actions = el('div', 'hqpu-alert-actions')
|
|
1025
|
+
const dismiss = el('button', 'hqpu-alert-dismiss', copy.ui.dismiss)
|
|
1026
|
+
dismiss.type = 'button'
|
|
1027
|
+
dismiss.addEventListener('click', onDismiss)
|
|
1028
|
+
actions.append(dismiss)
|
|
1029
|
+
body.append(actions)
|
|
908
1030
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
row(copy.runtime.source, copy.runtime.notConfigured)
|
|
914
|
-
}
|
|
915
|
-
} else {
|
|
916
|
-
row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown)
|
|
917
|
-
if (runtime.updateAvailable && latestVersion !== null) {
|
|
918
|
-
row(copy.runtime.latest, latestVersion)
|
|
919
|
-
// v2 manifest metadata: download size before committing to the update.
|
|
920
|
-
const latestEntry = runtime.latest
|
|
921
|
-
const size = typeof latestEntry?.sizeBytes === 'number' ? formatMb(latestEntry.sizeBytes) : ''
|
|
922
|
-
if (size.length > 0) row(copy.runtime.sizeLabel, size)
|
|
923
|
-
}
|
|
924
|
-
if (report.manifestVersion !== null) {
|
|
925
|
-
row(copy.runtime.set, report.manifestVersion, true)
|
|
926
|
-
}
|
|
927
|
-
if (typeof runtime.checkedAt === 'string' && runtime.checkedAt.length > 0) {
|
|
928
|
-
row(copy.runtime.checkedAt, new Date(runtime.checkedAt).toLocaleString())
|
|
929
|
-
}
|
|
930
|
-
if (source !== null) {
|
|
931
|
-
sourceFactRow(facts, source, copy)
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
section.append(facts)
|
|
1031
|
+
card.append(body)
|
|
1032
|
+
section.append(card)
|
|
1033
|
+
return section
|
|
1034
|
+
}
|
|
935
1035
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1036
|
+
/** The HQ Edge runtime engine card per the mockup: current chip, "新版本 vY
|
|
1037
|
+
* 可用" with a 关键修复 tag when breaking/notes exist, size+summary line, a
|
|
1038
|
+
* 升级核心 button (existing arm→confirm flow), and the amber restart note. */
|
|
1039
|
+
function runtimeEngineCard(
|
|
1040
|
+
report: PluginUpdateReport,
|
|
1041
|
+
copy: UpdateCopy,
|
|
1042
|
+
actions: RuntimeActions,
|
|
1043
|
+
): HTMLElement | null {
|
|
1044
|
+
const runtime = report.runtime
|
|
1045
|
+
const latestVersion = runtimeLatestVersion(runtime)
|
|
1046
|
+
const card = el('section', 'hqpu-engine')
|
|
1047
|
+
card.dataset.group = 'runtime'
|
|
1048
|
+
|
|
1049
|
+
const head = el('div', 'hqpu-engine-head')
|
|
1050
|
+
const titleWrap = el('div', 'hqpu-engine-title')
|
|
1051
|
+
titleWrap.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.33333 2L4.66667 8H8L6.66667 14L11.33333 8H8L9.33333 2Z"/></svg>`
|
|
1052
|
+
titleWrap.append(el('span', '', copy.ui.runtimeEngine))
|
|
1053
|
+
head.append(titleWrap)
|
|
1054
|
+
if (typeof runtime.currentVersion === 'string' && runtime.currentVersion.length > 0) {
|
|
1055
|
+
head.append(el('span', 'hqpu-chip', copy.ui.currentChip(runtime.currentVersion)))
|
|
942
1056
|
}
|
|
1057
|
+
card.append(head)
|
|
943
1058
|
|
|
944
|
-
//
|
|
945
|
-
// Only offered when hq-edge reports a newer distribution AND named its
|
|
946
|
-
// version: the request needs `targetVersion`, so an entry without one has
|
|
947
|
-
// nothing to send.
|
|
1059
|
+
// Update row (only when a newer distribution is known).
|
|
948
1060
|
if (runtime.updateAvailable && latestVersion !== null) {
|
|
949
|
-
const row = el('div', 'hqpu-
|
|
1061
|
+
const row = el('div', 'hqpu-engine-update-row')
|
|
1062
|
+
const left = el('div')
|
|
1063
|
+
const newLine = el('div', 'hqpu-engine-new-line')
|
|
1064
|
+
newLine.append(el('span', '', copy.ui.newVersion(latestVersion)))
|
|
1065
|
+
const latestEntry = runtime.latest
|
|
1066
|
+
const hasCritical =
|
|
1067
|
+
(Array.isArray(latestEntry?.breaking) && (latestEntry?.breaking?.length ?? 0) > 0) ||
|
|
1068
|
+
(Array.isArray(latestEntry?.notes) && (latestEntry?.notes?.length ?? 0) > 0)
|
|
1069
|
+
if (hasCritical) newLine.append(el('span', 'hqpu-tag--brand', copy.ui.breakingTag))
|
|
1070
|
+
left.append(newLine)
|
|
1071
|
+
const size = typeof latestEntry?.sizeBytes === 'number' ? formatMb(latestEntry.sizeBytes) : ''
|
|
1072
|
+
const summary = typeof latestEntry?.summary === 'string' ? latestEntry.summary : ''
|
|
1073
|
+
if (size.length > 0 || summary.length > 0) {
|
|
1074
|
+
left.append(el('p', 'hqpu-engine-size', copy.ui.sizeLine(size, summary)))
|
|
1075
|
+
}
|
|
1076
|
+
row.append(left)
|
|
1077
|
+
|
|
1078
|
+
// Upgrade action: arm → confirm, accepted/busy states preserved.
|
|
950
1079
|
if (actions.accepted) {
|
|
951
|
-
|
|
952
|
-
// "update" here could only produce a transport error.
|
|
953
|
-
const line = el('span', 'hqpu-status hqpu-status--dot')
|
|
954
|
-
line.setAttribute('role', 'status')
|
|
955
|
-
line.append(el('span', 'hqpu-spinner'), el('span', '', copy.runtime.updateAccepted))
|
|
956
|
-
row.append(line)
|
|
1080
|
+
row.append(el('span', 'hqpu-status', copy.runtime.updateAccepted))
|
|
957
1081
|
} else if (actions.busy) {
|
|
958
|
-
|
|
959
|
-
// minutes. Say what is happening instead of an open-ended spinner.
|
|
960
|
-
const line = el('span', 'hqpu-status hqpu-status--dot')
|
|
961
|
-
line.setAttribute('role', 'status')
|
|
962
|
-
line.append(el('span', 'hqpu-spinner'), el('span', '', copy.runtime.updating))
|
|
963
|
-
row.append(line)
|
|
1082
|
+
row.append(el('span', 'hqpu-status', copy.runtime.updating))
|
|
964
1083
|
} else if (actions.armed) {
|
|
965
|
-
|
|
966
|
-
|
|
1084
|
+
const confirmWrap = el('div')
|
|
1085
|
+
confirmWrap.append(el('p', 'hqpu-status', copy.runtime.updateConfirm(latestVersion)))
|
|
1086
|
+
const yes = el('button', 'hqpu-btn hqpu-btn--engine', copy.confirmInstall)
|
|
967
1087
|
yes.type = 'button'
|
|
968
1088
|
yes.dataset.runtimeConfirm = ''
|
|
969
1089
|
yes.addEventListener('click', actions.onConfirm)
|
|
970
|
-
const no = el('button', 'hqpu-btn hqpu-btn--
|
|
1090
|
+
const no = el('button', 'hqpu-btn hqpu-btn--engine', copy.cancel)
|
|
971
1091
|
no.type = 'button'
|
|
972
1092
|
no.addEventListener('click', actions.onCancel)
|
|
973
|
-
|
|
1093
|
+
confirmWrap.append(yes, no)
|
|
1094
|
+
row.append(confirmWrap)
|
|
974
1095
|
} else {
|
|
975
|
-
const start = el('button', 'hqpu-btn hqpu-btn--
|
|
1096
|
+
const start = el('button', 'hqpu-btn hqpu-btn--engine', copy.ui.upgradeCore)
|
|
976
1097
|
start.type = 'button'
|
|
977
1098
|
start.dataset.runtimeUpdate = ''
|
|
978
|
-
start.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.runtime.update}</span>`
|
|
979
1099
|
start.addEventListener('click', actions.onArm)
|
|
980
1100
|
row.append(start)
|
|
981
1101
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1102
|
+
card.append(row)
|
|
1103
|
+
const note = el('p', 'hqpu-engine-note')
|
|
1104
|
+
note.innerHTML =
|
|
1105
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5L14 13H2L8 2.5Z"/></svg><span>${copy.ui.runtimeRestartNote}</span>`
|
|
1106
|
+
card.append(note)
|
|
1107
|
+
} else if (typeof runtime.lastError === 'string' && runtime.lastError.length > 0) {
|
|
1108
|
+
card.append(el('p', 'hqpu-status', `${copy.lastError}: ${runtime.lastError}`))
|
|
987
1109
|
}
|
|
988
|
-
|
|
989
|
-
// The host's own report of the last runtime swap (succeeded / rolled_back /
|
|
990
|
-
// failed). Before this it was read from disk by hq-edge and never shown — a
|
|
991
|
-
// rolled-back swap was indistinguishable from "no update ever happened".
|
|
992
|
-
const lastResult = runtime.lastResult
|
|
993
|
-
if (lastResult !== null && lastResult !== undefined) {
|
|
994
|
-
const state = lastResult.state
|
|
995
|
-
const line = el('p', 'hqpu-status hqpu-status--dot')
|
|
996
|
-
line.setAttribute('role', 'status')
|
|
997
|
-
const dot = el('span', 'hqpu-dot')
|
|
998
|
-
dot.dataset.state = state === 'succeeded' ? 'done' : state === 'failed' ? 'error' : 'warning'
|
|
999
|
-
const head =
|
|
1000
|
-
state === 'succeeded'
|
|
1001
|
-
? copy.runtime.lastResult.succeeded
|
|
1002
|
-
: state === 'failed'
|
|
1003
|
-
? copy.runtime.lastResult.failed
|
|
1004
|
-
: copy.runtime.lastResult.rolledBack
|
|
1005
|
-
const parts = [head]
|
|
1006
|
-
if (lastResult.targetVersion) parts.push(`→ ${lastResult.targetVersion}`)
|
|
1007
|
-
if (lastResult.installedVersion) parts.push(`(${lastResult.installedVersion})`)
|
|
1008
|
-
if (lastResult.at) parts.push(new Date(lastResult.at).toLocaleString())
|
|
1009
|
-
line.append(dot, el('span', '', parts.join(' ')))
|
|
1010
|
-
section.append(line)
|
|
1011
|
-
if (typeof lastResult.message === 'string' && lastResult.message.length > 0) {
|
|
1012
|
-
section.append(el('p', 'hqpu-status', lastResult.message))
|
|
1013
|
-
}
|
|
1014
|
-
if (typeof lastResult.errorCode === 'string' && lastResult.errorCode.length > 0) {
|
|
1015
|
-
section.append(el('p', 'hqpu-status hqpu-job-meta', lastResult.errorCode))
|
|
1016
|
-
}
|
|
1017
|
-
if (state !== 'succeeded') {
|
|
1018
|
-
section.append(el('p', 'hqpu-status hqpu-job-meta', copy.runtime.lastResult.elevation))
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
if (typeof runtime.lastError === 'string' && runtime.lastError.length > 0) {
|
|
1023
|
-
const line = el('p', 'hqpu-status hqpu-status--dot')
|
|
1024
|
-
line.setAttribute('role', 'status')
|
|
1025
|
-
const dot = el('span', 'hqpu-dot')
|
|
1026
|
-
dot.dataset.state = 'error'
|
|
1027
|
-
line.append(dot, el('span', '', `${copy.lastError}: ${runtime.lastError}`))
|
|
1028
|
-
section.append(line)
|
|
1029
|
-
}
|
|
1030
|
-
return section
|
|
1110
|
+
return card
|
|
1031
1111
|
}
|
|
1032
1112
|
|
|
1033
1113
|
function renderList(
|
|
1034
1114
|
section: HTMLElement,
|
|
1035
1115
|
report: PluginUpdateReport | null,
|
|
1036
1116
|
copy: UpdateCopy,
|
|
1037
|
-
|
|
1038
|
-
|
|
1117
|
+
changelogOpen: Set<string>,
|
|
1118
|
+
allOpen: boolean,
|
|
1119
|
+
onToggleCard: (name: string) => void,
|
|
1120
|
+
onToggleAll: () => void,
|
|
1039
1121
|
onRetryCheck: () => void,
|
|
1040
1122
|
): void {
|
|
1041
1123
|
section.textContent = ''
|
|
@@ -1071,7 +1153,7 @@ function renderList(
|
|
|
1071
1153
|
[copy.group.current, byState('current')],
|
|
1072
1154
|
]
|
|
1073
1155
|
for (const [heading, items] of order) {
|
|
1074
|
-
const node = group(heading, items, copy,
|
|
1156
|
+
const node = group(heading, items, copy, changelogOpen, allOpen, onToggleCard, onToggleAll)
|
|
1075
1157
|
if (node !== null) section.append(node)
|
|
1076
1158
|
}
|
|
1077
1159
|
}
|
|
@@ -1081,10 +1163,15 @@ function renderRuntime(
|
|
|
1081
1163
|
report: PluginUpdateReport | null,
|
|
1082
1164
|
copy: UpdateCopy,
|
|
1083
1165
|
actions: RuntimeActions,
|
|
1166
|
+
dismissedAlert: boolean,
|
|
1167
|
+
onDismissAlert: () => void,
|
|
1084
1168
|
): void {
|
|
1085
1169
|
section.textContent = ''
|
|
1086
1170
|
if (report === null) return
|
|
1087
|
-
|
|
1171
|
+
const alert = runtimeResultAlert(report, copy, dismissedAlert, onDismissAlert)
|
|
1172
|
+
if (alert !== null) section.append(alert)
|
|
1173
|
+
const engine = runtimeEngineCard(report, copy, actions)
|
|
1174
|
+
if (engine !== null) section.append(engine)
|
|
1088
1175
|
}
|
|
1089
1176
|
|
|
1090
1177
|
/** The plugin-source group, above the plugin groups, hidden behind an advanced
|
|
@@ -1157,66 +1244,98 @@ function showToast(message: string, tone: 'success' | 'error'): void {
|
|
|
1157
1244
|
}, TOAST_HOLD_MS)
|
|
1158
1245
|
}
|
|
1159
1246
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1247
|
+
/**
|
|
1248
|
+
* The collapsible "更新源配置 (高级)" panel (gear-toggled). It hosts the
|
|
1249
|
+
* EXISTING plugin-source and runtime-source editors (POST /plugin-source,
|
|
1250
|
+
* POST /runtime-source, save→check→refresh already wired). No new endpoints:
|
|
1251
|
+
* if a manifest source has no data, that field is omitted rather than faked.
|
|
1252
|
+
*/
|
|
1253
|
+
function renderSettingsPanel(
|
|
1254
|
+
panel: HTMLElement,
|
|
1162
1255
|
report: PluginUpdateReport | null,
|
|
1163
1256
|
copy: UpdateCopy,
|
|
1164
|
-
|
|
1165
|
-
|
|
1257
|
+
open: boolean,
|
|
1258
|
+
pluginActions: SourceEditorActions,
|
|
1259
|
+
runtimeActions: SourceEditorActions,
|
|
1166
1260
|
): void {
|
|
1167
|
-
|
|
1168
|
-
|
|
1261
|
+
panel.textContent = ''
|
|
1262
|
+
panel.style.display = open ? '' : 'none'
|
|
1263
|
+
if (!open || report === null) return
|
|
1264
|
+
|
|
1265
|
+
const row = el('div', 'hqpu-settings-row')
|
|
1266
|
+
row.append(el('div', 'hqpu-settings-title', copy.ui.settingsTitle))
|
|
1267
|
+
// Origin badge: custom override (amber) vs the default configured source (green).
|
|
1268
|
+
const pluginSource = normalizeSource(report.pluginSource)
|
|
1269
|
+
const runtimeSource = normalizeSource(report.runtime.source)
|
|
1270
|
+
const overridden = (pluginSource?.override ?? false) || (runtimeSource?.override ?? false)
|
|
1271
|
+
const badge = el('span', 'hqpu-origin-badge', overridden ? copy.ui.originCustom : copy.ui.originDefault)
|
|
1272
|
+
badge.dataset.origin = overridden ? 'override' : 'config'
|
|
1273
|
+
row.append(badge)
|
|
1274
|
+
panel.append(row)
|
|
1275
|
+
|
|
1276
|
+
if (pluginSource !== null) {
|
|
1277
|
+
panel.append(el('label', '', copy.ui.pluginUrlLabel))
|
|
1278
|
+
panel.append(sourceEditor('plugin', pluginSource, copy, pluginActions))
|
|
1279
|
+
}
|
|
1280
|
+
if (runtimeSource !== null) {
|
|
1281
|
+
panel.append(el('label', '', copy.ui.runtimeUrlLabel))
|
|
1282
|
+
panel.append(sourceEditor('runtime', runtimeSource, copy, runtimeActions))
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1169
1285
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1286
|
+
/**
|
|
1287
|
+
* The sticky footer: left meta ("准备安装 N 项兼容插件" / "版本集: vX"), a
|
|
1288
|
+
* full-width primary "一键更新全套插件 (vX)" (install = one durable job),
|
|
1289
|
+
* a secondary 忽略 wired to the existing ignore/unignore affordance, and the
|
|
1290
|
+
* footnote. The count EXCLUDES blocked / ignored / already-current entries.
|
|
1291
|
+
*/
|
|
1292
|
+
function renderFooter(
|
|
1293
|
+
footer: HTMLElement,
|
|
1294
|
+
installBtn: HTMLButtonElement,
|
|
1295
|
+
ignoreBtn: HTMLButtonElement,
|
|
1296
|
+
countEl: HTMLElement,
|
|
1297
|
+
setEl: HTMLElement,
|
|
1298
|
+
report: PluginUpdateReport | null,
|
|
1299
|
+
copy: UpdateCopy,
|
|
1300
|
+
installing: boolean,
|
|
1301
|
+
store: UpdateStoreLike,
|
|
1302
|
+
): void {
|
|
1303
|
+
if (report === null) {
|
|
1304
|
+
countEl.textContent = ''
|
|
1305
|
+
setEl.textContent = ''
|
|
1306
|
+
installBtn.disabled = true
|
|
1307
|
+
ignoreBtn.style.display = 'none'
|
|
1175
1308
|
return
|
|
1176
1309
|
}
|
|
1177
1310
|
|
|
1178
1311
|
const actionable = report.items.filter(
|
|
1179
1312
|
(item) => item.state === 'update' || item.state === 'install',
|
|
1180
1313
|
).length
|
|
1181
|
-
// Every write needs an evaluated set; hq-edge reports `null` until it has
|
|
1182
|
-
// read a manifest, and posting `null` would only earn a 400.
|
|
1183
1314
|
const hasSet = report.manifestVersion !== null
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
bar.append(installBtn)
|
|
1315
|
+
const setVersion: string = report.manifestVersion ?? ''
|
|
1316
|
+
|
|
1317
|
+
countEl.textContent = copy.ui.footerCount(actionable)
|
|
1318
|
+
setEl.textContent = hasSet ? copy.ui.footerSet(setVersion) : ''
|
|
1319
|
+
|
|
1320
|
+
// Install-all: honest set-level label; disabled while busy or nothing to do.
|
|
1321
|
+
installBtn.textContent = hasSet
|
|
1322
|
+
? copy.ui.installAllSet(setVersion)
|
|
1323
|
+
: copy.ui.installAllSet('—')
|
|
1324
|
+
installBtn.disabled = installing || actionable === 0 || !hasSet || report.ignoredSet
|
|
1325
|
+
installBtn.dataset.busy = installing ? 'true' : 'false'
|
|
1326
|
+
installBtn.onclick = () => {
|
|
1327
|
+
void runInstall(store, copy)
|
|
1198
1328
|
}
|
|
1199
1329
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
})
|
|
1210
|
-
bar.append(unignore)
|
|
1211
|
-
} else if (actionable > 0) {
|
|
1212
|
-
const ignore = el('button', 'hqpu-btn hqpu-btn--ghost hqpu-btn--sm', copy.ignore)
|
|
1213
|
-
ignore.type = 'button'
|
|
1214
|
-
ignore.addEventListener('click', () => {
|
|
1215
|
-
void ignoreOffer(report.manifestVersion as string)
|
|
1216
|
-
.then(() => store.refresh())
|
|
1217
|
-
.catch(() => undefined)
|
|
1218
|
-
})
|
|
1219
|
-
bar.append(ignore)
|
|
1330
|
+
// Secondary ignore button: only when there is an evaluated set to ignore.
|
|
1331
|
+
ignoreBtn.style.display = hasSet ? '' : 'none'
|
|
1332
|
+
ignoreBtn.textContent = report.ignoredSet ? copy.unignore : copy.ui.ignoreBtn
|
|
1333
|
+
ignoreBtn.onclick = () => {
|
|
1334
|
+
const version = report.manifestVersion as string
|
|
1335
|
+
const action = report.ignoredSet ? unignoreOffer(version) : ignoreOffer(version)
|
|
1336
|
+
void action
|
|
1337
|
+
.then(() => store.refresh())
|
|
1338
|
+
.catch(() => undefined)
|
|
1220
1339
|
}
|
|
1221
1340
|
}
|
|
1222
1341
|
|
|
@@ -1244,51 +1363,71 @@ export function openUpdateView(store: UpdateStoreLike, copy: UpdateCopy): void {
|
|
|
1244
1363
|
panel.setAttribute('aria-label', copy.viewTitle)
|
|
1245
1364
|
panel.tabIndex = -1
|
|
1246
1365
|
|
|
1247
|
-
// ──
|
|
1366
|
+
// ── Sticky header: brand icon + title + relative checked time, refresh +
|
|
1367
|
+
// gear (advanced source settings) + close ──
|
|
1248
1368
|
const head = el('div', 'hqpu-head')
|
|
1249
1369
|
const headMain = el('div', 'hqpu-head-main')
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
info.innerHTML = INFO_ICON(11)
|
|
1258
|
-
intro.append(info)
|
|
1259
|
-
headMain.append(intro)
|
|
1370
|
+
const brandIcon = el('span', 'hqpu-brand-icon')
|
|
1371
|
+
brandIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5.6 2.6a6 6 0 015.6 1.6M12.4 13.4a6 6 0 01-5.6-1.6M10.6 2V5.4H7.2M5.4 14V10.6H8.8"/></svg>`
|
|
1372
|
+
const titleBlock = el('div', 'hqpu-title-block')
|
|
1373
|
+
titleBlock.append(el('h1', 'hqpu-title', copy.ui.title))
|
|
1374
|
+
const sub = el('p', 'hqpu-sub')
|
|
1375
|
+
titleBlock.append(sub)
|
|
1376
|
+
headMain.append(brandIcon, titleBlock)
|
|
1260
1377
|
|
|
1261
1378
|
const toolbar = el('div', 'hqpu-toolbar')
|
|
1262
1379
|
const refreshBtn = iconButton('hqpu-icon-btn', REFRESH_ICON(16), copy.refresh, () => {
|
|
1263
|
-
void store.
|
|
1380
|
+
void store.check()
|
|
1264
1381
|
})
|
|
1265
1382
|
toolbar.append(refreshBtn)
|
|
1383
|
+
const gearBtn = iconButton(
|
|
1384
|
+
'hqpu-icon-btn',
|
|
1385
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2.2"/><path d="M13.5 9.2v-2.4l-1.8-.6a4.4 4.4 0 00-.5-1.2l.9-1.7-1.7-1.7-1.7.9a4.4 4.4 0 00-1.2-.5L7.2 2.5H4.8l-.6 1.8a4.4 4.4 0 00-1.2.5l-1.7-.9-1.7 1.7.9 1.7a4.4 4.4 0 00-.5 1.2L2.5 7.2v2.4l1.8.6a4.4 4.4 0 00.5 1.2l-.9 1.7 1.7 1.7 1.7-.9a4.4 4.4 0 001.2.5l.6 1.8h2.4l.6-1.8a4.4 4.4 0 001.2-.5l1.7.9 1.7-1.7-.9-1.7a4.4 4.4 0 00.5-1.2l1.8-.6z"/></svg>`,
|
|
1386
|
+
copy.ui.gearTooltip,
|
|
1387
|
+
() => {
|
|
1388
|
+
settingsOpen = !settingsOpen
|
|
1389
|
+
render()
|
|
1390
|
+
},
|
|
1391
|
+
)
|
|
1392
|
+
toolbar.append(gearBtn)
|
|
1266
1393
|
const closeBtn = iconButton('hqpu-icon-btn', CLOSE_ICON(16), copy.close, () => {
|
|
1267
1394
|
onClose()
|
|
1268
1395
|
})
|
|
1269
1396
|
toolbar.append(closeBtn)
|
|
1270
|
-
|
|
1271
1397
|
head.append(headMain, toolbar)
|
|
1272
1398
|
panel.append(head)
|
|
1273
1399
|
|
|
1274
|
-
// ──
|
|
1400
|
+
// ── Collapsible advanced settings panel (gear-toggled) ──
|
|
1401
|
+
const settingsPanel = el('div', 'hqpu-settings')
|
|
1402
|
+
panel.append(settingsPanel)
|
|
1403
|
+
|
|
1404
|
+
// ── Scrolling body: the narrow column ──
|
|
1275
1405
|
const scroll = el('div', 'hqpu-scroll')
|
|
1276
1406
|
const column = el('div', 'hqpu-column')
|
|
1277
1407
|
// The durable-job view leads: an active install/runtime update is resumed on
|
|
1278
1408
|
// open BEFORE anything else, and a recent terminal result is shown here too.
|
|
1279
1409
|
const jobSection = el('div', 'hqpu-stack')
|
|
1280
|
-
const pluginSourceSection = el('div', 'hqpu-stack')
|
|
1281
1410
|
const listSection = el('div', 'hqpu-stack')
|
|
1282
1411
|
const runtimeSection = el('div', 'hqpu-stack')
|
|
1283
1412
|
const toastSection = el('div', 'hqpu-toast-host')
|
|
1284
|
-
column.append(jobSection,
|
|
1413
|
+
column.append(jobSection, listSection, runtimeSection, toastSection)
|
|
1285
1414
|
scroll.append(column)
|
|
1286
1415
|
panel.append(scroll)
|
|
1287
1416
|
|
|
1288
|
-
// ──
|
|
1417
|
+
// ── Sticky footer ──
|
|
1289
1418
|
const footer = el('div', 'hqpu-footer')
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1419
|
+
const footMeta = el('div', 'hqpu-foot-meta')
|
|
1420
|
+
const footCount = el('span', '')
|
|
1421
|
+
const footSet = el('span', 'hqpu-foot-set')
|
|
1422
|
+
footMeta.append(footCount, footSet)
|
|
1423
|
+
const footActions = el('div', 'hqpu-foot-actions')
|
|
1424
|
+
const installAllBtn = el('button', 'hqpu-btn hqpu-btn--block')
|
|
1425
|
+
installAllBtn.type = 'button'
|
|
1426
|
+
const ignoreBtn = el('button', 'hqpu-btn--ignore', copy.ui.ignoreBtn)
|
|
1427
|
+
ignoreBtn.type = 'button'
|
|
1428
|
+
footActions.append(installAllBtn, ignoreBtn)
|
|
1429
|
+
const footnote = el('p', 'hqpu-footnote', copy.ui.footnote)
|
|
1430
|
+
footer.append(footMeta, footActions, footnote)
|
|
1292
1431
|
panel.append(footer)
|
|
1293
1432
|
|
|
1294
1433
|
overlay.append(scrim, panel)
|
|
@@ -1305,6 +1444,12 @@ export function openUpdateView(store: UpdateStoreLike, copy: UpdateCopy): void {
|
|
|
1305
1444
|
let runtimeBusy = false
|
|
1306
1445
|
let runtimeAccepted = false
|
|
1307
1446
|
|
|
1447
|
+
// ── Panel-closure UI state (survive store refreshes) ──
|
|
1448
|
+
const changelogOpen = new Set<string>()
|
|
1449
|
+
let allOpen = false
|
|
1450
|
+
let settingsOpen = false
|
|
1451
|
+
let dismissedAlert = false
|
|
1452
|
+
|
|
1308
1453
|
// Source-editor state, same reasoning: a background refresh must not discard
|
|
1309
1454
|
// a half-typed URL or silently close either editor.
|
|
1310
1455
|
const runtimeSourceState: SourceState = { editing: false, saving: false, draft: '' }
|
|
@@ -1507,18 +1652,46 @@ export function openUpdateView(store: UpdateStoreLike, copy: UpdateCopy): void {
|
|
|
1507
1652
|
const installing = store.installing() || jobActive
|
|
1508
1653
|
const stale = report === null
|
|
1509
1654
|
refreshBtn.disabled = stale
|
|
1655
|
+
refreshBtn.title = copy.refresh
|
|
1656
|
+
|
|
1657
|
+
// Header sub-line: "上次检查: N 分钟前" (from report.checkedAt).
|
|
1658
|
+
const ago = relativeAgo(report?.checkedAt, copy)
|
|
1659
|
+
sub.textContent = ago !== null ? copy.ui.checkedAgo(ago) : copy.ui.checkedUnknown
|
|
1660
|
+
|
|
1661
|
+
// Collapsible advanced settings panel (gear-toggled): existing source
|
|
1662
|
+
// editors — plugin + runtime manifest URL override, save→check→refresh.
|
|
1663
|
+
renderSettingsPanel(settingsPanel, report, copy, settingsOpen, pluginActions, runtimeSourceActions)
|
|
1664
|
+
|
|
1510
1665
|
renderJobSection()
|
|
1511
1666
|
if (stale) {
|
|
1512
1667
|
listSection.textContent = ''
|
|
1513
1668
|
listSection.append(skeleton(copy))
|
|
1514
1669
|
} else {
|
|
1515
|
-
renderList(
|
|
1516
|
-
|
|
1517
|
-
|
|
1670
|
+
renderList(
|
|
1671
|
+
listSection,
|
|
1672
|
+
report,
|
|
1673
|
+
copy,
|
|
1674
|
+
changelogOpen,
|
|
1675
|
+
allOpen,
|
|
1676
|
+
(name) => {
|
|
1677
|
+
if (changelogOpen.has(name)) changelogOpen.delete(name)
|
|
1678
|
+
else changelogOpen.add(name)
|
|
1679
|
+
render()
|
|
1680
|
+
},
|
|
1681
|
+
() => {
|
|
1682
|
+
allOpen = !allOpen
|
|
1683
|
+
render()
|
|
1684
|
+
},
|
|
1685
|
+
() => {
|
|
1686
|
+
void store.check()
|
|
1687
|
+
},
|
|
1688
|
+
)
|
|
1518
1689
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1690
|
+
renderRuntime(runtimeSection, report, copy, runtimeActions, dismissedAlert, () => {
|
|
1691
|
+
dismissedAlert = true
|
|
1692
|
+
render()
|
|
1693
|
+
})
|
|
1694
|
+
renderFooter(footer, installAllBtn, ignoreBtn, footCount, footSet, report, copy, installing, store)
|
|
1522
1695
|
}
|
|
1523
1696
|
|
|
1524
1697
|
const unsubscribe = store.subscribe(render)
|