#206: show the VS-NfD marking in web view header and footer
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m24s
CI / Build container images (pull_request) Successful in 4m24s
CI / Auth e2e pack (pull_request) Successful in 8m44s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Build and push images (push) Successful in 26s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m30s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 6m10s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m55s
CI / Import/export fidelity gate (push) Failing after 50s
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m24s
CI / Build container images (pull_request) Successful in 4m24s
CI / Auth e2e pack (pull_request) Successful in 8m44s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Build and push images (push) Successful in 26s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m30s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 6m10s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m55s
CI / Import/export fidelity gate (push) Failing after 50s
ClassificationBanner renders the fixed ADR-0022 wording above and below the content in reading view, editor and public page view; unclassified pages show nothing. Announced to assistive tech via a localized hidden prefix (de+en); styled from the plain text token only, so contrast holds in both themes and under every accent with no new color pair. Public content endpoint now carries the classification. New seed fixture classified-note; a11y pack asserts banner top+bottom and axe-clean in light and dark. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
488d0d06f1
commit
adceca7358
@ -311,6 +311,36 @@ async function seedContentFixtures(ownerId: string): Promise<void> {
|
||||
deriveContentOf(everyElementDoc),
|
||||
);
|
||||
|
||||
// "Classified Note" (issue #206, ADR 0022): a VS-NfD-marked page so e2e
|
||||
// (a11y pack) can assert the marking banner in both themes. Kept simple —
|
||||
// the marking, not the content, is what the fixture exists for.
|
||||
const classifiedDoc = editorSchema.node('doc', null, [
|
||||
editorSchema.node('heading', { level: 1 }, [editorSchema.text('Classified Note')]),
|
||||
editorSchema.node('paragraph', null, [
|
||||
editorSchema.text('This fixture page carries the VS-NfD marking.'),
|
||||
]),
|
||||
]);
|
||||
const classifiedYdoc = new Y.Doc();
|
||||
prosemirrorJSONToYXmlFragment(
|
||||
editorSchema,
|
||||
classifiedDoc.toJSON(),
|
||||
classifiedYdoc.getXmlFragment('default'),
|
||||
);
|
||||
const classifiedState = new Uint8Array(Y.encodeStateAsUpdate(classifiedYdoc));
|
||||
classifiedYdoc.destroy();
|
||||
const classifiedPageId = await upsertFixturePage(
|
||||
pond.id,
|
||||
'classified-note',
|
||||
'Classified Note',
|
||||
ownerId,
|
||||
classifiedState,
|
||||
deriveContentOf(classifiedDoc),
|
||||
);
|
||||
await prisma.page.update({
|
||||
where: { id: classifiedPageId },
|
||||
data: { classification: 'VS_NFD' },
|
||||
});
|
||||
|
||||
// "Fixture Image": one real, servable uploaded image (the Markdown
|
||||
// fixture above only carries a placeholder fileId for round-trip
|
||||
// testing — this is the one that actually resolves via /media/:fileId).
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import type { PageCommentsView } from '@dorfteich/shared';
|
||||
import { Pond, User } from '@prisma/client';
|
||||
import type { PageClassification, PageCommentsView } from '@dorfteich/shared';
|
||||
import { Page, Pond, User } from '@prisma/client';
|
||||
|
||||
import { CommentsService } from '../comments/comments.service';
|
||||
import { TasksService } from '../pages/tasks.service';
|
||||
@ -18,12 +18,21 @@ export interface PublicPageContent {
|
||||
slug: string;
|
||||
/** Pre-rendered body HTML from the content cache (issue #24). */
|
||||
html: string;
|
||||
/** VS-NfD marking level (ADR 0022, issue #206) — the read view renders it
|
||||
* above and below the content. */
|
||||
classification: PageClassification;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ResolvedPage {
|
||||
pond: Pond;
|
||||
page: { id: string; pondId: string; slug: string; title: string };
|
||||
page: {
|
||||
id: string;
|
||||
pondId: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
classification: Page['classification'];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,7 +63,7 @@ export class PublicService {
|
||||
if (!pond) throw new NotFoundException();
|
||||
const page = await this.prisma.page.findFirst({
|
||||
where: { pondId: pond.id, slug: pageSlug, deletedAt: null },
|
||||
select: { id: true, pondId: true, slug: true, title: true },
|
||||
select: { id: true, pondId: true, slug: true, title: true, classification: true },
|
||||
});
|
||||
// Hide existence: no read access (incl. anonymous without a public grant) → 404.
|
||||
if (!page || !(await this.permissions.canAccessPage(user, page, 'read'))) {
|
||||
@ -80,6 +89,7 @@ export class PublicService {
|
||||
title: page.title,
|
||||
slug: page.slug,
|
||||
html: styleTag + resolveMediaUrls(body),
|
||||
classification: page.classification.toLowerCase() as PageClassification,
|
||||
updatedAt: (cache?.updatedAt ?? new Date()).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -63,6 +63,24 @@ for (const scheme of SCHEMES) {
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test(`a classified page shows the marking top+bottom and passes axe (${scheme})`, async ({
|
||||
browser,
|
||||
}) => {
|
||||
const context = await contextForUser(browser, BASE, 'fixture-user');
|
||||
const page = await context.newPage();
|
||||
await page.emulateMedia({ colorScheme: scheme });
|
||||
await page.goto('/p/content-fixtures/classified-note');
|
||||
await page.waitForLoadState('networkidle');
|
||||
// Kennzeichnung oben UND unten (issue #206, ADR 0022) — fester
|
||||
// Wortlaut, nicht lokalisiert.
|
||||
const banners = page.locator('.classification-banner');
|
||||
await expect(banners).toHaveCount(2);
|
||||
await expect(banners.first()).toContainText('VS – NUR FÜR DEN DIENSTGEBRAUCH');
|
||||
await expect(banners.last()).toContainText('VS – NUR FÜR DEN DIENSTGEBRAUCH');
|
||||
await expectClean(page, `Eingestufte Seite classified-note (${scheme})`);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test(`user settings pass the axe WCAG A/AA scan (${scheme})`, async ({ browser }) => {
|
||||
const context = await contextForUser(browser, BASE, 'fixture-user');
|
||||
const page = await context.newPage();
|
||||
|
||||
26
apps/web/src/components/ClassificationBanner.tsx
Normal file
26
apps/web/src/components/ClassificationBanner.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { PageClassification, classificationMarking } from '@dorfteich/shared';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* The VS-NfD marking of a page (issue #206, ADR 0022), rendered at the top
|
||||
* AND bottom of every page representation. The wording comes solely from
|
||||
* `classificationMarking()` and is deliberately not translated (a marking
|
||||
* is a fixed formula); only the assistive-tech prefix naming the element is
|
||||
* localized. Unclassified pages render nothing — marking everything trains
|
||||
* users to ignore markings (ADR 0022).
|
||||
*/
|
||||
export function ClassificationBanner({
|
||||
classification,
|
||||
}: {
|
||||
classification: PageClassification | undefined;
|
||||
}): React.JSX.Element | null {
|
||||
const { t } = useTranslation('common');
|
||||
const marking = classification ? classificationMarking(classification) : null;
|
||||
if (!marking) return null;
|
||||
return (
|
||||
<p className="classification-banner">
|
||||
<span className="visually-hidden">{t('classification.label')}: </span>
|
||||
{marking}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@ -12,6 +12,7 @@ import * as Y from 'yjs';
|
||||
|
||||
import { useAuth } from '../auth/auth-context';
|
||||
import { CommentsSection } from '../comments/CommentsSection';
|
||||
import { ClassificationBanner } from '../components/ClassificationBanner';
|
||||
import { FormError } from '../components/forms';
|
||||
import { useToast } from '../components/Toast';
|
||||
import { AccessRevokedDialog } from '../editor/AccessRevokedDialog';
|
||||
@ -563,6 +564,9 @@ export function PageEditorPage(): React.JSX.Element {
|
||||
actionsSlot.element,
|
||||
)}
|
||||
<div className="editor-page">
|
||||
{/* VS-NfD marking above and below the content (issue #206, ADR
|
||||
0022) — in reading AND edit mode; unclassified pages show none. */}
|
||||
<ClassificationBanner classification={page.data?.classification} />
|
||||
<div className="editor-page__header">
|
||||
{/* The visible title is an input; give assistive tech the page
|
||||
heading it expects on an article view (#166). */}
|
||||
@ -610,6 +614,7 @@ export function PageEditorPage(): React.JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ClassificationBanner classification={page.data?.classification} />
|
||||
{/* "Linked from" appears below the content in read mode (issue #48);
|
||||
the inline discussion (issue #133) and the local neighborhood graph
|
||||
(issue #113) follow it, in that order. */}
|
||||
|
||||
@ -3,7 +3,10 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { PageClassification } from '@dorfteich/shared';
|
||||
|
||||
import { PublicComments } from '../comments/CommentsSection';
|
||||
import { ClassificationBanner } from '../components/ClassificationBanner';
|
||||
import { ApiError, apiGet } from '../lib/api';
|
||||
import { useDocumentTitle } from '../lib/use-document-title';
|
||||
import { countWords, htmlToText } from '../lib/word-count';
|
||||
@ -16,6 +19,7 @@ interface PublicPageContent {
|
||||
title: string;
|
||||
slug: string;
|
||||
html: string;
|
||||
classification: PageClassification;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@ -51,6 +55,8 @@ export function PublicPageView(): React.JSX.Element {
|
||||
const page = query.data;
|
||||
return (
|
||||
<article className="public-page">
|
||||
{/* VS-NfD marking above and below the content (issue #206, ADR 0022). */}
|
||||
<ClassificationBanner classification={page.classification} />
|
||||
<p className="public-page__badge">{t('readOnlyBadge')}</p>
|
||||
<p className="public-page__pond">{page.pondName}</p>
|
||||
<h1 className="public-page__title">{page.title}</h1>
|
||||
@ -58,6 +64,7 @@ export function PublicPageView(): React.JSX.Element {
|
||||
{/* The HTML comes from the server's content cache (issue #24), derived
|
||||
from the sanitized editor schema — safe to render. */}
|
||||
<div className="public-page__body" dangerouslySetInnerHTML={{ __html: page.html }} />
|
||||
<ClassificationBanner classification={page.classification} />
|
||||
{/* Existing comments, read-only for anonymous visitors (issue #133). */}
|
||||
<PublicComments pondSlug={pondSlug} pageSlug={pageSlug} />
|
||||
</article>
|
||||
|
||||
@ -1476,6 +1476,22 @@ button {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* VS-NfD classification marking (issue #206, ADR 0022): rendered at the top
|
||||
and bottom of every page representation. Deliberately built from the
|
||||
normal text token only — full contrast in both themes and under every
|
||||
accent, with no new color pair for the contrast fence. */
|
||||
.classification-banner {
|
||||
margin: var(--space-2) 0;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-top: 2px solid currentColor;
|
||||
border-bottom: 2px solid currentColor;
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.editor-page__body {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
|
||||
@ -52,13 +52,13 @@ _Meilenstein: `M26 — VS-NfD: classification metadata`_
|
||||
Instance-Setting · 2 AT · #204
|
||||
- [x] Vererbung im Seitenbaum, Herabstufung nur mit eigenem Recht + Audit · 3 AT · #205
|
||||
- [ ] Durchreichen in alle Ausgabekanäle · 8–12 AT · #206–#212
|
||||
- Web-Ansicht (Kopf/Fuß) · 1 AT · #206
|
||||
- **Print-CSS** (`@media print`, Kopf/Fuß je Seite) — fehlt komplett · 1 AT · #207
|
||||
- PDF via gotenberg (`pdf-html.ts` Header/Footer-Template) · 1 AT · #208
|
||||
- DOCX/ODT via pandoc (Reference-Doc mit Kopf-/Fußzeile) · 2–3 AT · #209
|
||||
- Markdown-ZIP (Frontmatter + Aufdruck) · 1 AT · #210
|
||||
- Atom-Feeds, Public-API, Suchergebnisse, No-JS-Shell · 2–3 AT · #211
|
||||
- Attachment-Download (Dateiname-Präfix + Begleitdatei) · 1–2 AT · #212
|
||||
- [x] Web-Ansicht (Kopf/Fuß) · 1 AT · #206
|
||||
- [ ] **Print-CSS** (`@media print`, Kopf/Fuß je Seite) — fehlt komplett · 1 AT · #207
|
||||
- [ ] PDF via gotenberg (`pdf-html.ts` Header/Footer-Template) · 1 AT · #208
|
||||
- [ ] DOCX/ODT via pandoc (Reference-Doc mit Kopf-/Fußzeile) · 2–3 AT · #209
|
||||
- [ ] Markdown-ZIP (Frontmatter + Aufdruck) · 1 AT · #210
|
||||
- [ ] Atom-Feeds, Public-API, Suchergebnisse, No-JS-Shell · 2–3 AT · #211
|
||||
- [ ] Attachment-Download (Dateiname-Präfix + Begleitdatei) · 1–2 AT · #212
|
||||
- [ ] Warnung/Sperre beim Anhängen an eingestufte Seiten · 1 AT · #213
|
||||
|
||||
### P1-3 Verifizierter Offline-/Airgap-Pfad · 8–10 AT ⟵ neu aus Roadmap
|
||||
|
||||
@ -97,5 +97,8 @@
|
||||
"settingsNav": {
|
||||
"label": "Abschnitte"
|
||||
},
|
||||
"tableActions": "Aktionen"
|
||||
"tableActions": "Aktionen",
|
||||
"classification": {
|
||||
"label": "Einstufung"
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,5 +97,8 @@
|
||||
"settingsNav": {
|
||||
"label": "Sections"
|
||||
},
|
||||
"tableActions": "Actions"
|
||||
"tableActions": "Actions",
|
||||
"classification": {
|
||||
"label": "Classification"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user