}
{report.integrity?.verification_status && report.integrity.verification_status !== "valid" && (
! {t("evaluator.validationError")}
@@ -2167,6 +2223,7 @@ function ReportDetailSections({ report, followups }: { report: Report; followups
return (
+ {report.demo && }
{hasVerificationWarning && (
+ {followup.demo && }
{hasVerificationWarning && (
{
expect(profiles[0].reports).toHaveLength(1);
expect(profiles[0].evaluator_notes).toHaveLength(0);
});
+
+ it("varsler og lagrer markering når import inneholder demodata", () => {
+ const demoReport = { ...report, id: "demo-arbeidspuls-evaluator", demo: true };
+ const result = importExportPayload(
+ JSON.stringify({ ...payload([demoReport]), data_mode: "demo", contains_demo_data: true }),
+ "Demo"
+ );
+
+ expect(result.demo_entries_detected).toBe(1);
+ expect(result.profile.contains_demo_data).toBe(true);
+ expect(listEvaluatorProfiles()[0].contains_demo_data).toBe(true);
+ });
});
diff --git a/src/evaluatorStorage.ts b/src/evaluatorStorage.ts
index 3d92f8d..9aaa11e 100644
--- a/src/evaluatorStorage.ts
+++ b/src/evaluatorStorage.ts
@@ -9,6 +9,7 @@ export type ImportResult = {
added: number;
skipped: number;
verification: ReturnType;
+ demo_entries_detected: number;
target_resolution: ImportTargetResolution;
matched_person_id: string | null;
matched_report_ids: string[];
@@ -89,7 +90,9 @@ export function deleteEvaluatorNote(
}
export function importExportPayload(jsonText: string, displayName: string, targetProfileId?: string): ImportResult {
- const reports = extractReports(JSON.parse(jsonText)).map(verifyEntryIntegrity);
+ const parsed = JSON.parse(jsonText);
+ const reports = extractReports(parsed).map(verifyEntryIntegrity);
+ const demoEntriesDetected = countDemoEntries(reports, parsed);
const profiles = readProfiles().map(normalizeProfile);
const fingerprint = makeFingerprint(reports);
const target = resolveImportTarget(reports, profiles, targetProfileId);
@@ -124,6 +127,7 @@ export function importExportPayload(jsonText: string, displayName: string, targe
display_name: targetProfile.display_name,
updated_at: now,
source_fingerprint: targetProfile.source_fingerprint ?? fingerprint,
+ contains_demo_data: targetProfile.contains_demo_data || demoEntriesDetected > 0 || nextReports.some(isDemoEntry),
reports: nextReports.sort((a, b) => getEntryTime(b) - getEntryTime(a))
}
: {
@@ -132,6 +136,7 @@ export function importExportPayload(jsonText: string, displayName: string, targe
created_at: now,
updated_at: now,
source_fingerprint: fingerprint,
+ contains_demo_data: demoEntriesDetected > 0 || nextReports.some(isDemoEntry),
reports: nextReports.sort((a, b) => getEntryTime(b) - getEntryTime(a)),
evaluator_notes: []
};
@@ -142,6 +147,7 @@ export function importExportPayload(jsonText: string, displayName: string, targe
added,
skipped,
verification: summarizeVerification(reports),
+ demo_entries_detected: demoEntriesDetected,
target_resolution: targetProfile ? "matched_existing_person" : "created_new_person",
matched_person_id: targetProfile?.id ?? null,
matched_report_ids: target.matchedReportIds
@@ -228,9 +234,11 @@ function normalizeImportedEntry(entry: StoredEntry): StoredEntry {
}
function normalizeProfile(profile: EvaluatorProfile): EvaluatorProfile {
+ const reports = (profile.reports ?? []).map(normalizeImportedEntry).sort((a, b) => getEntryTime(b) - getEntryTime(a));
return {
...profile,
- reports: (profile.reports ?? []).map(normalizeImportedEntry).sort((a, b) => getEntryTime(b) - getEntryTime(a)),
+ reports,
+ contains_demo_data: profile.contains_demo_data || reports.some(isDemoEntry),
evaluator_notes: (profile.evaluator_notes ?? []).map((note) => ({
...note,
note_type: note.note_type ?? (note.related_report_id ? "report" : note.related_followup_id ? "followup" : "person")
@@ -238,6 +246,19 @@ function normalizeProfile(profile: EvaluatorProfile): EvaluatorProfile {
};
}
+function isDemoEntry(entry: Partial) {
+ return entry.demo === true || typeof entry.id === "string" && entry.id.startsWith("demo-arbeidspuls");
+}
+
+function countDemoEntries(entries: StoredEntry[], payload: unknown) {
+ const payloadHasDemoFlag =
+ !Array.isArray(payload) &&
+ typeof payload === "object" &&
+ payload !== null &&
+ ((payload as ExportPayload).data_mode === "demo" || (payload as ExportPayload).contains_demo_data === true);
+ return payloadHasDemoFlag && entries.length > 0 ? Math.max(1, entries.filter(isDemoEntry).length) : entries.filter(isDemoEntry).length;
+}
+
function upsertNoteInProfile(
profile: EvaluatorProfile,
text: string,
diff --git a/src/i18n.tsx b/src/i18n.tsx
index 7409dc1..a1f23fe 100644
--- a/src/i18n.tsx
+++ b/src/i18n.tsx
@@ -57,6 +57,12 @@ const nb = {
overview: "Oversikt",
privacy: "Personvern"
},
+ demo: {
+ enter: "Demo",
+ exit: "Forlat demo",
+ label: "Demodata",
+ notice: "Du er i demo-modus. Data her er adskilt fra dine faktiske rapporter, og eksport/import gjelder bare demodata."
+ },
footer: {
norwegian: "Norsk",
english: "English",
@@ -139,6 +145,7 @@ const nb = {
},
privacy: {
warning: "Rapportene kan inneholde sensitive helseopplysninger og lagres bare lokalt i denne nettleseren.",
+ demoWarning: "Demo-modus bruker egen lokal lagring. Disse rapportene er testdata og blandes ikke med faktiske rapporter.",
exportJson: "Eksporter JSON",
importBackup: "Importer sikkerhetskopi",
deleteAll: "Slett alle lokale data",
@@ -146,6 +153,7 @@ const nb = {
importFrom: "Importer data fra {fileName}",
importStats:
"Nye rapporter: {reports}. Nye oppfølginger: {followups}. Duplikater hoppet over: {duplicates}. Konflikter: {conflicts}.",
+ demoImportWarning: "Importen inneholder {count} demodata-oppføringer og kan bare importeres i demo-modus.",
cancelImport: "Avbryt import",
importData: "Importer data",
baseCopy:
@@ -179,6 +187,7 @@ const nb = {
exportBackup: "Eksporter veilederdata",
importBackup: "Importer veilederbackup",
importedBackup: "Veilederbackup er importert.",
+ demoDataWarning: "Denne importen inneholder demodata. Ikke bruk den som faktisk bruker-/helsegrunnlag.",
importStart: "Importer en JSON-fil for å starte gjennomgang.",
saveName: "Lagre navn",
deletePerson: "Slett bruker og rapporter",
@@ -219,6 +228,7 @@ const nb = {
technical: "Teknisk informasjon",
rawData: "Rådata",
integrity: "Integritetskontroll",
+ dataMode: "Datamodus",
time: "Tidspunkt",
workTime: "Arbeidstid",
status: "Status",
@@ -326,6 +336,12 @@ const en: TranslationTree = {
},
app: { eyebrow: "Local self-assessment", title: "Arbeidspuls", evaluator: "Guide", navLabel: "Main navigation" },
nav: { register: "Register", followup: "Follow-up", overview: "Overview", privacy: "Privacy" },
+ demo: {
+ enter: "Demo",
+ exit: "Exit demo",
+ label: "Demo data",
+ notice: "You are in demo mode. This data is separate from real reports, and export/import only applies to demo data."
+ },
footer: {
norwegian: "Norsk",
english: "English",
@@ -408,12 +424,14 @@ const en: TranslationTree = {
},
privacy: {
warning: "Reports may contain sensitive health information and are stored only locally in this browser.",
+ demoWarning: "Demo mode uses separate local storage. These reports are test data and are not mixed with real reports.",
exportJson: "Export JSON",
importBackup: "Import backup",
deleteAll: "Delete all local data",
importHelp: "Import previously exported JSON if you changed device or lost local data. Imported data is stored locally in this browser.",
importFrom: "Import data from {fileName}",
importStats: "New reports: {reports}. New follow-ups: {followups}. Duplicates skipped: {duplicates}. Conflicts: {conflicts}.",
+ demoImportWarning: "The import contains {count} demo data entries and can only be imported in demo mode.",
cancelImport: "Cancel import",
importData: "Import data",
baseCopy:
@@ -447,6 +465,7 @@ const en: TranslationTree = {
exportBackup: "Export guide data",
importBackup: "Import guide backup",
importedBackup: "Guide backup has been imported.",
+ demoDataWarning: "This import contains demo data. Do not use it as real user or health information.",
importStart: "Import a JSON file to start reviewing.",
saveName: "Save name",
deletePerson: "Delete user and reports",
@@ -487,6 +506,7 @@ const en: TranslationTree = {
technical: "Technical information",
rawData: "Raw data",
integrity: "Integrity check",
+ dataMode: "Data mode",
time: "Time",
workTime: "Work time",
status: "Status",
diff --git a/src/integrity.test.ts b/src/integrity.test.ts
index 7a072eb..d99a874 100644
--- a/src/integrity.test.ts
+++ b/src/integrity.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { attachIntegrity, buildCanonicalReportPayload, canonicalizeReportForIntegrity, createExportIntegrity, verifyEntryIntegrity } from "./integrity";
+import { demoEntries } from "./demoData";
import type { Report } from "./types";
const report: Report = {
@@ -104,6 +105,18 @@ describe("integritetskontroll", () => {
expect(verifyEntryIntegrity(withLocalFields).integrity?.verification_status).toBe("valid");
});
+ it("lar demodataflagg være metadata uten å endre rapportintegritet", () => {
+ const signed = attachIntegrity(report);
+
+ expect(verifyEntryIntegrity({ ...signed, demo: true }).integrity?.verification_status).toBe("valid");
+ });
+
+ it("validerer innebygde demorapporter mot medfølgende integritetsdata", () => {
+ expect(demoEntries.map((entry) => verifyEntryIntegrity(entry).integrity?.verification_status)).toEqual(
+ demoEntries.map(() => "valid")
+ );
+ });
+
it("validerer rapport med arbeidssted og legacy energidetaljer uten false modified", () => {
const raw = {
id: "aac87f66-2d39-4a6f-8169-7d39aa5fc0e6",
diff --git a/src/integrity.ts b/src/integrity.ts
index c66c01f..c5b6be3 100644
--- a/src/integrity.ts
+++ b/src/integrity.ts
@@ -20,7 +20,10 @@ const EXCLUDED_KEYS = new Set([
"ui_state",
"total_score_percent",
"score_band",
- "normalized_period_score"
+ "normalized_period_score",
+ "demo",
+ "data_mode",
+ "contains_demo_data"
]);
const ALIAS_FIELDS: Record = {
diff --git a/src/storage.test.ts b/src/storage.test.ts
index ef21a25..dfbf369 100644
--- a/src/storage.test.ts
+++ b/src/storage.test.ts
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { importReportsFromJson, listReports, previewImportReports } from "./storage";
+import { exportReports, importReportsFromJson, listReports, previewImportReports, setDemoModeEnabled } from "./storage";
import type { ExportPayload, Report } from "./types";
const store = new Map();
@@ -68,4 +68,24 @@ describe("ordinær brukerimport", () => {
expect(summary.conflicts_detected).toBe(1);
expect(summary.reports_added).toBe(0);
});
+
+ it("holder demodata adskilt fra normale rapporter og blokkerer normal import", () => {
+ setDemoModeEnabled(true);
+
+ expect(listReports(true).length).toBeGreaterThan(0);
+ expect(listReports()).toHaveLength(0);
+
+ const demoExport = exportReports(true);
+ expect(JSON.parse(demoExport).data_mode).toBe("demo");
+ expect(() => previewImportReports(demoExport)).toThrow("demodata");
+ });
+
+ it("tillater import av demodata i demo-modus", () => {
+ const demoReport = { ...report, id: "demo-arbeidspuls-test", demo: true };
+ const summary = importReportsFromJson(JSON.stringify({ ...payload([demoReport]), data_mode: "demo", contains_demo_data: true }), true);
+
+ expect(summary.demo_entries_detected).toBe(1);
+ expect(listReports(true).some((entry) => entry.id === "demo-arbeidspuls-test" && entry.demo)).toBe(true);
+ expect(listReports()).toHaveLength(0);
+ });
});
diff --git a/src/storage.ts b/src/storage.ts
index f761e88..4e46eca 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -1,24 +1,59 @@
import { calculateReportScore, getEntryTime } from "./scoring";
import { attachIntegrity, createExportIntegrity, summarizeVerification, verifyEntryIntegrity } from "./integrity";
+import { demoEntries } from "./demoData";
import type { DelayedFollowup, ExportPayload, FollowupDraft, ImportSummary, Report, ReportDraft, StoredEntry } from "./types";
const STORAGE_KEY = "arbeidsevne-egenvurdering:entries";
+const DEMO_STORAGE_KEY = "arbeidspuls:demo-entries";
+const DEMO_MODE_KEY = "arbeidspuls:demo-mode";
+const DEMO_SEEDED_KEY = "arbeidspuls:demo-seeded";
-function readEntries(): StoredEntry[] {
- const raw = localStorage.getItem(STORAGE_KEY);
+export type DataMode = "normal" | "demo";
+
+function storageKey(mode: DataMode) {
+ return mode === "demo" ? DEMO_STORAGE_KEY : STORAGE_KEY;
+}
+
+function modeFromDemoFlag(demoMode = false): DataMode {
+ return demoMode ? "demo" : "normal";
+}
+
+export function isDemoModeEnabled() {
+ return localStorage.getItem(DEMO_MODE_KEY) === "true";
+}
+
+export function setDemoModeEnabled(enabled: boolean) {
+ localStorage.setItem(DEMO_MODE_KEY, enabled ? "true" : "false");
+ if (enabled) ensureDemoData();
+}
+
+export function ensureDemoData() {
+ const existing = readEntries("demo");
+ if (existing.length > 0) return existing;
+ if (localStorage.getItem(DEMO_SEEDED_KEY) === "true") return [];
+ const seeded = demoEntries.map((entry) => markDemoEntry(normalizeStoredEntry(entry)));
+ writeEntries(seeded, "demo");
+ localStorage.setItem(DEMO_SEEDED_KEY, "true");
+ return seeded;
+}
+
+function readEntries(mode: DataMode = "normal"): StoredEntry[] {
+ const raw = localStorage.getItem(storageKey(mode));
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
- const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.reports) ? parsed.reports : [];
- return entries.map(normalizeStoredEntry);
+ const candidate = parsed && typeof parsed === "object" ? (parsed as { reports?: unknown }).reports : undefined;
+ const entries: StoredEntry[] = Array.isArray(parsed) ? parsed : Array.isArray(candidate) ? candidate as StoredEntry[] : [];
+ return entries.map(normalizeStoredEntry).map((entry) => (mode === "demo" ? markDemoEntry(entry) : stripDemoMarker(entry)));
} catch {
return [];
}
}
-function writeEntries(entries: StoredEntry[]) {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
+function writeEntries(entries: StoredEntry[], mode: DataMode = "normal") {
+ const normalized = entries.map((entry) => (mode === "demo" ? markDemoEntry(entry) : stripDemoMarker(entry)));
+ localStorage.setItem(storageKey(mode), JSON.stringify(normalized));
}
function dateParts(now = new Date()) {
@@ -47,7 +82,7 @@ function baseFields() {
};
}
-export function createReport(draft: ReportDraft): Report {
+export function createReport(draft: ReportDraft, demoMode = false): Report {
if (
!draft.work_ability ||
!draft.energy_level ||
@@ -82,12 +117,13 @@ export function createReport(draft: ReportDraft): Report {
};
report.total_score_percent = calculateReportScore(report) ?? undefined;
- const signedReport = attachIntegrity(report);
- writeEntries([signedReport, ...readEntries()]);
+ const mode = modeFromDemoFlag(demoMode);
+ const signedReport = demoMode ? markDemoEntry(attachIntegrity(report)) as Report : attachIntegrity(report);
+ writeEntries([signedReport, ...readEntries(mode)], mode);
return signedReport;
}
-export function createFollowup(draft: FollowupDraft): DelayedFollowup {
+export function createFollowup(draft: FollowupDraft, demoMode = false): DelayedFollowup {
if (!draft.worse_than_before) {
throw new Error("Oppfølgingen mangler svar på om du er verre enn før økten.");
}
@@ -103,57 +139,84 @@ export function createFollowup(draft: FollowupDraft): DelayedFollowup {
note: draft.note.trim() || undefined
};
- const signedFollowup = attachIntegrity(followup);
- writeEntries([signedFollowup, ...readEntries()]);
+ const mode = modeFromDemoFlag(demoMode);
+ const signedFollowup = demoMode ? markDemoEntry(attachIntegrity(followup)) as DelayedFollowup : attachIntegrity(followup);
+ writeEntries([signedFollowup, ...readEntries(mode)], mode);
return signedFollowup;
}
-export function listReports(): StoredEntry[] {
- return readEntries().sort((a, b) => getEntryTime(b) - getEntryTime(a));
+export function listReports(demoMode = false): StoredEntry[] {
+ if (demoMode) ensureDemoData();
+ return readEntries(modeFromDemoFlag(demoMode)).sort((a, b) => getEntryTime(b) - getEntryTime(a));
}
-export function getLastWorkReport(): Report | undefined {
- return listReports().find((entry): entry is Report => entry.type === "work_report");
+export function getLastWorkReport(demoMode = false): Report | undefined {
+ return listReports(demoMode).find((entry): entry is Report => entry.type === "work_report");
}
-export function getReport(id: string): StoredEntry | undefined {
- return readEntries().find((entry) => entry.id === id);
+export function getReport(id: string, demoMode = false): StoredEntry | undefined {
+ return readEntries(modeFromDemoFlag(demoMode)).find((entry) => entry.id === id);
}
-export function deleteReport(id: string) {
- writeEntries(readEntries().filter((entry) => entry.id !== id));
+export function deleteReport(id: string, demoMode = false) {
+ const mode = modeFromDemoFlag(demoMode);
+ writeEntries(readEntries(mode).filter((entry) => entry.id !== id), mode);
}
-export function deleteAllReports() {
- writeEntries([]);
+export function deleteAllReports(demoMode = false) {
+ writeEntries([], modeFromDemoFlag(demoMode));
}
-export function exportReports() {
- const reports = listReports();
+export function exportReports(demoMode = false) {
+ const reports = listReports(demoMode);
const payload: ExportPayload = {
schema_version: 3,
export_version: 3,
exported_at: new Date().toISOString(),
+ data_mode: demoMode ? "demo" : "normal",
+ contains_demo_data: demoMode || reports.some(isDemoEntry),
export_integrity: createExportIntegrity(reports),
reports
};
return JSON.stringify(payload, null, 2);
}
-export function previewImportReports(jsonText: string) {
- const imported = parseImportEntries(jsonText).map(normalizeStoredEntry).map(verifyEntryIntegrity);
- return { ...summarizeImport(imported, readEntries()), verification: summarizeVerification(imported) };
+export function previewImportReports(jsonText: string, demoMode = false) {
+ const parsed = JSON.parse(jsonText);
+ const imported = parseImportEntriesFromParsed(parsed).map(normalizeStoredEntry).map(verifyEntryIntegrity);
+ const demoEntriesDetected = countDemoEntries(imported, parsed);
+ if (!demoMode && demoEntriesDetected > 0) {
+ throw new Error("Importen inneholder demodata. Bytt til demo-modus for å importere den.");
+ }
+ const mode = modeFromDemoFlag(demoMode);
+ const prepared = demoMode ? imported.map(markDemoEntry) : imported.map(stripDemoMarker);
+ return {
+ ...summarizeImport(prepared, readEntries(mode)),
+ demo_entries_detected: demoEntriesDetected,
+ verification: summarizeVerification(prepared)
+ };
}
-export function importReportsFromJson(jsonText: string): ImportSummary {
- const imported = parseImportEntries(jsonText).map(normalizeStoredEntry).map(verifyEntryIntegrity);
- const existing = readEntries();
- const summary = { ...summarizeImport(imported, existing), verification: summarizeVerification(imported) };
+export function importReportsFromJson(jsonText: string, demoMode = false): ImportSummary {
+ const parsed = JSON.parse(jsonText);
+ const imported = parseImportEntriesFromParsed(parsed).map(normalizeStoredEntry).map(verifyEntryIntegrity);
+ const demoEntriesDetected = countDemoEntries(imported, parsed);
+ if (!demoMode && demoEntriesDetected > 0) {
+ throw new Error("Importen inneholder demodata. Bytt til demo-modus for å importere den.");
+ }
+ const mode = modeFromDemoFlag(demoMode);
+ const prepared = demoMode ? imported.map(markDemoEntry) : imported.map(stripDemoMarker);
+ const existing = readEntries(mode);
+ const summary = {
+ ...summarizeImport(prepared, existing),
+ demo_entries_detected: demoEntriesDetected,
+ verification: summarizeVerification(prepared)
+ };
const existingSignatures = new Set(existing.map(entrySignature));
const existingById = new Map(existing.map((entry) => [entry.id, entry]));
const additions: StoredEntry[] = [];
- for (const entry of imported) {
+ for (const entry of prepared) {
const existingWithId = existingById.get(entry.id);
if (existingWithId && entryContentSignature(existingWithId) !== entryContentSignature(entry)) continue;
if (existingSignatures.has(entrySignature(entry))) continue;
@@ -161,13 +224,13 @@ export function importReportsFromJson(jsonText: string): ImportSummary {
additions.push(entry);
}
- writeEntries([...additions, ...existing].sort((a, b) => getEntryTime(b) - getEntryTime(a)));
+ writeEntries([...additions, ...existing].sort((a, b) => getEntryTime(b) - getEntryTime(a)), mode);
return summary;
}
-function parseImportEntries(jsonText: string): StoredEntry[] {
- const parsed = JSON.parse(jsonText);
- const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.reports) ? parsed.reports : null;
+function parseImportEntriesFromParsed(parsed: unknown): StoredEntry[] {
+ const candidate = parsed && typeof parsed === "object" ? (parsed as { reports?: unknown }).reports : undefined;
+ const entries = Array.isArray(parsed) ? parsed : Array.isArray(candidate) ? candidate : null;
if (!entries) throw new Error("Filen inneholder ikke en gjenkjennbar rapporteksport.");
return entries;
}
@@ -248,3 +311,25 @@ function normalizeStoredEntry(entry: StoredEntry): StoredEntry {
delayed_symptoms: entry.delayed_symptoms ?? []
};
}
+
+export function isDemoEntry(entry: Partial) {
+ return entry.demo === true || typeof entry.id === "string" && entry.id.startsWith("demo-arbeidspuls");
+}
+
+function countDemoEntries(entries: StoredEntry[], payload: unknown) {
+ const payloadHasDemoFlag =
+ !Array.isArray(payload) &&
+ typeof payload === "object" &&
+ payload !== null &&
+ ((payload as ExportPayload).data_mode === "demo" || (payload as ExportPayload).contains_demo_data === true);
+ return payloadHasDemoFlag && entries.length > 0 ? Math.max(1, entries.filter(isDemoEntry).length) : entries.filter(isDemoEntry).length;
+}
+
+function markDemoEntry(entry: T): T {
+ return { ...entry, demo: true };
+}
+
+function stripDemoMarker(entry: T): T {
+ const { demo, ...rest } = entry;
+ return rest as T;
+}
diff --git a/src/styles.css b/src/styles.css
index 72667c0..0151119 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -38,6 +38,11 @@ button {
padding: max(16px, env(safe-area-inset-top)) 14px max(24px, env(safe-area-inset-bottom));
}
+.app-shell.demo-active {
+ outline: 3px solid rgba(178, 100, 42, 0.16);
+ outline-offset: -3px;
+}
+
.topbar {
display: flex;
align-items: center;
@@ -46,6 +51,14 @@ button {
padding: 10px 2px 14px;
}
+.topbar-actions {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: flex-end;
+}
+
.topbar h1,
.field-head h2,
.filters h2,
@@ -663,9 +676,11 @@ textarea {
}
.text-link {
+ background: transparent;
border: 1px solid #ccd7d4;
border-radius: 999px;
color: #235b5e;
+ cursor: pointer;
font-size: 0.82rem;
font-weight: 850;
padding: 9px 12px;
@@ -673,6 +688,52 @@ textarea {
white-space: nowrap;
}
+.text-link.active {
+ background: #235b5e;
+ border-color: #235b5e;
+ color: #fff;
+}
+
+.demo-banner {
+ align-items: center;
+ background: #1f4f51;
+ border: 1px solid rgba(255, 255, 255, 0.4);
+ border-radius: 999px;
+ box-shadow: 0 12px 30px rgba(31, 79, 81, 0.22);
+ color: #fff;
+ display: inline-flex;
+ font-size: 0.78rem;
+ font-weight: 950;
+ gap: 0.5rem;
+ letter-spacing: 0;
+ padding: 0.42rem 0.46rem 0.42rem 0.74rem;
+ position: fixed;
+ right: max(12px, env(safe-area-inset-right));
+ top: max(12px, env(safe-area-inset-top));
+ z-index: 40;
+}
+
+.demo-banner button {
+ align-items: center;
+ background: rgba(255, 255, 255, 0.16);
+ border: 1px solid rgba(255, 255, 255, 0.42);
+ border-radius: 999px;
+ color: #fff;
+ cursor: pointer;
+ display: inline-flex;
+ font: inherit;
+ height: 1.35rem;
+ justify-content: center;
+ line-height: 1;
+ padding: 0;
+ width: 1.35rem;
+}
+
+.demo-meta-pill {
+ background: #fbecd7;
+ color: #7d4118;
+}
+
.inline-head {
display: flex;
align-items: center;
diff --git a/src/types.ts b/src/types.ts
index 740440e..1c12ad2 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -99,6 +99,7 @@ export interface Report {
total_score_percent?: number;
integrity?: ReportIntegrity;
note?: string;
+ demo?: boolean;
}
export interface ReportDraft {
@@ -134,6 +135,7 @@ export interface DelayedFollowup {
recovery_status?: RecoveryStatus;
integrity?: ReportIntegrity;
note?: string;
+ demo?: boolean;
}
export interface FollowupDraft {
@@ -164,6 +166,7 @@ export interface EvaluatorProfile {
created_at: string;
updated_at: string;
source_fingerprint?: string;
+ contains_demo_data?: boolean;
reports: StoredEntry[];
evaluator_notes: EvaluatorNote[];
}
@@ -172,6 +175,8 @@ export interface ExportPayload {
schema_version: number;
export_version: number;
exported_at: string;
+ data_mode?: "normal" | "demo";
+ contains_demo_data?: boolean;
export_integrity?: ExportIntegrity;
reports: StoredEntry[];
}
@@ -181,6 +186,7 @@ export interface ImportSummary {
followups_added: number;
duplicates_skipped: number;
conflicts_detected: number;
+ demo_entries_detected?: number;
verification?: VerificationSummary;
}