@huaqiu/dsh-tool-plugin-update 0.6.6 → 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 +1852 -397
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +36 -4
- package/package.json +3 -3
- package/src/client/api.ts +281 -1
- package/src/client/copy.ts +230 -6
- package/src/client/index.ts +881 -12
- package/src/client/update-view.ts +848 -242
- package/src/routes.ts +49 -7
package/lib/index.mjs
CHANGED
|
@@ -65,6 +65,18 @@ function readBody(req) {
|
|
|
65
65
|
* forwarder answers 405 rather than forwarding a GET to a POST-only upstream
|
|
66
66
|
* and surfacing hq-edge's 404 as a plugin bug.
|
|
67
67
|
*/
|
|
68
|
+
/**
|
|
69
|
+
* Match a dynamic `/status/<jobId>` sub-path. Returns the job id for a single
|
|
70
|
+
* non-empty segment (no nested slashes), else null. The id is opaque — this
|
|
71
|
+
* route forwards it verbatim; hq-edge owns validation and the 404 for unknown
|
|
72
|
+
* ids.
|
|
73
|
+
*/
|
|
74
|
+
function matchJobStatusPath(sub) {
|
|
75
|
+
if (!sub.startsWith("/status/")) return null;
|
|
76
|
+
const id = sub.slice(8);
|
|
77
|
+
if (id.length === 0 || id.includes("/") || id.includes("?")) return null;
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
68
80
|
function routeFor(sub, method) {
|
|
69
81
|
if (sub === "/runtime-source") {
|
|
70
82
|
if (method === "GET") return {
|
|
@@ -92,6 +104,15 @@ function routeFor(sub, method) {
|
|
|
92
104
|
method: "GET",
|
|
93
105
|
path: "/api/v1/plugin-updates"
|
|
94
106
|
} : null;
|
|
107
|
+
if (sub === "/status") return method === "GET" ? {
|
|
108
|
+
method: "GET",
|
|
109
|
+
path: "/api/v1/plugin-updates/status"
|
|
110
|
+
} : null;
|
|
111
|
+
const jobId = matchJobStatusPath(sub);
|
|
112
|
+
if (jobId !== null) return method === "GET" ? {
|
|
113
|
+
method: "GET",
|
|
114
|
+
path: `/api/v1/plugin-updates/status/${encodeURIComponent(jobId)}`
|
|
115
|
+
} : null;
|
|
95
116
|
if (sub === "/runtime") return method === "GET" ? {
|
|
96
117
|
method: "GET",
|
|
97
118
|
path: "/api/v1/runtime-update"
|
|
@@ -119,8 +140,9 @@ function routeFor(sub, method) {
|
|
|
119
140
|
} : null;
|
|
120
141
|
return null;
|
|
121
142
|
}
|
|
122
|
-
/** Every sub-path this forwarder serves — used to tell a 404 from a
|
|
123
|
-
|
|
143
|
+
/** Every static sub-path this forwarder serves — used to tell a 404 from a
|
|
144
|
+
* 405. The dynamic `/status/<id>` path is matched separately. */
|
|
145
|
+
const STATIC_SUB_PATHS = /* @__PURE__ */ new Set([
|
|
124
146
|
"",
|
|
125
147
|
"/report",
|
|
126
148
|
"/check",
|
|
@@ -130,8 +152,17 @@ const ROUTE_SUB_PATHS = /* @__PURE__ */ new Set([
|
|
|
130
152
|
"/runtime",
|
|
131
153
|
"/runtime-request",
|
|
132
154
|
"/runtime-source",
|
|
133
|
-
"/plugin-source"
|
|
155
|
+
"/plugin-source",
|
|
156
|
+
"/status"
|
|
134
157
|
]);
|
|
158
|
+
/**
|
|
159
|
+
* Whether a sub-path names a route this forwarder recognizes at all. A
|
|
160
|
+
* recognized sub-path hit with an unsupported method is a 405; an unrecognized
|
|
161
|
+
* one is a 404. `/status/<id>` is recognized for any single-segment id.
|
|
162
|
+
*/
|
|
163
|
+
function isKnownSubPath(sub) {
|
|
164
|
+
return STATIC_SUB_PATHS.has(sub) || matchJobStatusPath(sub) !== null;
|
|
165
|
+
}
|
|
135
166
|
/** Create the prefix-route handler forwarding to hq-edge. */
|
|
136
167
|
function createUpdateForwarder(baseUrlResolver, timeoutMs = DEFAULT_FORWARD_TIMEOUT_MS) {
|
|
137
168
|
return async (req, res) => {
|
|
@@ -142,7 +173,8 @@ function createUpdateForwarder(baseUrlResolver, timeoutMs = DEFAULT_FORWARD_TIME
|
|
|
142
173
|
}
|
|
143
174
|
const target = routeFor(sub, req.method === "HEAD" ? "GET" : req.method ?? "");
|
|
144
175
|
if (target === null) {
|
|
145
|
-
|
|
176
|
+
const known = isKnownSubPath(sub);
|
|
177
|
+
sendJson(res, known ? 405 : 404, { error: known ? `method ${req.method} not allowed on ${sub || "/"}` : "not found" });
|
|
146
178
|
return;
|
|
147
179
|
}
|
|
148
180
|
if (req.method !== "GET" && req.method !== "POST" && req.method !== "HEAD") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-tool-plugin-update",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.mjs",
|
|
6
6
|
"types": "./lib/index.d.mts",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.2"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@huaqiu/dsh-plugin-log": "0.6.
|
|
35
|
-
"@huaqiu/dsh-sidebar-entry": "0.6.
|
|
34
|
+
"@huaqiu/dsh-plugin-log": "0.6.8",
|
|
35
|
+
"@huaqiu/dsh-sidebar-entry": "0.6.8"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"lib",
|
package/src/client/api.ts
CHANGED
|
@@ -29,6 +29,15 @@ export interface PluginUpdateItem {
|
|
|
29
29
|
ignored?: boolean
|
|
30
30
|
reasons?: string[]
|
|
31
31
|
notes?: string[]
|
|
32
|
+
/**
|
|
33
|
+
* User-facing release metadata (schemaVersion-2 manifests). ALL optional and
|
|
34
|
+
* absent for v1 manifests — the view falls back to the package short name
|
|
35
|
+
* and version line when they are missing.
|
|
36
|
+
*/
|
|
37
|
+
displayName?: string
|
|
38
|
+
description?: string
|
|
39
|
+
sizeBytes?: number
|
|
40
|
+
changes?: string[]
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
/**
|
|
@@ -49,6 +58,33 @@ export interface RuntimeUpdateEntry {
|
|
|
49
58
|
arch?: string
|
|
50
59
|
/** Exact absolute HTTPS URL of the release archive. */
|
|
51
60
|
url: string
|
|
61
|
+
/**
|
|
62
|
+
* Optional user-facing metadata (schemaVersion-2 runtime manifest). All
|
|
63
|
+
* absent for v1 manifests; the panel degrades gracefully without them.
|
|
64
|
+
*/
|
|
65
|
+
publishedAt?: string
|
|
66
|
+
sizeBytes?: number
|
|
67
|
+
summary?: string
|
|
68
|
+
notesUrl?: string
|
|
69
|
+
notes?: string[]
|
|
70
|
+
breaking?: string[]
|
|
71
|
+
channel?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The EDA host's own report of the last runtime replacement, read from
|
|
76
|
+
* `runtime-update-result.json` by hq-edge. This is the authoritative outcome
|
|
77
|
+
* after HQ Edge exits and the host swaps the runtime — it must be rendered,
|
|
78
|
+
* not discarded. `state` is `succeeded`, `rolled_back` (swap failed but the
|
|
79
|
+
* previous runtime was restored) or `failed` (no confirmed rollback).
|
|
80
|
+
*/
|
|
81
|
+
export interface RuntimeLastResult {
|
|
82
|
+
state: 'succeeded' | 'rolled_back' | 'failed'
|
|
83
|
+
targetVersion?: string | null
|
|
84
|
+
installedVersion?: string | null
|
|
85
|
+
at?: string
|
|
86
|
+
errorCode?: string
|
|
87
|
+
message?: string
|
|
52
88
|
}
|
|
53
89
|
|
|
54
90
|
export interface RuntimeUpdateInfo {
|
|
@@ -59,6 +95,8 @@ export interface RuntimeUpdateInfo {
|
|
|
59
95
|
updateAvailable: boolean
|
|
60
96
|
checkedAt?: string | null
|
|
61
97
|
lastError?: string | null
|
|
98
|
+
/** The host-reported outcome of the last runtime swap; null when none. */
|
|
99
|
+
lastResult?: RuntimeLastResult | null
|
|
62
100
|
/**
|
|
63
101
|
* Where the runtime-update manifest is read from. Optional: an older hq-edge
|
|
64
102
|
* does not send it, and the panel must degrade to a read-only source row.
|
|
@@ -110,6 +148,21 @@ export function runtimeLatestVersion(
|
|
|
110
148
|
return null
|
|
111
149
|
}
|
|
112
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Set-level release metadata from a schemaVersion-2 plugin manifest. ALL
|
|
153
|
+
* fields optional: v1 manifests send none, and the panel must fall back to
|
|
154
|
+
* showing just the version then.
|
|
155
|
+
*/
|
|
156
|
+
export interface ReleaseInfo {
|
|
157
|
+
publishedAt?: string
|
|
158
|
+
channel?: string
|
|
159
|
+
requiresRestart?: boolean
|
|
160
|
+
summary?: string
|
|
161
|
+
notesUrl?: string
|
|
162
|
+
notes?: string[]
|
|
163
|
+
breaking?: string[]
|
|
164
|
+
}
|
|
165
|
+
|
|
113
166
|
export interface PluginUpdateReport {
|
|
114
167
|
/** The evaluated release set; null while no manifest has been read. */
|
|
115
168
|
manifestVersion: string | null
|
|
@@ -118,7 +171,13 @@ export interface PluginUpdateReport {
|
|
|
118
171
|
actionableCount: number
|
|
119
172
|
ignoredSet: boolean
|
|
120
173
|
fetchedAt?: string
|
|
174
|
+
/** When the last plugin discovery ran (ISO); drives the "上次检查" row. */
|
|
175
|
+
checkedAt?: string | null
|
|
176
|
+
/** Non-empty when the last plugin discovery FAILED — the panel must show
|
|
177
|
+
* an error row with a Retry, never the silent "no updates" empty state. */
|
|
121
178
|
lastError?: string | null
|
|
179
|
+
/** Set-level release metadata; absent for v1 manifests. */
|
|
180
|
+
release?: ReleaseInfo | null
|
|
122
181
|
runtime: RuntimeUpdateInfo
|
|
123
182
|
/**
|
|
124
183
|
* Where the plugin release manifest is read from. Optional: an older
|
|
@@ -127,6 +186,198 @@ export interface PluginUpdateReport {
|
|
|
127
186
|
pluginSource?: PluginUpdateSource
|
|
128
187
|
}
|
|
129
188
|
|
|
189
|
+
// ── Durable update-job lifecycle (GET /status, GET /status/:id) ────────────
|
|
190
|
+
|
|
191
|
+
/** Lifecycle phases of one durable update job. Terminal phases end the poll. */
|
|
192
|
+
export type UpdatePhase =
|
|
193
|
+
| 'queued'
|
|
194
|
+
| 'resolving'
|
|
195
|
+
| 'downloading'
|
|
196
|
+
| 'verifying'
|
|
197
|
+
| 'staging'
|
|
198
|
+
| 'activating'
|
|
199
|
+
| 'awaiting-host'
|
|
200
|
+
| 'restarting'
|
|
201
|
+
| 'verifying-after-restart'
|
|
202
|
+
| 'done'
|
|
203
|
+
| 'failed'
|
|
204
|
+
| 'rolled-back'
|
|
205
|
+
| 'abandoned'
|
|
206
|
+
|
|
207
|
+
/** Phases after which a job never runs again — polling stops and a result view
|
|
208
|
+
* replaces the active-job view. */
|
|
209
|
+
export const TERMINAL_PHASES: ReadonlySet<UpdatePhase> = new Set<UpdatePhase>([
|
|
210
|
+
'done',
|
|
211
|
+
'failed',
|
|
212
|
+
'rolled-back',
|
|
213
|
+
'abandoned',
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
export type UpdateKind = 'plugin-set' | 'runtime'
|
|
217
|
+
|
|
218
|
+
export interface UpdateJobProgress {
|
|
219
|
+
/** 0..100 when the phase has a meaningful percentage; null otherwise. */
|
|
220
|
+
percent: number | null
|
|
221
|
+
bytesDone?: number
|
|
222
|
+
bytesTotal?: number
|
|
223
|
+
bytesPerSecond?: number
|
|
224
|
+
etaSeconds?: number
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface UpdateStep {
|
|
228
|
+
name: string
|
|
229
|
+
state: 'pending' | 'running' | 'installed' | 'skipped' | 'failed'
|
|
230
|
+
percent?: number
|
|
231
|
+
message?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface UpdateJobResult {
|
|
235
|
+
ok: boolean
|
|
236
|
+
installedVersion: string | null
|
|
237
|
+
errorCode?: string
|
|
238
|
+
message?: string
|
|
239
|
+
rolledBack: boolean
|
|
240
|
+
finishedAt: string
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* One durable update job, as hq-edge reports it (`GET /status` and
|
|
245
|
+
* `/status/:id`). The UI is a projection of this job: opening the panel
|
|
246
|
+
* resumes whatever the server says is active, rather than owning the
|
|
247
|
+
* lifecycle in a DOM closure.
|
|
248
|
+
*/
|
|
249
|
+
export interface UpdateJob {
|
|
250
|
+
id: string
|
|
251
|
+
kind: UpdateKind
|
|
252
|
+
targetVersion: string
|
|
253
|
+
previousVersion: string | null
|
|
254
|
+
startedAt: string
|
|
255
|
+
updatedAt: string
|
|
256
|
+
phase: UpdatePhase
|
|
257
|
+
message?: string
|
|
258
|
+
progress: UpdateJobProgress
|
|
259
|
+
steps?: UpdateStep[]
|
|
260
|
+
result?: UpdateJobResult
|
|
261
|
+
logHint?: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** `GET /status`: the current active job plus a bounded list of recent ones. */
|
|
265
|
+
export interface UpdateJobStatus {
|
|
266
|
+
activeJob: UpdateJob | null
|
|
267
|
+
recentJobs: UpdateJob[]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const PHASE_VALUES: ReadonlySet<string> = new Set<string>([
|
|
271
|
+
'queued',
|
|
272
|
+
'resolving',
|
|
273
|
+
'downloading',
|
|
274
|
+
'verifying',
|
|
275
|
+
'staging',
|
|
276
|
+
'activating',
|
|
277
|
+
'awaiting-host',
|
|
278
|
+
'restarting',
|
|
279
|
+
'verifying-after-restart',
|
|
280
|
+
'done',
|
|
281
|
+
'failed',
|
|
282
|
+
'rolled-back',
|
|
283
|
+
'abandoned',
|
|
284
|
+
])
|
|
285
|
+
|
|
286
|
+
function asStringArray(value: unknown): string[] | undefined {
|
|
287
|
+
if (!Array.isArray(value)) return undefined
|
|
288
|
+
const out = value.filter((entry): entry is string => typeof entry === 'string')
|
|
289
|
+
return out.length > 0 ? out : undefined
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function asFiniteNumber(value: unknown): number | undefined {
|
|
293
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Defensively normalize one job payload from the wire into {@link UpdateJob}.
|
|
297
|
+
* Unknown/older servers may omit fields; a malformed `phase` falls back to
|
|
298
|
+
* `queued` rather than crashing the view. */
|
|
299
|
+
export function normalizeJob(raw: unknown): UpdateJob | null {
|
|
300
|
+
if (raw === null || typeof raw !== 'object') return null
|
|
301
|
+
const r = raw as Record<string, unknown>
|
|
302
|
+
if (typeof r.id !== 'string' || r.id.length === 0) return null
|
|
303
|
+
const phase = typeof r.phase === 'string' && PHASE_VALUES.has(r.phase)
|
|
304
|
+
? (r.phase as UpdatePhase)
|
|
305
|
+
: 'queued'
|
|
306
|
+
const progressRaw = (r.progress ?? {}) as Record<string, unknown>
|
|
307
|
+
const percentRaw = asFiniteNumber(progressRaw.percent)
|
|
308
|
+
const progress: UpdateJobProgress = {
|
|
309
|
+
percent: percentRaw === undefined ? null : Math.min(100, Math.max(0, percentRaw)),
|
|
310
|
+
}
|
|
311
|
+
const bytesDone = asFiniteNumber(progressRaw.bytesDone)
|
|
312
|
+
const bytesTotal = asFiniteNumber(progressRaw.bytesTotal)
|
|
313
|
+
const bytesPerSecond = asFiniteNumber(progressRaw.bytesPerSecond)
|
|
314
|
+
const etaSeconds = asFiniteNumber(progressRaw.etaSeconds)
|
|
315
|
+
if (bytesDone !== undefined) progress.bytesDone = bytesDone
|
|
316
|
+
if (bytesTotal !== undefined) progress.bytesTotal = bytesTotal
|
|
317
|
+
if (bytesPerSecond !== undefined) progress.bytesPerSecond = bytesPerSecond
|
|
318
|
+
if (etaSeconds !== undefined) progress.etaSeconds = etaSeconds
|
|
319
|
+
|
|
320
|
+
const job: UpdateJob = {
|
|
321
|
+
id: r.id,
|
|
322
|
+
kind: r.kind === 'runtime' ? 'runtime' : 'plugin-set',
|
|
323
|
+
targetVersion: typeof r.targetVersion === 'string' ? r.targetVersion : '',
|
|
324
|
+
previousVersion:
|
|
325
|
+
typeof r.previousVersion === 'string' ? r.previousVersion : null,
|
|
326
|
+
startedAt: typeof r.startedAt === 'string' ? r.startedAt : '',
|
|
327
|
+
updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : '',
|
|
328
|
+
phase,
|
|
329
|
+
progress,
|
|
330
|
+
}
|
|
331
|
+
if (typeof r.message === 'string' && r.message.length > 0) job.message = r.message
|
|
332
|
+
if (typeof r.logHint === 'string' && r.logHint.length > 0) job.logHint = r.logHint
|
|
333
|
+
|
|
334
|
+
if (Array.isArray(r.steps)) {
|
|
335
|
+
const steps: UpdateStep[] = []
|
|
336
|
+
for (const entry of r.steps) {
|
|
337
|
+
if (entry === null || typeof entry !== 'object') continue
|
|
338
|
+
const s = entry as Record<string, unknown>
|
|
339
|
+
if (typeof s.name !== 'string') continue
|
|
340
|
+
const state =
|
|
341
|
+
s.state === 'running' || s.state === 'installed' || s.state === 'skipped' || s.state === 'failed'
|
|
342
|
+
? s.state
|
|
343
|
+
: 'pending'
|
|
344
|
+
const step: UpdateStep = { name: s.name, state }
|
|
345
|
+
const pct = asFiniteNumber(s.percent)
|
|
346
|
+
if (pct !== undefined) step.percent = pct
|
|
347
|
+
if (typeof s.message === 'string' && s.message.length > 0) step.message = s.message
|
|
348
|
+
steps.push(step)
|
|
349
|
+
}
|
|
350
|
+
if (steps.length > 0) job.steps = steps
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (r.result !== null && typeof r.result === 'object') {
|
|
354
|
+
const res = r.result as Record<string, unknown>
|
|
355
|
+
const result: UpdateJobResult = {
|
|
356
|
+
ok: res.ok === true,
|
|
357
|
+
installedVersion:
|
|
358
|
+
typeof res.installedVersion === 'string' ? res.installedVersion : null,
|
|
359
|
+
rolledBack: res.rolledBack === true,
|
|
360
|
+
finishedAt: typeof res.finishedAt === 'string' ? res.finishedAt : '',
|
|
361
|
+
}
|
|
362
|
+
if (typeof res.errorCode === 'string' && res.errorCode.length > 0) result.errorCode = res.errorCode
|
|
363
|
+
if (typeof res.message === 'string' && res.message.length > 0) result.message = res.message
|
|
364
|
+
job.result = result
|
|
365
|
+
}
|
|
366
|
+
return job
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Normalize a `GET /status` payload; an empty/absent body degrades to "no
|
|
370
|
+
* active job" rather than throwing. */
|
|
371
|
+
export function normalizeJobStatus(raw: unknown): UpdateJobStatus {
|
|
372
|
+
if (raw === null || typeof raw !== 'object') return { activeJob: null, recentJobs: [] }
|
|
373
|
+
const r = raw as Record<string, unknown>
|
|
374
|
+
const activeJob = normalizeJob(r.activeJob)
|
|
375
|
+
const recentJobs = Array.isArray(r.recentJobs)
|
|
376
|
+
? r.recentJobs.map(normalizeJob).filter((j): j is UpdateJob => j !== null)
|
|
377
|
+
: []
|
|
378
|
+
return { activeJob, recentJobs }
|
|
379
|
+
}
|
|
380
|
+
|
|
130
381
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
131
382
|
const res = await fetch(`${UPDATE_BASE}${path}`, {
|
|
132
383
|
cache: 'no-store',
|
|
@@ -153,7 +404,14 @@ export function checkUpdates(): Promise<PluginUpdateReport> {
|
|
|
153
404
|
}
|
|
154
405
|
|
|
155
406
|
export interface InstallResult {
|
|
156
|
-
|
|
407
|
+
/**
|
|
408
|
+
* New durable contract: `202 { jobId, state: 'accepted' }`. The install is
|
|
409
|
+
* now an async job — poll `/status/:jobId` for progress, don't read success
|
|
410
|
+
* off this response. Legacy boolean fields are kept for older hq-edge.
|
|
411
|
+
*/
|
|
412
|
+
jobId?: string
|
|
413
|
+
state?: string
|
|
414
|
+
ok?: boolean
|
|
157
415
|
installedSetVersion?: string
|
|
158
416
|
installed?: number
|
|
159
417
|
skipped?: number
|
|
@@ -183,6 +441,8 @@ export function ignoreOffer(setVersion: string): Promise<unknown> {
|
|
|
183
441
|
* updating → `busy`), 422 (preflight/manifest/download refused) or 503.
|
|
184
442
|
*/
|
|
185
443
|
export interface RuntimeUpdateRequestResult {
|
|
444
|
+
/** Durable job id — poll `/status/:jobId` for the accepted job's progress. */
|
|
445
|
+
jobId?: string
|
|
186
446
|
requestId?: string
|
|
187
447
|
targetVersion?: string
|
|
188
448
|
state?: string
|
|
@@ -258,3 +518,23 @@ export function setPluginUpdateSource(url: string): Promise<PluginUpdateSource>
|
|
|
258
518
|
body: JSON.stringify({ url }),
|
|
259
519
|
})
|
|
260
520
|
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* GET the durable job status: the currently active job (if any) plus a bounded
|
|
524
|
+
* list of recent jobs. This is what the panel reads BEFORE running discovery
|
|
525
|
+
* on open, so an in-flight install/runtime update is resumed instead of
|
|
526
|
+
* forgotten. An older hq-edge without the status route 404s — callers catch
|
|
527
|
+
* that and degrade to "no active job".
|
|
528
|
+
*/
|
|
529
|
+
export function fetchJobStatus(): Promise<UpdateJobStatus> {
|
|
530
|
+
return request<unknown>('/status').then(normalizeJobStatus)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** GET one job by id; rejects (upstream 404 `unknown job`) for an unknown id. */
|
|
534
|
+
export function fetchJob(id: string): Promise<UpdateJob> {
|
|
535
|
+
return request<unknown>(`/status/${encodeURIComponent(id)}`).then((raw) => {
|
|
536
|
+
const job = normalizeJob(raw)
|
|
537
|
+
if (job === null) throw new Error('malformed job payload')
|
|
538
|
+
return job
|
|
539
|
+
})
|
|
540
|
+
}
|