@camstack/system 1.2.199 → 1.2.201
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/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +38 -9
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +234 -81
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +234 -81
- package/dist/builtins/backup-orchestrator/destination-policy.d.ts +32 -14
- package/dist/builtins/backup-orchestrator/schedule-store.d.ts +12 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-BSSH6C-h.mjs → dist-BmJNnb_s.mjs} +52 -18
- package/dist/{dist-HAP38XvL.js → dist-CcV3k-hD.js} +52 -18
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-BYiTW-q4.js → retired-settings-keys-Bd8fuwO1.js} +1 -1
- package/dist/{retired-settings-keys-DAl6xuAs.mjs → retired-settings-keys-nLVKlhe-.mjs} +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { It as BaseAddon, M as backupCapability, cn as EventCategory } from "../../dist-
|
|
1
|
+
import { It as BaseAddon, M as backupCapability, cn as EventCategory } from "../../dist-BmJNnb_s.mjs";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { Buffer as Buffer$1 } from "buffer";
|
|
4
4
|
import * as fsp from "node:fs/promises";
|
|
@@ -4294,14 +4294,17 @@ function previewNextRuns(cron, count, after) {
|
|
|
4294
4294
|
/**
|
|
4295
4295
|
* @durable class=config owner=backup-orchestrator
|
|
4296
4296
|
* write="`ensureDefault` auto-creates one row the first time a `backups` storage
|
|
4297
|
-
* location is listed (enabled, retentionCount 7,
|
|
4297
|
+
* location is listed (enabled, retentionCount 7, no cadence of its own), an operator
|
|
4298
4298
|
* edit rewrites it (`upsert` = delete + insert, carrying `last_run_at` forward so a
|
|
4299
|
-
* label change does not reset the
|
|
4299
|
+
* label change does not reset the baseline), and `recordAttempt` stamps
|
|
4300
|
+
* `last_run_at` + `last_error` on every attempted write to the destination"
|
|
4300
4301
|
* retention="none — `delete(locationId)` exists but NOTHING calls it, so a policy row
|
|
4301
4302
|
* outlives the `StorageLocation` it names and the table only ever grows, by one row
|
|
4302
4303
|
* per backups location ever seen. Losing a row is recoverable: the next
|
|
4303
4304
|
* `listDestinations` pass re-creates it from the defaults, which silently discards
|
|
4304
|
-
* the operator's own retention count
|
|
4305
|
+
* the operator's own retention count. The legacy `cron` column is written NULL and
|
|
4306
|
+
* never read — it scheduled nothing after 2026-07-28 and reading it made a
|
|
4307
|
+
* destination advertise a nightly run nobody performed (D384)."
|
|
4305
4308
|
*/
|
|
4306
4309
|
var BACKUP_DESTINATION_POLICIES = "backup_destination_policies";
|
|
4307
4310
|
var BACKUP_DESTINATION_POLICIES_COLUMNS = [
|
|
@@ -4333,6 +4336,10 @@ var BACKUP_DESTINATION_POLICIES_COLUMNS = [
|
|
|
4333
4336
|
{
|
|
4334
4337
|
name: "last_run_at",
|
|
4335
4338
|
type: "INTEGER"
|
|
4339
|
+
},
|
|
4340
|
+
{
|
|
4341
|
+
name: "last_error",
|
|
4342
|
+
type: "TEXT"
|
|
4336
4343
|
}
|
|
4337
4344
|
];
|
|
4338
4345
|
var BackupDestinationPolicyService = class {
|
|
@@ -4417,10 +4424,11 @@ var BackupDestinationPolicyService = class {
|
|
|
4417
4424
|
* pass so newly-added storage locations are surfaced as enabled
|
|
4418
4425
|
* destinations without an explicit configuration step.
|
|
4419
4426
|
*
|
|
4420
|
-
* Defaults are passed by the orchestrator (currently
|
|
4421
|
-
*
|
|
4422
|
-
*
|
|
4423
|
-
*
|
|
4427
|
+
* Defaults are passed by the orchestrator (currently `retentionCount=7`).
|
|
4428
|
+
* A fresh destination is enabled and keeps 7 archives; it gets NO cadence
|
|
4429
|
+
* of its own — a destination runs when a schedule fans out to it, and
|
|
4430
|
+
* seeding a cron here would advertise a nightly run nothing performs
|
|
4431
|
+
* (D384).
|
|
4424
4432
|
*/
|
|
4425
4433
|
async ensureDefault(locationId, defaults) {
|
|
4426
4434
|
const existing = await this.get(locationId);
|
|
@@ -4428,12 +4436,32 @@ var BackupDestinationPolicyService = class {
|
|
|
4428
4436
|
const fresh = {
|
|
4429
4437
|
locationId,
|
|
4430
4438
|
enabled: true,
|
|
4431
|
-
retentionCount: defaults.retentionCount
|
|
4432
|
-
...defaults.cron !== void 0 ? { cron: defaults.cron } : {}
|
|
4439
|
+
retentionCount: defaults.retentionCount
|
|
4433
4440
|
};
|
|
4434
4441
|
await this.upsert(fresh);
|
|
4435
4442
|
return fresh;
|
|
4436
4443
|
}
|
|
4444
|
+
/**
|
|
4445
|
+
* Stamp the outcome of ONE attempted write to this destination.
|
|
4446
|
+
*
|
|
4447
|
+
* Read-modify-write on the policy row so the operator's `enabled`,
|
|
4448
|
+
* `retentionCount` and `label` survive. A destination with no row yet
|
|
4449
|
+
* gets one — an attempt on an unseen location must still be recorded, or
|
|
4450
|
+
* the failure disappears with the process (which is exactly how a 03:00
|
|
4451
|
+
* SMB refusal stayed invisible until the operator went looking).
|
|
4452
|
+
*/
|
|
4453
|
+
async recordAttempt(locationId, outcome, defaults) {
|
|
4454
|
+
const { lastError: _cleared, ...carried } = await this.get(locationId) ?? {
|
|
4455
|
+
locationId,
|
|
4456
|
+
enabled: true,
|
|
4457
|
+
retentionCount: defaults.retentionCount
|
|
4458
|
+
};
|
|
4459
|
+
await this.upsert({
|
|
4460
|
+
...carried,
|
|
4461
|
+
lastAttemptAt: outcome.attemptAt,
|
|
4462
|
+
...outcome.error !== void 0 ? { lastError: outcome.error } : {}
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4437
4465
|
};
|
|
4438
4466
|
function mapPolicyToRow(p) {
|
|
4439
4467
|
return {
|
|
@@ -4441,8 +4469,9 @@ function mapPolicyToRow(p) {
|
|
|
4441
4469
|
enabled: p.enabled ? 1 : 0,
|
|
4442
4470
|
retention_count: p.retentionCount,
|
|
4443
4471
|
label: p.label ?? null,
|
|
4444
|
-
cron:
|
|
4445
|
-
last_run_at: p.
|
|
4472
|
+
cron: null,
|
|
4473
|
+
last_run_at: p.lastAttemptAt ?? null,
|
|
4474
|
+
last_error: p.lastError ?? null
|
|
4446
4475
|
};
|
|
4447
4476
|
}
|
|
4448
4477
|
/**
|
|
@@ -4456,17 +4485,17 @@ function mapRowToPolicy(row) {
|
|
|
4456
4485
|
if (!locationId) return null;
|
|
4457
4486
|
const labelRaw = row["label"];
|
|
4458
4487
|
const label = typeof labelRaw === "string" && labelRaw.length > 0 ? labelRaw : void 0;
|
|
4459
|
-
const cronRaw = row["cron"];
|
|
4460
|
-
const cron = typeof cronRaw === "string" && cronRaw.length > 0 ? cronRaw : void 0;
|
|
4461
4488
|
const lastRunRaw = row["last_run_at"];
|
|
4462
|
-
const
|
|
4489
|
+
const lastAttemptAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
|
|
4490
|
+
const lastErrorRaw = row["last_error"];
|
|
4491
|
+
const lastError = typeof lastErrorRaw === "string" && lastErrorRaw.length > 0 ? lastErrorRaw : void 0;
|
|
4463
4492
|
return {
|
|
4464
4493
|
locationId,
|
|
4465
4494
|
enabled: row["enabled"] === 1 || row["enabled"] === true,
|
|
4466
4495
|
retentionCount: Number(row["retention_count"] ?? 0),
|
|
4467
4496
|
...label !== void 0 ? { label } : {},
|
|
4468
|
-
...
|
|
4469
|
-
...
|
|
4497
|
+
...lastAttemptAt !== void 0 ? { lastAttemptAt } : {},
|
|
4498
|
+
...lastError !== void 0 ? { lastError } : {}
|
|
4470
4499
|
};
|
|
4471
4500
|
}
|
|
4472
4501
|
//#endregion
|
|
@@ -4873,7 +4902,12 @@ var BackupRunQueue = class {
|
|
|
4873
4902
|
const progress = this.progressFor(record);
|
|
4874
4903
|
try {
|
|
4875
4904
|
const value = await this.executor(record.request, record.controller.signal, progress);
|
|
4876
|
-
|
|
4905
|
+
if (record.controller.signal.aborted) this.finish(record, "cancelled");
|
|
4906
|
+
else {
|
|
4907
|
+
const { destinationIds, completedDestinationIds, failedDestinationIds } = record.snapshot;
|
|
4908
|
+
if (destinationIds.length > 0 && completedDestinationIds.length === 0) this.finish(record, "failed", `no destination accepted the archive (${(failedDestinationIds.length > 0 ? failedDestinationIds : destinationIds).join(", ")})`);
|
|
4909
|
+
else this.finish(record, "succeeded");
|
|
4910
|
+
}
|
|
4877
4911
|
record.resolveResult(value);
|
|
4878
4912
|
} catch (err) {
|
|
4879
4913
|
if (err instanceof BackupCancelledError || record.controller.signal.aborted) {
|
|
@@ -4946,7 +4980,8 @@ function requestSignature(request) {
|
|
|
4946
4980
|
* delete + insert: cron, enabled, the fan-out `location_ids`, `retention_count`,
|
|
4947
4981
|
* optional `data_sources`), plus a `last_run_at` stamp the orchestrator writes on
|
|
4948
4982
|
* every due tick BEFORE the archive runs, so an upload outliving the tick cannot
|
|
4949
|
-
* fire the same schedule twice
|
|
4983
|
+
* fire the same schedule twice, and a `last_success_at` / `last_error` pair it
|
|
4984
|
+
* writes AFTER the run resolves"
|
|
4950
4985
|
* retention="only the operator's `delete(id)` — no sweep, no expiry, and `retention_count`
|
|
4951
4986
|
* bounds the ARCHIVES at the destination, never these rows. Losing the table stops
|
|
4952
4987
|
* every scheduled backup silently: no cron remains to fire, and the fan-out sets and
|
|
@@ -4996,6 +5031,14 @@ var BACKUP_SCHEDULES_COLUMNS = [
|
|
|
4996
5031
|
{
|
|
4997
5032
|
name: "last_run_at",
|
|
4998
5033
|
type: "INTEGER"
|
|
5034
|
+
},
|
|
5035
|
+
{
|
|
5036
|
+
name: "last_success_at",
|
|
5037
|
+
type: "INTEGER"
|
|
5038
|
+
},
|
|
5039
|
+
{
|
|
5040
|
+
name: "last_error",
|
|
5041
|
+
type: "TEXT"
|
|
4999
5042
|
}
|
|
5000
5043
|
];
|
|
5001
5044
|
var BackupScheduleService = class {
|
|
@@ -5074,7 +5117,9 @@ function mapScheduleToRow(s) {
|
|
|
5074
5117
|
location_ids: [...s.locationIds],
|
|
5075
5118
|
retention_count: s.retentionCount,
|
|
5076
5119
|
data_sources: s.dataSources ? [...s.dataSources] : null,
|
|
5077
|
-
last_run_at: s.
|
|
5120
|
+
last_run_at: s.lastAttemptAt ?? null,
|
|
5121
|
+
last_success_at: s.lastSuccessAt ?? null,
|
|
5122
|
+
last_error: s.lastError ?? null
|
|
5078
5123
|
};
|
|
5079
5124
|
}
|
|
5080
5125
|
/**
|
|
@@ -5102,7 +5147,11 @@ function mapRowToSchedule(row) {
|
|
|
5102
5147
|
const dataSourcesRaw = row["data_sources"];
|
|
5103
5148
|
const dataSources = dataSourcesRaw === null || dataSourcesRaw === void 0 ? void 0 : parseStringArray(dataSourcesRaw);
|
|
5104
5149
|
const lastRunRaw = row["last_run_at"];
|
|
5105
|
-
const
|
|
5150
|
+
const lastAttemptAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
|
|
5151
|
+
const lastSuccessRaw = row["last_success_at"];
|
|
5152
|
+
const lastSuccessAt = typeof lastSuccessRaw === "number" && lastSuccessRaw > 0 ? lastSuccessRaw : void 0;
|
|
5153
|
+
const lastErrorRaw = row["last_error"];
|
|
5154
|
+
const lastError = typeof lastErrorRaw === "string" && lastErrorRaw.length > 0 ? lastErrorRaw : void 0;
|
|
5106
5155
|
return {
|
|
5107
5156
|
id,
|
|
5108
5157
|
label: typeof row["label"] === "string" ? String(row["label"]) : "",
|
|
@@ -5111,7 +5160,9 @@ function mapRowToSchedule(row) {
|
|
|
5111
5160
|
locationIds: parseStringArray(row["location_ids"]),
|
|
5112
5161
|
retentionCount: Number(row["retention_count"] ?? 7),
|
|
5113
5162
|
...dataSources !== void 0 ? { dataSources } : {},
|
|
5114
|
-
...
|
|
5163
|
+
...lastAttemptAt !== void 0 ? { lastAttemptAt } : {},
|
|
5164
|
+
...lastSuccessAt !== void 0 ? { lastSuccessAt } : {},
|
|
5165
|
+
...lastError !== void 0 ? { lastError } : {}
|
|
5115
5166
|
};
|
|
5116
5167
|
}
|
|
5117
5168
|
//#endregion
|
|
@@ -5273,13 +5324,6 @@ function selectBackupTargets(input) {
|
|
|
5273
5324
|
* the panel used to expose).
|
|
5274
5325
|
*/
|
|
5275
5326
|
var DEFAULT_RETENTION_COUNT = 7;
|
|
5276
|
-
/**
|
|
5277
|
-
* Default cron stamped on a freshly-discovered `backups` location.
|
|
5278
|
-
* Operators can edit it from the destination row; setting empty in
|
|
5279
|
-
* the editor flips the destination back to manual-only. Daily 03:00
|
|
5280
|
-
* server-local mirrors the previous global default.
|
|
5281
|
-
*/
|
|
5282
|
-
var DEFAULT_CRON = "0 3 * * *";
|
|
5283
5327
|
var SCHEDULE_TICK_MS = 6e4;
|
|
5284
5328
|
/**
|
|
5285
5329
|
* Build the human-facing archive label. Every archive always carries
|
|
@@ -5323,9 +5367,11 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5323
5367
|
*/
|
|
5324
5368
|
stagingDir = "";
|
|
5325
5369
|
scheduleTimer = null;
|
|
5370
|
+
/** Guards the disarmed-scheduler complaint to once per process. */
|
|
5371
|
+
warnedSchedulerDisarmed = false;
|
|
5326
5372
|
/**
|
|
5327
|
-
* Baseline used as the "after" anchor when a
|
|
5328
|
-
* has never
|
|
5373
|
+
* Baseline used as the "after" anchor when a schedule has a cron but
|
|
5374
|
+
* has never fired yet (no `lastAttemptAt`). Set on orchestrator boot so a
|
|
5329
5375
|
* pattern that fires every minute doesn't immediately replay every
|
|
5330
5376
|
* historical "missed" run from epoch 0.
|
|
5331
5377
|
*/
|
|
@@ -5419,7 +5465,37 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5419
5465
|
}
|
|
5420
5466
|
}));
|
|
5421
5467
|
const lastSuccessByLoc = new Map(lastSuccess);
|
|
5422
|
-
|
|
5468
|
+
const cadence = await this.cadenceByLocation();
|
|
5469
|
+
return locations.map((loc) => this.toDestinationWire(loc, policies.get(loc.id), lastSuccessByLoc.get(loc.id), cadence.get(loc.id)));
|
|
5470
|
+
}
|
|
5471
|
+
/**
|
|
5472
|
+
* Join the enabled schedules by the locations they fan out to.
|
|
5473
|
+
* Comma-joined crons (a location can be targeted by several schedules),
|
|
5474
|
+
* earliest next firing. Computed from `Date.now()` — an operator asking
|
|
5475
|
+
* "when next" means from now, never from a stale run cursor.
|
|
5476
|
+
*/
|
|
5477
|
+
async cadenceByLocation() {
|
|
5478
|
+
const out = /* @__PURE__ */ new Map();
|
|
5479
|
+
if (!this.schedules) return out;
|
|
5480
|
+
const schedules = await this.schedules.list().catch((err) => {
|
|
5481
|
+
this.ctx.logger.warn("backup-orchestrator: schedule list failed — cadence unknown", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
5482
|
+
return [];
|
|
5483
|
+
});
|
|
5484
|
+
const now = Date.now();
|
|
5485
|
+
for (const s of schedules) {
|
|
5486
|
+
if (!s.enabled || s.cron.trim().length === 0) continue;
|
|
5487
|
+
const next = this.safeNextDue(s.cron, now);
|
|
5488
|
+
for (const id of s.locationIds) {
|
|
5489
|
+
const prev = out.get(id);
|
|
5490
|
+
const crons = prev ? [...prev.crons, s.cron] : [s.cron];
|
|
5491
|
+
const nextRunAt = next === void 0 ? prev?.nextRunAt : prev?.nextRunAt === void 0 ? next : Math.min(prev.nextRunAt, next);
|
|
5492
|
+
out.set(id, {
|
|
5493
|
+
crons,
|
|
5494
|
+
...nextRunAt !== void 0 ? { nextRunAt } : {}
|
|
5495
|
+
});
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
return out;
|
|
5423
5499
|
}
|
|
5424
5500
|
async fetchBackupLocations() {
|
|
5425
5501
|
try {
|
|
@@ -5450,10 +5526,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5450
5526
|
for (const id of locationIds) {
|
|
5451
5527
|
if (out.has(id)) continue;
|
|
5452
5528
|
try {
|
|
5453
|
-
const fresh = await this.policies.ensureDefault(id, {
|
|
5454
|
-
retentionCount: DEFAULT_RETENTION_COUNT,
|
|
5455
|
-
cron: DEFAULT_CRON
|
|
5456
|
-
});
|
|
5529
|
+
const fresh = await this.policies.ensureDefault(id, { retentionCount: DEFAULT_RETENTION_COUNT });
|
|
5457
5530
|
out.set(id, fresh);
|
|
5458
5531
|
} catch (err) {
|
|
5459
5532
|
this.ctx.logger.warn("backup-orchestrator: ensureDefault failed", { meta: {
|
|
@@ -5469,11 +5542,10 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5469
5542
|
return {
|
|
5470
5543
|
locationId,
|
|
5471
5544
|
enabled: true,
|
|
5472
|
-
retentionCount: DEFAULT_RETENTION_COUNT
|
|
5473
|
-
cron: DEFAULT_CRON
|
|
5545
|
+
retentionCount: DEFAULT_RETENTION_COUNT
|
|
5474
5546
|
};
|
|
5475
5547
|
}
|
|
5476
|
-
toDestinationWire(loc, policy, latest) {
|
|
5548
|
+
toDestinationWire(loc, policy, latest, cadence) {
|
|
5477
5549
|
const effective = policy ?? this.fallbackPolicy(loc.id);
|
|
5478
5550
|
const subId = extractSlug(loc.id);
|
|
5479
5551
|
const displayName = effective.label && effective.label.length > 0 ? effective.label : loc.displayName;
|
|
@@ -5490,9 +5562,10 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5490
5562
|
...effective.label !== void 0 ? { label: effective.label } : {},
|
|
5491
5563
|
...latest !== void 0 ? { lastSuccessAt: latest.createdAt } : {},
|
|
5492
5564
|
...latest !== void 0 ? { lastSuccessSizeBytes: latest.sizeBytes } : {},
|
|
5493
|
-
...
|
|
5494
|
-
...
|
|
5495
|
-
...effective.
|
|
5565
|
+
...cadence !== void 0 ? { cron: cadence.crons.join(", ") } : {},
|
|
5566
|
+
...cadence?.nextRunAt !== void 0 ? { nextRunAt: cadence.nextRunAt } : {},
|
|
5567
|
+
...effective.lastAttemptAt !== void 0 ? { lastAttemptAt: effective.lastAttemptAt } : {},
|
|
5568
|
+
...effective.lastError !== void 0 ? { lastError: effective.lastError } : {}
|
|
5496
5569
|
};
|
|
5497
5570
|
}
|
|
5498
5571
|
/**
|
|
@@ -5654,7 +5727,10 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5654
5727
|
if (!this.systemBackup) throw new Error("orchestrator not initialized");
|
|
5655
5728
|
const targets = await this.resolveTargets(input.destinations !== void 0 ? [...input.destinations] : void 0);
|
|
5656
5729
|
if (targets.length === 0) {
|
|
5657
|
-
this.ctx.logger.warn("backup trigger: no destinations selected"
|
|
5730
|
+
this.ctx.logger.warn("backup trigger: no destinations selected — this archive is being dropped", { meta: {
|
|
5731
|
+
requestedDestinationIds: input.destinations !== void 0 ? [...input.destinations] : [],
|
|
5732
|
+
label: input.label ?? null
|
|
5733
|
+
} });
|
|
5658
5734
|
return [];
|
|
5659
5735
|
}
|
|
5660
5736
|
progress.setDestinations(targets.map((t) => t.id));
|
|
@@ -5698,6 +5774,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5698
5774
|
});
|
|
5699
5775
|
entries.push(stored);
|
|
5700
5776
|
progress.destinationCompleted(dest.id);
|
|
5777
|
+
await this.recordDestinationOutcome(dest.id, createdAt);
|
|
5701
5778
|
this.ctx.eventBus.emit({
|
|
5702
5779
|
id: randomUUID(),
|
|
5703
5780
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -5715,10 +5792,12 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5715
5792
|
} catch (err) {
|
|
5716
5793
|
if (err instanceof BackupCancelledError) continue;
|
|
5717
5794
|
progress.destinationFailed(dest.id);
|
|
5795
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5718
5796
|
this.ctx.logger.error("backup trigger: destination failed", { meta: {
|
|
5719
5797
|
destinationId: dest.id,
|
|
5720
|
-
error:
|
|
5798
|
+
error: message
|
|
5721
5799
|
} });
|
|
5800
|
+
await this.recordDestinationOutcome(dest.id, Date.now(), message);
|
|
5722
5801
|
}
|
|
5723
5802
|
}
|
|
5724
5803
|
return entries;
|
|
@@ -5837,30 +5916,29 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5837
5916
|
/**
|
|
5838
5917
|
* Upsert the per-destination policy row. Used by the admin-UI
|
|
5839
5918
|
* destinations table — toggle enabled / change retention / set
|
|
5840
|
-
* label
|
|
5919
|
+
* label. No-ops with a logged warning if the
|
|
5841
5920
|
* settings backend lacks structured-table support (no persistence
|
|
5842
5921
|
* available).
|
|
5843
5922
|
*
|
|
5844
|
-
*
|
|
5845
|
-
*
|
|
5846
|
-
*
|
|
5847
|
-
*
|
|
5923
|
+
* There is deliberately NO cadence here. A destination does not run on
|
|
5924
|
+
* its own timer — a schedule fans out to it — and the `cron` this method
|
|
5925
|
+
* used to accept had scheduled nothing since 2026-07-28 while still being
|
|
5926
|
+
* rendered as "daily at 03:00" (D384, and D62's "a switch that writes a
|
|
5927
|
+
* store nobody reads").
|
|
5848
5928
|
*/
|
|
5849
5929
|
async upsertDestinationPolicy(input) {
|
|
5850
5930
|
if (!this.policies) {
|
|
5851
5931
|
this.ctx.logger.warn("backup upsertDestinationPolicy: no settings-store backend — change will not persist", { meta: { locationId: input.locationId } });
|
|
5852
5932
|
return;
|
|
5853
5933
|
}
|
|
5854
|
-
const cron = typeof input.cron === "string" ? input.cron.trim() : "";
|
|
5855
|
-
if (cron.length > 0 && !isValidCron(cron)) throw new Error(`Invalid cron expression: "${cron}"`);
|
|
5856
5934
|
const existing = await this.policies.get(input.locationId).catch(() => null);
|
|
5857
5935
|
await this.policies.upsert({
|
|
5858
5936
|
locationId: input.locationId,
|
|
5859
5937
|
enabled: input.enabled,
|
|
5860
5938
|
retentionCount: input.retentionCount,
|
|
5861
5939
|
...input.label !== void 0 ? { label: input.label } : {},
|
|
5862
|
-
...
|
|
5863
|
-
...existing?.
|
|
5940
|
+
...existing?.lastAttemptAt !== void 0 ? { lastAttemptAt: existing.lastAttemptAt } : {},
|
|
5941
|
+
...existing?.lastError !== void 0 ? { lastError: existing.lastError } : {}
|
|
5864
5942
|
});
|
|
5865
5943
|
}
|
|
5866
5944
|
/**
|
|
@@ -5998,6 +6076,29 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
5998
6076
|
if (selection.targets.length === 0 && selection.skippedDisabled.length > 0) throw new Error(`every selected backup destination is disabled (${selection.skippedDisabled.join(", ")}) — enable its storage location on the Storage screen, or choose another destination. Refusing to write an archive to a location the operator turned off.`);
|
|
5999
6077
|
return selection.targets.map((t) => ({ id: t.id }));
|
|
6000
6078
|
}
|
|
6079
|
+
/**
|
|
6080
|
+
* Persist the outcome of one attempted write to one destination.
|
|
6081
|
+
*
|
|
6082
|
+
* Best-effort by design: an unavailable settings backend must not turn a
|
|
6083
|
+
* successful backup into a failed one. But it is LOGGED when it cannot be
|
|
6084
|
+
* written — a status field that silently stops being updated is the whole
|
|
6085
|
+
* defect this method exists to close.
|
|
6086
|
+
*/
|
|
6087
|
+
async recordDestinationOutcome(destinationId, attemptAt, error) {
|
|
6088
|
+
if (!this.policies) return;
|
|
6089
|
+
try {
|
|
6090
|
+
await this.policies.recordAttempt(destinationId, {
|
|
6091
|
+
attemptAt,
|
|
6092
|
+
...error !== void 0 ? { error } : {}
|
|
6093
|
+
}, { retentionCount: DEFAULT_RETENTION_COUNT });
|
|
6094
|
+
} catch (err) {
|
|
6095
|
+
this.ctx.logger.warn("backup: could not persist the destination outcome", { meta: {
|
|
6096
|
+
destinationId,
|
|
6097
|
+
outcome: error === void 0 ? "succeeded" : "failed",
|
|
6098
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6099
|
+
} });
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6001
6102
|
startScheduleTimer() {
|
|
6002
6103
|
if (this.scheduleTimer) return;
|
|
6003
6104
|
this.scheduleTimer = setInterval(() => {
|
|
@@ -6012,19 +6113,25 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
6012
6113
|
/**
|
|
6013
6114
|
* Schedule-driven backup scheduling. Walks every enabled backup
|
|
6014
6115
|
* SCHEDULE (the N:M entry), computes the next due time after
|
|
6015
|
-
* `
|
|
6116
|
+
* `lastAttemptAt` (or the orchestrator's first-tick baseline if never
|
|
6016
6117
|
* run), and fires a SINGLE archive fanned out to ALL of the
|
|
6017
6118
|
* schedule's `locationIds` when due — with the schedule's per-entry
|
|
6018
6119
|
* retention applied to each. Each schedule keeps its own cadence.
|
|
6019
6120
|
*
|
|
6020
6121
|
* Errors per schedule are isolated; one misconfigured cron doesn't
|
|
6021
|
-
* block the others. The schedule's `
|
|
6122
|
+
* block the others. The schedule's `lastAttemptAt` is updated BEFORE the
|
|
6022
6123
|
* trigger to dedupe (the tick runs every SCHEDULE_TICK_MS — without
|
|
6023
6124
|
* the up-front update a long-running upload could overlap with the
|
|
6024
6125
|
* next tick's same-due check).
|
|
6025
6126
|
*/
|
|
6026
6127
|
async onScheduleTick() {
|
|
6027
|
-
if (!this.schedules)
|
|
6128
|
+
if (!this.schedules) {
|
|
6129
|
+
if (!this.warnedSchedulerDisarmed) {
|
|
6130
|
+
this.warnedSchedulerDisarmed = true;
|
|
6131
|
+
this.ctx.logger.error("backup scheduler is DISARMED — no settings backend, so no schedule can fire");
|
|
6132
|
+
}
|
|
6133
|
+
return;
|
|
6134
|
+
}
|
|
6028
6135
|
const schedules = await this.schedules.list();
|
|
6029
6136
|
const now = Date.now();
|
|
6030
6137
|
for (const s of schedules) {
|
|
@@ -6032,7 +6139,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
6032
6139
|
if (s.locationIds.length === 0) continue;
|
|
6033
6140
|
let due;
|
|
6034
6141
|
try {
|
|
6035
|
-
due = computeNextDue(s.cron, s.
|
|
6142
|
+
due = computeNextDue(s.cron, s.lastAttemptAt ?? this.scheduleBaselineAt);
|
|
6036
6143
|
} catch (err) {
|
|
6037
6144
|
this.ctx.logger.warn("Invalid cron — skipping schedule", { meta: {
|
|
6038
6145
|
scheduleId: s.id,
|
|
@@ -6044,36 +6151,76 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
6044
6151
|
if (due === null || due > now) continue;
|
|
6045
6152
|
const stamped = {
|
|
6046
6153
|
...s,
|
|
6047
|
-
|
|
6154
|
+
lastAttemptAt: now
|
|
6048
6155
|
};
|
|
6049
6156
|
try {
|
|
6050
6157
|
await this.schedules.upsert(stamped);
|
|
6051
6158
|
} catch (err) {
|
|
6052
6159
|
this.ctx.logger.warn("Failed to persist scheduled-run timestamp", { meta: {
|
|
6053
6160
|
scheduleId: s.id,
|
|
6161
|
+
scheduleLabel: s.label,
|
|
6162
|
+
destinationIds: [...s.locationIds],
|
|
6054
6163
|
error: err instanceof Error ? err.message : String(err)
|
|
6055
6164
|
} });
|
|
6056
6165
|
}
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6166
|
+
this.ctx.logger.info("Scheduled backup firing", { meta: {
|
|
6167
|
+
scheduleId: s.id,
|
|
6168
|
+
label: s.label,
|
|
6169
|
+
destinationIds: [...s.locationIds],
|
|
6170
|
+
cron: s.cron,
|
|
6171
|
+
dueAt: due
|
|
6172
|
+
} });
|
|
6173
|
+
await this.runScheduledBackup(stamped, now);
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
/**
|
|
6177
|
+
* Fire one schedule and RECORD what happened to it, naming every
|
|
6178
|
+
* destination. Three outcomes, three different persisted states —
|
|
6179
|
+
* because "fired" and "backed up" were the same field until D384:
|
|
6180
|
+
*
|
|
6181
|
+
* - archive landed somewhere → `lastSuccessAt`, `lastError` cleared;
|
|
6182
|
+
* - the run threw, or landed nowhere → `lastError`, no success stamp;
|
|
6183
|
+
* - the stamp could not be written → logged, named, not swallowed.
|
|
6184
|
+
*/
|
|
6185
|
+
async runScheduledBackup(schedule, firedAt) {
|
|
6186
|
+
const destinationIds = [...schedule.locationIds];
|
|
6187
|
+
let failure;
|
|
6188
|
+
try {
|
|
6189
|
+
const result = await this.triggerBackup({
|
|
6190
|
+
destinations: destinationIds,
|
|
6191
|
+
...schedule.dataSources && schedule.dataSources.length > 0 ? { locations: [...schedule.dataSources] } : {},
|
|
6192
|
+
label: schedule.label,
|
|
6193
|
+
retentionCount: schedule.retentionCount
|
|
6194
|
+
});
|
|
6195
|
+
if (result.entries.length === 0 && !result.cancelled) failure = `no destination accepted the archive (${destinationIds.join(", ")})`;
|
|
6196
|
+
} catch (err) {
|
|
6197
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
6198
|
+
}
|
|
6199
|
+
if (failure === void 0) this.ctx.logger.info("Scheduled backup completed", { meta: {
|
|
6200
|
+
scheduleId: schedule.id,
|
|
6201
|
+
label: schedule.label,
|
|
6202
|
+
destinationIds
|
|
6203
|
+
} });
|
|
6204
|
+
else this.ctx.logger.error("Scheduled backup failed", { meta: {
|
|
6205
|
+
scheduleId: schedule.id,
|
|
6206
|
+
label: schedule.label,
|
|
6207
|
+
destinationIds,
|
|
6208
|
+
error: failure
|
|
6209
|
+
} });
|
|
6210
|
+
if (!this.schedules) return;
|
|
6211
|
+
const { lastError: _cleared, ...carried } = schedule;
|
|
6212
|
+
try {
|
|
6213
|
+
await this.schedules.upsert({
|
|
6214
|
+
...carried,
|
|
6215
|
+
...failure === void 0 ? { lastSuccessAt: firedAt } : { lastError: failure }
|
|
6216
|
+
});
|
|
6217
|
+
} catch (err) {
|
|
6218
|
+
this.ctx.logger.warn("Failed to persist the scheduled-run outcome", { meta: {
|
|
6219
|
+
scheduleId: schedule.id,
|
|
6220
|
+
label: schedule.label,
|
|
6221
|
+
destinationIds,
|
|
6222
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6223
|
+
} });
|
|
6077
6224
|
}
|
|
6078
6225
|
}
|
|
6079
6226
|
async listSchedules() {
|
|
@@ -6092,7 +6239,9 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
6092
6239
|
locationIds: [...s.locationIds],
|
|
6093
6240
|
retentionCount: s.retentionCount,
|
|
6094
6241
|
...s.dataSources !== void 0 ? { dataSources: [...s.dataSources] } : {},
|
|
6095
|
-
...s.
|
|
6242
|
+
...s.lastAttemptAt !== void 0 ? { lastAttemptAt: s.lastAttemptAt } : {},
|
|
6243
|
+
...s.lastSuccessAt !== void 0 ? { lastSuccessAt: s.lastSuccessAt } : {},
|
|
6244
|
+
...s.lastError !== void 0 ? { lastError: s.lastError } : {},
|
|
6096
6245
|
...nextRunAt !== void 0 ? { nextRunAt } : {}
|
|
6097
6246
|
};
|
|
6098
6247
|
});
|
|
@@ -6111,7 +6260,9 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
6111
6260
|
locationIds: [...input.locationIds],
|
|
6112
6261
|
retentionCount: input.retentionCount,
|
|
6113
6262
|
...input.dataSources && input.dataSources.length > 0 ? { dataSources: [...input.dataSources] } : {},
|
|
6114
|
-
...existing?.
|
|
6263
|
+
...existing?.lastAttemptAt !== void 0 ? { lastAttemptAt: existing.lastAttemptAt } : {},
|
|
6264
|
+
...existing?.lastSuccessAt !== void 0 ? { lastSuccessAt: existing.lastSuccessAt } : {},
|
|
6265
|
+
...existing?.lastError !== void 0 ? { lastError: existing.lastError } : {}
|
|
6115
6266
|
};
|
|
6116
6267
|
await this.schedules.upsert(next);
|
|
6117
6268
|
let nextRunAt;
|
|
@@ -6127,7 +6278,9 @@ var BackupOrchestratorAddon = class extends BaseAddon {
|
|
|
6127
6278
|
locationIds: [...next.locationIds],
|
|
6128
6279
|
retentionCount: next.retentionCount,
|
|
6129
6280
|
...next.dataSources !== void 0 ? { dataSources: [...next.dataSources] } : {},
|
|
6130
|
-
...next.
|
|
6281
|
+
...next.lastAttemptAt !== void 0 ? { lastAttemptAt: next.lastAttemptAt } : {},
|
|
6282
|
+
...next.lastSuccessAt !== void 0 ? { lastSuccessAt: next.lastSuccessAt } : {},
|
|
6283
|
+
...next.lastError !== void 0 ? { lastError: next.lastError } : {},
|
|
6131
6284
|
...nextRunAt !== void 0 ? { nextRunAt } : {}
|
|
6132
6285
|
};
|
|
6133
6286
|
}
|
|
@@ -19,16 +19,19 @@ export interface BackupDestinationPolicy {
|
|
|
19
19
|
readonly retentionCount: number;
|
|
20
20
|
readonly label?: string;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* seconds field).
|
|
22
|
+
* ms-epoch of the last time a run ATTEMPTED to write an archive here,
|
|
23
|
+
* whatever the outcome. Never a success stamp — `lastError` says whether
|
|
24
|
+
* the attempt landed, and the newest manifest entry says when one last
|
|
25
|
+
* did. Rows written before 2026-07-28 carry the old per-location
|
|
26
|
+
* scheduler's stamp, which had exactly this meaning.
|
|
28
27
|
*/
|
|
29
|
-
readonly
|
|
30
|
-
/**
|
|
31
|
-
|
|
28
|
+
readonly lastAttemptAt?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Why the last attempt failed, verbatim from the provider. Cleared on the
|
|
31
|
+
* next attempt that lands. A destination that has never been written to
|
|
32
|
+
* has neither field, and that is REPORTED as never — D384.
|
|
33
|
+
*/
|
|
34
|
+
readonly lastError?: string;
|
|
32
35
|
}
|
|
33
36
|
export declare const BACKUP_DESTINATION_POLICIES_COLUMNS: readonly CollectionColumn[];
|
|
34
37
|
export declare class BackupDestinationPolicyService {
|
|
@@ -58,13 +61,28 @@ export declare class BackupDestinationPolicyService {
|
|
|
58
61
|
* pass so newly-added storage locations are surfaced as enabled
|
|
59
62
|
* destinations without an explicit configuration step.
|
|
60
63
|
*
|
|
61
|
-
* Defaults are passed by the orchestrator (currently
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
64
|
+
* Defaults are passed by the orchestrator (currently `retentionCount=7`).
|
|
65
|
+
* A fresh destination is enabled and keeps 7 archives; it gets NO cadence
|
|
66
|
+
* of its own — a destination runs when a schedule fans out to it, and
|
|
67
|
+
* seeding a cron here would advertise a nightly run nothing performs
|
|
68
|
+
* (D384).
|
|
65
69
|
*/
|
|
66
70
|
ensureDefault(locationId: string, defaults: {
|
|
67
71
|
retentionCount: number;
|
|
68
|
-
cron?: string;
|
|
69
72
|
}): Promise<BackupDestinationPolicy>;
|
|
73
|
+
/**
|
|
74
|
+
* Stamp the outcome of ONE attempted write to this destination.
|
|
75
|
+
*
|
|
76
|
+
* Read-modify-write on the policy row so the operator's `enabled`,
|
|
77
|
+
* `retentionCount` and `label` survive. A destination with no row yet
|
|
78
|
+
* gets one — an attempt on an unseen location must still be recorded, or
|
|
79
|
+
* the failure disappears with the process (which is exactly how a 03:00
|
|
80
|
+
* SMB refusal stayed invisible until the operator went looking).
|
|
81
|
+
*/
|
|
82
|
+
recordAttempt(locationId: string, outcome: {
|
|
83
|
+
attemptAt: number;
|
|
84
|
+
error?: string;
|
|
85
|
+
}, defaults: {
|
|
86
|
+
retentionCount: number;
|
|
87
|
+
}): Promise<void>;
|
|
70
88
|
}
|
|
@@ -15,8 +15,18 @@ export interface BackupSchedule {
|
|
|
15
15
|
readonly locationIds: readonly string[];
|
|
16
16
|
readonly retentionCount: number;
|
|
17
17
|
readonly dataSources?: readonly string[];
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* ms-epoch of the last tick that FIRED this schedule. Stamped BEFORE the
|
|
20
|
+
* archive runs — it is the dedupe anchor and the cron's "after" cursor —
|
|
21
|
+
* so it means "attempted", never "succeeded". It was documented as the
|
|
22
|
+
* latter and rendered as "last run", which is how a schedule whose only
|
|
23
|
+
* destination refused every byte read as a healthy nightly backup (D384).
|
|
24
|
+
*/
|
|
25
|
+
readonly lastAttemptAt?: number;
|
|
26
|
+
/** ms-epoch of the last fired run that landed at ≥1 destination. */
|
|
27
|
+
readonly lastSuccessAt?: number;
|
|
28
|
+
/** Why the last fired run failed, verbatim. Cleared by the next success. */
|
|
29
|
+
readonly lastError?: string;
|
|
20
30
|
}
|
|
21
31
|
export declare const BACKUP_SCHEDULES_COLUMNS: readonly CollectionColumn[];
|
|
22
32
|
export declare class BackupScheduleService {
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CcV3k-hD.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
//#region src/builtins/console-logging/console-destination.ts
|
|
9
9
|
var LEVEL_RANK = {
|