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" ? "Active" : "Paused"
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" ? "Forever" : text }
174
- if let days = integer(value) { return "\(days) days" }
175
- return "Unavailable"
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 Healthy"
197
- case .attention: heading = "Salidium Needs Attention"
198
- case .critical: heading = "Salidium Critical"
199
- case .offline: heading = "Salidium Stopped"
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)" } ?? "Daemon · Not running")
205
- addLabel("Collection · \(snapshot.collection)")
206
- addLabel("Queue · \(queueLabel())")
207
- addLabel("Storage · \(storageLabel())")
208
- addLabel(snapshot.activeAlerts == 0 ? "Alerts · None active" : "Alerts · \(snapshot.activeAlerts) active")
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("Drain Queue Toward Empty", #selector(drainQueue), enabled: true)
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) 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 formatter = ByteCountFormatter()
267
- formatter.countStyle = .file
268
- formatter.allowedUnits = [.useKB, .useMB, .useGB]
269
- formatter.includesUnit = true
270
- formatter.isAdaptive = true
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) {
@@ -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 durable queue item is at least ${values2.alerts.queueAgeMinutes.value} minutes old.`
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: "The durable queue is growing",
24652
- detail: `Net growth crossed ${values2.alerts.queueGrowthFiles.value} files in the sampled window.`
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: `The SQLite store and recovery log use ${snapshot.store.totalBytes} bytes.`
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 ? "Collection loss is active" : "A new collection gap was recorded",
24668
- detail: "The gap ledger changed. Exact dropped-event counts remain unavailable."
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" ? "The daemon is not answering" : "The daemon is stopped",
24676
- detail: `Daemon state changed to ${snapshot.daemon.state}.`
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 needs recovery",
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: `${hook.name} hook trust changed`,
24696
- detail: `Trust changed from ${prior} to ${hook.trust}.`
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
- databaseSizeBytes: 5 * 1024 * 1024 * 1024,
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.state === "recovered" ? `Recovered: ${alert.title}` : alert.title,
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 >= 1024 * 1024 * 1024)
32502
+ if (storeBytes >= 1e3 * 1e3 * 1e3)
32446
32503
  process.stdout.write(
32447
- inspectStoreLayout(db).optimized ? "Storage warning: history is over 1 GiB. 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"
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 "Unavailable (needs at least two exact samples)";
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 sign = estimate2.value < 0 ? "\u2212" : estimate2.value > 0 ? "+" : "";
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 running: pid ${health.daemon.pid} \xB7 not answering
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(health.estimates.drainRate)}
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 formatBytes(bytes) {
33224
- if (bytes < 1024) return `${bytes} B`;
33225
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
33226
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
33227
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
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)}`;