mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Redesign the Storage Monitor as a storage-composition breakdown (snippets · datasets · app)
This commit is contained in:
@@ -344,52 +344,54 @@ const KEY = 'astrolabe:ux-prefs'; // { panelLayout: { libraryWidth, previewWidth
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage Tiers, Budgets & Quota Monitoring
|
||||
## 6. Storage Tiers & the Composition Monitor
|
||||
|
||||
Astrolabe has three tiers with different capacities and risk profiles:
|
||||
|
||||
| Tier | Backing | Holds | Budget & behavior |
|
||||
| -------------------- | -------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Snippet store** | IndexedDB `snippets` | All snippet records | Practical budget ~**5 MB**. A storage monitor estimates usage and surfaces a warning as it fills. Snippets are user-authored and irreplaceable, so we fail **loudly**. |
|
||||
| **Dataset store** | IndexedDB `datasets` | All dataset payloads | Separate, **high-capacity**; suited to large payloads. Loaded in full today; lazy loading is a §3 target. |
|
||||
| **Settings & prefs** | localStorage | `UserSettings` + app/UI prefs (§5) | Small; effectively unbounded for this use. |
|
||||
| Tier | Backing | Holds | Behavior |
|
||||
| -------------------- | -------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| **Snippet store** | IndexedDB `snippets` | All snippet records | Snippets are user-authored and irreplaceable, so writes fail **loudly** on quota (below). |
|
||||
| **Dataset store** | IndexedDB `datasets` | All dataset payloads | Separate, **high-capacity**; suited to large payloads. Loaded in full today; lazy loading is a §3 target. |
|
||||
| **Settings & prefs** | localStorage | `UserSettings` + app/UI prefs (§5) | Small; effectively unbounded for this use. |
|
||||
|
||||
Splitting snippets and datasets into separate stores means a few large datasets can't crowd out the snippet budget, and the snippet monitor can report a meaningful "how full is my library" number without summing dataset bytes.
|
||||
Splitting snippets and datasets into separate stores means a few large datasets can't crowd out snippets, and lets the storage monitor break usage down **by tier**.
|
||||
|
||||
### Estimating usage
|
||||
### Composing the storage breakdown
|
||||
|
||||
Read the origin's **Storage Manager** estimate (`navigator.storage.estimate()` → `usage`/`quota`) and derive a presentation-ready summary in the **pure core**, so the thresholds are unit-tested without a browser and the adapter only does I/O. When the API is missing or rejects, the summary is marked unusable rather than guessed.
|
||||
The monitor shows what storage is **made of** — Snippets · Datasets · App — not a "used of quota" gauge. The browser's `quota` is a padded, deliberately fuzzed approximation, not a real free-space figure (web.dev → _storage-for-the-web_), so a budget fraction is false precision. We use only the reliable `usage` (bytes actually stored for the origin) and measure our own tiers, deriving the rest:
|
||||
|
||||
- **Snippets / Datasets** — measured directly: snippets serialized (`jsonByteSize`), datasets summed from each record's `size`. Always available, no API needed.
|
||||
- **App** = `usage − snippets − datasets` — the precached app shell + IndexedDB overhead. Shown only when the Storage Manager API yields `usage`; otherwise the breakdown is just snippets + datasets.
|
||||
- **Hidden below a floor.** The whole monitor stays hidden until _user_ data (snippets + datasets) reaches `STORAGE_MONITOR_MIN_USER_BYTES` (10 MB). Keyed off user bytes, not total: the ~precache baseline is roughly constant, so gating on total would make it always-visible and the bar App-dominated.
|
||||
|
||||
The pure summarizer lives in core (unit-tested without a browser); the adapter only does I/O.
|
||||
|
||||
```ts
|
||||
// src/core/storage-estimate.ts — pure, unit-tested (no I/O)
|
||||
export type StorageLevel = 'ok' | 'warning' | 'critical';
|
||||
|
||||
export interface StorageSummary {
|
||||
usedBytes: number;
|
||||
quotaBytes: number;
|
||||
fraction: number; // usedBytes / quotaBytes, clamped 0..1
|
||||
level: StorageLevel; // escalates from `fraction`
|
||||
available: boolean; // false when usage/quota are missing/invalid
|
||||
export interface StorageComposition {
|
||||
segments: { key: 'snippets' | 'datasets' | 'app'; label: string; bytes: number }[];
|
||||
totalBytes: number; // origin usage when measured, else snippets + datasets
|
||||
originMeasured: boolean; // false when the estimate API is absent
|
||||
}
|
||||
|
||||
export const WARNING_THRESHOLD = 0.8; // begin warning
|
||||
export const CRITICAL_THRESHOLD = 0.95; // escalate to "nearly full"
|
||||
|
||||
export function summarizeStorage(input: { usage?: number; quota?: number }): StorageSummary {
|
||||
// Clamp the fraction, derive the level, and mark `available: false` when
|
||||
// usage/quota are absent or non-finite — we don't warn on data we lack.
|
||||
export function summarizeStorage(input: {
|
||||
usageBytes?: number;
|
||||
snippetBytes: number;
|
||||
datasetBytes: number;
|
||||
}): StorageComposition {
|
||||
/* app = usage − known, included only when usage ≥ known (else estimate lag) */
|
||||
}
|
||||
export function jsonByteSize(value: unknown): number; // UTF-8 bytes of JSON.stringify(value)
|
||||
|
||||
// src/app/infrastructure/storage-estimate.ts — the only browser-touching part
|
||||
export async function readStorageEstimate(): Promise<StorageSummary> {
|
||||
export async function readOriginUsage(): Promise<number | undefined> {
|
||||
const storage = typeof navigator !== 'undefined' ? navigator.storage : undefined;
|
||||
if (!storage || typeof storage.estimate !== 'function') return summarizeStorage({});
|
||||
const { usage, quota } = await storage.estimate();
|
||||
return summarizeStorage({ usage, quota });
|
||||
if (!storage || typeof storage.estimate !== 'function') return undefined;
|
||||
const { usage } = await storage.estimate(); // quota deliberately ignored
|
||||
return typeof usage === 'number' ? usage : undefined;
|
||||
}
|
||||
```
|
||||
|
||||
> **Open divergence (spec §02 ↔ code).** Spec §02 frames the monitor as the **snippet** storage budget specifically ("datasets are stored separately with far greater capacity"). The shipped `readStorageEstimate` instead reports **whole-origin** `usage`/`quota` (snippets + datasets + everything the origin holds). Reconcile deliberately — either scope the estimate to the snippet store, or update spec §02 to describe the whole-origin reading.
|
||||
**The bar is decorative; the legend is the data.** There is no `role="meter"` — a meter needs a meaningful maximum, which a composition with no fixed ceiling lacks (APG → meter). The legend's text labels + sizes are the accessible source of truth, so meaning never rests on hue (WCAG 1.4.1). Dropping the old 0.8/0.95 "almost full" thresholds is deliberate: they keyed off the untrustworthy quota; the genuine out-of-room event surfaces at save time (below), where it is accurate. (Resolves the former spec §02 ↔ code "whole-origin vs. snippet budget" divergence; council resolution recorded in `docs/architecture/10`.)
|
||||
|
||||
### Fail loudly, never silently lose data
|
||||
|
||||
|
||||
@@ -263,6 +263,31 @@ relative date and size), in a fixed-width leading slot so it never shifts adjace
|
||||
_(Consulted via /council → GOV.UK Tag, Carbon status-indicator-pattern, APG. This bullet is the
|
||||
contract; cite it, not the external source.)_
|
||||
|
||||
**Resolved — storage composition indicator.** The library-footer Storage Monitor (spec §02)
|
||||
shows what storage is **made of** — Snippets · Datasets · App — as a proportional bar plus a
|
||||
labelled legend, **not** a "used of quota" gauge. The browser quota is a padded, unreliable
|
||||
approximation (web.dev → _storage-for-the-web_), so a budget fraction is false precision; we
|
||||
show real measured sizes instead.
|
||||
|
||||
- **Not a meter.** No `role="meter"`/`progressbar`: a meter needs a meaningful maximum, and a
|
||||
composition with no trustworthy ceiling has none (APG → `meter`: _"should not be used to
|
||||
represent a value … [without] a meaningful maximum"_). The visual bar is **decorative**
|
||||
(`aria-hidden`); the **legend's text labels + sizes are the accessible source of truth**, so
|
||||
meaning never rests on hue (WCAG 1.4.1).
|
||||
- **Part-to-whole in a tiny space.** A single proportional stacked bar suits a **few** segments
|
||||
(we have three) — FT Visual Vocabulary (Part-to-whole) + Datawrapper (stacked bar for "a few
|
||||
shares"; bar "when precise reading matters") — paired with absolute byte labels for the precise read.
|
||||
- **Unavailable degrades, not disappears.** Snippets + datasets are measured from our own data, so
|
||||
they always show; only the **App** segment (which needs the origin estimate) drops out when the
|
||||
Storage Manager API is absent.
|
||||
- **No proactive "almost full" warning.** Dropping the old 0.8/0.95 thresholds is intentional —
|
||||
they keyed off the untrustworthy quota, and a fake fuel gauge fails NN/g #1 (_visibility of system
|
||||
status_) more than it serves it. The genuine out-of-room event surfaces at **save time** as an
|
||||
actionable error (`services/storage-errors.ts` → recover by deleting), satisfying NN/g #9.
|
||||
|
||||
_(Consulted via /council → WAI-ARIA APG `meter`, FT Visual Vocabulary + Datawrapper (part-to-whole),
|
||||
web.dev storage, NN/g #1/#9, WCAG 1.4.1. This bullet is the contract; cite it, not the sources.)_
|
||||
|
||||
**Resolved — library search (Carbon active-search).** The snippet-library search (spec
|
||||
§02) is an **unlabelled active-search input** pinned above the list: `type="search"` with a
|
||||
leading magnifier and `aria-label="Search snippets"` (no visible label — the icon +
|
||||
|
||||
Reference in New Issue
Block a user