@hearthlog/shell 0.0.0-stage → 0.1.0
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/LICENSE +235 -0
- package/package.json +51 -3
- package/src/app.template.html +184 -0
- package/src/components/ai/DecisionReviewSheet.svelte +226 -0
- package/src/components/ai/SuggestButton.svelte +23 -0
- package/src/components/ai/SuggestionChip.svelte +13 -0
- package/src/components/ai/SuggestionMark.svelte +15 -0
- package/src/components/calendar/CalendarFeedsDrawer.svelte +414 -0
- package/src/components/calendar/CalendarMonthSummary.svelte +122 -0
- package/src/components/calendar/calendar-weeks.svelte.ts +27 -0
- package/src/components/calendar/source-icons.ts +19 -0
- package/src/components/data-table/DataTableValue.svelte +14 -0
- package/src/components/data-table/EntityActionsCell.svelte +36 -0
- package/src/components/data-table/EntityRefsCell.svelte +58 -0
- package/src/components/data-table/EntityValue.svelte +42 -0
- package/src/components/data-table/entity-columns.ts +87 -0
- package/src/components/data-table/sqlite-columns.ts +42 -0
- package/src/components/entities/CalendarSourceFields.svelte +149 -0
- package/src/components/history/ChangeRecord.svelte +30 -0
- package/src/components/history/ForkComposer.svelte +174 -0
- package/src/components/history/HistoryScrubber.svelte +245 -0
- package/src/components/history/LogHistory.svelte +334 -0
- package/src/components/history/LogMap.svelte +293 -0
- package/src/components/history/LogTree.svelte +124 -0
- package/src/components/layout/AccountMenu.svelte +111 -0
- package/src/components/layout/AppearanceOptions.svelte +78 -0
- package/src/components/layout/BrandWordmark.svelte +12 -0
- package/src/components/layout/ErrorToast.svelte +34 -0
- package/src/components/layout/LogCreateDrawer.svelte +111 -0
- package/src/components/layout/LogPanel.svelte +225 -0
- package/src/components/layout/LogStatusChip.svelte +137 -0
- package/src/components/layout/MobilePageAction.svelte +44 -0
- package/src/components/layout/MovedBanner.svelte +51 -0
- package/src/components/layout/NavBar.svelte +603 -0
- package/src/components/layout/NavFlyout.svelte +132 -0
- package/src/components/layout/NavItem.svelte +36 -0
- package/src/components/layout/NavMoreDialog.svelte +179 -0
- package/src/components/layout/PageDataStatus.svelte +36 -0
- package/src/components/layout/PhoneGlance.svelte +96 -0
- package/src/components/layout/SidebarResizer.svelte +122 -0
- package/src/components/layout/SyncIndicator.svelte +24 -0
- package/src/components/layout/feature-icons.ts +24 -0
- package/src/components/layout/nav-indicator.ts +128 -0
- package/src/components/layout/sidebar-size.ts +31 -0
- package/src/components/sync/SyncIssuesDrawer.svelte +149 -0
- package/src/components/sync/SyncStateBadge.svelte +17 -0
- package/src/pages/calendar/Page.svelte +1341 -0
- package/src/pages/calendar/load.ts +77 -0
- package/src/pages/developer/DatabaseBrowser.svelte +325 -0
- package/src/pages/developer/Event.svelte +51 -0
- package/src/pages/developer/EventLogManager.svelte +268 -0
- package/src/pages/developer/IndexedDbStats.svelte +93 -0
- package/src/pages/developer/LedgerAdmin.svelte +877 -0
- package/src/pages/developer/LocalSqliteStats.svelte +56 -0
- package/src/pages/developer/Page.svelte +590 -0
- package/src/pages/developer/PendingWorkImport.svelte +49 -0
- package/src/pages/developer/PerformanceDashboard.svelte +262 -0
- package/src/pages/developer/StackInfo.svelte +117 -0
- package/src/pages/developer/SyncLog.svelte +188 -0
- package/src/pages/developer/load.ts +28 -0
- package/src/pages/entities/Page.svelte +1069 -0
- package/src/pages/entities/load.ts +18 -0
- package/src/pages/entity/Page.svelte +655 -0
- package/src/pages/entity/load.ts +79 -0
- package/src/pages/error/ErrorPage.svelte +147 -0
- package/src/pages/layout/Layout.svelte +597 -0
- package/src/pages/layout/load.ts +28 -0
- package/src/pages/redirects/history.ts +20 -0
- package/src/pages/redirects/start.ts +11 -0
- package/src/pages/redirects/today.ts +9 -0
- package/src/service-worker/index.ts +108 -0
- package/src/service-worker/offline-bundle.ts +116 -0
- package/src/service-worker/virtual.d.ts +3 -0
- package/src/styles/index.css +11 -0
- package/src/styles/shell.css +296 -0
- package/static/_headers +4 -0
- package/static/favicon.ico +0 -0
- package/static/offline.html +44 -0
- package/static/replay/sw.js +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { eventLogFilename } from "@hearthlog/runtime/store/event-log.svelte.js";
|
|
3
|
+
import { MODULE_REFRESH_KEYS } from '#generated/modules';
|
|
4
|
+
|
|
5
|
+
import { LedgerClient, loadLedgerConfig } from "@hearthlog/runtime/ledger/index.js";
|
|
6
|
+
import { createAndActivateLog } from "@hearthlog/runtime/log-management.js";
|
|
7
|
+
import { logs } from "@hearthlog/runtime/store/logs.svelte.js";
|
|
8
|
+
|
|
9
|
+
import { refreshPageData as invalidate } from "@hearthlog/runtime/runtime/refresh.js";
|
|
10
|
+
import {
|
|
11
|
+
syncStatus,
|
|
12
|
+
loadStoredLogSnapshot,
|
|
13
|
+
runSyncCoordinator,
|
|
14
|
+
stopRealtime,
|
|
15
|
+
} from "@hearthlog/runtime/store/sync.svelte.js";
|
|
16
|
+
import ConfirmDialog from "@hearthlog/ui/components/ConfirmDialog.svelte";
|
|
17
|
+
import IconDeleteOutline from "~icons/mdi/delete-outline";
|
|
18
|
+
import IconPlus from "~icons/mdi/plus";
|
|
19
|
+
import IconRefresh from "~icons/mdi/refresh";
|
|
20
|
+
|
|
21
|
+
let files = $state(logs.value);
|
|
22
|
+
let newFileName = $state("");
|
|
23
|
+
let status = $state("");
|
|
24
|
+
let showDeleteConfirm = $state(false);
|
|
25
|
+
let logToDelete = $state<{ id: string; name: string } | null>(null);
|
|
26
|
+
|
|
27
|
+
let client: LedgerClient | null = null;
|
|
28
|
+
|
|
29
|
+
function getClient(): LedgerClient {
|
|
30
|
+
if (!client) {
|
|
31
|
+
const config = loadLedgerConfig();
|
|
32
|
+
client = new LedgerClient(config);
|
|
33
|
+
}
|
|
34
|
+
return client;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requestDeleteLog(logId: string, logName: string) {
|
|
38
|
+
logToDelete = { id: logId, name: logName };
|
|
39
|
+
showDeleteConfirm = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function confirmDeleteLog() {
|
|
43
|
+
if (!logToDelete) return;
|
|
44
|
+
|
|
45
|
+
const { id, name } = logToDelete;
|
|
46
|
+
showDeleteConfirm = false;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
status = `Deleting log "${name}"...`;
|
|
50
|
+
|
|
51
|
+
const client = getClient();
|
|
52
|
+
await client.deleteLog(id);
|
|
53
|
+
|
|
54
|
+
// If we deleted the active log, clear it
|
|
55
|
+
if (eventLogFilename.value === id) {
|
|
56
|
+
await stopRealtime();
|
|
57
|
+
await eventLogFilename.set("", "");
|
|
58
|
+
await loadStoredLogSnapshot(null, []);
|
|
59
|
+
await invalidateAllViews();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await logs.refresh();
|
|
63
|
+
files = logs.value;
|
|
64
|
+
status = `Successfully deleted "${name}".`;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
status = `Error deleting log: ${error instanceof Error ? error.message : String(error)}`;
|
|
67
|
+
syncStatus.hasError = true;
|
|
68
|
+
syncStatus.errorMessage = "Failed to delete log";
|
|
69
|
+
} finally {
|
|
70
|
+
logToDelete = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cancelDeleteLog() {
|
|
75
|
+
showDeleteConfirm = false;
|
|
76
|
+
logToDelete = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function invalidateAllViews() {
|
|
80
|
+
await Promise.all([
|
|
81
|
+
...MODULE_REFRESH_KEYS.map((key) => invalidate(key)),
|
|
82
|
+
invalidate("app:entities"),
|
|
83
|
+
invalidate("app:entity-types"),
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function loadFiles() {
|
|
88
|
+
try {
|
|
89
|
+
await logs.loadCached();
|
|
90
|
+
files = logs.value;
|
|
91
|
+
await logs.refresh();
|
|
92
|
+
files = logs.value;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
status = `Error loading logs: ${error instanceof Error ? error.message : String(error)}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function createNewLog() {
|
|
99
|
+
const fileName = newFileName.trim();
|
|
100
|
+
try {
|
|
101
|
+
status = `Creating new log "${fileName}"...`;
|
|
102
|
+
|
|
103
|
+
await createAndActivateLog(fileName);
|
|
104
|
+
|
|
105
|
+
newFileName = "";
|
|
106
|
+
files = logs.value;
|
|
107
|
+
status = `Successfully created and selected "${fileName}".`;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
status = `Error creating new log: ${error instanceof Error ? error.message : String(error)}`;
|
|
110
|
+
syncStatus.hasError = true;
|
|
111
|
+
syncStatus.errorMessage = "Failed to create log";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function onFileSelectionChange(event: Event) {
|
|
116
|
+
const target = event.target as HTMLSelectElement;
|
|
117
|
+
const selectedLogId = target.value;
|
|
118
|
+
|
|
119
|
+
if (!selectedLogId) return;
|
|
120
|
+
|
|
121
|
+
const selectedLog = files.find((log) => log.id === selectedLogId);
|
|
122
|
+
if (!selectedLog) return;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
status = `Loading log "${selectedLog.name}"...`;
|
|
126
|
+
await stopRealtime();
|
|
127
|
+
await eventLogFilename.set(selectedLog.id, selectedLog.name);
|
|
128
|
+
|
|
129
|
+
await loadStoredLogSnapshot(selectedLog.id, []);
|
|
130
|
+
await runSyncCoordinator("manual");
|
|
131
|
+
await invalidateAllViews();
|
|
132
|
+
|
|
133
|
+
status = `Successfully loaded and applied "${selectedLog.name}".`;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
status = `Error loading log: ${error instanceof Error ? error.message : String(error)}`;
|
|
136
|
+
syncStatus.hasError = true;
|
|
137
|
+
syncStatus.errorMessage = "Failed to load log";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$effect(() => {
|
|
142
|
+
loadFiles();
|
|
143
|
+
});
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<div class="stor-panel min-w-0 p-[var(--stor-panel-padding)]">
|
|
147
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
148
|
+
<div>
|
|
149
|
+
<h2 class="text-base font-semibold text-stone-900">
|
|
150
|
+
Event logs
|
|
151
|
+
</h2>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="self-start rounded-full bg-[var(--stor-surface-muted)] px-3 py-1 text-xs font-semibold text-stone-600">
|
|
154
|
+
{files.length} logs
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="mt-4 space-y-3">
|
|
159
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
160
|
+
<div>
|
|
161
|
+
<label
|
|
162
|
+
for="event-log-select"
|
|
163
|
+
class="mb-1.5 block text-xs font-semibold text-[var(--stor-muted-text)]"
|
|
164
|
+
>
|
|
165
|
+
Active log
|
|
166
|
+
</label>
|
|
167
|
+
<div class="relative">
|
|
168
|
+
<select
|
|
169
|
+
id="event-log-select"
|
|
170
|
+
class="block w-full appearance-none rounded-lg border border-[var(--stor-field-border)] bg-white py-2.5 pl-3 pr-10 text-sm outline-none focus:border-[var(--stor-focus)] focus:ring-2 focus:ring-[var(--stor-selected)]"
|
|
171
|
+
onchange={onFileSelectionChange}
|
|
172
|
+
value={eventLogFilename.value}
|
|
173
|
+
>
|
|
174
|
+
{#if files.length === 0}
|
|
175
|
+
<option disabled>No logs found in Ledger Service</option>
|
|
176
|
+
{:else}
|
|
177
|
+
{#each files as log}
|
|
178
|
+
<option value={log.id}>{log.name}</option>
|
|
179
|
+
{/each}
|
|
180
|
+
{/if}
|
|
181
|
+
</select>
|
|
182
|
+
<div
|
|
183
|
+
class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-[var(--stor-muted-text)]"
|
|
184
|
+
>
|
|
185
|
+
<svg
|
|
186
|
+
class="h-4 w-4"
|
|
187
|
+
fill="none"
|
|
188
|
+
stroke="currentColor"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
><path
|
|
191
|
+
d="M19 9l-7 7-7-7"
|
|
192
|
+
stroke-linecap="round"
|
|
193
|
+
stroke-linejoin="round"
|
|
194
|
+
stroke-width="2"
|
|
195
|
+
/></svg
|
|
196
|
+
>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div>
|
|
202
|
+
<label
|
|
203
|
+
for="new-log-name"
|
|
204
|
+
class="mb-1.5 block text-xs font-semibold text-[var(--stor-muted-text)]"
|
|
205
|
+
>
|
|
206
|
+
New log
|
|
207
|
+
</label>
|
|
208
|
+
<input
|
|
209
|
+
id="new-log-name"
|
|
210
|
+
type="text"
|
|
211
|
+
bind:value={newFileName}
|
|
212
|
+
placeholder="Log name..."
|
|
213
|
+
class="block w-full rounded-lg border border-[var(--stor-field-border)] px-3 py-2.5 text-sm outline-none focus:border-[var(--stor-focus)] focus:ring-2 focus:ring-[var(--stor-selected)]"
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div class="grid gap-2 sm:grid-cols-3">
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
class="inline-flex min-h-11 items-center justify-center gap-2 rounded-xl bg-[var(--stor-action)] px-4 text-sm font-semibold text-[var(--stor-action-text)] transition-colors hover:bg-[var(--stor-action-hover)] disabled:opacity-50 sm:min-h-9"
|
|
222
|
+
onclick={createNewLog}
|
|
223
|
+
disabled={!newFileName}
|
|
224
|
+
>
|
|
225
|
+
<IconPlus class="h-4 w-4" />
|
|
226
|
+
Create
|
|
227
|
+
</button>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
class="inline-flex items-center justify-center gap-2 rounded-lg bg-[var(--stor-surface-muted)] px-4 py-2.5 text-sm font-semibold text-stone-700 transition-colors hover:bg-stone-200"
|
|
231
|
+
onclick={loadFiles}
|
|
232
|
+
>
|
|
233
|
+
<IconRefresh class="h-4 w-4" />
|
|
234
|
+
Refresh
|
|
235
|
+
</button>
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
class="inline-flex items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-semibold text-red-700 transition-colors hover:bg-red-100 disabled:opacity-50"
|
|
239
|
+
onclick={() => {
|
|
240
|
+
const log = files.find(
|
|
241
|
+
(f) => f.id === eventLogFilename.value,
|
|
242
|
+
);
|
|
243
|
+
if (log) requestDeleteLog(log.id, log.name);
|
|
244
|
+
}}
|
|
245
|
+
disabled={!eventLogFilename.value}
|
|
246
|
+
>
|
|
247
|
+
<IconDeleteOutline class="h-4 w-4" />
|
|
248
|
+
Delete
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{#if status}
|
|
253
|
+
<div
|
|
254
|
+
class="rounded-lg bg-[var(--stor-surface-muted)] p-3 text-sm text-stone-700"
|
|
255
|
+
>
|
|
256
|
+
{status}
|
|
257
|
+
</div>
|
|
258
|
+
{/if}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<ConfirmDialog
|
|
263
|
+
show={showDeleteConfirm}
|
|
264
|
+
title="Delete Event Log"
|
|
265
|
+
message={`Are you sure you want to delete "${logToDelete?.name}"? This action cannot be undone.`}
|
|
266
|
+
onConfirm={confirmDeleteLog}
|
|
267
|
+
onCancel={cancelDeleteLog}
|
|
268
|
+
/>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import Button from '@hearthlog/ui/components/ui/Button.svelte';
|
|
4
|
+
import { formatSqliteBytes } from '@hearthlog/runtime/db/local-statistics.js';
|
|
5
|
+
import { loadIndexedDbStats, type IndexedDbStats } from '@hearthlog/runtime/storage/indexeddb-statistics.js';
|
|
6
|
+
import IconDatabaseOutline from '~icons/mdi/database-outline';
|
|
7
|
+
import IconRefresh from '~icons/mdi/refresh';
|
|
8
|
+
|
|
9
|
+
let stats = $state<IndexedDbStats | null>(null);
|
|
10
|
+
let loading = $state(true);
|
|
11
|
+
let error = $state('');
|
|
12
|
+
|
|
13
|
+
async function refresh() {
|
|
14
|
+
loading = true;
|
|
15
|
+
error = '';
|
|
16
|
+
try {
|
|
17
|
+
stats = await loadIndexedDbStats();
|
|
18
|
+
} catch (cause) {
|
|
19
|
+
error = cause instanceof Error ? cause.message : 'Could not inspect IndexedDB.';
|
|
20
|
+
} finally {
|
|
21
|
+
loading = false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onMount(() => { void refresh(); });
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<section class="stor-panel min-w-0 p-[var(--stor-panel-padding)]" aria-labelledby="indexeddb-stats-heading" data-testid="indexeddb-stats">
|
|
29
|
+
<div class="flex items-start justify-between gap-3">
|
|
30
|
+
<div class="flex items-start gap-3">
|
|
31
|
+
<IconDatabaseOutline class="mt-0.5 h-5 w-5 shrink-0 text-[var(--stor-accent-strong)]" />
|
|
32
|
+
<div>
|
|
33
|
+
<h2 id="indexeddb-stats-heading" class="text-base font-semibold text-[var(--stor-ink)]">IndexedDB files</h2>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<Button variant="ghost" size="icon" aria-label="Refresh IndexedDB statistics" onclick={refresh} disabled={loading}>
|
|
37
|
+
<IconRefresh class="h-4 w-4" />
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{#if stats}
|
|
42
|
+
<div class="mt-5 flex flex-wrap items-end gap-x-8 gap-y-4">
|
|
43
|
+
<div>
|
|
44
|
+
<p class="text-3xl font-semibold tabular-nums tracking-tight text-[var(--stor-ink)]" data-testid="indexeddb-file-size">{formatSqliteBytes(stats.fileBytes)}</p>
|
|
45
|
+
<p class="mt-1 text-xs text-[var(--stor-muted-text)]">Known file payloads</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<p class="text-2xl font-semibold tabular-nums text-[var(--stor-ink)]" data-testid="indexeddb-file-count">{stats.fileCount}</p>
|
|
49
|
+
<p class="mt-1 text-xs text-[var(--stor-muted-text)]">Saved files</p>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<p class="text-2xl font-semibold tabular-nums text-[var(--stor-ink)]">{stats.databases.length}</p>
|
|
53
|
+
<p class="mt-1 text-xs text-[var(--stor-muted-text)]">Databases</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="mt-5 divide-y divide-[var(--stor-border)] border-t border-[var(--stor-border)]">
|
|
58
|
+
{#each stats.databases as database (database.name)}
|
|
59
|
+
<div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-1 py-3 text-sm">
|
|
60
|
+
<div class="min-w-0">
|
|
61
|
+
<p class="font-medium text-[var(--stor-ink)]">{database.label}</p>
|
|
62
|
+
<p class="text-xs text-[var(--stor-muted-text)]">
|
|
63
|
+
{database.name} · {database.stores.map((store) => `${store.name}: ${store.entries}`).join(' · ') || 'No stores'}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
{#if database.measuresFiles}
|
|
67
|
+
<p class="tabular-nums text-[var(--stor-ink)]">
|
|
68
|
+
{database.fileCount} {database.fileCount === 1 ? 'file' : 'files'} · {formatSqliteBytes(database.fileBytes)}
|
|
69
|
+
{#if database.pdfCount || database.isdocCount}
|
|
70
|
+
<span class="block text-xs text-[var(--stor-muted-text)]">{database.pdfCount} PDF · {database.isdocCount} ISDOC</span>
|
|
71
|
+
{/if}
|
|
72
|
+
</p>
|
|
73
|
+
{:else}
|
|
74
|
+
<p class="text-xs text-[var(--stor-muted-text)]">Entries only</p>
|
|
75
|
+
{/if}
|
|
76
|
+
</div>
|
|
77
|
+
{:else}
|
|
78
|
+
<p class="py-4 text-sm text-[var(--stor-muted-text)]">No IndexedDB databases found.</p>
|
|
79
|
+
{/each}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<p class="mt-3 text-xs text-[var(--stor-muted-text)]">File sizes count stored Blob bytes. IndexedDB metadata, indexes and disk overhead are not included.</p>
|
|
83
|
+
{#if stats.originUsage !== null}
|
|
84
|
+
<p class="mt-2 text-xs text-[var(--stor-muted-text)]" data-testid="origin-storage-usage">
|
|
85
|
+
Browser-reported site storage: {formatSqliteBytes(stats.originUsage)}
|
|
86
|
+
{#if stats.originQuota !== null} · browser quota {formatSqliteBytes(stats.originQuota)}{/if}.
|
|
87
|
+
Includes SQLite, IndexedDB, caches and browser overhead; it cannot be split precisely by database.
|
|
88
|
+
</p>
|
|
89
|
+
{/if}
|
|
90
|
+
{/if}
|
|
91
|
+
{#if loading}<p class="mt-4 text-sm text-[var(--stor-muted-text)]" role="status">Reading IndexedDB statistics…</p>{/if}
|
|
92
|
+
{#if error}<p class="mt-4 text-sm text-[var(--stor-negative)]" role="alert">{error}</p>{/if}
|
|
93
|
+
</section>
|