toolroll 0.9.77 → 0.9.78
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/CHANGELOG.md +81 -0
- package/dist/agent-fence.js +18 -2
- package/dist/backup-ui.d.ts +9 -1
- package/dist/backup-ui.js +28 -3
- package/dist/backup.js +4 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/builder.d.ts +4 -3
- package/dist/builder.js +14 -8
- package/dist/check-failure.js +31 -3
- package/dist/check-report.d.ts +18 -3
- package/dist/check-report.js +67 -7
- package/dist/claim.d.ts +9 -0
- package/dist/claim.js +9 -1
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/contracts/release-terms.d.ts +41 -0
- package/dist/contracts/release-terms.js +53 -0
- package/dist/controller-service.js +4 -1
- package/dist/deploy-rescue.d.ts +41 -0
- package/dist/deploy-rescue.js +94 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +4 -3
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +15 -3
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +15 -1
- package/dist/guides.js +18 -8
- package/dist/latency-timeline.d.ts +46 -0
- package/dist/latency-timeline.js +120 -0
- package/dist/live-backup.d.ts +39 -0
- package/dist/live-backup.js +271 -0
- package/dist/operate.js +203 -16
- package/dist/planner.js +2 -2
- package/dist/release-adapters.d.ts +32 -1
- package/dist/release-adapters.js +99 -7
- package/dist/release-carry.d.ts +21 -1
- package/dist/release-carry.js +22 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +74 -5
- package/dist/release-gate.d.ts +5 -0
- package/dist/release-gate.js +1 -1
- package/dist/release-recheck.js +3 -2
- package/dist/release.d.ts +37 -4
- package/dist/release.js +128 -43
- package/dist/runner-process.d.ts +67 -0
- package/dist/runner-process.js +219 -0
- package/dist/runner.d.ts +48 -6
- package/dist/runner.js +71 -8
- package/dist/scope.d.ts +19 -1
- package/dist/scope.js +19 -2
- package/dist/scout.js +2 -2
- package/dist/serve.js +11 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/pages.js +7 -3
- package/dist/server/remote-hooks.d.ts +1 -1
- package/dist/server/remote-hooks.js +5 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/settings.js +22 -2
- package/dist/server/tasks.js +4 -1
- package/dist/storage-sweep.js +1 -1
- package/dist/store.d.ts +125 -2
- package/dist/store.js +250 -12
- package/dist/surface.js +3 -1
- package/dist/teardown-git.js +5 -5
- package/dist/toolroll-update.js +2 -2
- package/dist/up-role-child.js +6 -2
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.78 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
- **Toolroll says when it is restarting.** The worker and console now record
|
|
6
|
+
when they start, keep a heartbeat every 15 seconds, and record when they
|
|
7
|
+
begin stopping and when they have stopped. While one is stopping it takes no
|
|
8
|
+
new work, `/healthz` answers "draining" at once, pages say "Toolroll is
|
|
9
|
+
restarting", and a new start waits only until the old one has finished
|
|
10
|
+
stopping instead of a fixed window. A worker that stopped cleanly never
|
|
11
|
+
holds its projects. A second live `up` is still refused.
|
|
12
|
+
- **The database is copied every few minutes.** Besides scheduled backups,
|
|
13
|
+
the worker copies the database online every 5 minutes (change it, or the
|
|
14
|
+
folder, in Settings → Backups), checks each copy and keeps the newest 24
|
|
15
|
+
and one a day for a week. It never holds up the console or the worker. A
|
|
16
|
+
deploy uses the latest copy when it is recent and matches the database
|
|
17
|
+
exactly, instead of making a new one. Restoring stays a manual
|
|
18
|
+
`toolroll restore`.
|
|
19
|
+
- **See where the time went.** A task's page has a "Where the time went"
|
|
20
|
+
section under Details: when each part of its release check started and
|
|
21
|
+
ended, and each step of its deploys, including what a deploy waited for.
|
|
22
|
+
Console pages that take over a second keep a short record of where the
|
|
23
|
+
time went, which an instance operator reads on `/health`. Only page names,
|
|
24
|
+
step names and reasons are kept, never addresses or form contents.
|
|
25
|
+
- **A crashed worker is noticed in a minute, not three.** Workers check in
|
|
26
|
+
every 15 seconds and are treated as gone after a minute of silence. A
|
|
27
|
+
project lease is never taken for longer than a minute.
|
|
28
|
+
- **A release's version bump alone no longer owes the upgrade path.** Toolroll
|
|
29
|
+
now decides from both commits' verified files that package.json and
|
|
30
|
+
package-lock.json changed only in the project's own version (the check's
|
|
31
|
+
own rule); any other change to them still owes it.
|
|
32
|
+
- **The macOS suite runs on the candidate before it merges.** Pull requests
|
|
33
|
+
and main pushes now run the unit suite on hosted macOS with Node 22 and 24,
|
|
34
|
+
in four shards each, joined into `node 22 · macos-latest` and
|
|
35
|
+
`node 24 · macos-latest` exactly like the Linux checks. `toolroll release`
|
|
36
|
+
waits for them; main's protection not requiring them yet is reported, not
|
|
37
|
+
refused. After the merge, publication waits only for the tag's publish job,
|
|
38
|
+
no longer for main's own CI run on the merge commit. The nightly and tag
|
|
39
|
+
macOS jobs are renamed so no other workflow shares a required name.
|
|
40
|
+
- **A release is published without waiting for Homebrew.** `toolroll release`
|
|
41
|
+
ends with "Published; Homebrew pending" once npm and the GitHub release are
|
|
42
|
+
verified, instead of waiting up to 45 minutes for the tap's checks. Homebrew
|
|
43
|
+
is its own state in the release journal (pending, failed or current), and
|
|
44
|
+
running the same command again finishes it: it waits for the tap change's
|
|
45
|
+
checks, then merges exactly that change, with the checksum still taken from
|
|
46
|
+
npm's tarball. A tap already on a newer version is never moved back, checked
|
|
47
|
+
again right before the merge.
|
|
48
|
+
- **A release whose checks run outside the sandbox now reruns its own check
|
|
49
|
+
after Toolroll's machine fails.** Such a release can't carry later commits,
|
|
50
|
+
so it used to file without release terms, and a check stopped by Toolroll's
|
|
51
|
+
own shutdown, a lost build or a command that never started waited for a
|
|
52
|
+
person. Its approval now seals a release check purpose instead: the check
|
|
53
|
+
is held to the release's rules (a complete report, no CI standing in for
|
|
54
|
+
its units), and only a failure Toolroll itself confirmed as its own
|
|
55
|
+
infrastructure reruns, on the same commit under the same approval, at most
|
|
56
|
+
twice. Another commit, a changed scope, release check or check access, or
|
|
57
|
+
an approval that no longer stands never reruns. It never carries a later
|
|
58
|
+
commit. (Schema v138 adds the column; saved scopes are unchanged.)
|
|
59
|
+
- **A deploy no longer gets stuck behind an `up` that won't stop.** When the
|
|
60
|
+
old service's processes outlive their stop (a supervisor that exited while
|
|
61
|
+
its `up` ignored SIGTERM), the deployment stops them itself, but only
|
|
62
|
+
processes proved, by exact runtime path and arguments, to be that service's
|
|
63
|
+
supervisor, `up` or console and worker. Anything it can't prove refuses and
|
|
64
|
+
nothing is signalled.
|
|
65
|
+
- **A killed `up` no longer blocks the next start for three minutes.** `up`
|
|
66
|
+
records each process it gives its worker credential to. A new start that
|
|
67
|
+
proves every one of them has exited signs the old worker off and takes over
|
|
68
|
+
at once; a second live `up` is still refused.
|
|
69
|
+
- **`up` signs off however it is stopped.** SIGTERM, Ctrl-C, its supervisor
|
|
70
|
+
dying or its channel closing all end with the worker retired. A stop that
|
|
71
|
+
hangs past the worker's grace stops the console and worker outright
|
|
72
|
+
instead of waiting for launchd to kill it.
|
|
73
|
+
- **Deployments wait long enough, and say why.** Restoring the previous
|
|
74
|
+
service, starting the new one and waiting for its project leases now allow
|
|
75
|
+
two minutes (past a stale worker heartbeat's one) and name what they are
|
|
76
|
+
waiting for. "Current work must finish first" now says what is running and
|
|
77
|
+
what to do.
|
|
78
|
+
- **One command puts back a deployment that stopped half way.**
|
|
79
|
+
`toolroll release --recover-deploy` shows what recovery will do for the
|
|
80
|
+
unfinished deployment; `--yes` does it: stops only that service's proved
|
|
81
|
+
leftovers, starts the previous service again and lifts the update pause.
|
|
82
|
+
It is safe to run again.
|
|
83
|
+
|
|
3
84
|
## 0.9.77 — 2026-10-29
|
|
4
85
|
|
|
5
86
|
- **CI finishes in about a third of the time.** The unit suite runs in four
|
package/dist/agent-fence.js
CHANGED
|
@@ -33,6 +33,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
|
|
|
33
33
|
import { readMonitoring } from "./monitoring-settings.js";
|
|
34
34
|
import { existingFolders } from "./names.js";
|
|
35
35
|
import { ensureGitViewsRoot, gitViewsRoot } from "./git-views.js";
|
|
36
|
+
import { ensureRunnerProcessFolder, RUNNER_PROCESS_DIR } from "./runner-process.js";
|
|
36
37
|
import { DEPS_FOLDER, linkedKey, readyCopy, sharedCopies } from "./shared-deps.js";
|
|
37
38
|
import { SEATBELT_PROBE, sandboxRuntimeProblem, srtSpawn } from "./sandbox-runtime.js";
|
|
38
39
|
import { protectedStorageDenies, protectedStorageNames } from "./protected-storage.js";
|
|
@@ -167,6 +168,21 @@ export function agentFence(options) {
|
|
|
167
168
|
catch { /* fenced below either way */ }
|
|
168
169
|
}
|
|
169
170
|
fence.add(runFolder);
|
|
171
|
+
const runnerFolder = join(state, RUNNER_PROCESS_DIR);
|
|
172
|
+
try {
|
|
173
|
+
ensureRunnerProcessFolder(runnerFolder);
|
|
174
|
+
}
|
|
175
|
+
catch { /* both the name and its target stay denied */ }
|
|
176
|
+
fence.add(runnerFolder);
|
|
177
|
+
fence.add(real(runnerFolder));
|
|
178
|
+
// A copy may first appear after launch. Fence its permanent namespace even when empty.
|
|
179
|
+
const backups = join(state, "backups");
|
|
180
|
+
try {
|
|
181
|
+
mkdirSync(backups, { mode: 0o700 });
|
|
182
|
+
}
|
|
183
|
+
catch { /* name and target remain denied */ }
|
|
184
|
+
fence.add(backups);
|
|
185
|
+
fence.add(real(backups));
|
|
170
186
|
// Only a folder that is Toolroll's own (~/.config/toolroll or an older name,
|
|
171
187
|
// the desktop app's "Toolroll") is fenced whole: a database kept
|
|
172
188
|
// in a shared folder (a projects folder, a test's temp folder) must not
|
|
@@ -214,7 +230,7 @@ export function agentFence(options) {
|
|
|
214
230
|
// Secret, state, database, explicit-extra and home-config denies are never exemptible.
|
|
215
231
|
for (const path of options.boundary?.protectedShell ?? [])
|
|
216
232
|
fence.add(real(path));
|
|
217
|
-
const protectedPaths = new Set([...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]));
|
|
233
|
+
const protectedPaths = new Set([...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups"].includes(basename(path)) ? [path, real(path)] : [real(path)]));
|
|
218
234
|
// Protected storage (chat apps, browser profiles, password managers): every launch, every policy, never exemptible,
|
|
219
235
|
// kept by the name it was resolved under as well as where it leads.
|
|
220
236
|
const storage = options.protectedStorage ?? protectedStorageDenies(home);
|
|
@@ -226,7 +242,7 @@ export function agentFence(options) {
|
|
|
226
242
|
fence.add(path);
|
|
227
243
|
const binding = options.binding;
|
|
228
244
|
const needed = binding === undefined || binding === null ? [] : [binding.gitdir, real(join(binding.main, ".git"))];
|
|
229
|
-
return [...new Set([...[...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
|
|
245
|
+
return [...new Set([...[...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups"].includes(basename(path)) ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
|
|
230
246
|
if (protectedPaths.has(path) || !needed.some(target => target === path || inside(target, path)))
|
|
231
247
|
return true;
|
|
232
248
|
console.warn(`Dropped build fence entry covering this checkout's own Git data: ${path}`);
|
package/dist/backup-ui.d.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* disclosures.
|
|
5
5
|
*/
|
|
6
6
|
import { type Html } from "./html.js";
|
|
7
|
-
import type { BackupRun, BackupSettings } from "./store.js";
|
|
7
|
+
import type { BackupRun, BackupSettings, LiveBackupState } from "./store.js";
|
|
8
8
|
/** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
|
|
9
9
|
export declare const BACKUP_EVERY_HOURS: readonly [1, 6, 12, 24];
|
|
10
|
+
/** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
|
|
11
|
+
export declare const LIVE_EVERY: readonly [1, 5, 15, 30];
|
|
10
12
|
export declare const MAX_KEEP = 100;
|
|
11
13
|
export declare const BACKUP_CSS: string;
|
|
12
14
|
export type BackupView = {
|
|
@@ -16,6 +18,12 @@ export type BackupView = {
|
|
|
16
18
|
runs: BackupRun[];
|
|
17
19
|
csrf: string;
|
|
18
20
|
now: Date;
|
|
21
|
+
/** The continuous copy (live-backup.ts): its settings, latest verified copy and folder. */
|
|
22
|
+
live?: {
|
|
23
|
+
state: LiveBackupState;
|
|
24
|
+
folder: string;
|
|
25
|
+
defaultFolder: string;
|
|
26
|
+
};
|
|
19
27
|
};
|
|
20
28
|
export declare function backupHtml(view: BackupView, notice: {
|
|
21
29
|
said?: string | null;
|
package/dist/backup-ui.js
CHANGED
|
@@ -8,6 +8,8 @@ import { bytesWords } from "./storage.js";
|
|
|
8
8
|
import { whenUtc } from "./when-html.js";
|
|
9
9
|
/** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
|
|
10
10
|
export const BACKUP_EVERY_HOURS = [1, 6, 12, 24];
|
|
11
|
+
/** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
|
|
12
|
+
export const LIVE_EVERY = [1, 5, 15, 30];
|
|
11
13
|
export const MAX_KEEP = 100;
|
|
12
14
|
const when = (at) => whenUtc(at);
|
|
13
15
|
const whenWords = (at) => `${at.slice(0, 16).replace("T", " ")} UTC`;
|
|
@@ -23,6 +25,19 @@ export const BACKUP_CSS = `.backups{max-width:720px;min-width:0;overflow-wrap:an
|
|
|
23
25
|
`.backups li{display:flex;gap:8px 12px;flex-wrap:wrap;padding:8px 0;border-top:1px solid var(--line, rgba(0,0,0,.08));font-size:.875rem}` +
|
|
24
26
|
`.backups li .failed{color:var(--danger)}.backups code{white-space:nowrap}.backups pre{overflow-x:auto;margin:8px 0}` +
|
|
25
27
|
`@media (max-width:900px){.backups .backup-state form,.backups .backup-state button{width:100%}.backups .pair{grid-template-columns:1fr}}`;
|
|
28
|
+
/** The continuous copy in one line: its latest verified copy, or why there is none. */
|
|
29
|
+
function liveLine(view) {
|
|
30
|
+
const live = view.live;
|
|
31
|
+
if (live === undefined)
|
|
32
|
+
return "";
|
|
33
|
+
if (!live.state.settings.enabled)
|
|
34
|
+
return html `<p class="meta">Continuous copy is off.</p>`;
|
|
35
|
+
if (live.state.problem !== null)
|
|
36
|
+
return html `<p class="problem" role="alert">The continuous copy failed: ${live.state.problem}</p>`;
|
|
37
|
+
if (live.state.latest === null)
|
|
38
|
+
return html `<p class="meta">The first continuous copy is made within a few minutes of Toolroll starting.</p>`;
|
|
39
|
+
return html `<p class="meta" data-live-copy>Continuous copy checked ${when(live.state.latest.madeAt)}, every ${live.state.settings.everyMinutes === 1 ? "minute" : `${live.state.settings.everyMinutes} minutes`}.</p>`;
|
|
40
|
+
}
|
|
26
41
|
/** The state in one line, and the next backup (if any), for the top of the page. */
|
|
27
42
|
function state(view) {
|
|
28
43
|
const last = view.runs[0];
|
|
@@ -31,13 +46,13 @@ function state(view) {
|
|
|
31
46
|
: good === undefined ? "The first scheduled backup runs within a minute or two of the console starting."
|
|
32
47
|
: `Next one about ${whenWords(new Date(Date.parse(good.startedAt) + view.settings.everyHours * 3_600_000).toISOString())}.`;
|
|
33
48
|
if (last === undefined)
|
|
34
|
-
return html `<h2>No backups yet</h2><p class="meta">${next}</p
|
|
49
|
+
return html `<h2>No backups yet</h2><p class="meta">${next}</p>${liveLine(view)}`;
|
|
35
50
|
if (last.ok === null)
|
|
36
51
|
return html `<h2>Backing up</h2><p class="meta">Started ${when(last.startedAt)}.</p>`;
|
|
37
52
|
if (last.ok === false) {
|
|
38
53
|
return html `<h2 data-backup-state="failed">Last backup failed</h2><p class="problem" role="alert">${when(last.startedAt)}: ${last.error ?? "no reason was recorded"}</p><p class="meta">${good === undefined ? "No backup has succeeded yet." : html `Last good backup ${when(good.startedAt)}.`} ${next}</p>`;
|
|
39
54
|
}
|
|
40
|
-
return html `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}</p
|
|
55
|
+
return html `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}</p>${liveLine(view)}`;
|
|
41
56
|
}
|
|
42
57
|
export function backupHtml(view, notice) {
|
|
43
58
|
const s = view.settings;
|
|
@@ -46,5 +61,15 @@ export function backupHtml(view, notice) {
|
|
|
46
61
|
...BACKUP_EVERY_HOURS.map(hours => html `<option value="${hours}"${s.enabled && s.everyHours === hours ? html ` selected` : ""}>${everyWords(hours)}</option>`)];
|
|
47
62
|
const runs = view.runs.map(run => html `<li data-backup-run="${run.id}"><span>${when(run.startedAt)}</span>${run.ok === true ? html `<span>Succeeded${run.bytes === null ? "" : ` · ${bytesWords(run.bytes)}`}</span>${run.file === null ? "" : html `<span class="mono meta">${fileName(run.file)}</span>`}`
|
|
48
63
|
: run.ok === false ? html `<span class="failed">Failed: ${run.error ?? "no reason recorded"}</span>` : html `<span>Running</span>`}<span class="meta">${run.trigger === "manual" ? "by hand" : "scheduled"}</span></li>`);
|
|
49
|
-
return html `<section class="backups">${note}<div class="card backup-state"><div>${state(view)}</div>${postForm("/settings/backups/now", html `<button type="submit">Back up now</button>`)}</div><h2>Schedule</h2>${postForm("/settings/backups", html `<fieldset><div class="pair"><label>Back up<select name="every">${choices}</select></label><label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div><label>Folder<input type="text" name="folder" value="${s.folder ?? ""}" placeholder="${view.defaultFolder}" spellcheck="false"></label>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}</fieldset><button type="submit">Save</button>`)}<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs.length === 0 ? html `<p class="meta">None yet.</p>` : html `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${view.folder}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details><details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore <file> --dry-run</code></pre><p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p
|
|
64
|
+
return html `<section class="backups">${note}<div class="card backup-state"><div>${state(view)}</div>${postForm("/settings/backups/now", html `<button type="submit">Back up now</button>`)}</div><h2>Schedule</h2>${postForm("/settings/backups", html `<fieldset><div class="pair"><label>Back up<select name="every">${choices}</select></label><label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div><label>Folder<input type="text" name="folder" value="${s.folder ?? ""}" placeholder="${view.defaultFolder}" spellcheck="false"></label>${liveFields(view)}${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}</fieldset><button type="submit">Save</button>`)}<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs.length === 0 ? html `<p class="meta">None yet.</p>` : html `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${view.folder}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details><details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore <file> --dry-run</code></pre><p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p>${view.live === undefined ? "" : html `<p class="meta">Continuous copies are in <span class="mono">${view.live.folder}</span>; restore one the same way.</p>`}</details></section>`;
|
|
65
|
+
}
|
|
66
|
+
/** The continuous copy's two choices, beside the schedule's: how often, and where (blank: beside the database). */
|
|
67
|
+
function liveFields(view) {
|
|
68
|
+
const live = view.live;
|
|
69
|
+
if (live === undefined)
|
|
70
|
+
return "";
|
|
71
|
+
const on = live.state.settings;
|
|
72
|
+
const choices = [html `<option value="off"${on.enabled ? "" : html ` selected`}>Off</option>`,
|
|
73
|
+
...LIVE_EVERY.map(minutes => html `<option value="${minutes}"${on.enabled && on.everyMinutes === minutes ? html ` selected` : ""}>${minutes === 1 ? "Every minute" : `Every ${minutes} minutes`}</option>`)];
|
|
74
|
+
return html `<div class="pair"><label>Copy continuously<select name="live_every">${choices}</select></label><label>Copy folder<input type="text" name="live_folder" value="${on.folder ?? ""}" placeholder="${live.defaultFolder}" spellcheck="false"></label></div>`;
|
|
50
75
|
}
|
package/dist/backup.js
CHANGED
|
@@ -19,6 +19,7 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import { BACKUP_EVERY_HOURS, MAX_KEEP } from "./backup-ui.js";
|
|
21
21
|
import { verifyLedgerChain } from "./ledger-chain.js";
|
|
22
|
+
import { DEFAULT_LIVENESS_MS } from "./runner.js";
|
|
22
23
|
import { openStore, readSchemaVersion, SCHEMA_VERSION } from "./store.js";
|
|
23
24
|
/** A scheduled backup's name: the database it came from (see `databaseTag`), then when it was made. Backups made
|
|
24
25
|
* before the tag was added have none. Retention only ever removes files named like this. */
|
|
@@ -28,8 +29,8 @@ export const OLDEST_RESTORABLE = 103;
|
|
|
28
29
|
export { BACKUP_EVERY_HOURS, MAX_KEEP };
|
|
29
30
|
/** How long the console's lease lasts without a renewal: a console that stopped holds it no longer than this. */
|
|
30
31
|
const LEASE_MS = 3 * 60_000;
|
|
31
|
-
/** A worker that heartbeat this recently is taken to be running. */
|
|
32
|
-
const RUNNER_FRESH_MS =
|
|
32
|
+
/** A worker that heartbeat this recently is taken to be running (runner.ts DEFAULT_LIVENESS_MS). */
|
|
33
|
+
const RUNNER_FRESH_MS = DEFAULT_LIVENESS_MS;
|
|
33
34
|
export const defaultBackupFolder = (databaseFile) => join(dirname(databaseFile), "backups");
|
|
34
35
|
export const backupFolderOf = (settings, databaseFile) => settings.folder ?? defaultBackupFolder(databaseFile);
|
|
35
36
|
/** 2026-09-28T05:34:39.123Z → 2026-09-28-053439 (UTC). */
|
|
@@ -287,7 +288,7 @@ export async function restoreDatabase(options) {
|
|
|
287
288
|
try {
|
|
288
289
|
const running = runningNow(store, now);
|
|
289
290
|
if (running.length > 0)
|
|
290
|
-
refuse("running", `Toolroll is running (${running.join(" and ")}). Stop it first, then restore. If it has just stopped, wait a
|
|
291
|
+
refuse("running", `Toolroll is running (${running.join(" and ")}). Stop it first, then restore. If it has just stopped, wait a minute.`);
|
|
291
292
|
folder = backupFolderOf(store.backupSettings(), databaseFile);
|
|
292
293
|
}
|
|
293
294
|
finally {
|