feat(ai-act-kompass): include training register in JSON backup (schema v3)
Some checks failed
security-scan / secret-scan (push) Failing after 5s

The training register incl. scan attachments and checksums is now part
of the backup export/import — the Art. 4 evidence chain survives device
moves. Older backups (v1/v2) remain importable.

Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
This commit is contained in:
Claude 2026-07-04 11:37:14 +00:00
parent dc09999472
commit 94b539ed24
No known key found for this signature in database
3 changed files with 22 additions and 7 deletions

View File

@ -25,17 +25,20 @@ export function saveTraining(records: readonly TrainingRecord[]): void {
/** Format der Export-/Import-Datei. */ /** Format der Export-/Import-Datei. */
export interface ExportFile { export interface ExportFile {
readonly app: 'ai-act-kompass'; readonly app: 'ai-act-kompass';
readonly schemaVersion: 1 | 2; readonly schemaVersion: 1 | 2 | 3;
readonly exportedAt: string; readonly exportedAt: string;
readonly systems: readonly AISystem[]; readonly systems: readonly AISystem[];
/** Ab schemaVersion 2 enthalten. */ /** Ab schemaVersion 2 enthalten. */
readonly profile?: CompanyProfile; readonly profile?: CompanyProfile;
/** Ab schemaVersion 3 enthalten: Schulungsregister inkl. Scan-Belegen. */
readonly training?: readonly TrainingRecord[];
} }
/** Ergebnis eines Imports (Profil optional, ältere Backups haben keins). */ /** Ergebnis eines Imports (ältere Backups haben kein Profil/Register). */
export interface ImportResult { export interface ImportResult {
readonly systems: readonly AISystem[]; readonly systems: readonly AISystem[];
readonly profile: CompanyProfile | null; readonly profile: CompanyProfile | null;
readonly training: readonly TrainingRecord[] | null;
} }
/** Leeres Firmenprofil (Default). */ /** Leeres Firmenprofil (Default). */
@ -106,13 +109,15 @@ export function exportData(
systems: readonly AISystem[], systems: readonly AISystem[],
profile: CompanyProfile, profile: CompanyProfile,
exportedAt: string, exportedAt: string,
training: readonly TrainingRecord[] = [],
): string { ): string {
const file: ExportFile = { const file: ExportFile = {
app: 'ai-act-kompass', app: 'ai-act-kompass',
schemaVersion: 2, schemaVersion: 3,
exportedAt, exportedAt,
systems, systems,
profile, profile,
training,
}; };
return JSON.stringify(file, null, 2); return JSON.stringify(file, null, 2);
} }
@ -139,6 +144,7 @@ export function importData(json: string): ImportResult {
return { return {
systems: file.systems, systems: file.systems,
profile: file.profile ? { ...emptyProfile(), ...file.profile } : null, profile: file.profile ? { ...emptyProfile(), ...file.profile } : null,
training: Array.isArray(file.training) ? file.training : null,
}; };
} }

View File

@ -211,10 +211,12 @@ export function App(): JSX.Element {
<SettingsView <SettingsView
systems={systems} systems={systems}
profile={profile} profile={profile}
training={training}
onProfileChange={setProfile} onProfileChange={setProfile}
onImport={(imported, importedProfile) => { onImport={(imported, importedProfile, importedTraining) => {
setSystems(imported); setSystems(imported);
if (importedProfile) setProfile(importedProfile); if (importedProfile) setProfile(importedProfile);
if (importedTraining) setTraining(importedTraining);
}} }}
onClear={() => { onClear={() => {
setSystems([]); setSystems([]);

View File

@ -1,4 +1,5 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import type { TrainingRecord } from '../engine/quiz';
import type { AISystem, CompanyProfile } from '../engine/types'; import type { AISystem, CompanyProfile } from '../engine/types';
import { useI18n } from '../i18n/context'; import { useI18n } from '../i18n/context';
import { LOCALE_NAMES, LOCALES } from '../i18n/types'; import { LOCALE_NAMES, LOCALES } from '../i18n/types';
@ -9,8 +10,13 @@ import { downloadText, todayISO } from './common';
interface SettingsViewProps { interface SettingsViewProps {
readonly systems: readonly AISystem[]; readonly systems: readonly AISystem[];
readonly profile: CompanyProfile; readonly profile: CompanyProfile;
readonly training: readonly TrainingRecord[];
readonly onProfileChange: (profile: CompanyProfile) => void; readonly onProfileChange: (profile: CompanyProfile) => void;
readonly onImport: (systems: readonly AISystem[], profile: CompanyProfile | null) => void; readonly onImport: (
systems: readonly AISystem[],
profile: CompanyProfile | null,
training: readonly TrainingRecord[] | null,
) => void;
readonly onClear: () => void; readonly onClear: () => void;
} }
@ -22,6 +28,7 @@ type ProfileTextKey = Exclude<keyof CompanyProfile, 'logoDataUrl' | 'countries'>
export function SettingsView({ export function SettingsView({
systems, systems,
profile, profile,
training,
onProfileChange, onProfileChange,
onImport, onImport,
onClear, onClear,
@ -45,7 +52,7 @@ export function SettingsView({
const handleImport = async (file: File): Promise<void> => { const handleImport = async (file: File): Promise<void> => {
try { try {
const result = importData(await file.text()); const result = importData(await file.text());
onImport(result.systems, result.profile); onImport(result.systems, result.profile, result.training);
setMessage( setMessage(
fmt(t.importOk, { fmt(t.importOk, {
count: result.systems.length, count: result.systems.length,
@ -190,7 +197,7 @@ export function SettingsView({
onClick={() => onClick={() =>
downloadText( downloadText(
`ai-act-kompass-backup-${todayISO()}.json`, `ai-act-kompass-backup-${todayISO()}.json`,
exportData(systems, profile, new Date().toISOString()), exportData(systems, profile, new Date().toISOString(), training),
'application/json', 'application/json',
) )
} }