@flame0510/project-aether 1.5.0 → 1.5.1
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.
|
@@ -68,45 +68,56 @@ export async function GET(request: Request): Promise<NextResponse> {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
return NextResponse.json({ installed, latest, updateAvailable, package: getPackageInfo().name });
|
|
71
|
+
return NextResponse.json({ installed, latest, updateAvailable, package: getPackageInfo().name, log: 'update.log' });
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* POST /api/update-check
|
|
76
76
|
*
|
|
77
|
-
* Spawns rev4a update as a detached background process
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* Spawns `rev4a update` as a detached background process: it installs the latest
|
|
78
|
+
* version, writes `.restart-flag` and lets `rev4a serve` restart the child (`rev4a
|
|
79
|
+
* _run`) with the new binary. Nothing here has to restart anything — the update kills
|
|
80
|
+
* this very process on its way in (`fuser -k <port>/tcp`), so any callback meant to run
|
|
81
|
+
* after it is dead code; the CLI's own flag is the only mechanism that has to work.
|
|
82
|
+
*
|
|
83
|
+
* The child's output goes to `update.log` in the data directory: since the update
|
|
84
|
+
* restarts this process mid-run, that file is the only record of what happened.
|
|
81
85
|
*/
|
|
82
86
|
export async function POST(request: Request): Promise<NextResponse> {
|
|
83
87
|
const denied = await requireAuthJWT(request);
|
|
84
88
|
if (denied) return denied;
|
|
85
89
|
|
|
86
|
-
// Spawn rev4a update in background, then write restart flag directly
|
|
87
|
-
// from this process so rev4a serve detects it and restarts.
|
|
88
90
|
const { spawn } = require('child_process');
|
|
89
91
|
const { join } = require('path');
|
|
90
|
-
const {
|
|
92
|
+
const { openSync, closeSync, appendFileSync } = require('fs');
|
|
91
93
|
|
|
92
94
|
const dataDir = process.env.REV4A_DATA_DIR ||
|
|
93
95
|
join(process.env.HOME || '/root', '.config', 'rev4a', 'data');
|
|
96
|
+
const logPath = join(dataDir, 'update.log');
|
|
97
|
+
|
|
98
|
+
let log: number | null = null;
|
|
99
|
+
try {
|
|
100
|
+
log = openSync(logPath, 'a');
|
|
101
|
+
appendFileSync(logPath, `\n[${new Date().toISOString()}] rev4a update, started from the dashboard\n`);
|
|
102
|
+
} catch {
|
|
103
|
+
// Unwritable log: run the update anyway, we just lose the record.
|
|
104
|
+
}
|
|
94
105
|
|
|
95
106
|
const child = spawn('rev4a', ['update'], {
|
|
96
|
-
stdio: 'ignore',
|
|
107
|
+
stdio: log === null ? 'ignore' : ['ignore', log, log],
|
|
97
108
|
detached: true,
|
|
109
|
+
cwd: dataDir,
|
|
98
110
|
});
|
|
111
|
+
if (log !== null) closeSync(log);
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
child.on('exit', () => {
|
|
103
|
-
try { writeFileSync(join(dataDir, '.restart-flag'), ''); } catch {}
|
|
113
|
+
child.on('error', (e: Error) => {
|
|
114
|
+
try { appendFileSync(logPath, `[dashboard] could not start the update: ${e.message}\n`); } catch {}
|
|
104
115
|
});
|
|
105
|
-
|
|
106
116
|
child.unref();
|
|
107
117
|
|
|
108
118
|
return NextResponse.json({
|
|
109
119
|
success: true,
|
|
110
|
-
message: 'Update started.
|
|
120
|
+
message: 'Update started. Rev4a restarts itself once the new version is installed.',
|
|
121
|
+
log: 'update.log',
|
|
111
122
|
});
|
|
112
123
|
}
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* "Update available" banner for Rev4a itself.
|
|
5
|
+
*
|
|
6
|
+
* The update is spawned by `POST /api/update-check` and restarts the server, so this
|
|
7
|
+
* component cannot be told the outcome by the response. It records what it asked for,
|
|
8
|
+
* waits for the server to answer again, and on the next mount compares: if the
|
|
9
|
+
* installed version moved it was updated, and if the same update is still offered it
|
|
10
|
+
* did not complete (the log file says why).
|
|
11
|
+
*/
|
|
3
12
|
import { useEffect, useState } from 'react';
|
|
4
13
|
|
|
5
14
|
interface VersionInfo {
|
|
6
15
|
installed: string;
|
|
7
16
|
latest: string | null;
|
|
8
17
|
updateAvailable: boolean;
|
|
18
|
+
log?: string;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
const DISMISS_KEY = 'rev4a-update-dismissed';
|
|
22
|
+
/** What we asked for and from which version, so the next mount can judge the outcome. */
|
|
23
|
+
const PENDING_KEY = 'rev4a-update-pending';
|
|
24
|
+
|
|
25
|
+
/** How long to poll for the restart before reloading anyway. */
|
|
26
|
+
const WAIT_MS = 120_000;
|
|
27
|
+
|
|
28
|
+
interface Pending { target: string; from: string }
|
|
29
|
+
|
|
30
|
+
function readPending(): Pending | null {
|
|
31
|
+
try {
|
|
32
|
+
const raw = sessionStorage.getItem(PENDING_KEY);
|
|
33
|
+
if (!raw) return null;
|
|
34
|
+
const parsed = JSON.parse(raw) as Pending;
|
|
35
|
+
return parsed && typeof parsed.target === 'string' && typeof parsed.from === 'string' ? parsed : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
12
40
|
|
|
13
41
|
export default function VersionBanner() {
|
|
14
42
|
const [info, setInfo] = useState<VersionInfo | null>(null);
|
|
@@ -18,17 +46,37 @@ export default function VersionBanner() {
|
|
|
18
46
|
const [updateOk, setUpdateOk] = useState<string | null>(null);
|
|
19
47
|
|
|
20
48
|
useEffect(() => {
|
|
49
|
+
let active = true;
|
|
21
50
|
fetch('/api/update-check?check=1')
|
|
22
51
|
.then((r) => r.json())
|
|
23
52
|
.then((d: VersionInfo) => {
|
|
53
|
+
if (!active) return;
|
|
54
|
+
const pending = readPending();
|
|
55
|
+
// Back from an update: did the installed version actually move?
|
|
56
|
+
if (pending && d.installed !== pending.from) {
|
|
57
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
58
|
+
setUpdateOk(`Updated to v${d.installed}`);
|
|
59
|
+
setTimeout(() => { if (active) window.location.reload(); }, 2000);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (pending) {
|
|
63
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
64
|
+
setInfo(d);
|
|
65
|
+
setDismissed(false);
|
|
66
|
+
setUpdateError(
|
|
67
|
+
`The update to v${pending.target} did not complete: Rev4a is still on v${d.installed}.`
|
|
68
|
+
+ ` See ${d.log ?? 'update.log'} in the Rev4a data directory.`,
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
24
72
|
if (d.updateAvailable) {
|
|
25
73
|
setInfo(d);
|
|
26
74
|
const saved = localStorage.getItem(DISMISS_KEY);
|
|
27
|
-
|
|
28
|
-
else setDismissed(false);
|
|
75
|
+
setDismissed(saved === d.latest);
|
|
29
76
|
}
|
|
30
77
|
})
|
|
31
78
|
.catch(() => {});
|
|
79
|
+
return () => { active = false; };
|
|
32
80
|
}, []);
|
|
33
81
|
|
|
34
82
|
function handleDismiss() {
|
|
@@ -39,6 +87,7 @@ export default function VersionBanner() {
|
|
|
39
87
|
}
|
|
40
88
|
|
|
41
89
|
async function handleUpdate() {
|
|
90
|
+
if (!info) return;
|
|
42
91
|
setUpdating(true);
|
|
43
92
|
setUpdateError(null);
|
|
44
93
|
setUpdateOk(null);
|
|
@@ -49,21 +98,37 @@ export default function VersionBanner() {
|
|
|
49
98
|
headers: { 'Content-Type': 'application/json' },
|
|
50
99
|
});
|
|
51
100
|
const data = await res.json();
|
|
52
|
-
if (data.success) {
|
|
53
|
-
setUpdateOk('Update started. Restarting\u2026');
|
|
54
|
-
localStorage.removeItem(DISMISS_KEY);
|
|
55
|
-
setTimeout(function() { window.location.reload(); }, 5000);
|
|
56
|
-
} else {
|
|
101
|
+
if (!data.success) {
|
|
57
102
|
setUpdateError(data.error || 'Update failed');
|
|
58
103
|
setUpdating(false);
|
|
104
|
+
return;
|
|
59
105
|
}
|
|
106
|
+
sessionStorage.setItem(PENDING_KEY, JSON.stringify({ target: info.latest ?? '', from: info.installed }));
|
|
107
|
+
localStorage.removeItem(DISMISS_KEY);
|
|
108
|
+
setUpdateOk('Update started. Rev4a is restarting…');
|
|
109
|
+
// The server goes down and comes back on the new version: wait for its answer,
|
|
110
|
+
// then reload. A timeout reloads too — the next mount reports the outcome.
|
|
111
|
+
const deadline = Date.now() + WAIT_MS;
|
|
112
|
+
const poll = async () => {
|
|
113
|
+
if (Date.now() > deadline) { window.location.reload(); return; }
|
|
114
|
+
try {
|
|
115
|
+
const r = await fetch('/api/update-check', { cache: 'no-store' });
|
|
116
|
+
const d = (await r.json()) as VersionInfo;
|
|
117
|
+
if (d.installed !== info.installed) { window.location.reload(); return; }
|
|
118
|
+
} catch { /* the server is down for the update: expected */ }
|
|
119
|
+
setTimeout(poll, 3000);
|
|
120
|
+
};
|
|
121
|
+
setTimeout(poll, 3000);
|
|
60
122
|
} catch (err) {
|
|
61
123
|
setUpdateError(err instanceof Error ? err.message : 'Network error');
|
|
62
124
|
setUpdating(false);
|
|
63
125
|
}
|
|
64
126
|
}
|
|
65
127
|
|
|
66
|
-
|
|
128
|
+
// The result states stand on their own (there may be no `info` when the update just
|
|
129
|
+
// succeeded); the "available" state needs the version info and no dismissal.
|
|
130
|
+
const show = Boolean(updateOk) || Boolean(updateError) || Boolean(info?.updateAvailable && !dismissed);
|
|
131
|
+
if (!show) return null;
|
|
67
132
|
|
|
68
133
|
return (
|
|
69
134
|
<div className="update-banner">
|
|
@@ -73,25 +138,25 @@ export default function VersionBanner() {
|
|
|
73
138
|
<span className="update-banner__ok">{updateOk}</span>
|
|
74
139
|
) : updateError ? (
|
|
75
140
|
<span className="update-banner__err">{updateError}</span>
|
|
76
|
-
) : (
|
|
141
|
+
) : info ? (
|
|
77
142
|
<>
|
|
78
143
|
<span className="update-banner__label">Update available</span>
|
|
79
144
|
<span className="update-banner__sep">—</span>
|
|
80
145
|
<span className="update-banner__version">v{info.latest}</span>
|
|
81
146
|
<span className="update-banner__sep">(installed: v{info.installed})</span>
|
|
82
147
|
</>
|
|
83
|
-
)}
|
|
148
|
+
) : null}
|
|
84
149
|
</span>
|
|
85
150
|
|
|
86
151
|
{!updateOk && (
|
|
87
152
|
<div className="update-banner__actions">
|
|
88
153
|
{updateError ? (
|
|
89
154
|
<button className="ui-btn ui-btn--primary ui-btn--sm" onClick={handleUpdate} disabled={updating}>
|
|
90
|
-
{updating ? 'Updating
|
|
155
|
+
{updating ? 'Updating…' : 'Retry'}
|
|
91
156
|
</button>
|
|
92
157
|
) : (
|
|
93
158
|
<button className="ui-btn ui-btn--primary ui-btn--sm" onClick={handleUpdate} disabled={updating}>
|
|
94
|
-
{updating ? 'Updating
|
|
159
|
+
{updating ? 'Updating…' : 'Update now'}
|
|
95
160
|
</button>
|
|
96
161
|
)}
|
|
97
162
|
<button className="ui-btn ui-btn--ghost ui-btn--sm" onClick={handleDismiss}>
|
|
@@ -2559,14 +2559,28 @@ Installed version against the latest published one.
|
|
|
2559
2559
|
**Query params:** `check=1` queries the registry; without it the answer comes
|
|
2560
2560
|
from local state only.
|
|
2561
2561
|
|
|
2562
|
-
**Response:** `{ "installed": "1.
|
|
2562
|
+
**Response:** `{ "installed": "1.4.2", "latest": "1.5.0", "updateAvailable": true, "package": "@flame0510/project-aether", "log": "update.log" }`
|
|
2563
|
+
|
|
2564
|
+
`log` is the file name, in the Rev4a data directory, where the update's output is
|
|
2565
|
+
written.
|
|
2563
2566
|
|
|
2564
2567
|
### `POST /api/update-check`
|
|
2565
|
-
Run `rev4a update` as a detached background process.
|
|
2566
|
-
|
|
2568
|
+
Run `rev4a update` as a detached background process. Nothing here restarts anything:
|
|
2569
|
+
the update kills this process on its way in (`fuser -k <port>/tcp`) and `rev4a serve`
|
|
2570
|
+
brings the child back on the new binary via its `.restart-flag`, so the connection
|
|
2571
|
+
drops and the page has to wait for the server to answer again.
|
|
2572
|
+
|
|
2573
|
+
The child's output goes to `update.log` in the data directory — the update restarts
|
|
2574
|
+
the process that spawned it, so that file is the only record of what happened.
|
|
2567
2575
|
|
|
2568
2576
|
**Auth:** browser cookie or bearer token
|
|
2569
2577
|
|
|
2578
|
+
**Response:** `{ "success": true, "message": "...", "log": "update.log" }`
|
|
2579
|
+
|
|
2580
|
+
The banner records what it asked for, polls `/api/version` until the installed version
|
|
2581
|
+
moves (or two minutes pass), reloads, and on the next mount either confirms
|
|
2582
|
+
"Updated to vX" or reports that the update did not complete and points at the log.
|
|
2583
|
+
|
|
2570
2584
|
### `GET /api/alerts/smoke`
|
|
2571
2585
|
Send a test alert, to verify the Telegram alerting path end to end.
|
|
2572
2586
|
|