diff --git a/.changeset/overview-preview-connections.md b/.changeset/overview-preview-connections.md
new file mode 100644
index 00000000..844ebc3a
--- /dev/null
+++ b/.changeset/overview-preview-connections.md
@@ -0,0 +1,5 @@
+---
+"@luzzle/web": patch
+---
+
+Connect Published and Preview overviews and offer fresh saved-source previews when a preview fails or expires.
diff --git a/apps/web/src/lib/pieces/viewer/OverviewActions.svelte b/apps/web/src/lib/pieces/viewer/OverviewActions.svelte
new file mode 100644
index 00000000..63c401e8
--- /dev/null
+++ b/apps/web/src/lib/pieces/viewer/OverviewActions.svelte
@@ -0,0 +1,76 @@
+
+
+{#if preview}
+
+{/if}
+{#if hasPublishedVersion}
+ View published
+{/if}
+
+
diff --git a/apps/web/src/lib/pieces/viewer/PieceOverview.svelte b/apps/web/src/lib/pieces/viewer/PieceOverview.svelte
index f8a16095..054daeec 100644
--- a/apps/web/src/lib/pieces/viewer/PieceOverview.svelte
+++ b/apps/web/src/lib/pieces/viewer/PieceOverview.svelte
@@ -67,7 +67,7 @@
padding: var(--space-2) var(--space-3);
border-top: 1px solid var(--color-outline-variant);
text-align: left;
- vertical-align: top;
+ vertical-align: middle;
overflow-wrap: anywhere;
}
.overview :global(.overview-facts th) {
@@ -112,7 +112,7 @@
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
border-top: 1px solid var(--color-outline-variant);
padding: 0;
- align-items: start;
+ align-items: center;
}
.overview :global(.overview-facts th),
.overview :global(.overview-facts td) {
diff --git a/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/+layout.server.ts b/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/+layout.server.ts
index 5ce43cc5..64e1471d 100644
--- a/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/+layout.server.ts
+++ b/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/+layout.server.ts
@@ -1,6 +1,9 @@
import type { LayoutServerLoad } from './$types'
+import { db } from '#lib/server/database/index.js'
+import { getWebPiece } from '#lib/server/pieces.js'
-export const load: LayoutServerLoad = ({ locals }) => {
+export const load: LayoutServerLoad = async ({ locals, params }) => {
locals.inlineConfiguredStyles()
- return {}
+ const published = await getWebPiece(db, params.path)
+ return { hasPublishedVersion: !!published }
}
diff --git a/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/+layout.svelte b/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/+layout.svelte
index 7e00d98f..791dfeff 100644
--- a/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/+layout.svelte
+++ b/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/+layout.svelte
@@ -146,6 +146,7 @@
undefined}>
import type { ProgressPhase } from '#lib/components/progress.js'
import { formatDuration, previewDuration } from './duration.js'
+ import OverviewActions from '#lib/pieces/viewer/OverviewActions.svelte'
interface Props {
file: string
type?: string
status: string
+ hasPublishedVersion?: boolean
streamDisconnected?: boolean
createdAt: string | null
finishedAt: string | null
@@ -16,6 +18,7 @@
file,
type,
status,
+ hasPublishedVersion = false,
streamDisconnected = false,
createdAt,
finishedAt,
@@ -84,9 +87,7 @@
{#if !streamDisconnected && (status === 'enqueued' || status === 'waiting')}
Waiting for worker…
{:else if !streamDisconnected && status === 'expired'}
-
- Assets are purged after 2 days. Re-run the preview from the editor.
-
+ Assets are purged after 2 days.
{/if}
@@ -131,6 +132,18 @@
{/if}
+ {#if hasPublishedVersion}
+
+ | Publication |
+ |
+
+ {/if}
+ {#if status === 'failed' || status === 'expired'}
+
+ | Preview |
+ |
+
+ {/if}
diff --git a/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/PreviewFacts.test.ts b/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/PreviewFacts.test.ts
index 2418828e..653af745 100644
--- a/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/PreviewFacts.test.ts
+++ b/apps/web/src/routes/(auth)/admin/piece/[...path]/preview/[jobId]/PreviewFacts.test.ts
@@ -1,10 +1,15 @@
-import { readFileSync } from 'node:fs'
-import { expect, test } from 'vitest'
+import { expect, test, vi } from 'vitest'
import { render } from 'svelte/server'
import type { ComponentProps } from 'svelte'
import type { ProgressPhase } from '#lib/components/progress.js'
import PreviewFacts from './PreviewFacts.svelte'
+vi.mock('$app/forms', () => ({ enhance: vi.fn() }))
+vi.mock('$app/paths', () => ({
+ resolve: (route: string, params: { path: string }) =>
+ route.replace('/(auth)', '').replace('[...path]', () => params.path)
+}))
+
function facts(overrides: Partial> = {}) {
return render(PreviewFacts, {
props: {
@@ -45,52 +50,6 @@ function field(html: string, name: string) {
return rows(html).find(([label]) => label === name)?.[1]
}
-test('fixed UI copy is literal markup rather than JavaScript label maps', () => {
- const source = readFileSync(new URL('./PreviewFacts.svelte', import.meta.url), 'utf8')
- const markup = source
- .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/g, '')
- .replace(/\{[^}]*\}/g, '')
- .replace(/\s+/g, ' ')
-
- expect(source).not.toMatch(/\b(?:statusLabels|durationLabels)\b|\bnew\s+Map\s*\(/)
- for (const label of [
- 'Preview',
- 'File',
- 'Type',
- 'State',
- 'Duration',
- 'Created',
- 'Finished',
- 'Unknown',
- 'Queued',
- 'Building',
- 'Completed',
- 'Failed',
- 'Expired',
- 'Refresh to reconnect',
- 'Waiting for worker…',
- 'Assets are purged after 2 days. Re-run the preview from the editor.',
- 'elapsed',
- 'total time',
- 'stopped at',
- 'UTC'
- ])
- expect(markup).toContain(label)
-})
-
-test('facts have no state icon imports, markup, classification, motion, or local badge styles', () => {
- const source = readFileSync(new URL('./PreviewFacts.svelte', import.meta.url), 'utf8')
- const style = source.match(/