salidium 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -138,7 +138,7 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate
|
|
|
138
138
|
snapshot.pid = integer(daemonHealth["pid"])
|
|
139
139
|
if let collection = health["collection"] as? [String: Any],
|
|
140
140
|
let state = collection["state"] as? String {
|
|
141
|
-
snapshot.collection = state == "active" ? "
|
|
141
|
+
snapshot.collection = state == "active" ? "On" : "Paused"
|
|
142
142
|
}
|
|
143
143
|
if let queue = health["queue"] as? [String: Any] {
|
|
144
144
|
snapshot.queueFiles = integer(queue["files"])
|
|
@@ -170,9 +170,9 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
private static func retentionLabel(_ value: Any) -> String {
|
|
173
|
-
if let text = value as? String { return text == "forever" ? "
|
|
174
|
-
if let days = integer(value) { return "\(days) days" }
|
|
175
|
-
return "
|
|
173
|
+
if let text = value as? String { return text == "forever" ? "kept forever" : text }
|
|
174
|
+
if let days = integer(value) { return "kept \(days) days" }
|
|
175
|
+
return "retention unavailable"
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
private static func titleCase(_ value: String) -> String {
|
|
@@ -191,21 +191,24 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate
|
|
|
191
191
|
menu.removeAllItems()
|
|
192
192
|
configureStatusIcon()
|
|
193
193
|
|
|
194
|
+
// No em dash in anything the product says. `printedVoice.test.ts` reads this file for them.
|
|
194
195
|
let heading: String
|
|
195
196
|
switch snapshot.health {
|
|
196
|
-
case .healthy: heading = "Salidium
|
|
197
|
-
case .attention: heading = "Salidium
|
|
198
|
-
case .critical: heading = "Salidium
|
|
199
|
-
case .offline: heading = "Salidium
|
|
197
|
+
case .healthy: heading = "Salidium · Healthy"
|
|
198
|
+
case .attention: heading = "Salidium · Needs Attention"
|
|
199
|
+
case .critical: heading = "Salidium · Critical"
|
|
200
|
+
case .offline: heading = "Salidium · Not Running"
|
|
200
201
|
}
|
|
201
202
|
addLabel(heading, emphasized: true)
|
|
202
203
|
menu.addItem(.separator())
|
|
203
204
|
|
|
204
|
-
addLabel(snapshot.pid.map { "Running · PID \($0)" } ?? "
|
|
205
|
-
addLabel("
|
|
206
|
-
addLabel("
|
|
207
|
-
addLabel("
|
|
208
|
-
addLabel(snapshot.activeAlerts == 0
|
|
205
|
+
addLabel(snapshot.pid.map { "Running · PID \($0)" } ?? "Not running")
|
|
206
|
+
addLabel("Recording · \(snapshot.collection)")
|
|
207
|
+
addLabel("Waiting to be stored · \(queueLabel())")
|
|
208
|
+
addLabel("On this Mac · \(storageLabel())")
|
|
209
|
+
addLabel(snapshot.activeAlerts == 0
|
|
210
|
+
? "Nothing needs attention"
|
|
211
|
+
: "\(snapshot.activeAlerts) need\(snapshot.activeAlerts == 1 ? "s" : "") attention")
|
|
209
212
|
if let maintenance = snapshot.maintenance { addLabel("Maintenance · \(maintenance)") }
|
|
210
213
|
|
|
211
214
|
menu.addItem(.separator())
|
|
@@ -217,7 +220,7 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate
|
|
|
217
220
|
addAction(paused ? "Resume Collection" : "Pause Collection",
|
|
218
221
|
paused ? #selector(resumeCollection) : #selector(pauseCollection),
|
|
219
222
|
enabled: true)
|
|
220
|
-
addAction("
|
|
223
|
+
addAction("Store One Batch Now", #selector(drainQueue), enabled: true)
|
|
221
224
|
addAction("Stop Salidium", #selector(stopSalidium), enabled: true)
|
|
222
225
|
}
|
|
223
226
|
addAction("Refresh Now", #selector(refreshNow), key: "r", enabled: true)
|
|
@@ -259,16 +262,23 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate
|
|
|
259
262
|
|
|
260
263
|
private func storageLabel() -> String {
|
|
261
264
|
guard let bytes = snapshot.storeBytes else { return "Unavailable" }
|
|
262
|
-
return "\(Self.byteLabel(bytes)) · \(snapshot.retention)
|
|
265
|
+
return "\(Self.byteLabel(bytes)) · \(snapshot.retention)"
|
|
263
266
|
}
|
|
264
267
|
|
|
268
|
+
/*
|
|
269
|
+
* The same rendering as `formatBytes` in `@salidium/core`, which the app and the CLI use.
|
|
270
|
+
*
|
|
271
|
+
* Decimal, matching Finder, but not `ByteCountFormatter`: its adaptive mode renders one byte as
|
|
272
|
+
* "0 KB" and 999999 as "1 MB", and the same function formats rates where that loses the value.
|
|
273
|
+
* `byteLabelVectors` in that module pins the cases, and `macosService.test.ts` checks them
|
|
274
|
+
* against this function, because Swift cannot import it.
|
|
275
|
+
*/
|
|
265
276
|
private static func byteLabel(_ bytes: Int) -> String {
|
|
266
|
-
let
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return formatter.string(fromByteCount: Int64(bytes))
|
|
277
|
+
let value = Double(bytes)
|
|
278
|
+
if bytes < 1000 { return "\(bytes) B" }
|
|
279
|
+
if bytes < 1000 * 1000 { return String(format: "%.1f KB", value / 1000) }
|
|
280
|
+
if bytes < 1000 * 1000 * 1000 { return String(format: "%.1f MB", value / (1000 * 1000)) }
|
|
281
|
+
return String(format: "%.2f GB", value / (1000 * 1000 * 1000))
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
private func addLabel(_ title: String, emphasized: Bool = false) {
|
package/bundle/salidium.mjs
CHANGED
|
@@ -277,6 +277,17 @@ function headlineOf(text, max = 160) {
|
|
|
277
277
|
return "";
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
// ../core/dist/format/bytes.js
|
|
281
|
+
function formatBytes(bytes) {
|
|
282
|
+
if (bytes < 1e3)
|
|
283
|
+
return `${bytes} B`;
|
|
284
|
+
if (bytes < 1e3 * 1e3)
|
|
285
|
+
return `${(bytes / 1e3).toFixed(1)} KB`;
|
|
286
|
+
if (bytes < 1e3 * 1e3 * 1e3)
|
|
287
|
+
return `${(bytes / (1e3 * 1e3)).toFixed(1)} MB`;
|
|
288
|
+
return `${(bytes / (1e3 * 1e3 * 1e3)).toFixed(2)} GB`;
|
|
289
|
+
}
|
|
290
|
+
|
|
280
291
|
// ../core/dist/verification/classifyCommand.js
|
|
281
292
|
var RULES = [
|
|
282
293
|
{ runner: "vitest", method: "test", pattern: /^vitest(\s|$)/ },
|
|
@@ -19089,6 +19100,17 @@ var LocalAlertSchema = external_exports.object({
|
|
|
19089
19100
|
state: external_exports.enum(["active", "acknowledged", "recovered"]),
|
|
19090
19101
|
title: external_exports.string().min(1).max(160),
|
|
19091
19102
|
detail: external_exports.string().min(1).max(500),
|
|
19103
|
+
/*
|
|
19104
|
+
* What to say once the condition is over.
|
|
19105
|
+
*
|
|
19106
|
+
* Without these a recovered alert can only be shown in the words that announced it, and every
|
|
19107
|
+
* surface that renders one inherits the present tense: the macOS notification read "Recovered:
|
|
19108
|
+
* The durable queue is growing", which is the all-clear and the alarm in one line. Optional
|
|
19109
|
+
* because a ledger written before this field existed still has to parse; readers fall back to
|
|
19110
|
+
* `title` and `detail`.
|
|
19111
|
+
*/
|
|
19112
|
+
recoveryTitle: external_exports.string().min(1).max(160).optional(),
|
|
19113
|
+
recoveryDetail: external_exports.string().min(1).max(500).optional(),
|
|
19092
19114
|
firstSeenAt: CanonicalTimestampSchema,
|
|
19093
19115
|
lastSeenAt: CanonicalTimestampSchema,
|
|
19094
19116
|
lastTransitionAt: CanonicalTimestampSchema,
|
|
@@ -24640,7 +24662,9 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24640
24662
|
kind: "queue-age",
|
|
24641
24663
|
severity: "warning",
|
|
24642
24664
|
title: "Queued work is aging",
|
|
24643
|
-
detail: `The oldest
|
|
24665
|
+
detail: `The oldest item waiting to be stored is at least ${values2.alerts.queueAgeMinutes.value} minutes old.`,
|
|
24666
|
+
recoveryTitle: "Queued work is moving again",
|
|
24667
|
+
recoveryDetail: `Nothing has been waiting longer than ${values2.alerts.queueAgeMinutes.value} minutes. No action is needed.`
|
|
24644
24668
|
});
|
|
24645
24669
|
const velocity = snapshot.estimates.queueVelocity;
|
|
24646
24670
|
if (velocity && velocity.value > 0 && velocity.value * (velocity.sampleWindowSeconds / 60) >= values2.alerts.queueGrowthFiles.value)
|
|
@@ -24648,8 +24672,10 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24648
24672
|
key: "queue-growth",
|
|
24649
24673
|
kind: "queue-growth",
|
|
24650
24674
|
severity: "warning",
|
|
24651
|
-
title: "
|
|
24652
|
-
detail: `
|
|
24675
|
+
title: "Salidium is falling behind",
|
|
24676
|
+
detail: `Your agents are producing work faster than Salidium is storing it: ${values2.alerts.queueGrowthFiles.value} more files are waiting than when this window started. Nothing is lost while it waits.`,
|
|
24677
|
+
recoveryTitle: "Salidium caught up",
|
|
24678
|
+
recoveryDetail: "The backlog stopped growing. No action is needed."
|
|
24653
24679
|
});
|
|
24654
24680
|
if (snapshot.store.totalBytes !== null && snapshot.store.totalBytes >= values2.alerts.databaseSizeBytes.value)
|
|
24655
24681
|
out.push({
|
|
@@ -24657,31 +24683,45 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24657
24683
|
kind: "database-size",
|
|
24658
24684
|
severity: "notice",
|
|
24659
24685
|
title: "Local storage crossed its warning size",
|
|
24660
|
-
detail: `
|
|
24686
|
+
detail: `Salidium is using ${formatBytes(snapshot.store.totalBytes)} on this Mac, past the ${formatBytes(values2.alerts.databaseSizeBytes.value)} mark. Retention is set to ${snapshot.store.retention === "forever" ? "keep everything forever" : `${snapshot.store.retention} days`}.`,
|
|
24687
|
+
recoveryTitle: "Local storage is back under its warning size",
|
|
24688
|
+
/*
|
|
24689
|
+
* No measurement in here. Recovery wording is composed while the condition is still true and
|
|
24690
|
+
* refreshed only for as long as it stays true, so quoting `store.totalBytes` would put the
|
|
24691
|
+
* size that raised the alert into the sentence saying the alert is over: "Salidium is using
|
|
24692
|
+
* 5.01 GB, below the 5.00 GB mark". The threshold is config and does not have that problem.
|
|
24693
|
+
*/
|
|
24694
|
+
recoveryDetail: `Salidium is back below ${formatBytes(values2.alerts.databaseSizeBytes.value)}. Open Salidium to see the current size.`
|
|
24661
24695
|
});
|
|
24662
24696
|
if (snapshot.gaps.latestFingerprint && (snapshot.gaps.active > 0 || snapshot.gaps.latestFingerprint !== priorGapFingerprint))
|
|
24663
24697
|
out.push({
|
|
24664
24698
|
key: `collection-gap:${snapshot.gaps.latestFingerprint}`,
|
|
24665
24699
|
kind: "collection-gap",
|
|
24666
24700
|
severity: snapshot.gaps.active > 0 ? "critical" : "warning",
|
|
24667
|
-
title: snapshot.gaps.active > 0 ? "
|
|
24668
|
-
detail: "
|
|
24701
|
+
title: snapshot.gaps.active > 0 ? "Salidium is missing some activity" : "Salidium missed some activity",
|
|
24702
|
+
detail: snapshot.gaps.active > 0 ? "Agent activity is happening that Salidium is not recording. Reports covering this period will be incomplete. How much was missed cannot be counted." : "A period of agent activity went unrecorded. Reports covering it will be incomplete. How much was missed cannot be counted.",
|
|
24703
|
+
recoveryTitle: "Salidium is recording everything again",
|
|
24704
|
+
recoveryDetail: "Collection is complete from here on. Reports covering the earlier gap stay incomplete."
|
|
24669
24705
|
});
|
|
24670
24706
|
if (snapshot.daemon.state !== "running")
|
|
24671
24707
|
out.push({
|
|
24672
24708
|
key: "daemon-health",
|
|
24673
24709
|
kind: "daemon-health",
|
|
24674
24710
|
severity: snapshot.daemon.state === "unresponsive" ? "critical" : "warning",
|
|
24675
|
-
title: snapshot.daemon.state === "unresponsive" ? "
|
|
24676
|
-
detail:
|
|
24711
|
+
title: snapshot.daemon.state === "unresponsive" ? "Salidium is not responding" : "Salidium has stopped",
|
|
24712
|
+
detail: snapshot.daemon.state === "unresponsive" ? "Salidium is running but not answering. Agent activity is not being recorded while this lasts." : "Salidium is not running. Agent activity is not being recorded until it starts again.",
|
|
24713
|
+
recoveryTitle: "Salidium is running again",
|
|
24714
|
+
recoveryDetail: "Recording has resumed. No action is needed."
|
|
24677
24715
|
});
|
|
24678
24716
|
if (snapshot.maintenance?.phase === "failure" || snapshot.maintenance?.phase === "recovery")
|
|
24679
24717
|
out.push({
|
|
24680
24718
|
key: `maintenance-failure:${snapshot.maintenance.operationId}`,
|
|
24681
24719
|
kind: "maintenance-failure",
|
|
24682
24720
|
severity: "critical",
|
|
24683
|
-
title: "Maintenance
|
|
24684
|
-
detail: (snapshot.maintenance.failure ?? snapshot.maintenance.message).slice(0, 500)
|
|
24721
|
+
title: "Maintenance did not finish",
|
|
24722
|
+
detail: (snapshot.maintenance.failure ?? snapshot.maintenance.message).slice(0, 500),
|
|
24723
|
+
recoveryTitle: "Maintenance finished",
|
|
24724
|
+
recoveryDetail: "The operation that needed attention completed. No action is needed."
|
|
24685
24725
|
});
|
|
24686
24726
|
for (const hook of snapshot.hooks) {
|
|
24687
24727
|
const prior = priorHookTrust[hook.id];
|
|
@@ -24692,8 +24732,10 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24692
24732
|
key: `hook-trust-change:${hook.id}`,
|
|
24693
24733
|
kind: "hook-trust-change",
|
|
24694
24734
|
severity: hook.trust === "modified" || hook.trust === "untrusted" ? "critical" : "notice",
|
|
24695
|
-
title:
|
|
24696
|
-
detail: `
|
|
24735
|
+
title: unsafe ? `The ${hook.name} hook is no longer approved` : `The ${hook.name} hook was approved`,
|
|
24736
|
+
detail: unsafe ? `The hook file changed since you approved it (${prior} to ${hook.trust}). Salidium will not trust it until you approve the new version.` : `Approval state went from ${prior} to ${hook.trust}.`,
|
|
24737
|
+
recoveryTitle: `The ${hook.name} hook is approved again`,
|
|
24738
|
+
recoveryDetail: "The hook file matches an approved version. No action is needed."
|
|
24697
24739
|
});
|
|
24698
24740
|
}
|
|
24699
24741
|
return out;
|
|
@@ -24724,6 +24766,8 @@ async function evaluateLocalAlerts(home, snapshot, config2, options = {}) {
|
|
|
24724
24766
|
existing.severity = condition.severity;
|
|
24725
24767
|
existing.title = condition.title;
|
|
24726
24768
|
existing.detail = condition.detail;
|
|
24769
|
+
existing.recoveryTitle = condition.recoveryTitle;
|
|
24770
|
+
existing.recoveryDetail = condition.recoveryDetail;
|
|
24727
24771
|
existing.notificationEligible = false;
|
|
24728
24772
|
continue;
|
|
24729
24773
|
}
|
|
@@ -24737,6 +24781,8 @@ async function evaluateLocalAlerts(home, snapshot, config2, options = {}) {
|
|
|
24737
24781
|
state: "active",
|
|
24738
24782
|
title: condition.title,
|
|
24739
24783
|
detail: condition.detail,
|
|
24784
|
+
recoveryTitle: condition.recoveryTitle,
|
|
24785
|
+
recoveryDetail: condition.recoveryDetail,
|
|
24740
24786
|
firstSeenAt: at,
|
|
24741
24787
|
lastSeenAt: at,
|
|
24742
24788
|
lastTransitionAt: at,
|
|
@@ -24814,7 +24860,13 @@ var DEFAULT_OPERATIONAL_CONFIG = {
|
|
|
24814
24860
|
alerts: {
|
|
24815
24861
|
queueAgeMinutes: 10,
|
|
24816
24862
|
queueGrowthFiles: 100,
|
|
24817
|
-
|
|
24863
|
+
/*
|
|
24864
|
+
* Decimal, to pair with how the size is shown. Left at 5 GiB it read "warns at 5.37 GB", which
|
|
24865
|
+
* is not a number anyone chose, and the rail's own picker offered GiB steps that no longer
|
|
24866
|
+
* matched any label. This lowers the warning by about seven percent; it is a notice about a
|
|
24867
|
+
* local file, and `alerts.databaseSizeBytes` still overrides it.
|
|
24868
|
+
*/
|
|
24869
|
+
databaseSizeBytes: 5 * 1e3 * 1e3 * 1e3,
|
|
24818
24870
|
cooldownMinutes: 30,
|
|
24819
24871
|
nativeNotifications: false
|
|
24820
24872
|
},
|
|
@@ -27715,9 +27767,14 @@ function runStorageOptimizationMaintenance(home, options = {}) {
|
|
|
27715
27767
|
import { spawn as spawn3 } from "node:child_process";
|
|
27716
27768
|
import { delimiter as delimiter3 } from "node:path";
|
|
27717
27769
|
function notificationText(alert) {
|
|
27770
|
+
if (alert.state === "recovered")
|
|
27771
|
+
return {
|
|
27772
|
+
title: alert.recoveryTitle ?? `Recovered: ${alert.title}`,
|
|
27773
|
+
detail: alert.recoveryDetail ?? alert.detail
|
|
27774
|
+
};
|
|
27718
27775
|
const detail = alert.kind === "maintenance-failure" ? "Maintenance needs local review. Open Salidium or run salidium maintenance status for details." : alert.detail;
|
|
27719
27776
|
return {
|
|
27720
|
-
title: alert.
|
|
27777
|
+
title: alert.title,
|
|
27721
27778
|
detail: `${detail} Open Salidium or run salidium status for details.`
|
|
27722
27779
|
};
|
|
27723
27780
|
}
|
|
@@ -32442,9 +32499,9 @@ Change with: salidium explanations off|when-done|each-reply
|
|
|
32442
32499
|
`);
|
|
32443
32500
|
process.stdout.write(`Store: ${formatBytes(storeBytes)}
|
|
32444
32501
|
`);
|
|
32445
|
-
if (storeBytes >=
|
|
32502
|
+
if (storeBytes >= 1e3 * 1e3 * 1e3)
|
|
32446
32503
|
process.stdout.write(
|
|
32447
|
-
inspectStoreLayout(db).optimized ? "Storage warning: history is over 1
|
|
32504
|
+
inspectStoreLayout(db).optimized ? "Storage warning: history is over 1 GB. Preview exactly what retention would delete before opting in.\n" : "Lossless storage optimization is available before deleting history. Run `salidium storage`, then `salidium storage optimize`; it coordinates queue drain and daemon stop.\n"
|
|
32448
32505
|
);
|
|
32449
32506
|
process.stdout.write(`Pinned: ${store.pinnedSessionIds().length}
|
|
32450
32507
|
`);
|
|
@@ -32767,10 +32824,11 @@ async function readOperationsOverview(daemon, presence, providers) {
|
|
|
32767
32824
|
alerts: await evaluateLocalAlerts(salidiumHome, health, config2)
|
|
32768
32825
|
};
|
|
32769
32826
|
}
|
|
32770
|
-
function estimateLabel(estimate2) {
|
|
32771
|
-
if (!estimate2) return
|
|
32827
|
+
function estimateLabel(estimate2, absent = "Unavailable (needs two measurements)") {
|
|
32828
|
+
if (!estimate2) return absent;
|
|
32772
32829
|
const value2 = estimate2.unit === "bytes/minute" ? `${formatBytes(Math.abs(estimate2.value))}/minute` : Math.abs(estimate2.value).toFixed(1);
|
|
32773
|
-
const
|
|
32830
|
+
const zero = /^[0.]+(?: B\/minute)?$/.test(value2);
|
|
32831
|
+
const sign = zero ? "" : estimate2.value < 0 ? "\u2212" : "+";
|
|
32774
32832
|
const unit = estimate2.unit === "bytes/minute" ? "" : ` ${estimate2.unit}`;
|
|
32775
32833
|
return `${sign}${value2}${unit} \xB7 derived from ${estimate2.samples} samples over ${Math.round(estimate2.sampleWindowSeconds)}s`;
|
|
32776
32834
|
}
|
|
@@ -32934,10 +32992,9 @@ async function statusCommand(options) {
|
|
|
32934
32992
|
);
|
|
32935
32993
|
} else {
|
|
32936
32994
|
const health = operations.health;
|
|
32937
|
-
if (health.daemon.state === "stopped") process.stdout.write("not running\n");
|
|
32938
32995
|
process.stdout.write(
|
|
32939
32996
|
health.daemon.state === "running" ? `Daemon: Running \xB7 pid ${health.daemon.pid} \xB7 since ${health.daemon.startedAt}
|
|
32940
|
-
` : health.daemon.state === "unresponsive" ? `Daemon
|
|
32997
|
+
` : health.daemon.state === "unresponsive" ? `Daemon: Not answering \xB7 pid ${health.daemon.pid}
|
|
32941
32998
|
` : "Daemon: Stopped\n"
|
|
32942
32999
|
);
|
|
32943
33000
|
process.stdout.write(
|
|
@@ -32963,12 +33020,24 @@ async function statusCommand(options) {
|
|
|
32963
33020
|
`Store (exact): ${health.store.totalBytes === null ? "Unavailable" : formatBytes(health.store.totalBytes)}, retention ${retentionLabel(health.store.retention)}, last ingest ${health.store.lastIngestAt ?? "Unavailable"}
|
|
32964
33021
|
`
|
|
32965
33022
|
);
|
|
33023
|
+
const projection = storageProjection(
|
|
33024
|
+
health.store.totalBytes,
|
|
33025
|
+
health.estimates.storageGrowth,
|
|
33026
|
+
operations.config.values.alerts.databaseSizeBytes.value
|
|
33027
|
+
);
|
|
33028
|
+
if (projection) process.stdout.write(`Store outlook: ${projection}
|
|
33029
|
+
`);
|
|
32966
33030
|
process.stdout.write(
|
|
32967
33031
|
`Queue velocity (estimate): ${estimateLabel(health.estimates.queueVelocity)}
|
|
32968
33032
|
`
|
|
32969
33033
|
);
|
|
33034
|
+
const sampled = health.estimates.queueVelocity !== null;
|
|
33035
|
+
const notShrinking = "Unavailable (the queue is not shrinking)";
|
|
32970
33036
|
process.stdout.write(
|
|
32971
|
-
`Drain rate (estimate): ${estimateLabel(
|
|
33037
|
+
`Drain rate (estimate): ${estimateLabel(
|
|
33038
|
+
health.estimates.drainRate,
|
|
33039
|
+
sampled ? notShrinking : void 0
|
|
33040
|
+
)}
|
|
32972
33041
|
`
|
|
32973
33042
|
);
|
|
32974
33043
|
process.stdout.write(
|
|
@@ -32976,7 +33045,7 @@ async function statusCommand(options) {
|
|
|
32976
33045
|
`
|
|
32977
33046
|
);
|
|
32978
33047
|
process.stdout.write(
|
|
32979
|
-
`Time to empty (estimate): ${health.estimates.timeToEmpty ? `${Math.round(health.estimates.timeToEmpty.value)} seconds \xB7 derived` : "Unavailable"}
|
|
33048
|
+
`Time to empty (estimate): ${health.estimates.timeToEmpty ? `${Math.round(health.estimates.timeToEmpty.value)} seconds \xB7 derived` : health.queue.files === 0 ? "Already empty" : sampled ? notShrinking : "Unavailable (needs two measurements)"}
|
|
32980
33049
|
`
|
|
32981
33050
|
);
|
|
32982
33051
|
process.stdout.write(
|
|
@@ -32986,7 +33055,7 @@ async function statusCommand(options) {
|
|
|
32986
33055
|
process.stdout.write(`Explanations: ${explanationStateLabel(explanations)}
|
|
32987
33056
|
`);
|
|
32988
33057
|
process.stdout.write(
|
|
32989
|
-
`Maintenance: ${health.maintenance ? `${health.maintenance.phase} \xB7 ${health.maintenance.message}` : "Idle"}
|
|
33058
|
+
`Maintenance: ${health.maintenance ? `${maintenancePhaseLabel(health.maintenance.phase)} \xB7 ${health.maintenance.message}` : "Idle"}
|
|
32990
33059
|
`
|
|
32991
33060
|
);
|
|
32992
33061
|
process.stdout.write(
|
|
@@ -33220,11 +33289,25 @@ async function maintenanceCommand(action, args, options) {
|
|
|
33220
33289
|
process.stderr.write("maintenance accepts status, queue, drain, optimize, or acknowledge\n");
|
|
33221
33290
|
return 2;
|
|
33222
33291
|
}
|
|
33223
|
-
function
|
|
33224
|
-
if (
|
|
33225
|
-
if (
|
|
33226
|
-
if (
|
|
33227
|
-
|
|
33292
|
+
function maintenancePhaseLabel(phase) {
|
|
33293
|
+
if (phase === "failure") return "Did not finish";
|
|
33294
|
+
if (phase === "recovery") return "Recovering";
|
|
33295
|
+
if (phase === "completed") return "Finished";
|
|
33296
|
+
if (phase === "running") return "Running";
|
|
33297
|
+
return phase;
|
|
33298
|
+
}
|
|
33299
|
+
function storageProjection(total, growth, warnAt) {
|
|
33300
|
+
const parts = [];
|
|
33301
|
+
if (growth && growth.value > 0)
|
|
33302
|
+
parts.push(`about ${formatBytes(growth.value * 60 * 24)} a day at this rate`);
|
|
33303
|
+
if (total !== null) {
|
|
33304
|
+
const mark2 = formatBytes(warnAt);
|
|
33305
|
+
const room = formatBytes(warnAt - total);
|
|
33306
|
+
parts.push(
|
|
33307
|
+
total >= warnAt ? `past its ${mark2} warning mark` : room === mark2 ? `warns at ${mark2}` : `${room} below its ${mark2} warning mark`
|
|
33308
|
+
);
|
|
33309
|
+
}
|
|
33310
|
+
return parts.join(" \xB7 ");
|
|
33228
33311
|
}
|
|
33229
33312
|
function queueLabel(status) {
|
|
33230
33313
|
return `${status.queue.files.toLocaleString()} file${status.queue.files === 1 ? "" : "s"}, ${formatBytes(status.queue.bytes)}`;
|