@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,877 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
const drawerId = $props.id();
|
|
3
|
+
const drawerFormId0 = drawerId + "-0";
|
|
4
|
+
|
|
5
|
+
import { onMount } from "svelte";
|
|
6
|
+
import CommandConfirmDialog from "@hearthlog/ui/components/commands/CommandConfirmDialog.svelte";
|
|
7
|
+
import CommandDrawer from "@hearthlog/ui/components/commands/CommandDrawer.svelte";
|
|
8
|
+
import EmptyState from "@hearthlog/ui/components/EmptyState.svelte";
|
|
9
|
+
import JsonViewer from "@hearthlog/ui/components/JsonViewer.svelte";
|
|
10
|
+
import Button from "@hearthlog/ui/components/ui/Button.svelte";
|
|
11
|
+
import {
|
|
12
|
+
LedgerAdminClient,
|
|
13
|
+
LedgerAdminError,
|
|
14
|
+
LedgerAdminUnauthorizedError,
|
|
15
|
+
type AdminApplicationExport,
|
|
16
|
+
type AdminAppSummary,
|
|
17
|
+
type AdminAppTokenResponse,
|
|
18
|
+
type AdminLogSummary,
|
|
19
|
+
type AdminSession,
|
|
20
|
+
} from "@hearthlog/runtime/ledger/admin-client.js";
|
|
21
|
+
import IconCheck from "~icons/mdi/check";
|
|
22
|
+
import IconCodeJson from "~icons/mdi/code-json";
|
|
23
|
+
import IconContentCopy from "~icons/mdi/content-copy";
|
|
24
|
+
import IconDatabaseEye from "~icons/mdi/database-eye-outline";
|
|
25
|
+
import IconKeyChange from "~icons/mdi/key-change";
|
|
26
|
+
import IconPlus from "~icons/mdi/plus";
|
|
27
|
+
import IconRefresh from "~icons/mdi/refresh";
|
|
28
|
+
import IconServerSecurity from "~icons/mdi/server-security";
|
|
29
|
+
import IconShieldLock from "~icons/mdi/shield-lock-outline";
|
|
30
|
+
|
|
31
|
+
type ViewState =
|
|
32
|
+
| "authorized"
|
|
33
|
+
| "error"
|
|
34
|
+
| "loading"
|
|
35
|
+
| "unauthorized"
|
|
36
|
+
| "unavailable";
|
|
37
|
+
type PendingAction = {
|
|
38
|
+
app: AdminAppSummary;
|
|
39
|
+
kind: "revoke" | "rotate";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const client = new LedgerAdminClient();
|
|
43
|
+
|
|
44
|
+
let viewState = $state<ViewState>("loading");
|
|
45
|
+
let session = $state<AdminSession | null>(null);
|
|
46
|
+
let apps = $state<AdminAppSummary[]>([]);
|
|
47
|
+
let pageError = $state("");
|
|
48
|
+
let refreshing = $state(false);
|
|
49
|
+
let operationAppId = $state<string | null>(null);
|
|
50
|
+
|
|
51
|
+
let drawerOpen = $state(false);
|
|
52
|
+
let createName = $state("");
|
|
53
|
+
let drawerError = $state("");
|
|
54
|
+
let submitting = $state(false);
|
|
55
|
+
let outcomeUnknown = $state(false);
|
|
56
|
+
let tokenResult = $state<AdminAppTokenResponse | null>(null);
|
|
57
|
+
let copied = $state(false);
|
|
58
|
+
|
|
59
|
+
let pendingAction = $state<PendingAction | null>(null);
|
|
60
|
+
|
|
61
|
+
let detailsOpen = $state(false);
|
|
62
|
+
let detailsApp = $state<AdminAppSummary | null>(null);
|
|
63
|
+
let detailsLogs = $state<AdminLogSummary[]>([]);
|
|
64
|
+
let detailsLoading = $state(false);
|
|
65
|
+
let detailsError = $state("");
|
|
66
|
+
|
|
67
|
+
let exportOpen = $state(false);
|
|
68
|
+
let exportApp = $state<AdminAppSummary | null>(null);
|
|
69
|
+
let exportValue = $state<AdminApplicationExport | null>(null);
|
|
70
|
+
let exportLoading = $state(false);
|
|
71
|
+
let exportError = $state("");
|
|
72
|
+
|
|
73
|
+
const activeApps = $derived(apps.filter((app) => app.revokedAt === null).length);
|
|
74
|
+
const revokedApps = $derived(apps.length - activeApps);
|
|
75
|
+
const drawerTitle = $derived(
|
|
76
|
+
tokenResult ? "Save application token" : "Create Ledger application",
|
|
77
|
+
);
|
|
78
|
+
const drawerDescription = $derived(
|
|
79
|
+
tokenResult
|
|
80
|
+
? "This token is returned once and cannot be recovered later."
|
|
81
|
+
: "Provision an isolated Ledger application and its first credential.",
|
|
82
|
+
);
|
|
83
|
+
const confirmTitle = $derived(
|
|
84
|
+
pendingAction?.kind === "rotate"
|
|
85
|
+
? "Rotate application token?"
|
|
86
|
+
: "Revoke Ledger application?",
|
|
87
|
+
);
|
|
88
|
+
const confirmMessage = $derived(
|
|
89
|
+
pendingAction?.kind === "rotate"
|
|
90
|
+
? `Rotate the token for ${pendingAction.app.name}? The current token will stop working immediately.`
|
|
91
|
+
: pendingAction
|
|
92
|
+
? `Revoke ${pendingAction.app.name}? All of its active credentials and connections will stop working.`
|
|
93
|
+
: "",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
onMount(() => {
|
|
97
|
+
void initialize();
|
|
98
|
+
return () => {
|
|
99
|
+
clearSensitiveState();
|
|
100
|
+
clearExportState();
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
async function initialize(): Promise<void> {
|
|
105
|
+
viewState = "loading";
|
|
106
|
+
pageError = "";
|
|
107
|
+
session = null;
|
|
108
|
+
apps = [];
|
|
109
|
+
try {
|
|
110
|
+
session = await client.getSession();
|
|
111
|
+
apps = sortApps(await client.listApps());
|
|
112
|
+
viewState = "authorized";
|
|
113
|
+
} catch (error) {
|
|
114
|
+
setViewError(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function refreshApps(): Promise<void> {
|
|
119
|
+
if (refreshing) return;
|
|
120
|
+
refreshing = true;
|
|
121
|
+
pageError = "";
|
|
122
|
+
try {
|
|
123
|
+
apps = sortApps(await client.listApps());
|
|
124
|
+
} catch (error) {
|
|
125
|
+
setViewError(error);
|
|
126
|
+
} finally {
|
|
127
|
+
refreshing = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function openCreateDrawer(): void {
|
|
132
|
+
closeDetailsDrawer();
|
|
133
|
+
closeExportViewer();
|
|
134
|
+
clearSensitiveState();
|
|
135
|
+
createName = "";
|
|
136
|
+
drawerOpen = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function closeDrawer(): void {
|
|
140
|
+
if (submitting) return;
|
|
141
|
+
drawerOpen = false;
|
|
142
|
+
clearSensitiveState();
|
|
143
|
+
createName = "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function openDetailsDrawer(app: AdminAppSummary): Promise<void> {
|
|
147
|
+
closeDrawer();
|
|
148
|
+
closeExportViewer();
|
|
149
|
+
detailsApp = app;
|
|
150
|
+
detailsLogs = [];
|
|
151
|
+
detailsError = "";
|
|
152
|
+
detailsLoading = true;
|
|
153
|
+
detailsOpen = true;
|
|
154
|
+
try {
|
|
155
|
+
detailsLogs = await client.listAppLogs(app.id);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof LedgerAdminUnauthorizedError) {
|
|
158
|
+
closeDetailsDrawer();
|
|
159
|
+
setViewError(error);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
detailsError = errorMessage(error);
|
|
163
|
+
} finally {
|
|
164
|
+
detailsLoading = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function closeDetailsDrawer(): void {
|
|
169
|
+
detailsOpen = false;
|
|
170
|
+
detailsApp = null;
|
|
171
|
+
detailsLogs = [];
|
|
172
|
+
detailsError = "";
|
|
173
|
+
detailsLoading = false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function openExportViewer(app: AdminAppSummary): Promise<void> {
|
|
177
|
+
closeDetailsDrawer();
|
|
178
|
+
exportApp = app;
|
|
179
|
+
exportOpen = true;
|
|
180
|
+
await loadApplicationExport(app);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function loadApplicationExport(app: AdminAppSummary): Promise<void> {
|
|
184
|
+
exportValue = null;
|
|
185
|
+
exportError = "";
|
|
186
|
+
exportLoading = true;
|
|
187
|
+
try {
|
|
188
|
+
exportValue = await client.getAppExport(app.id);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof LedgerAdminUnauthorizedError) {
|
|
191
|
+
closeExportViewer();
|
|
192
|
+
setViewError(error);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
exportError = errorMessage(error);
|
|
196
|
+
} finally {
|
|
197
|
+
exportLoading = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function closeExportViewer(): void {
|
|
202
|
+
exportOpen = false;
|
|
203
|
+
clearExportState();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function clearExportState(): void {
|
|
207
|
+
exportApp = null;
|
|
208
|
+
exportValue = null;
|
|
209
|
+
exportLoading = false;
|
|
210
|
+
exportError = "";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function rotateFromDetails(app: AdminAppSummary): void {
|
|
214
|
+
if (app.revokedAt !== null) return;
|
|
215
|
+
closeDetailsDrawer();
|
|
216
|
+
requestAction("rotate", app);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function createApplication(event: SubmitEvent): Promise<void> {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
const name = createName.trim();
|
|
222
|
+
if (!name || submitting || outcomeUnknown) return;
|
|
223
|
+
|
|
224
|
+
submitting = true;
|
|
225
|
+
drawerError = "";
|
|
226
|
+
try {
|
|
227
|
+
const result = await client.createApp(name);
|
|
228
|
+
upsertApp(result.app);
|
|
229
|
+
showToken(result);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
setDrawerError(error);
|
|
232
|
+
if (error instanceof LedgerAdminError && error.outcomeUnknown) {
|
|
233
|
+
await reconcileUnknownOutcome();
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
submitting = false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function requestAction(
|
|
241
|
+
kind: PendingAction["kind"],
|
|
242
|
+
app: AdminAppSummary,
|
|
243
|
+
): void {
|
|
244
|
+
if (app.revokedAt !== null || operationAppId !== null) return;
|
|
245
|
+
pendingAction = { app, kind };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function executeAction(): Promise<void> {
|
|
249
|
+
const action = pendingAction;
|
|
250
|
+
pendingAction = null;
|
|
251
|
+
if (!action || operationAppId !== null) return;
|
|
252
|
+
|
|
253
|
+
operationAppId = action.app.id;
|
|
254
|
+
pageError = "";
|
|
255
|
+
try {
|
|
256
|
+
if (action.kind === "rotate") {
|
|
257
|
+
const result = await client.rotateAppToken(action.app.id);
|
|
258
|
+
upsertApp(result.app);
|
|
259
|
+
showToken(result);
|
|
260
|
+
} else {
|
|
261
|
+
upsertApp(await client.revokeApp(action.app.id));
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
setOperationError(error);
|
|
265
|
+
if (error instanceof LedgerAdminError && error.outcomeUnknown) {
|
|
266
|
+
await reconcileUnknownOutcome();
|
|
267
|
+
}
|
|
268
|
+
} finally {
|
|
269
|
+
operationAppId = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function showToken(result: AdminAppTokenResponse): void {
|
|
274
|
+
closeDetailsDrawer();
|
|
275
|
+
clearSensitiveState();
|
|
276
|
+
tokenResult = result;
|
|
277
|
+
copied = false;
|
|
278
|
+
drawerOpen = true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function copyToken(): Promise<void> {
|
|
282
|
+
if (!tokenResult) return;
|
|
283
|
+
try {
|
|
284
|
+
await navigator.clipboard.writeText(tokenResult.token);
|
|
285
|
+
copied = true;
|
|
286
|
+
drawerError = "";
|
|
287
|
+
} catch {
|
|
288
|
+
drawerError = "Copy failed. Select and copy the token manually.";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function recoverUnknownOutcome(): Promise<void> {
|
|
293
|
+
closeDrawer();
|
|
294
|
+
await refreshApps();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function reconcileUnknownOutcome(): Promise<void> {
|
|
298
|
+
try {
|
|
299
|
+
apps = sortApps(await client.listApps());
|
|
300
|
+
} catch {
|
|
301
|
+
// Keep the mutation warning visible. Refresh remains available for
|
|
302
|
+
// another read-only reconciliation attempt.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function upsertApp(app: AdminAppSummary): void {
|
|
307
|
+
const next = apps.filter((current) => current.id !== app.id);
|
|
308
|
+
next.push(app);
|
|
309
|
+
apps = sortApps(next);
|
|
310
|
+
if (detailsApp?.id === app.id) detailsApp = app;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function setViewError(error: unknown): void {
|
|
314
|
+
drawerOpen = false;
|
|
315
|
+
closeDetailsDrawer();
|
|
316
|
+
closeExportViewer();
|
|
317
|
+
pendingAction = null;
|
|
318
|
+
clearSensitiveState();
|
|
319
|
+
session = null;
|
|
320
|
+
apps = [];
|
|
321
|
+
if (error instanceof LedgerAdminUnauthorizedError) {
|
|
322
|
+
viewState = "unauthorized";
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
error instanceof LedgerAdminError &&
|
|
327
|
+
error.code === "ledger_admin_gateway_unconfigured"
|
|
328
|
+
) {
|
|
329
|
+
viewState = "unavailable";
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
viewState = "error";
|
|
333
|
+
pageError = errorMessage(error);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function setDrawerError(error: unknown): void {
|
|
337
|
+
if (error instanceof LedgerAdminUnauthorizedError) {
|
|
338
|
+
closeDrawer();
|
|
339
|
+
setViewError(error);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
drawerError = errorMessage(error);
|
|
343
|
+
outcomeUnknown =
|
|
344
|
+
error instanceof LedgerAdminError && error.outcomeUnknown;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function setOperationError(error: unknown): void {
|
|
348
|
+
if (error instanceof LedgerAdminUnauthorizedError) {
|
|
349
|
+
setViewError(error);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
pageError = errorMessage(error);
|
|
353
|
+
if (error instanceof LedgerAdminError && error.outcomeUnknown) {
|
|
354
|
+
pageError += " Refresh the list before trying another action.";
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function clearSensitiveState(): void {
|
|
359
|
+
tokenResult = null;
|
|
360
|
+
copied = false;
|
|
361
|
+
drawerError = "";
|
|
362
|
+
outcomeUnknown = false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function errorMessage(error: unknown): string {
|
|
366
|
+
return error instanceof Error
|
|
367
|
+
? error.message
|
|
368
|
+
: "Ledger administrator request failed.";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function sortApps(values: AdminAppSummary[]): AdminAppSummary[] {
|
|
372
|
+
return [...values].sort((left, right) =>
|
|
373
|
+
right.createdAt.localeCompare(left.createdAt),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function formatDate(value: string | null): string {
|
|
378
|
+
if (!value) return "Never";
|
|
379
|
+
const date = new Date(value);
|
|
380
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
381
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
382
|
+
dateStyle: "medium",
|
|
383
|
+
timeStyle: "short",
|
|
384
|
+
}).format(date);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function formatEventCount(value: number): string {
|
|
388
|
+
return new Intl.NumberFormat().format(value);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
</script>
|
|
392
|
+
|
|
393
|
+
<section class="mt-4 lg:mt-6" data-testid="ledger-admin">
|
|
394
|
+
{#if viewState === "loading"}
|
|
395
|
+
<div class="stor-panel px-5 py-14 text-center">
|
|
396
|
+
<div class="mx-auto h-8 w-8 animate-spin rounded-full border-2 border-[var(--stor-border)] border-t-[var(--stor-accent)]"></div>
|
|
397
|
+
<p class="mt-4 text-sm font-medium text-stone-600">
|
|
398
|
+
Checking Ledger administrator access…
|
|
399
|
+
</p>
|
|
400
|
+
</div>
|
|
401
|
+
{:else if viewState === "unauthorized"}
|
|
402
|
+
<EmptyState
|
|
403
|
+
icon={IconShieldLock}
|
|
404
|
+
title="You don’t have Ledger administrator access"
|
|
405
|
+
description="Ledger application data and administrator actions are limited to the configured administrator."
|
|
406
|
+
/>
|
|
407
|
+
{:else if viewState === "unavailable"}
|
|
408
|
+
<EmptyState
|
|
409
|
+
icon={IconServerSecurity}
|
|
410
|
+
title="Ledger admin is not configured"
|
|
411
|
+
description="The Access verifier or server-side Ledger administrator credential is missing."
|
|
412
|
+
/>
|
|
413
|
+
{:else if viewState === "error"}
|
|
414
|
+
<EmptyState
|
|
415
|
+
icon={IconServerSecurity}
|
|
416
|
+
title="Ledger admin could not be loaded"
|
|
417
|
+
description={pageError}
|
|
418
|
+
>
|
|
419
|
+
<Button variant="secondary" onclick={initialize}>
|
|
420
|
+
<IconRefresh class="h-4 w-4" />
|
|
421
|
+
Try again
|
|
422
|
+
</Button>
|
|
423
|
+
</EmptyState>
|
|
424
|
+
{:else}
|
|
425
|
+
<div class="space-y-4">
|
|
426
|
+
<div class="stor-panel p-[var(--stor-panel-padding)]">
|
|
427
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
428
|
+
<div>
|
|
429
|
+
<div class="flex items-center gap-2">
|
|
430
|
+
<IconServerSecurity class="h-5 w-5 text-[var(--stor-accent-strong)]" />
|
|
431
|
+
<h2 class="text-lg font-semibold text-stone-950">
|
|
432
|
+
Ledger applications
|
|
433
|
+
</h2>
|
|
434
|
+
</div>
|
|
435
|
+
<p class="mt-1 text-sm text-[var(--stor-muted-text)]">
|
|
436
|
+
Signed in as <span class="font-medium text-stone-700">{session?.email}</span>
|
|
437
|
+
</p>
|
|
438
|
+
</div>
|
|
439
|
+
<div class="flex gap-2">
|
|
440
|
+
<Button
|
|
441
|
+
variant="secondary"
|
|
442
|
+
size="sm"
|
|
443
|
+
onclick={refreshApps}
|
|
444
|
+
disabled={refreshing || operationAppId !== null}
|
|
445
|
+
>
|
|
446
|
+
<IconRefresh class="h-4 w-4 {refreshing ? 'animate-spin' : ''}" />
|
|
447
|
+
Refresh
|
|
448
|
+
</Button>
|
|
449
|
+
<Button
|
|
450
|
+
size="sm"
|
|
451
|
+
onclick={openCreateDrawer}
|
|
452
|
+
disabled={operationAppId !== null}
|
|
453
|
+
>
|
|
454
|
+
<IconPlus class="h-4 w-4" />
|
|
455
|
+
Create Ledger app
|
|
456
|
+
</Button>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
<div class="mt-4 grid grid-cols-3 gap-2">
|
|
461
|
+
<div class="rounded-xl bg-[var(--stor-surface-muted)] px-3 py-2.5">
|
|
462
|
+
<p class="text-[10.5px] font-semibold uppercase tracking-[0.1em] text-[var(--stor-muted-text)]">Total</p>
|
|
463
|
+
<p class="mt-1 text-lg font-semibold text-stone-900">{apps.length}</p>
|
|
464
|
+
</div>
|
|
465
|
+
<div class="rounded-xl bg-emerald-50 px-3 py-2.5">
|
|
466
|
+
<p class="text-[10.5px] font-semibold uppercase tracking-[0.1em] text-emerald-600">Active</p>
|
|
467
|
+
<p class="mt-1 text-lg font-semibold text-emerald-900">{activeApps}</p>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="rounded-xl bg-[var(--stor-surface-muted)] px-3 py-2.5">
|
|
470
|
+
<p class="text-[10.5px] font-semibold uppercase tracking-[0.1em] text-[var(--stor-muted-text)]">Revoked</p>
|
|
471
|
+
<p class="mt-1 text-lg font-semibold text-stone-800">{revokedApps}</p>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
{#if pageError}
|
|
477
|
+
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800" role="alert">
|
|
478
|
+
{pageError}
|
|
479
|
+
</div>
|
|
480
|
+
{/if}
|
|
481
|
+
|
|
482
|
+
{#if apps.length === 0}
|
|
483
|
+
<EmptyState
|
|
484
|
+
icon={IconServerSecurity}
|
|
485
|
+
title="No Ledger applications"
|
|
486
|
+
description="Create the first application to receive its initial token."
|
|
487
|
+
>
|
|
488
|
+
<Button onclick={openCreateDrawer}>
|
|
489
|
+
<IconPlus class="h-4 w-4" />
|
|
490
|
+
Create Ledger app
|
|
491
|
+
</Button>
|
|
492
|
+
</EmptyState>
|
|
493
|
+
{:else}
|
|
494
|
+
<div class="stor-panel hidden overflow-hidden md:block" data-testid="ledger-admin-table">
|
|
495
|
+
<div class="overflow-x-auto">
|
|
496
|
+
<table class="w-full min-w-[58rem] text-left text-sm">
|
|
497
|
+
<thead class="border-b border-[var(--stor-border)] bg-[var(--stor-surface-muted)] text-xs uppercase tracking-[0.08em] text-[var(--stor-muted-text)]">
|
|
498
|
+
<tr>
|
|
499
|
+
<th class="px-4 py-3 font-semibold">Application</th>
|
|
500
|
+
<th class="px-4 py-3 font-semibold">Status</th>
|
|
501
|
+
<th class="px-4 py-3 font-semibold">Created</th>
|
|
502
|
+
<th class="px-4 py-3 font-semibold">Token issued</th>
|
|
503
|
+
<th class="px-4 py-3 text-right font-semibold">Actions</th>
|
|
504
|
+
</tr>
|
|
505
|
+
</thead>
|
|
506
|
+
<tbody class="divide-y divide-stone-100">
|
|
507
|
+
{#each apps as app (app.id)}
|
|
508
|
+
<tr>
|
|
509
|
+
<td class="px-4 py-3">
|
|
510
|
+
<p class="font-semibold text-stone-900">{app.name}</p>
|
|
511
|
+
<code class="mt-0.5 block max-w-64 truncate text-xs text-[var(--stor-muted-text)]" title={app.id}>{app.id}</code>
|
|
512
|
+
</td>
|
|
513
|
+
<td class="px-4 py-3">
|
|
514
|
+
<span class="inline-flex rounded-full px-2.5 py-1 text-xs font-semibold {app.revokedAt ? 'bg-[var(--stor-surface-muted)] text-stone-600' : 'bg-emerald-50 text-emerald-700'}">
|
|
515
|
+
{app.revokedAt ? "Revoked" : "Active"}
|
|
516
|
+
</span>
|
|
517
|
+
</td>
|
|
518
|
+
<td class="whitespace-nowrap px-4 py-3 text-stone-600">{formatDate(app.createdAt)}</td>
|
|
519
|
+
<td class="whitespace-nowrap px-4 py-3 text-stone-600">{formatDate(app.activeTokenCreatedAt)}</td>
|
|
520
|
+
<td class="px-4 py-3">
|
|
521
|
+
<div class="flex justify-end gap-2">
|
|
522
|
+
<Button
|
|
523
|
+
variant="ghost"
|
|
524
|
+
size="sm"
|
|
525
|
+
aria-label={`View logs and data for ${app.name}`}
|
|
526
|
+
onclick={() => void openDetailsDrawer(app)}
|
|
527
|
+
disabled={operationAppId !== null}
|
|
528
|
+
>
|
|
529
|
+
<IconDatabaseEye class="h-4 w-4" />
|
|
530
|
+
View
|
|
531
|
+
</Button>
|
|
532
|
+
<Button
|
|
533
|
+
variant="secondary"
|
|
534
|
+
size="sm"
|
|
535
|
+
aria-label={`Rotate token for ${app.name}`}
|
|
536
|
+
onclick={() => requestAction("rotate", app)}
|
|
537
|
+
disabled={app.revokedAt !== null || operationAppId !== null}
|
|
538
|
+
>
|
|
539
|
+
<IconKeyChange class="h-4 w-4" />
|
|
540
|
+
Rotate
|
|
541
|
+
</Button>
|
|
542
|
+
<Button
|
|
543
|
+
variant="ghost"
|
|
544
|
+
size="sm"
|
|
545
|
+
class="text-red-700 hover:bg-red-50 hover:text-red-800"
|
|
546
|
+
aria-label={`Revoke ${app.name}`}
|
|
547
|
+
onclick={() => requestAction("revoke", app)}
|
|
548
|
+
disabled={app.revokedAt !== null || operationAppId !== null}
|
|
549
|
+
>
|
|
550
|
+
Revoke
|
|
551
|
+
</Button>
|
|
552
|
+
</div>
|
|
553
|
+
</td>
|
|
554
|
+
</tr>
|
|
555
|
+
{/each}
|
|
556
|
+
</tbody>
|
|
557
|
+
</table>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
<div class="space-y-3 md:hidden" data-testid="ledger-admin-cards">
|
|
562
|
+
{#each apps as app (app.id)}
|
|
563
|
+
<article class="stor-panel p-4">
|
|
564
|
+
<div class="flex items-start justify-between gap-3">
|
|
565
|
+
<div class="min-w-0">
|
|
566
|
+
<h3 class="truncate font-semibold text-stone-900">{app.name}</h3>
|
|
567
|
+
<code class="mt-1 block truncate text-xs text-[var(--stor-muted-text)]">{app.id}</code>
|
|
568
|
+
</div>
|
|
569
|
+
<span class="shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold {app.revokedAt ? 'bg-[var(--stor-surface-muted)] text-stone-600' : 'bg-emerald-50 text-emerald-700'}">
|
|
570
|
+
{app.revokedAt ? "Revoked" : "Active"}
|
|
571
|
+
</span>
|
|
572
|
+
</div>
|
|
573
|
+
<dl class="mt-4 grid grid-cols-2 gap-3 text-xs">
|
|
574
|
+
<div>
|
|
575
|
+
<dt class="text-[var(--stor-muted-text)]">Created</dt>
|
|
576
|
+
<dd class="mt-1 font-medium text-stone-700">{formatDate(app.createdAt)}</dd>
|
|
577
|
+
</div>
|
|
578
|
+
<div>
|
|
579
|
+
<dt class="text-[var(--stor-muted-text)]">Token issued</dt>
|
|
580
|
+
<dd class="mt-1 font-medium text-stone-700">{formatDate(app.activeTokenCreatedAt)}</dd>
|
|
581
|
+
</div>
|
|
582
|
+
</dl>
|
|
583
|
+
<div class="mt-4 grid grid-cols-2 gap-2">
|
|
584
|
+
<Button
|
|
585
|
+
variant="secondary"
|
|
586
|
+
size="sm"
|
|
587
|
+
class="col-span-2"
|
|
588
|
+
aria-label={`View logs and data for ${app.name}`}
|
|
589
|
+
onclick={() => void openDetailsDrawer(app)}
|
|
590
|
+
disabled={operationAppId !== null}
|
|
591
|
+
>
|
|
592
|
+
<IconDatabaseEye class="h-4 w-4" />
|
|
593
|
+
View logs and data
|
|
594
|
+
</Button>
|
|
595
|
+
<Button
|
|
596
|
+
variant="secondary"
|
|
597
|
+
size="sm"
|
|
598
|
+
aria-label={`Rotate token for ${app.name}`}
|
|
599
|
+
onclick={() => requestAction("rotate", app)}
|
|
600
|
+
disabled={app.revokedAt !== null || operationAppId !== null}
|
|
601
|
+
>
|
|
602
|
+
<IconKeyChange class="h-4 w-4" />
|
|
603
|
+
Rotate
|
|
604
|
+
</Button>
|
|
605
|
+
<Button
|
|
606
|
+
variant="ghost"
|
|
607
|
+
size="sm"
|
|
608
|
+
class="text-red-700 hover:bg-red-50 hover:text-red-800"
|
|
609
|
+
aria-label={`Revoke ${app.name}`}
|
|
610
|
+
onclick={() => requestAction("revoke", app)}
|
|
611
|
+
disabled={app.revokedAt !== null || operationAppId !== null}
|
|
612
|
+
>
|
|
613
|
+
Revoke
|
|
614
|
+
</Button>
|
|
615
|
+
</div>
|
|
616
|
+
</article>
|
|
617
|
+
{/each}
|
|
618
|
+
</div>
|
|
619
|
+
{/if}
|
|
620
|
+
</div>
|
|
621
|
+
{/if}
|
|
622
|
+
</section>
|
|
623
|
+
|
|
624
|
+
<CommandDrawer
|
|
625
|
+
show={drawerOpen}
|
|
626
|
+
title={drawerTitle}
|
|
627
|
+
description={drawerDescription}
|
|
628
|
+
onClose={closeDrawer}
|
|
629
|
+
>
|
|
630
|
+
{#if tokenResult}
|
|
631
|
+
<div class="space-y-5">
|
|
632
|
+
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
|
633
|
+
Save the token for <strong>{tokenResult.app.name}</strong> now. Closing this drawer permanently removes it from the UI.
|
|
634
|
+
</div>
|
|
635
|
+
<div>
|
|
636
|
+
<label for="ledger-one-time-token" class="text-sm font-semibold text-stone-700">
|
|
637
|
+
One-time application token
|
|
638
|
+
</label>
|
|
639
|
+
<textarea
|
|
640
|
+
id="ledger-one-time-token"
|
|
641
|
+
data-testid="ledger-one-time-token"
|
|
642
|
+
readonly
|
|
643
|
+
rows="7"
|
|
644
|
+
value={tokenResult.token}
|
|
645
|
+
class="mt-2 w-full resize-none rounded-xl border border-[var(--stor-field-border)] bg-white p-3 font-mono text-xs leading-5 text-stone-800 outline-none"
|
|
646
|
+
></textarea>
|
|
647
|
+
</div>
|
|
648
|
+
{#if drawerError}
|
|
649
|
+
<p class="text-sm text-red-700" role="alert">{drawerError}</p>
|
|
650
|
+
{/if}
|
|
651
|
+
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
652
|
+
<Button variant="secondary" onclick={closeDrawer}>Done</Button>
|
|
653
|
+
<Button onclick={copyToken}>
|
|
654
|
+
{#if copied}
|
|
655
|
+
<IconCheck class="h-4 w-4" />
|
|
656
|
+
Copied
|
|
657
|
+
{:else}
|
|
658
|
+
<IconContentCopy class="h-4 w-4" />
|
|
659
|
+
Copy token
|
|
660
|
+
{/if}
|
|
661
|
+
</Button>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
{:else}
|
|
665
|
+
<form id={drawerFormId0} class="space-y-5" onsubmit={createApplication}>
|
|
666
|
+
<div>
|
|
667
|
+
<label for="ledger-app-name" class="text-sm font-semibold text-stone-700">
|
|
668
|
+
Application name
|
|
669
|
+
</label>
|
|
670
|
+
<input
|
|
671
|
+
id="ledger-app-name"
|
|
672
|
+
name="name"
|
|
673
|
+
type="text"
|
|
674
|
+
autocomplete="off"
|
|
675
|
+
bind:value={createName}
|
|
676
|
+
disabled={submitting || outcomeUnknown}
|
|
677
|
+
placeholder="e.g. reporting"
|
|
678
|
+
class="mt-2 h-11 w-full rounded-xl border border-[var(--stor-field-border)] bg-white px-3 text-sm outline-none focus:border-[var(--stor-accent)] focus:ring-2 focus:ring-[var(--stor-accent-soft)] disabled:bg-[var(--stor-surface-muted)]"
|
|
679
|
+
/>
|
|
680
|
+
</div>
|
|
681
|
+
{#if drawerError}
|
|
682
|
+
<div class="rounded-xl border border-red-200 bg-red-50 p-3 text-sm text-red-800" role="alert">
|
|
683
|
+
{drawerError}
|
|
684
|
+
</div>
|
|
685
|
+
{/if}
|
|
686
|
+
</form>
|
|
687
|
+
|
|
688
|
+
{/if}
|
|
689
|
+
{#snippet footer()}
|
|
690
|
+
{#if !tokenResult}
|
|
691
|
+
<Button
|
|
692
|
+
variant="secondary"
|
|
693
|
+
type="button"
|
|
694
|
+
onclick={outcomeUnknown ? recoverUnknownOutcome : closeDrawer}
|
|
695
|
+
disabled={submitting}
|
|
696
|
+
>
|
|
697
|
+
{outcomeUnknown ? "Close and refresh" : "Cancel"}
|
|
698
|
+
</Button>
|
|
699
|
+
{#if !outcomeUnknown}
|
|
700
|
+
<Button type="submit" form={drawerFormId0} disabled={!createName.trim() || submitting}>
|
|
701
|
+
<IconPlus class="h-4 w-4" />
|
|
702
|
+
{submitting ? "Creating…" : "Create app"}
|
|
703
|
+
</Button>
|
|
704
|
+
{/if}
|
|
705
|
+
|
|
706
|
+
{/if}
|
|
707
|
+
{/snippet}
|
|
708
|
+
</CommandDrawer>
|
|
709
|
+
|
|
710
|
+
<CommandDrawer
|
|
711
|
+
show={detailsOpen}
|
|
712
|
+
title={detailsApp ? `${detailsApp.name} data` : "Application data"}
|
|
713
|
+
description="Inspect Ledger logs, credential status, and the complete logical dataset."
|
|
714
|
+
size="lg"
|
|
715
|
+
onClose={closeDetailsDrawer}
|
|
716
|
+
>
|
|
717
|
+
{#if detailsApp}
|
|
718
|
+
<div class="space-y-5" data-testid="ledger-app-details">
|
|
719
|
+
<div class="rounded-xl bg-[var(--stor-surface-muted)] p-4">
|
|
720
|
+
<div class="flex items-start justify-between gap-3">
|
|
721
|
+
<div class="min-w-0">
|
|
722
|
+
<p class="font-semibold text-stone-900">{detailsApp.name}</p>
|
|
723
|
+
<code class="mt-1 block break-all text-xs text-[var(--stor-muted-text)]">{detailsApp.id}</code>
|
|
724
|
+
</div>
|
|
725
|
+
<span class="shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold {detailsApp.revokedAt ? 'bg-stone-200 text-stone-600' : 'bg-emerald-100 text-emerald-700'}">
|
|
726
|
+
{detailsApp.revokedAt ? "Revoked" : "Active"}
|
|
727
|
+
</span>
|
|
728
|
+
</div>
|
|
729
|
+
<dl class="mt-4 grid grid-cols-2 gap-3 text-xs">
|
|
730
|
+
<div>
|
|
731
|
+
<dt class="text-[var(--stor-muted-text)]">Created</dt>
|
|
732
|
+
<dd class="mt-1 font-medium text-stone-700">{formatDate(detailsApp.createdAt)}</dd>
|
|
733
|
+
</div>
|
|
734
|
+
<div>
|
|
735
|
+
<dt class="text-[var(--stor-muted-text)]">Key issued</dt>
|
|
736
|
+
<dd class="mt-1 font-medium text-stone-700">{formatDate(detailsApp.activeTokenCreatedAt)}</dd>
|
|
737
|
+
</div>
|
|
738
|
+
</dl>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
|
|
742
|
+
<p class="text-sm font-semibold text-amber-950">Application key</p>
|
|
743
|
+
<div class="mt-2 rounded-lg border border-amber-200 bg-white px-3 py-2 font-mono text-sm tracking-[0.1em] text-[var(--stor-muted-text)]">
|
|
744
|
+
••••••••••••••••
|
|
745
|
+
</div>
|
|
746
|
+
<p class="mt-2 text-xs leading-5 text-amber-900">
|
|
747
|
+
Ledger shows a key only once when it is created or rotated. The current key cannot be recovered from this workspace.
|
|
748
|
+
</p>
|
|
749
|
+
{#if detailsApp.revokedAt === null}
|
|
750
|
+
<Button
|
|
751
|
+
variant="secondary"
|
|
752
|
+
size="sm"
|
|
753
|
+
class="mt-3"
|
|
754
|
+
onclick={() => rotateFromDetails(detailsApp!)}
|
|
755
|
+
>
|
|
756
|
+
<IconKeyChange class="h-4 w-4" />
|
|
757
|
+
Rotate and reveal new key
|
|
758
|
+
</Button>
|
|
759
|
+
{/if}
|
|
760
|
+
</div>
|
|
761
|
+
|
|
762
|
+
<div>
|
|
763
|
+
<div class="flex items-center justify-between gap-3">
|
|
764
|
+
<div>
|
|
765
|
+
<h3 class="font-semibold text-stone-900">Logs</h3>
|
|
766
|
+
<p class="mt-0.5 text-xs text-[var(--stor-muted-text)]">
|
|
767
|
+
{detailsLoading ? "Loading…" : `${detailsLogs.length} ${detailsLogs.length === 1 ? "log" : "logs"}`}
|
|
768
|
+
</p>
|
|
769
|
+
</div>
|
|
770
|
+
<Button
|
|
771
|
+
size="sm"
|
|
772
|
+
onclick={() => void openExportViewer(detailsApp!)}
|
|
773
|
+
aria-label={`View JSON export for ${detailsApp.name}`}
|
|
774
|
+
>
|
|
775
|
+
<IconCodeJson class="h-4 w-4" />
|
|
776
|
+
View JSON export
|
|
777
|
+
</Button>
|
|
778
|
+
</div>
|
|
779
|
+
|
|
780
|
+
{#if detailsLoading}
|
|
781
|
+
<div class="mt-3 rounded-xl bg-[var(--stor-surface-muted)] px-4 py-8 text-center text-sm text-[var(--stor-muted-text)]">
|
|
782
|
+
Loading application logs…
|
|
783
|
+
</div>
|
|
784
|
+
{:else if detailsError}
|
|
785
|
+
<div class="mt-3 rounded-xl border border-red-200 bg-red-50 p-3 text-sm text-red-800" role="alert">
|
|
786
|
+
{detailsError}
|
|
787
|
+
</div>
|
|
788
|
+
{:else if detailsLogs.length === 0}
|
|
789
|
+
<div class="mt-3 rounded-xl border border-dashed border-[var(--stor-field-border)] px-4 py-8 text-center text-sm text-[var(--stor-muted-text)]">
|
|
790
|
+
This application has no logs.
|
|
791
|
+
</div>
|
|
792
|
+
{:else}
|
|
793
|
+
<div class="mt-3 space-y-2" data-testid="ledger-app-logs">
|
|
794
|
+
{#each detailsLogs as log (log.id)}
|
|
795
|
+
<article class="stor-panel p-3">
|
|
796
|
+
<div class="flex items-start justify-between gap-3">
|
|
797
|
+
<div class="min-w-0">
|
|
798
|
+
<p class="truncate text-sm font-semibold text-stone-900">{log.name}</p>
|
|
799
|
+
<code class="mt-1 block break-all text-[11px] text-[var(--stor-muted-text)]">{log.id}</code>
|
|
800
|
+
</div>
|
|
801
|
+
<span class="shrink-0 rounded-lg bg-[var(--stor-surface-muted)] px-2 py-1 text-xs font-semibold text-stone-700">
|
|
802
|
+
{formatEventCount(log.headVersion)} {log.headVersion === 1 ? "event" : "events"}
|
|
803
|
+
</span>
|
|
804
|
+
</div>
|
|
805
|
+
<dl class="mt-3 grid grid-cols-2 gap-3 text-xs">
|
|
806
|
+
<div>
|
|
807
|
+
<dt class="text-[var(--stor-muted-text)]">Created</dt>
|
|
808
|
+
<dd class="mt-1 text-stone-700">{formatDate(log.createdAt)}</dd>
|
|
809
|
+
</div>
|
|
810
|
+
<div>
|
|
811
|
+
<dt class="text-[var(--stor-muted-text)]">Last updated</dt>
|
|
812
|
+
<dd class="mt-1 text-stone-700">{formatDate(log.lastUpdatedAt)}</dd>
|
|
813
|
+
</div>
|
|
814
|
+
</dl>
|
|
815
|
+
</article>
|
|
816
|
+
{/each}
|
|
817
|
+
</div>
|
|
818
|
+
{/if}
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
{/if}
|
|
822
|
+
</CommandDrawer>
|
|
823
|
+
|
|
824
|
+
<CommandDrawer
|
|
825
|
+
show={exportOpen}
|
|
826
|
+
title={exportApp ? `${exportApp.name} export` : "Ledger export"}
|
|
827
|
+
description="Read-only logical snapshot of the application, its logs, and every exported event."
|
|
828
|
+
size="lg"
|
|
829
|
+
onClose={closeExportViewer}
|
|
830
|
+
>
|
|
831
|
+
<div data-testid="ledger-export-viewer">
|
|
832
|
+
{#if exportLoading}
|
|
833
|
+
<div class="rounded-xl bg-[var(--stor-surface-muted)] px-4 py-12 text-center">
|
|
834
|
+
<div class="mx-auto h-7 w-7 animate-spin rounded-full border-2 border-[var(--stor-border)] border-t-[var(--stor-accent)]"></div>
|
|
835
|
+
<p class="mt-3 text-sm font-medium text-stone-600">
|
|
836
|
+
Preparing JSON export…
|
|
837
|
+
</p>
|
|
838
|
+
</div>
|
|
839
|
+
{:else if exportError}
|
|
840
|
+
<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800" role="alert">
|
|
841
|
+
<p>{exportError}</p>
|
|
842
|
+
{#if exportApp}
|
|
843
|
+
<Button
|
|
844
|
+
variant="secondary"
|
|
845
|
+
size="sm"
|
|
846
|
+
class="mt-3"
|
|
847
|
+
onclick={() => void loadApplicationExport(exportApp!)}
|
|
848
|
+
>
|
|
849
|
+
<IconRefresh class="h-4 w-4" />
|
|
850
|
+
Try again
|
|
851
|
+
</Button>
|
|
852
|
+
{/if}
|
|
853
|
+
</div>
|
|
854
|
+
{:else if exportValue}
|
|
855
|
+
<div class="mb-3 flex flex-wrap items-center justify-between gap-2 text-xs text-[var(--stor-muted-text)]">
|
|
856
|
+
<span>
|
|
857
|
+
{exportValue.logs.length}
|
|
858
|
+
{exportValue.logs.length === 1 ? "log" : "logs"}
|
|
859
|
+
</span>
|
|
860
|
+
<span>Exported {formatDate(exportValue.exportedAt)}</span>
|
|
861
|
+
</div>
|
|
862
|
+
<JsonViewer
|
|
863
|
+
value={exportValue}
|
|
864
|
+
label={`${exportValue.application.name} Ledger export JSON`}
|
|
865
|
+
maxHeight="calc(100dvh - 15rem)"
|
|
866
|
+
/>
|
|
867
|
+
{/if}
|
|
868
|
+
</div>
|
|
869
|
+
</CommandDrawer>
|
|
870
|
+
|
|
871
|
+
<CommandConfirmDialog
|
|
872
|
+
show={pendingAction !== null}
|
|
873
|
+
title={confirmTitle}
|
|
874
|
+
message={confirmMessage}
|
|
875
|
+
onConfirm={executeAction}
|
|
876
|
+
onCancel={() => (pendingAction = null)}
|
|
877
|
+
/>
|