@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,79 @@
|
|
|
1
|
+
import { deferPage } from '@hearthlog/runtime/runtime/page-data.svelte.js';
|
|
2
|
+
import { getEntityDefinition, getPayloadKey } from '@hearthlog/contracts/event/entity-definitions';
|
|
3
|
+
import type { LoadEvent } from '@sveltejs/kit';
|
|
4
|
+
type PageLoad = (event: LoadEvent<{ type: string }>) => unknown;
|
|
5
|
+
import { error, redirect } from '@sveltejs/kit';
|
|
6
|
+
import { queryDatabase } from '@hearthlog/runtime/db/read-repositories.js';
|
|
7
|
+
import { tenant } from '@hearthlog/contracts/tenant/config';
|
|
8
|
+
import { tenantRouteRedirect } from '@hearthlog/contracts/tenant/route-access';
|
|
9
|
+
import { withTiming, measurementMetadata } from '@hearthlog/runtime/utils/perf.js';
|
|
10
|
+
|
|
11
|
+
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100] as const;
|
|
12
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
13
|
+
|
|
14
|
+
const loadData = async ({ params, url, depends }: Parameters<PageLoad>[0]) => {
|
|
15
|
+
|
|
16
|
+
const entityName = params.type;
|
|
17
|
+
|
|
18
|
+
const redirectTarget = tenantRouteRedirect(`/entities/${entityName}`, tenant);
|
|
19
|
+
if (redirectTarget) redirect(307, redirectTarget);
|
|
20
|
+
|
|
21
|
+
let rowCount = 0;
|
|
22
|
+
let referenceFieldCount = 0;
|
|
23
|
+
const metadata = measurementMetadata();
|
|
24
|
+
|
|
25
|
+
const result = await withTiming(
|
|
26
|
+
'ui',
|
|
27
|
+
'loadEntityPage',
|
|
28
|
+
() => queryDatabase(async (repositories) => {
|
|
29
|
+
const repository = repositories.entities;
|
|
30
|
+
const entityType = await repository.getEntityType(entityName);
|
|
31
|
+
if (!entityType) {
|
|
32
|
+
throw error(
|
|
33
|
+
404,
|
|
34
|
+
`Entity type "${entityName}" is not available in this view.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
referenceFieldCount = entityType.fields.filter((field) => field.type === 'reference').length;
|
|
38
|
+
const result = await repository.queryEntities(entityName, {
|
|
39
|
+
search: (url.searchParams.get('search') ?? '').trim(),
|
|
40
|
+
pageSize: parsePageSize(url.searchParams.get('pageSize')),
|
|
41
|
+
requestedPage: parsePage(url.searchParams.get('page')),
|
|
42
|
+
sortField: url.searchParams.get('sort'),
|
|
43
|
+
sortDirection: url.searchParams.get('direction') === 'desc' ? 'desc' : 'asc',
|
|
44
|
+
});
|
|
45
|
+
if (!result) throw error(404, `Entity type "${entityName}" not found.`);
|
|
46
|
+
rowCount = result.totalRows;
|
|
47
|
+
return result;
|
|
48
|
+
}),
|
|
49
|
+
() => ({
|
|
50
|
+
...metadata, kind: 'operation', route: `/entities/${entityName}`,
|
|
51
|
+
baselineMetric: 'entityPageLoadTime',
|
|
52
|
+
entityType: entityName,
|
|
53
|
+
rowCount,
|
|
54
|
+
referenceFieldCount,
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...result,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function parsePageSize(value: string | null): number {
|
|
64
|
+
const parsed = Number(value);
|
|
65
|
+
return PAGE_SIZE_OPTIONS.includes(parsed as (typeof PAGE_SIZE_OPTIONS)[number])
|
|
66
|
+
? parsed
|
|
67
|
+
: DEFAULT_PAGE_SIZE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parsePage(value: string | null): number {
|
|
71
|
+
const parsed = Number(value);
|
|
72
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const load = ((event) => {
|
|
76
|
+
event.depends('app:entities'); const target = tenantRouteRedirect(`/entities/${event.params.type}`, tenant); if (target) redirect(307, target);
|
|
77
|
+
const definition = getEntityDefinition(event.params.type);
|
|
78
|
+
return deferPage(event, { entityType: { name: event.params.type, display_name: definition?.displayName ?? event.params.type.replace(/(^|_)([a-z])/g, (_, separator, letter) => `${separator ? ' ' : ''}${letter.toUpperCase()}`), id: '', tenant_id: '', log_id: '', created_at: '', updated_at: '', is_archived: 0, is_static: Boolean(definition), table_name: event.params.type, fields_json: '[]', calendar_json: null }, fields: definition?.fields.map(field => ({ ...field, payloadKey: getPayloadKey(field) })) ?? [], entities: [], referenceOptions: {}, referencedBy: {}, totalRows: 0, totalPages: 1, currentPage: parsePage(event.url.searchParams.get('page')), pageSize: parsePageSize(event.url.searchParams.get('pageSize')), search: event.url.searchParams.get('search') ?? '', sortField: event.url.searchParams.get('sort'), sortDirection: event.url.searchParams.get('direction') === 'desc' ? 'desc' : 'asc' } as Awaited<ReturnType<typeof loadData>>, () => loadData(event));
|
|
79
|
+
}) satisfies PageLoad;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "$app/state";
|
|
3
|
+
import { localStoreStatus } from "@hearthlog/runtime/store/local-store.svelte.js";
|
|
4
|
+
import { onMount } from "svelte";
|
|
5
|
+
import type { LocalRecoverySummary } from "@hearthlog/runtime/recovery/local-recovery.js";
|
|
6
|
+
|
|
7
|
+
type LocalRecoveryModule = typeof import("@hearthlog/runtime/recovery/local-recovery.js");
|
|
8
|
+
let { error = null }: { error?: unknown } = $props();
|
|
9
|
+
|
|
10
|
+
let recoverySummary = $state<LocalRecoverySummary>({ pending: null, blocked: null });
|
|
11
|
+
let recoveryBusy = $state(false);
|
|
12
|
+
let recoveryMessage = $state("");
|
|
13
|
+
let confirmReset = $state(false);
|
|
14
|
+
let isOffline = $state(false);
|
|
15
|
+
let recoveryModulePromise: Promise<LocalRecoveryModule> | null = null;
|
|
16
|
+
|
|
17
|
+
const localStoreFailed = $derived(localStoreStatus.phase === "failed");
|
|
18
|
+
const offlinePageError = $derived(!localStoreFailed && isOffline);
|
|
19
|
+
const title = $derived(
|
|
20
|
+
localStoreFailed
|
|
21
|
+
? "Local data could not be opened"
|
|
22
|
+
: offlinePageError
|
|
23
|
+
? "Could not open this page offline"
|
|
24
|
+
: "stor could not open this page",
|
|
25
|
+
);
|
|
26
|
+
const description = $derived(
|
|
27
|
+
localStoreFailed
|
|
28
|
+
? localStoreStatus.errorMessage ??
|
|
29
|
+
"The local database did not finish starting. Your Ledger data has not been changed."
|
|
30
|
+
: offlinePageError
|
|
31
|
+
? "The offline app bundle or local data may be unavailable. Connect and retry."
|
|
32
|
+
: error instanceof Error ? error.message : page.error?.message ?? "An unexpected error interrupted this page.",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
function getRecoveryModule(): Promise<LocalRecoveryModule> {
|
|
36
|
+
recoveryModulePromise ??= import("@hearthlog/runtime/recovery/local-recovery.js");
|
|
37
|
+
return recoveryModulePromise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function retry(): Promise<void> {
|
|
41
|
+
if (!localStoreFailed) {
|
|
42
|
+
window.location.reload();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
recoveryBusy = true;
|
|
46
|
+
recoveryMessage = "Retrying the local migration…";
|
|
47
|
+
try {
|
|
48
|
+
const recovery = await getRecoveryModule();
|
|
49
|
+
await recovery.retryLocalMigration();
|
|
50
|
+
window.location.reload();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
recoveryMessage = error instanceof Error ? error.message : "The migration still could not complete.";
|
|
53
|
+
recoveryBusy = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function download(): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
const recovery = await getRecoveryModule();
|
|
60
|
+
await recovery.downloadLocalDatabase();
|
|
61
|
+
recoveryMessage = "SQLite backup downloaded.";
|
|
62
|
+
} catch (error) {
|
|
63
|
+
recoveryMessage = error instanceof Error ? error.message : "SQLite could not be downloaded.";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function reset(): Promise<void> {
|
|
68
|
+
if (!confirmReset) {
|
|
69
|
+
confirmReset = true;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
recoveryBusy = true;
|
|
73
|
+
recoveryMessage = "Stopping synchronization and deleting local data…";
|
|
74
|
+
try {
|
|
75
|
+
const recovery = await getRecoveryModule();
|
|
76
|
+
await recovery.resetLocalDataFromLedger();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
recoveryMessage = error instanceof Error ? error.message : "Local data could not be reset.";
|
|
79
|
+
recoveryBusy = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onMount(() => {
|
|
84
|
+
window.dispatchEvent(new CustomEvent("stor:app-mounted"));
|
|
85
|
+
const updateOnlineState = () => (isOffline = !navigator.onLine);
|
|
86
|
+
updateOnlineState();
|
|
87
|
+
window.addEventListener("online", updateOnlineState);
|
|
88
|
+
window.addEventListener("offline", updateOnlineState);
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener("online", updateOnlineState);
|
|
91
|
+
window.removeEventListener("offline", updateOnlineState);
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
$effect(() => {
|
|
96
|
+
if (!localStoreFailed) return;
|
|
97
|
+
void getRecoveryModule()
|
|
98
|
+
.then((recovery) => recovery.getLocalRecoverySummary())
|
|
99
|
+
.then((summary) => (recoverySummary = summary));
|
|
100
|
+
});
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<svelte:head>
|
|
104
|
+
<title>{title} | stor</title>
|
|
105
|
+
</svelte:head>
|
|
106
|
+
|
|
107
|
+
<main class="flex min-h-screen items-center justify-center bg-[var(--stor-surface-muted)] px-4 py-12">
|
|
108
|
+
<section class="stor-panel w-full max-w-lg p-6 sm:p-8">
|
|
109
|
+
<a href="/" class="inline-flex items-center gap-2 font-mono font-semibold text-stone-950">
|
|
110
|
+
<span class="flex h-9 w-9 items-center justify-center rounded-xl bg-stone-950 text-lg font-black text-white">s</span>
|
|
111
|
+
stor
|
|
112
|
+
</a>
|
|
113
|
+
<p class="mt-8 font-mono text-[11px] font-semibold uppercase tracking-normal text-[var(--stor-muted-text)]">
|
|
114
|
+
{localStoreFailed ? "Local store" : error ? "Workspace unavailable" : `Error ${page.status}`}
|
|
115
|
+
</p>
|
|
116
|
+
<h1 class="mt-2 text-2xl font-semibold tracking-tight text-stone-950">{title}</h1>
|
|
117
|
+
<p class="mt-3 text-sm leading-6 text-stone-600">{description}</p>
|
|
118
|
+
<div class="mt-6 flex flex-wrap gap-2">
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class="inline-flex items-center justify-center rounded-xl bg-stone-950 px-4 py-2.5 text-sm font-semibold text-white hover:bg-stone-800"
|
|
122
|
+
onclick={() => void retry()}
|
|
123
|
+
disabled={recoveryBusy}
|
|
124
|
+
>
|
|
125
|
+
Retry
|
|
126
|
+
</button>
|
|
127
|
+
{#if !localStoreFailed}
|
|
128
|
+
<a
|
|
129
|
+
href="/"
|
|
130
|
+
class="inline-flex items-center justify-center rounded-xl bg-[var(--stor-surface-muted)] px-4 py-2.5 text-sm font-semibold text-stone-700 hover:bg-[var(--stor-surface-hover)]"
|
|
131
|
+
>
|
|
132
|
+
Go to start
|
|
133
|
+
</a>
|
|
134
|
+
{/if}
|
|
135
|
+
{#if localStoreFailed}
|
|
136
|
+
<button type="button" class="inline-flex items-center justify-center rounded-xl bg-[var(--stor-surface-muted)] px-4 py-2.5 text-sm font-semibold text-stone-700 hover:bg-[var(--stor-surface-hover)]" onclick={() => void download()} disabled={recoveryBusy}>Download SQLite</button>
|
|
137
|
+
<button type="button" class="inline-flex items-center justify-center rounded-xl border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-semibold text-red-700 hover:bg-red-100" onclick={() => void reset()} disabled={recoveryBusy}>{confirmReset ? "Confirm reset" : "Reset from Ledger"}</button>
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
{#if localStoreFailed}
|
|
141
|
+
<p class="mt-5 border-t border-[var(--stor-border)] pt-4 text-xs leading-5 text-[var(--stor-muted-text)]">
|
|
142
|
+
{recoverySummary.pending ?? "Unknown"} pending events · {recoverySummary.blocked ?? "Unknown"} blocked operations. Reset removes them from this device; remote events return through normal sync after reload.
|
|
143
|
+
</p>
|
|
144
|
+
{#if recoveryMessage}<p class="mt-3 text-xs font-medium text-stone-700">{recoveryMessage}</p>{/if}
|
|
145
|
+
{/if}
|
|
146
|
+
</section>
|
|
147
|
+
</main>
|