Compare commits
6 Commits
80d77a53e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
643d504c7a | ||
|
|
c9f5f6d665 | ||
|
|
3f3d5e530c | ||
|
|
5dac1d993c | ||
|
|
264d60c855 | ||
|
|
e6a0c2f6d6 |
@@ -201,7 +201,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- archiv-net
|
- archiv-net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/actuator/health | grep -q UP || exit 1"]
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/actuator/health | grep -q UP || exit 1"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# Used by docker-compose.yml (target: development). Source is bind-mounted in
|
# Used by docker-compose.yml (target: development). Source is bind-mounted in
|
||||||
# dev so the COPY . below is effectively replaced at runtime; the layer still
|
# dev so the COPY . below is effectively replaced at runtime; the layer still
|
||||||
# exists so the image is self-contained for cold starts (e.g. devcontainer).
|
# exists so the image is self-contained for cold starts (e.g. devcontainer).
|
||||||
FROM node:20.19.0-alpine3.21 AS development
|
FROM node:22-alpine3.21 AS development
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
@@ -14,7 +14,7 @@ CMD ["npm", "run", "dev"]
|
|||||||
|
|
||||||
# ── Build ────────────────────────────────────────────────────────────────────
|
# ── Build ────────────────────────────────────────────────────────────────────
|
||||||
# Compiles the SvelteKit Node-adapter output to /app/build.
|
# Compiles the SvelteKit Node-adapter output to /app/build.
|
||||||
FROM node:20.19.0-alpine3.21 AS build
|
FROM node:22-alpine3.21 AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# VITE_SENTRY_DSN is a build-time variable — Vite bakes it into the bundle.
|
# VITE_SENTRY_DSN is a build-time variable — Vite bakes it into the bundle.
|
||||||
# Passed via docker-compose build.args; empty string disables the SDK.
|
# Passed via docker-compose build.args; empty string disables the SDK.
|
||||||
@@ -27,7 +27,7 @@ RUN npm run build
|
|||||||
|
|
||||||
# ── Production ───────────────────────────────────────────────────────────────
|
# ── Production ───────────────────────────────────────────────────────────────
|
||||||
# Self-contained Node server. `node build` is the adapter-node entrypoint.
|
# Self-contained Node server. `node build` is the adapter-node entrypoint.
|
||||||
FROM node:20.19.0-alpine3.21 AS production
|
FROM node:22-alpine3.21 AS production
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
COPY --from=build /app/build ./build
|
COPY --from=build /app/build ./build
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ interface Props {
|
|||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_VISIBLE_TAGS = 6;
|
||||||
|
|
||||||
const { tags, compact = false }: Props = $props();
|
const { tags, compact = false }: Props = $props();
|
||||||
|
|
||||||
const visibleTags = $derived.by(() => tags.filter(hasAnyDocuments));
|
const visibleTags = $derived.by(() => tags.filter(hasAnyDocuments));
|
||||||
|
const shownTags = $derived(visibleTags.slice(0, MAX_VISIBLE_TAGS));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rounded-sm border border-line bg-surface p-5 shadow-sm">
|
<section class="rounded-sm border border-line bg-surface p-5 shadow-sm">
|
||||||
@@ -22,7 +25,7 @@ const visibleTags = $derived.by(() => tags.filter(hasAnyDocuments));
|
|||||||
</h2>
|
</h2>
|
||||||
<a
|
<a
|
||||||
href="/themen"
|
href="/themen"
|
||||||
class="font-sans text-xs text-brand-mint underline-offset-2 hover:underline focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none"
|
class="flex min-h-[44px] items-center text-[11px] font-semibold text-ink-2 no-underline focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none"
|
||||||
>
|
>
|
||||||
{m.themen_alle()} →
|
{m.themen_alle()} →
|
||||||
</a>
|
</a>
|
||||||
@@ -35,9 +38,9 @@ const visibleTags = $derived.by(() => tags.filter(hasAnyDocuments));
|
|||||||
class="grid gap-2 {compact ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}"
|
class="grid gap-2 {compact ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}"
|
||||||
data-compact={compact}
|
data-compact={compact}
|
||||||
>
|
>
|
||||||
{#each visibleTags as tag (tag.id)}
|
{#each shownTags as tag (tag.id)}
|
||||||
<a
|
<a
|
||||||
href="/?tag={encodeURIComponent(tag.name)}"
|
href="/documents?tag={encodeURIComponent(tag.name)}"
|
||||||
aria-label="{tag.name}{tag.documentCount > 0
|
aria-label="{tag.name}{tag.documentCount > 0
|
||||||
? ', ' + m.themen_dokumente({ count: tag.documentCount })
|
? ', ' + m.themen_dokumente({ count: tag.documentCount })
|
||||||
: ''}"
|
: ''}"
|
||||||
|
|||||||
@@ -59,37 +59,40 @@ const greetingText = $derived.by(() => {
|
|||||||
<h1 class="font-serif text-[2rem] text-ink">{greetingText}</h1>
|
<h1 class="font-serif text-[2rem] text-ink">{greetingText}</h1>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[1fr_320px] lg:items-start">
|
<div class="flex flex-col gap-5">
|
||||||
<div class="flex flex-col gap-5">
|
<DashboardResumeStrip resumeDoc={data.resumeDoc ?? null} />
|
||||||
<DashboardResumeStrip resumeDoc={data.resumeDoc ?? null} />
|
|
||||||
|
|
||||||
<EnrichmentBlock
|
<ThemenWidget tags={data.tagTree ?? []} />
|
||||||
topDocs={data.incompleteDocs ?? []}
|
|
||||||
totalCount={data.incompleteTotal ?? 0}
|
|
||||||
bannerCount={bannerCount}
|
|
||||||
onBannerClose={() => (bannerCount = 0)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section aria-label={m.dashboard_mission_caption()}>
|
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[1fr_320px] lg:items-start">
|
||||||
<h2 class="mb-3 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
<div class="flex flex-col gap-5">
|
||||||
{m.dashboard_mission_caption()}
|
<EnrichmentBlock
|
||||||
</h2>
|
topDocs={data.incompleteDocs ?? []}
|
||||||
<MissionControlStrip
|
totalCount={data.incompleteTotal ?? 0}
|
||||||
segmentationDocs={data.segmentationDocs ?? []}
|
bannerCount={bannerCount}
|
||||||
transcriptionDocs={data.transcriptionDocs ?? []}
|
onBannerClose={() => (bannerCount = 0)}
|
||||||
readyDocs={data.readyDocs ?? []}
|
|
||||||
weeklyStats={data.weeklyStats ?? null}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-5 lg:sticky lg:top-[80px]">
|
<section aria-label={m.dashboard_mission_caption()}>
|
||||||
<DashboardFamilyPulse pulse={data.pulse ?? null} />
|
<h2 class="mb-3 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||||
<ThemenWidget tags={data.tagTree ?? []} compact={true} />
|
{m.dashboard_mission_caption()}
|
||||||
<DashboardActivityFeed feed={data.activityFeed ?? []} />
|
</h2>
|
||||||
{#if data.canWrite}
|
<MissionControlStrip
|
||||||
<DropZone onUploadComplete={(count) => (bannerCount = count)} />
|
segmentationDocs={data.segmentationDocs ?? []}
|
||||||
{/if}
|
transcriptionDocs={data.transcriptionDocs ?? []}
|
||||||
|
readyDocs={data.readyDocs ?? []}
|
||||||
|
weeklyStats={data.weeklyStats ?? null}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-5 lg:sticky lg:top-[80px]">
|
||||||
|
<DashboardFamilyPulse pulse={data.pulse ?? null} />
|
||||||
|
<DashboardActivityFeed feed={data.activityFeed ?? []} />
|
||||||
|
{#if data.canWrite}
|
||||||
|
<DropZone onUploadComplete={(count) => (bannerCount = count)} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -421,7 +421,8 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate
|
|||||||
response: { ok: true },
|
response: { ok: true },
|
||||||
data: { items: [searchItem], totalElements: 1, pageNumber: 0, pageSize: 5, totalPages: 1 }
|
data: { items: [searchItem], totalElements: 1, pageNumber: 0, pageSize: 5, totalPages: 1 }
|
||||||
}) // search
|
}) // search
|
||||||
.mockResolvedValueOnce({ response: { ok: true }, data: [] }); // stories
|
.mockResolvedValueOnce({ response: { ok: true }, data: [] }) // stories
|
||||||
|
.mockResolvedValueOnce({ response: { ok: true }, data: [] }); // tags/tree
|
||||||
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
||||||
typeof createApiClient
|
typeof createApiClient
|
||||||
>);
|
>);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const visibleTree = $derived.by(() => data.tree.filter(hasAnyDocuments));
|
|||||||
></div>
|
></div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/?tag={encodeURIComponent(tag.name)}"
|
href="/documents?tag={encodeURIComponent(tag.name)}"
|
||||||
aria-label="{tag.name}{tag.documentCount > 0
|
aria-label="{tag.name}{tag.documentCount > 0
|
||||||
? ', ' + m.themen_dokumente({ count: tag.documentCount })
|
? ', ' + m.themen_dokumente({ count: tag.documentCount })
|
||||||
: ''}"
|
: ''}"
|
||||||
@@ -58,7 +58,7 @@ const visibleTree = $derived.by(() => data.tree.filter(hasAnyDocuments));
|
|||||||
|
|
||||||
{#each shownChildren as child (child.id)}
|
{#each shownChildren as child (child.id)}
|
||||||
<a
|
<a
|
||||||
href="/?tag={encodeURIComponent(child.name)}"
|
href="/documents?tag={encodeURIComponent(child.name)}"
|
||||||
class="flex min-h-[44px] items-center justify-between px-4 py-2.5 hover:bg-canvas focus-visible:bg-canvas focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none focus-visible:ring-inset"
|
class="flex min-h-[44px] items-center justify-between px-4 py-2.5 hover:bg-canvas focus-visible:bg-canvas focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none focus-visible:ring-inset"
|
||||||
>
|
>
|
||||||
<span class="font-sans text-sm text-ink">{child.name}</span>
|
<span class="font-sans text-sm text-ink">{child.name}</span>
|
||||||
@@ -71,7 +71,7 @@ const visibleTree = $derived.by(() => data.tree.filter(hasAnyDocuments));
|
|||||||
|
|
||||||
{#if hiddenCount > 0}
|
{#if hiddenCount > 0}
|
||||||
<a
|
<a
|
||||||
href="/?tag={encodeURIComponent(tag.name)}"
|
href="/documents?tag={encodeURIComponent(tag.name)}"
|
||||||
class="block min-h-[44px] px-4 py-2.5 font-sans text-sm text-ink-3 hover:bg-canvas hover:text-ink focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none focus-visible:ring-inset"
|
class="block min-h-[44px] px-4 py-2.5 font-sans text-sm text-ink-3 hover:bg-canvas hover:text-ink focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none focus-visible:ring-inset"
|
||||||
>
|
>
|
||||||
{m.themen_weitere({ count: hiddenCount })} →
|
{m.themen_weitere({ count: hiddenCount })} →
|
||||||
|
|||||||
Reference in New Issue
Block a user