@camstack/system 1.2.200 → 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-CCp74QLJ.mjs → dist-BmJNnb_s.mjs} +33 -14
- package/dist/{dist-KQ5D2wqr.js → dist-CcV3k-hD.js} +33 -14
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-2lWk7OeU.js → retired-settings-keys-Bd8fuwO1.js} +1 -1
- package/dist/{retired-settings-keys-7P9XwNdl.mjs → retired-settings-keys-nLVKlhe-.mjs} +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CcV3k-hD.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let buffer = require("buffer");
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -4309,14 +4309,17 @@ function previewNextRuns(cron, count, after) {
|
|
|
4309
4309
|
/**
|
|
4310
4310
|
* @durable class=config owner=backup-orchestrator
|
|
4311
4311
|
* write="`ensureDefault` auto-creates one row the first time a `backups` storage
|
|
4312
|
-
* location is listed (enabled, retentionCount 7,
|
|
4312
|
+
* location is listed (enabled, retentionCount 7, no cadence of its own), an operator
|
|
4313
4313
|
* edit rewrites it (`upsert` = delete + insert, carrying `last_run_at` forward so a
|
|
4314
|
-
* label change does not reset the
|
|
4314
|
+
* label change does not reset the baseline), and `recordAttempt` stamps
|
|
4315
|
+
* `last_run_at` + `last_error` on every attempted write to the destination"
|
|
4315
4316
|
* retention="none — `delete(locationId)` exists but NOTHING calls it, so a policy row
|
|
4316
4317
|
* outlives the `StorageLocation` it names and the table only ever grows, by one row
|
|
4317
4318
|
* per backups location ever seen. Losing a row is recoverable: the next
|
|
4318
4319
|
* `listDestinations` pass re-creates it from the defaults, which silently discards
|
|
4319
|
-
* the operator's own retention count
|
|
4320
|
+
* the operator's own retention count. The legacy `cron` column is written NULL and
|
|
4321
|
+
* never read — it scheduled nothing after 2026-07-28 and reading it made a
|
|
4322
|
+
* destination advertise a nightly run nobody performed (D384)."
|
|
4320
4323
|
*/
|
|
4321
4324
|
var BACKUP_DESTINATION_POLICIES = "backup_destination_policies";
|
|
4322
4325
|
var BACKUP_DESTINATION_POLICIES_COLUMNS = [
|
|
@@ -4348,6 +4351,10 @@ var BACKUP_DESTINATION_POLICIES_COLUMNS = [
|
|
|
4348
4351
|
{
|
|
4349
4352
|
name: "last_run_at",
|
|
4350
4353
|
type: "INTEGER"
|
|
4354
|
+
},
|
|
4355
|
+
{
|
|
4356
|
+
name: "last_error",
|
|
4357
|
+
type: "TEXT"
|
|
4351
4358
|
}
|
|
4352
4359
|
];
|
|
4353
4360
|
var BackupDestinationPolicyService = class {
|
|
@@ -4432,10 +4439,11 @@ var BackupDestinationPolicyService = class {
|
|
|
4432
4439
|
* pass so newly-added storage locations are surfaced as enabled
|
|
4433
4440
|
* destinations without an explicit configuration step.
|
|
4434
4441
|
*
|
|
4435
|
-
* Defaults are passed by the orchestrator (currently
|
|
4436
|
-
*
|
|
4437
|
-
*
|
|
4438
|
-
*
|
|
4442
|
+
* Defaults are passed by the orchestrator (currently `retentionCount=7`).
|
|
4443
|
+
* A fresh destination is enabled and keeps 7 archives; it gets NO cadence
|
|
4444
|
+
* of its own — a destination runs when a schedule fans out to it, and
|
|
4445
|
+
* seeding a cron here would advertise a nightly run nothing performs
|
|
4446
|
+
* (D384).
|
|
4439
4447
|
*/
|
|
4440
4448
|
async ensureDefault(locationId, defaults) {
|
|
4441
4449
|
const existing = await this.get(locationId);
|
|
@@ -4443,12 +4451,32 @@ var BackupDestinationPolicyService = class {
|
|
|
4443
4451
|
const fresh = {
|
|
4444
4452
|
locationId,
|
|
4445
4453
|
enabled: true,
|
|
4446
|
-
retentionCount: defaults.retentionCount
|
|
4447
|
-
...defaults.cron !== void 0 ? { cron: defaults.cron } : {}
|
|
4454
|
+
retentionCount: defaults.retentionCount
|
|
4448
4455
|
};
|
|
4449
4456
|
await this.upsert(fresh);
|
|
4450
4457
|
return fresh;
|
|
4451
4458
|
}
|
|
4459
|
+
/**
|
|
4460
|
+
* Stamp the outcome of ONE attempted write to this destination.
|
|
4461
|
+
*
|
|
4462
|
+
* Read-modify-write on the policy row so the operator's `enabled`,
|
|
4463
|
+
* `retentionCount` and `label` survive. A destination with no row yet
|
|
4464
|
+
* gets one — an attempt on an unseen location must still be recorded, or
|
|
4465
|
+
* the failure disappears with the process (which is exactly how a 03:00
|
|
4466
|
+
* SMB refusal stayed invisible until the operator went looking).
|
|
4467
|
+
*/
|
|
4468
|
+
async recordAttempt(locationId, outcome, defaults) {
|
|
4469
|
+
const { lastError: _cleared, ...carried } = await this.get(locationId) ?? {
|
|
4470
|
+
locationId,
|
|
4471
|
+
enabled: true,
|
|
4472
|
+
retentionCount: defaults.retentionCount
|
|
4473
|
+
};
|
|
4474
|
+
await this.upsert({
|
|
4475
|
+
...carried,
|
|
4476
|
+
lastAttemptAt: outcome.attemptAt,
|
|
4477
|
+
...outcome.error !== void 0 ? { lastError: outcome.error } : {}
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
4452
4480
|
};
|
|
4453
4481
|
function mapPolicyToRow(p) {
|
|
4454
4482
|
return {
|
|
@@ -4456,8 +4484,9 @@ function mapPolicyToRow(p) {
|
|
|
4456
4484
|
enabled: p.enabled ? 1 : 0,
|
|
4457
4485
|
retention_count: p.retentionCount,
|
|
4458
4486
|
label: p.label ?? null,
|
|
4459
|
-
cron:
|
|
4460
|
-
last_run_at: p.
|
|
4487
|
+
cron: null,
|
|
4488
|
+
last_run_at: p.lastAttemptAt ?? null,
|
|
4489
|
+
last_error: p.lastError ?? null
|
|
4461
4490
|
};
|
|
4462
4491
|
}
|
|
4463
4492
|
/**
|
|
@@ -4471,17 +4500,17 @@ function mapRowToPolicy(row) {
|
|
|
4471
4500
|
if (!locationId) return null;
|
|
4472
4501
|
const labelRaw = row["label"];
|
|
4473
4502
|
const label = typeof labelRaw === "string" && labelRaw.length > 0 ? labelRaw : void 0;
|
|
4474
|
-
const cronRaw = row["cron"];
|
|
4475
|
-
const cron = typeof cronRaw === "string" && cronRaw.length > 0 ? cronRaw : void 0;
|
|
4476
4503
|
const lastRunRaw = row["last_run_at"];
|
|
4477
|
-
const
|
|
4504
|
+
const lastAttemptAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
|
|
4505
|
+
const lastErrorRaw = row["last_error"];
|
|
4506
|
+
const lastError = typeof lastErrorRaw === "string" && lastErrorRaw.length > 0 ? lastErrorRaw : void 0;
|
|
4478
4507
|
return {
|
|
4479
4508
|
locationId,
|
|
4480
4509
|
enabled: row["enabled"] === 1 || row["enabled"] === true,
|
|
4481
4510
|
retentionCount: Number(row["retention_count"] ?? 0),
|
|
4482
4511
|
...label !== void 0 ? { label } : {},
|
|
4483
|
-
...
|
|
4484
|
-
...
|
|
4512
|
+
...lastAttemptAt !== void 0 ? { lastAttemptAt } : {},
|
|
4513
|
+
...lastError !== void 0 ? { lastError } : {}
|
|
4485
4514
|
};
|
|
4486
4515
|
}
|
|
4487
4516
|
//#endregion
|
|
@@ -4888,7 +4917,12 @@ var BackupRunQueue = class {
|
|
|
4888
4917
|
const progress = this.progressFor(record);
|
|
4889
4918
|
try {
|
|
4890
4919
|
const value = await this.executor(record.request, record.controller.signal, progress);
|
|
4891
|
-
|
|
4920
|
+
if (record.controller.signal.aborted) this.finish(record, "cancelled");
|
|
4921
|
+
else {
|
|
4922
|
+
const { destinationIds, completedDestinationIds, failedDestinationIds } = record.snapshot;
|
|
4923
|
+
if (destinationIds.length > 0 && completedDestinationIds.length === 0) this.finish(record, "failed", `no destination accepted the archive (${(failedDestinationIds.length > 0 ? failedDestinationIds : destinationIds).join(", ")})`);
|
|
4924
|
+
else this.finish(record, "succeeded");
|
|
4925
|
+
}
|
|
4892
4926
|
record.resolveResult(value);
|
|
4893
4927
|
} catch (err) {
|
|
4894
4928
|
if (err instanceof BackupCancelledError || record.controller.signal.aborted) {
|
|
@@ -4961,7 +4995,8 @@ function requestSignature(request) {
|
|
|
4961
4995
|
* delete + insert: cron, enabled, the fan-out `location_ids`, `retention_count`,
|
|
4962
4996
|
* optional `data_sources`), plus a `last_run_at` stamp the orchestrator writes on
|
|
4963
4997
|
* every due tick BEFORE the archive runs, so an upload outliving the tick cannot
|
|
4964
|
-
* fire the same schedule twice
|
|
4998
|
+
* fire the same schedule twice, and a `last_success_at` / `last_error` pair it
|
|
4999
|
+
* writes AFTER the run resolves"
|
|
4965
5000
|
* retention="only the operator's `delete(id)` — no sweep, no expiry, and `retention_count`
|
|
4966
5001
|
* bounds the ARCHIVES at the destination, never these rows. Losing the table stops
|
|
4967
5002
|
* every scheduled backup silently: no cron remains to fire, and the fan-out sets and
|
|
@@ -5011,6 +5046,14 @@ var BACKUP_SCHEDULES_COLUMNS = [
|
|
|
5011
5046
|
{
|
|
5012
5047
|
name: "last_run_at",
|
|
5013
5048
|
type: "INTEGER"
|
|
5049
|
+
},
|
|
5050
|
+
{
|
|
5051
|
+
name: "last_success_at",
|
|
5052
|
+
type: "INTEGER"
|
|
5053
|
+
},
|
|
5054
|
+
{
|
|
5055
|
+
name: "last_error",
|
|
5056
|
+
type: "TEXT"
|
|
5014
5057
|
}
|
|
5015
5058
|
];
|
|
5016
5059
|
var BackupScheduleService = class {
|
|
@@ -5089,7 +5132,9 @@ function mapScheduleToRow(s) {
|
|
|
5089
5132
|
location_ids: [...s.locationIds],
|
|
5090
5133
|
retention_count: s.retentionCount,
|
|
5091
5134
|
data_sources: s.dataSources ? [...s.dataSources] : null,
|
|
5092
|
-
last_run_at: s.
|
|
5135
|
+
last_run_at: s.lastAttemptAt ?? null,
|
|
5136
|
+
last_success_at: s.lastSuccessAt ?? null,
|
|
5137
|
+
last_error: s.lastError ?? null
|
|
5093
5138
|
};
|
|
5094
5139
|
}
|
|
5095
5140
|
/**
|
|
@@ -5117,7 +5162,11 @@ function mapRowToSchedule(row) {
|
|
|
5117
5162
|
const dataSourcesRaw = row["data_sources"];
|
|
5118
5163
|
const dataSources = dataSourcesRaw === null || dataSourcesRaw === void 0 ? void 0 : parseStringArray(dataSourcesRaw);
|
|
5119
5164
|
const lastRunRaw = row["last_run_at"];
|
|
5120
|
-
const
|
|
5165
|
+
const lastAttemptAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
|
|
5166
|
+
const lastSuccessRaw = row["last_success_at"];
|
|
5167
|
+
const lastSuccessAt = typeof lastSuccessRaw === "number" && lastSuccessRaw > 0 ? lastSuccessRaw : void 0;
|
|
5168
|
+
const lastErrorRaw = row["last_error"];
|
|
5169
|
+
const lastError = typeof lastErrorRaw === "string" && lastErrorRaw.length > 0 ? lastErrorRaw : void 0;
|
|
5121
5170
|
return {
|
|
5122
5171
|
id,
|
|
5123
5172
|
label: typeof row["label"] === "string" ? String(row["label"]) : "",
|
|
@@ -5126,7 +5175,9 @@ function mapRowToSchedule(row) {
|
|
|
5126
5175
|
locationIds: parseStringArray(row["location_ids"]),
|
|
5127
5176
|
retentionCount: Number(row["retention_count"] ?? 7),
|
|
5128
5177
|
...dataSources !== void 0 ? { dataSources } : {},
|
|
5129
|
-
...
|
|
5178
|
+
...lastAttemptAt !== void 0 ? { lastAttemptAt } : {},
|
|
5179
|
+
...lastSuccessAt !== void 0 ? { lastSuccessAt } : {},
|
|
5180
|
+
...lastError !== void 0 ? { lastError } : {}
|
|
5130
5181
|
};
|
|
5131
5182
|
}
|
|
5132
5183
|
//#endregion
|
|
@@ -5288,13 +5339,6 @@ function selectBackupTargets(input) {
|
|
|
5288
5339
|
* the panel used to expose).
|
|
5289
5340
|
*/
|
|
5290
5341
|
var DEFAULT_RETENTION_COUNT = 7;
|
|
5291
|
-
/**
|
|
5292
|
-
* Default cron stamped on a freshly-discovered `backups` location.
|
|
5293
|
-
* Operators can edit it from the destination row; setting empty in
|
|
5294
|
-
* the editor flips the destination back to manual-only. Daily 03:00
|
|
5295
|
-
* server-local mirrors the previous global default.
|
|
5296
|
-
*/
|
|
5297
|
-
var DEFAULT_CRON = "0 3 * * *";
|
|
5298
5342
|
var SCHEDULE_TICK_MS = 6e4;
|
|
5299
5343
|
/**
|
|
5300
5344
|
* Build the human-facing archive label. Every archive always carries
|
|
@@ -5338,9 +5382,11 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5338
5382
|
*/
|
|
5339
5383
|
stagingDir = "";
|
|
5340
5384
|
scheduleTimer = null;
|
|
5385
|
+
/** Guards the disarmed-scheduler complaint to once per process. */
|
|
5386
|
+
warnedSchedulerDisarmed = false;
|
|
5341
5387
|
/**
|
|
5342
|
-
* Baseline used as the "after" anchor when a
|
|
5343
|
-
* has never
|
|
5388
|
+
* Baseline used as the "after" anchor when a schedule has a cron but
|
|
5389
|
+
* has never fired yet (no `lastAttemptAt`). Set on orchestrator boot so a
|
|
5344
5390
|
* pattern that fires every minute doesn't immediately replay every
|
|
5345
5391
|
* historical "missed" run from epoch 0.
|
|
5346
5392
|
*/
|
|
@@ -5434,7 +5480,37 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5434
5480
|
}
|
|
5435
5481
|
}));
|
|
5436
5482
|
const lastSuccessByLoc = new Map(lastSuccess);
|
|
5437
|
-
|
|
5483
|
+
const cadence = await this.cadenceByLocation();
|
|
5484
|
+
return locations.map((loc) => this.toDestinationWire(loc, policies.get(loc.id), lastSuccessByLoc.get(loc.id), cadence.get(loc.id)));
|
|
5485
|
+
}
|
|
5486
|
+
/**
|
|
5487
|
+
* Join the enabled schedules by the locations they fan out to.
|
|
5488
|
+
* Comma-joined crons (a location can be targeted by several schedules),
|
|
5489
|
+
* earliest next firing. Computed from `Date.now()` — an operator asking
|
|
5490
|
+
* "when next" means from now, never from a stale run cursor.
|
|
5491
|
+
*/
|
|
5492
|
+
async cadenceByLocation() {
|
|
5493
|
+
const out = /* @__PURE__ */ new Map();
|
|
5494
|
+
if (!this.schedules) return out;
|
|
5495
|
+
const schedules = await this.schedules.list().catch((err) => {
|
|
5496
|
+
this.ctx.logger.warn("backup-orchestrator: schedule list failed — cadence unknown", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
5497
|
+
return [];
|
|
5498
|
+
});
|
|
5499
|
+
const now = Date.now();
|
|
5500
|
+
for (const s of schedules) {
|
|
5501
|
+
if (!s.enabled || s.cron.trim().length === 0) continue;
|
|
5502
|
+
const next = this.safeNextDue(s.cron, now);
|
|
5503
|
+
for (const id of s.locationIds) {
|
|
5504
|
+
const prev = out.get(id);
|
|
5505
|
+
const crons = prev ? [...prev.crons, s.cron] : [s.cron];
|
|
5506
|
+
const nextRunAt = next === void 0 ? prev?.nextRunAt : prev?.nextRunAt === void 0 ? next : Math.min(prev.nextRunAt, next);
|
|
5507
|
+
out.set(id, {
|
|
5508
|
+
crons,
|
|
5509
|
+
...nextRunAt !== void 0 ? { nextRunAt } : {}
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
return out;
|
|
5438
5514
|
}
|
|
5439
5515
|
async fetchBackupLocations() {
|
|
5440
5516
|
try {
|
|
@@ -5465,10 +5541,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5465
5541
|
for (const id of locationIds) {
|
|
5466
5542
|
if (out.has(id)) continue;
|
|
5467
5543
|
try {
|
|
5468
|
-
const fresh = await this.policies.ensureDefault(id, {
|
|
5469
|
-
retentionCount: DEFAULT_RETENTION_COUNT,
|
|
5470
|
-
cron: DEFAULT_CRON
|
|
5471
|
-
});
|
|
5544
|
+
const fresh = await this.policies.ensureDefault(id, { retentionCount: DEFAULT_RETENTION_COUNT });
|
|
5472
5545
|
out.set(id, fresh);
|
|
5473
5546
|
} catch (err) {
|
|
5474
5547
|
this.ctx.logger.warn("backup-orchestrator: ensureDefault failed", { meta: {
|
|
@@ -5484,11 +5557,10 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5484
5557
|
return {
|
|
5485
5558
|
locationId,
|
|
5486
5559
|
enabled: true,
|
|
5487
|
-
retentionCount: DEFAULT_RETENTION_COUNT
|
|
5488
|
-
cron: DEFAULT_CRON
|
|
5560
|
+
retentionCount: DEFAULT_RETENTION_COUNT
|
|
5489
5561
|
};
|
|
5490
5562
|
}
|
|
5491
|
-
toDestinationWire(loc, policy, latest) {
|
|
5563
|
+
toDestinationWire(loc, policy, latest, cadence) {
|
|
5492
5564
|
const effective = policy ?? this.fallbackPolicy(loc.id);
|
|
5493
5565
|
const subId = extractSlug(loc.id);
|
|
5494
5566
|
const displayName = effective.label && effective.label.length > 0 ? effective.label : loc.displayName;
|
|
@@ -5505,9 +5577,10 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5505
5577
|
...effective.label !== void 0 ? { label: effective.label } : {},
|
|
5506
5578
|
...latest !== void 0 ? { lastSuccessAt: latest.createdAt } : {},
|
|
5507
5579
|
...latest !== void 0 ? { lastSuccessSizeBytes: latest.sizeBytes } : {},
|
|
5508
|
-
...
|
|
5509
|
-
...
|
|
5510
|
-
...effective.
|
|
5580
|
+
...cadence !== void 0 ? { cron: cadence.crons.join(", ") } : {},
|
|
5581
|
+
...cadence?.nextRunAt !== void 0 ? { nextRunAt: cadence.nextRunAt } : {},
|
|
5582
|
+
...effective.lastAttemptAt !== void 0 ? { lastAttemptAt: effective.lastAttemptAt } : {},
|
|
5583
|
+
...effective.lastError !== void 0 ? { lastError: effective.lastError } : {}
|
|
5511
5584
|
};
|
|
5512
5585
|
}
|
|
5513
5586
|
/**
|
|
@@ -5669,7 +5742,10 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5669
5742
|
if (!this.systemBackup) throw new Error("orchestrator not initialized");
|
|
5670
5743
|
const targets = await this.resolveTargets(input.destinations !== void 0 ? [...input.destinations] : void 0);
|
|
5671
5744
|
if (targets.length === 0) {
|
|
5672
|
-
this.ctx.logger.warn("backup trigger: no destinations selected"
|
|
5745
|
+
this.ctx.logger.warn("backup trigger: no destinations selected — this archive is being dropped", { meta: {
|
|
5746
|
+
requestedDestinationIds: input.destinations !== void 0 ? [...input.destinations] : [],
|
|
5747
|
+
label: input.label ?? null
|
|
5748
|
+
} });
|
|
5673
5749
|
return [];
|
|
5674
5750
|
}
|
|
5675
5751
|
progress.setDestinations(targets.map((t) => t.id));
|
|
@@ -5713,6 +5789,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5713
5789
|
});
|
|
5714
5790
|
entries.push(stored);
|
|
5715
5791
|
progress.destinationCompleted(dest.id);
|
|
5792
|
+
await this.recordDestinationOutcome(dest.id, createdAt);
|
|
5716
5793
|
this.ctx.eventBus.emit({
|
|
5717
5794
|
id: (0, node_crypto.randomUUID)(),
|
|
5718
5795
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -5730,10 +5807,12 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5730
5807
|
} catch (err) {
|
|
5731
5808
|
if (err instanceof BackupCancelledError) continue;
|
|
5732
5809
|
progress.destinationFailed(dest.id);
|
|
5810
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5733
5811
|
this.ctx.logger.error("backup trigger: destination failed", { meta: {
|
|
5734
5812
|
destinationId: dest.id,
|
|
5735
|
-
error:
|
|
5813
|
+
error: message
|
|
5736
5814
|
} });
|
|
5815
|
+
await this.recordDestinationOutcome(dest.id, Date.now(), message);
|
|
5737
5816
|
}
|
|
5738
5817
|
}
|
|
5739
5818
|
return entries;
|
|
@@ -5852,30 +5931,29 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
5852
5931
|
/**
|
|
5853
5932
|
* Upsert the per-destination policy row. Used by the admin-UI
|
|
5854
5933
|
* destinations table — toggle enabled / change retention / set
|
|
5855
|
-
* label
|
|
5934
|
+
* label. No-ops with a logged warning if the
|
|
5856
5935
|
* settings backend lacks structured-table support (no persistence
|
|
5857
5936
|
* available).
|
|
5858
5937
|
*
|
|
5859
|
-
*
|
|
5860
|
-
*
|
|
5861
|
-
*
|
|
5862
|
-
*
|
|
5938
|
+
* There is deliberately NO cadence here. A destination does not run on
|
|
5939
|
+
* its own timer — a schedule fans out to it — and the `cron` this method
|
|
5940
|
+
* used to accept had scheduled nothing since 2026-07-28 while still being
|
|
5941
|
+
* rendered as "daily at 03:00" (D384, and D62's "a switch that writes a
|
|
5942
|
+
* store nobody reads").
|
|
5863
5943
|
*/
|
|
5864
5944
|
async upsertDestinationPolicy(input) {
|
|
5865
5945
|
if (!this.policies) {
|
|
5866
5946
|
this.ctx.logger.warn("backup upsertDestinationPolicy: no settings-store backend — change will not persist", { meta: { locationId: input.locationId } });
|
|
5867
5947
|
return;
|
|
5868
5948
|
}
|
|
5869
|
-
const cron = typeof input.cron === "string" ? input.cron.trim() : "";
|
|
5870
|
-
if (cron.length > 0 && !isValidCron(cron)) throw new Error(`Invalid cron expression: "${cron}"`);
|
|
5871
5949
|
const existing = await this.policies.get(input.locationId).catch(() => null);
|
|
5872
5950
|
await this.policies.upsert({
|
|
5873
5951
|
locationId: input.locationId,
|
|
5874
5952
|
enabled: input.enabled,
|
|
5875
5953
|
retentionCount: input.retentionCount,
|
|
5876
5954
|
...input.label !== void 0 ? { label: input.label } : {},
|
|
5877
|
-
...
|
|
5878
|
-
...existing?.
|
|
5955
|
+
...existing?.lastAttemptAt !== void 0 ? { lastAttemptAt: existing.lastAttemptAt } : {},
|
|
5956
|
+
...existing?.lastError !== void 0 ? { lastError: existing.lastError } : {}
|
|
5879
5957
|
});
|
|
5880
5958
|
}
|
|
5881
5959
|
/**
|
|
@@ -6013,6 +6091,29 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
6013
6091
|
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.`);
|
|
6014
6092
|
return selection.targets.map((t) => ({ id: t.id }));
|
|
6015
6093
|
}
|
|
6094
|
+
/**
|
|
6095
|
+
* Persist the outcome of one attempted write to one destination.
|
|
6096
|
+
*
|
|
6097
|
+
* Best-effort by design: an unavailable settings backend must not turn a
|
|
6098
|
+
* successful backup into a failed one. But it is LOGGED when it cannot be
|
|
6099
|
+
* written — a status field that silently stops being updated is the whole
|
|
6100
|
+
* defect this method exists to close.
|
|
6101
|
+
*/
|
|
6102
|
+
async recordDestinationOutcome(destinationId, attemptAt, error) {
|
|
6103
|
+
if (!this.policies) return;
|
|
6104
|
+
try {
|
|
6105
|
+
await this.policies.recordAttempt(destinationId, {
|
|
6106
|
+
attemptAt,
|
|
6107
|
+
...error !== void 0 ? { error } : {}
|
|
6108
|
+
}, { retentionCount: DEFAULT_RETENTION_COUNT });
|
|
6109
|
+
} catch (err) {
|
|
6110
|
+
this.ctx.logger.warn("backup: could not persist the destination outcome", { meta: {
|
|
6111
|
+
destinationId,
|
|
6112
|
+
outcome: error === void 0 ? "succeeded" : "failed",
|
|
6113
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6114
|
+
} });
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6016
6117
|
startScheduleTimer() {
|
|
6017
6118
|
if (this.scheduleTimer) return;
|
|
6018
6119
|
this.scheduleTimer = setInterval(() => {
|
|
@@ -6027,19 +6128,25 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
6027
6128
|
/**
|
|
6028
6129
|
* Schedule-driven backup scheduling. Walks every enabled backup
|
|
6029
6130
|
* SCHEDULE (the N:M entry), computes the next due time after
|
|
6030
|
-
* `
|
|
6131
|
+
* `lastAttemptAt` (or the orchestrator's first-tick baseline if never
|
|
6031
6132
|
* run), and fires a SINGLE archive fanned out to ALL of the
|
|
6032
6133
|
* schedule's `locationIds` when due — with the schedule's per-entry
|
|
6033
6134
|
* retention applied to each. Each schedule keeps its own cadence.
|
|
6034
6135
|
*
|
|
6035
6136
|
* Errors per schedule are isolated; one misconfigured cron doesn't
|
|
6036
|
-
* block the others. The schedule's `
|
|
6137
|
+
* block the others. The schedule's `lastAttemptAt` is updated BEFORE the
|
|
6037
6138
|
* trigger to dedupe (the tick runs every SCHEDULE_TICK_MS — without
|
|
6038
6139
|
* the up-front update a long-running upload could overlap with the
|
|
6039
6140
|
* next tick's same-due check).
|
|
6040
6141
|
*/
|
|
6041
6142
|
async onScheduleTick() {
|
|
6042
|
-
if (!this.schedules)
|
|
6143
|
+
if (!this.schedules) {
|
|
6144
|
+
if (!this.warnedSchedulerDisarmed) {
|
|
6145
|
+
this.warnedSchedulerDisarmed = true;
|
|
6146
|
+
this.ctx.logger.error("backup scheduler is DISARMED — no settings backend, so no schedule can fire");
|
|
6147
|
+
}
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6043
6150
|
const schedules = await this.schedules.list();
|
|
6044
6151
|
const now = Date.now();
|
|
6045
6152
|
for (const s of schedules) {
|
|
@@ -6047,7 +6154,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
6047
6154
|
if (s.locationIds.length === 0) continue;
|
|
6048
6155
|
let due;
|
|
6049
6156
|
try {
|
|
6050
|
-
due = computeNextDue(s.cron, s.
|
|
6157
|
+
due = computeNextDue(s.cron, s.lastAttemptAt ?? this.scheduleBaselineAt);
|
|
6051
6158
|
} catch (err) {
|
|
6052
6159
|
this.ctx.logger.warn("Invalid cron — skipping schedule", { meta: {
|
|
6053
6160
|
scheduleId: s.id,
|
|
@@ -6059,36 +6166,76 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
6059
6166
|
if (due === null || due > now) continue;
|
|
6060
6167
|
const stamped = {
|
|
6061
6168
|
...s,
|
|
6062
|
-
|
|
6169
|
+
lastAttemptAt: now
|
|
6063
6170
|
};
|
|
6064
6171
|
try {
|
|
6065
6172
|
await this.schedules.upsert(stamped);
|
|
6066
6173
|
} catch (err) {
|
|
6067
6174
|
this.ctx.logger.warn("Failed to persist scheduled-run timestamp", { meta: {
|
|
6068
6175
|
scheduleId: s.id,
|
|
6176
|
+
scheduleLabel: s.label,
|
|
6177
|
+
destinationIds: [...s.locationIds],
|
|
6069
6178
|
error: err instanceof Error ? err.message : String(err)
|
|
6070
6179
|
} });
|
|
6071
6180
|
}
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6181
|
+
this.ctx.logger.info("Scheduled backup firing", { meta: {
|
|
6182
|
+
scheduleId: s.id,
|
|
6183
|
+
label: s.label,
|
|
6184
|
+
destinationIds: [...s.locationIds],
|
|
6185
|
+
cron: s.cron,
|
|
6186
|
+
dueAt: due
|
|
6187
|
+
} });
|
|
6188
|
+
await this.runScheduledBackup(stamped, now);
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
/**
|
|
6192
|
+
* Fire one schedule and RECORD what happened to it, naming every
|
|
6193
|
+
* destination. Three outcomes, three different persisted states —
|
|
6194
|
+
* because "fired" and "backed up" were the same field until D384:
|
|
6195
|
+
*
|
|
6196
|
+
* - archive landed somewhere → `lastSuccessAt`, `lastError` cleared;
|
|
6197
|
+
* - the run threw, or landed nowhere → `lastError`, no success stamp;
|
|
6198
|
+
* - the stamp could not be written → logged, named, not swallowed.
|
|
6199
|
+
*/
|
|
6200
|
+
async runScheduledBackup(schedule, firedAt) {
|
|
6201
|
+
const destinationIds = [...schedule.locationIds];
|
|
6202
|
+
let failure;
|
|
6203
|
+
try {
|
|
6204
|
+
const result = await this.triggerBackup({
|
|
6205
|
+
destinations: destinationIds,
|
|
6206
|
+
...schedule.dataSources && schedule.dataSources.length > 0 ? { locations: [...schedule.dataSources] } : {},
|
|
6207
|
+
label: schedule.label,
|
|
6208
|
+
retentionCount: schedule.retentionCount
|
|
6209
|
+
});
|
|
6210
|
+
if (result.entries.length === 0 && !result.cancelled) failure = `no destination accepted the archive (${destinationIds.join(", ")})`;
|
|
6211
|
+
} catch (err) {
|
|
6212
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
6213
|
+
}
|
|
6214
|
+
if (failure === void 0) this.ctx.logger.info("Scheduled backup completed", { meta: {
|
|
6215
|
+
scheduleId: schedule.id,
|
|
6216
|
+
label: schedule.label,
|
|
6217
|
+
destinationIds
|
|
6218
|
+
} });
|
|
6219
|
+
else this.ctx.logger.error("Scheduled backup failed", { meta: {
|
|
6220
|
+
scheduleId: schedule.id,
|
|
6221
|
+
label: schedule.label,
|
|
6222
|
+
destinationIds,
|
|
6223
|
+
error: failure
|
|
6224
|
+
} });
|
|
6225
|
+
if (!this.schedules) return;
|
|
6226
|
+
const { lastError: _cleared, ...carried } = schedule;
|
|
6227
|
+
try {
|
|
6228
|
+
await this.schedules.upsert({
|
|
6229
|
+
...carried,
|
|
6230
|
+
...failure === void 0 ? { lastSuccessAt: firedAt } : { lastError: failure }
|
|
6231
|
+
});
|
|
6232
|
+
} catch (err) {
|
|
6233
|
+
this.ctx.logger.warn("Failed to persist the scheduled-run outcome", { meta: {
|
|
6234
|
+
scheduleId: schedule.id,
|
|
6235
|
+
label: schedule.label,
|
|
6236
|
+
destinationIds,
|
|
6237
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6238
|
+
} });
|
|
6092
6239
|
}
|
|
6093
6240
|
}
|
|
6094
6241
|
async listSchedules() {
|
|
@@ -6107,7 +6254,9 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
6107
6254
|
locationIds: [...s.locationIds],
|
|
6108
6255
|
retentionCount: s.retentionCount,
|
|
6109
6256
|
...s.dataSources !== void 0 ? { dataSources: [...s.dataSources] } : {},
|
|
6110
|
-
...s.
|
|
6257
|
+
...s.lastAttemptAt !== void 0 ? { lastAttemptAt: s.lastAttemptAt } : {},
|
|
6258
|
+
...s.lastSuccessAt !== void 0 ? { lastSuccessAt: s.lastSuccessAt } : {},
|
|
6259
|
+
...s.lastError !== void 0 ? { lastError: s.lastError } : {},
|
|
6111
6260
|
...nextRunAt !== void 0 ? { nextRunAt } : {}
|
|
6112
6261
|
};
|
|
6113
6262
|
});
|
|
@@ -6126,7 +6275,9 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
6126
6275
|
locationIds: [...input.locationIds],
|
|
6127
6276
|
retentionCount: input.retentionCount,
|
|
6128
6277
|
...input.dataSources && input.dataSources.length > 0 ? { dataSources: [...input.dataSources] } : {},
|
|
6129
|
-
...existing?.
|
|
6278
|
+
...existing?.lastAttemptAt !== void 0 ? { lastAttemptAt: existing.lastAttemptAt } : {},
|
|
6279
|
+
...existing?.lastSuccessAt !== void 0 ? { lastSuccessAt: existing.lastSuccessAt } : {},
|
|
6280
|
+
...existing?.lastError !== void 0 ? { lastError: existing.lastError } : {}
|
|
6130
6281
|
};
|
|
6131
6282
|
await this.schedules.upsert(next);
|
|
6132
6283
|
let nextRunAt;
|
|
@@ -6142,7 +6293,9 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
|
|
|
6142
6293
|
locationIds: [...next.locationIds],
|
|
6143
6294
|
retentionCount: next.retentionCount,
|
|
6144
6295
|
...next.dataSources !== void 0 ? { dataSources: [...next.dataSources] } : {},
|
|
6145
|
-
...next.
|
|
6296
|
+
...next.lastAttemptAt !== void 0 ? { lastAttemptAt: next.lastAttemptAt } : {},
|
|
6297
|
+
...next.lastSuccessAt !== void 0 ? { lastSuccessAt: next.lastSuccessAt } : {},
|
|
6298
|
+
...next.lastError !== void 0 ? { lastError: next.lastError } : {},
|
|
6146
6299
|
...nextRunAt !== void 0 ? { nextRunAt } : {}
|
|
6147
6300
|
};
|
|
6148
6301
|
}
|