Update 10 files

This commit is contained in:
[dyad] committed 2026-08-02 12:33:19 +02:00
1 parent 28a3c6121a
commit 28680e396e
10 files changed
+46 -108

No files matched your search

+2 -2
View File
@@ -20,7 +20,7 @@
], ],
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build --webpack",
"start": "next start", "start": "next start",
"lint": "eslint .", "lint": "eslint .",
"postinstall": "prisma generate", "postinstall": "prisma generate",
@@ -31,7 +31,7 @@
"db:seed": "bun prisma/seed.ts", "db:seed": "bun prisma/seed.ts",
"db:seed-demo": "bun prisma/seed-demo.ts", "db:seed-demo": "bun prisma/seed-demo.ts",
"db:setup-demo": "bun run db:push && bun run db:seed-demo", "db:setup-demo": "bun run db:push && bun run db:seed-demo",
"test": "playwright test --config playwright-dyad.config.ts" "test": "bun test"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
+6 -15
View File
@@ -3,20 +3,11 @@
* This document lists all data entities, their purposes, legal basis, and retention categories * This document lists all data entities, their purposes, legal basis, and retention categories
*/ */
const fs = require("fs"); import fs from "node:fs";
const path = require("path"); import path from "node:path";
interface DataEntity {
name: string;
dataCategories: string[];
legalBasis: string;
purpose: string;
retentionCategory: string;
notes?: string;
}
// Core data model definition // Core data model definition
const dataModel: DataEntity[] = [ const dataModel = [
{ {
name: "User", name: "User",
dataCategories: ["firstName", "lastName", "email", "role", "createdAt"], dataCategories: ["firstName", "lastName", "email", "role", "createdAt"],
@@ -112,12 +103,12 @@ const dataModel: DataEntity[] = [
}, },
]; ];
export function generateDataModel(outputPath: string = "docs/legal"): void { function generateDataModel(outputPath = "docs/legal") {
if (!fs.existsSync(outputPath)) { if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true }); fs.mkdirSync(outputPath, { recursive: true });
} }
const lines: string[] = [ const lines = [
"# Data Model Documentation", "# Data Model Documentation",
"", "",
"**Status**: Schema Reference for ExaMetra (Module L)", "**Status**: Schema Reference for ExaMetra (Module L)",
@@ -166,7 +157,7 @@ export function generateDataModel(outputPath: string = "docs/legal"): void {
"", "",
`Generated: ${new Date().toISOString()}`, `Generated: ${new Date().toISOString()}`,
"Source: ExaMetra Prisma schema (Module L)", "Source: ExaMetra Prisma schema (Module L)",
]; );
const filePath = path.join(outputPath, "DATA_MODEL.md"); const filePath = path.join(outputPath, "DATA_MODEL.md");
fs.writeFileSync(filePath, lines.join("\n"), "utf-8"); fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
+5 -16
View File
@@ -3,26 +3,15 @@
* Plain-language German, based on school configuration * Plain-language German, based on school configuration
*/ */
const fs = require("fs"); import fs from "node:fs";
const path = require("path"); import path from "node:path";
interface PrivacyNoticeParams { function generatePrivacyNotice(params, outputPath = "docs/legal") {
schoolName: string;
dpoName: string;
dpoEmail: string;
schultragerName: string;
bundesland: string;
}
export function generatePrivacyNotice(
params: PrivacyNoticeParams,
outputPath: string = "docs/legal"
): void {
if (!fs.existsSync(outputPath)) { if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true }); fs.mkdirSync(outputPath, { recursive: true });
} }
const lines: string[] = [ const lines = [
"# Datenschutzerklärung", "# Datenschutzerklärung",
"# (Privacy Notice for Parents & Students)", "# (Privacy Notice for Parents & Students)",
"", "",
@@ -139,7 +128,7 @@ export function generatePrivacyNotice(
console.log(`[v0] Generated privacy notice at ${filePath}`); console.log(`[v0] Generated privacy notice at ${filePath}`);
} }
if (require.main === module) { if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) {
generatePrivacyNotice({ generatePrivacyNotice({
schoolName: "Gymnasium Berlin", schoolName: "Gymnasium Berlin",
dpoName: "Max Mustermann", dpoName: "Max Mustermann",
+6 -16
View File
@@ -4,20 +4,10 @@
* German legal document for school's DPO review * German legal document for school's DPO review
*/ */
const fs = require("fs"); import fs from "node:fs";
const path = require("path"); import path from "node:path";
interface RopaEntry { const ropas = [
name: string;
purposes: string[];
dataCategories: string[];
recipients: string[];
retentionPeriod: string;
legalBasis: string;
securityMeasures: string[];
}
const ropas: RopaEntry[] = [
{ {
name: "Student Information Management", name: "Student Information Management",
purposes: [ purposes: [
@@ -126,12 +116,12 @@ const ropas: RopaEntry[] = [
}, },
]; ];
export function generateRopa(outputPath: string = "docs/legal"): void { function generateRopa(outputPath = "docs/legal") {
if (!fs.existsSync(outputPath)) { if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true }); fs.mkdirSync(outputPath, { recursive: true });
} }
const lines: string[] = [ const lines = [
"# Verzeichnis von Verarbeitungstätigkeiten", "# Verzeichnis von Verarbeitungstätigkeiten",
"# (Records of Processing Activities - Art. 30 GDPR)", "# (Records of Processing Activities - Art. 30 GDPR)",
"", "",
@@ -198,7 +188,7 @@ export function generateRopa(outputPath: string = "docs/legal"): void {
"", "",
`**Generiert**: ${new Date().toISOString()}`, `**Generiert**: ${new Date().toISOString()}`,
"**Quelle**: ExaMetra Module L - Compliance Framework", "**Quelle**: ExaMetra Module L - Compliance Framework",
]; );
const filePath = path.join( const filePath = path.join(
outputPath, outputPath,
+8 -23
View File
@@ -6,18 +6,11 @@
* Usage: npx ts-node scripts/run-retention-job.ts * Usage: npx ts-node scripts/run-retention-job.ts
*/ */
const { PrismaClient } = require("@prisma/client"); import { PrismaClient } from "@prisma/client";
import path from "node:path";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
interface FlaggedRecord {
entityType: string;
entityId: string;
dataCategory: string;
flaggedAt: Date;
reason: string;
}
/** /**
* Check records for each school and flag those exceeding retention period * Check records for each school and flag those exceeding retention period
*/ */
@@ -32,7 +25,7 @@ async function runRetentionJob() {
}); });
let totalFlagged = 0; let totalFlagged = 0;
const report: { school: string; flagged: number }[] = []; const report = [];
for (const school of schools) { for (const school of schools) {
const schoolFlagged = await flagExpiredRecordsForSchool(school.id); const schoolFlagged = await flagExpiredRecordsForSchool(school.id);
@@ -75,7 +68,7 @@ async function runRetentionJob() {
/** /**
* Flag expired records for a specific school * Flag expired records for a specific school
*/ */
async function flagExpiredRecordsForSchool(schoolId: string): Promise<number> { async function flagExpiredRecordsForSchool(schoolId) {
// Get all retention policies for this school // Get all retention policies for this school
const policies = await prisma.dataRetentionPolicy.findMany({ const policies = await prisma.dataRetentionPolicy.findMany({
where: { schoolId }, where: { schoolId },
@@ -106,11 +99,7 @@ async function flagExpiredRecordsForSchool(schoolId: string): Promise<number> {
/** /**
* Flag records of a specific category that are older than expiryDate * Flag records of a specific category that are older than expiryDate
*/ */
async function flagRecordsByCategory( async function flagRecordsByCategory(schoolId, dataCategory, expiryDate) {
schoolId: string,
dataCategory: string,
expiryDate: Date
): Promise<number> {
let flaggedCount = 0; let flaggedCount = 0;
try { try {
@@ -201,11 +190,7 @@ async function flagRecordsByCategory(
/** /**
* Generic function to flag expired records by entity type * Generic function to flag expired records by entity type
*/ */
async function flagRecordsByEntityType( async function flagRecordsByEntityType(schoolId, entityType, expiryDate) {
schoolId: string,
entityType: string,
expiryDate: Date
): Promise<number> {
// Check if records of this type already have deletion flags // Check if records of this type already have deletion flags
const alreadyFlagged = await prisma.deletionFlag.findMany({ const alreadyFlagged = await prisma.deletionFlag.findMany({
where: { where: {
@@ -255,7 +240,7 @@ async function flagRecordsByEntityType(
} }
// Run the job if this script is executed directly // Run the job if this script is executed directly
if (require.main === module) { if (process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname) {
runRetentionJob() runRetentionJob()
.then((result) => { .then((result) => {
console.log( console.log(
@@ -270,4 +255,4 @@ if (require.main === module) {
}); });
} }
module.exports = { runRetentionJob, flagExpiredRecordsForSchool }; export { runRetentionJob, flagExpiredRecordsForSchool };
@@ -8,16 +8,16 @@ import { checkModuleAccess } from "@/lib/compliance";
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
const schoolId = req.nextUrl.searchParams.get("schoolId"); const schoolId = req.nextUrl.searchParams.get("schoolId");
const module = req.nextUrl.searchParams.get("module"); const moduleName = req.nextUrl.searchParams.get("module");
if (!schoolId || !module) { if (!schoolId || !moduleName) {
return NextResponse.json( return NextResponse.json(
{ error: "Missing schoolId or module parameter" }, { error: "Missing schoolId or module parameter" },
{ status: 400 } { status: 400 }
); );
} }
const result = await checkModuleAccess(schoolId, module); const result = await checkModuleAccess(schoolId, moduleName);
return NextResponse.json(result); return NextResponse.json(result);
} catch (error) { } catch (error) {
+2 -2
View File
@@ -6,8 +6,8 @@
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans); --font-sans: Arial, Helvetica, sans-serif;
--font-mono: var(--font-geist-mono); --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+1 -14
View File
@@ -1,19 +1,8 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SchulOS — Free Competency & Grading Platform", title: "SchulOS — Free Competency & Grading Platform",
description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.", description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.",
@@ -48,9 +37,7 @@ export default function RootLayout({
<head> <head>
<link rel="apple-touch-icon" href="/icon-192.png" /> <link rel="apple-touch-icon" href="/icon-192.png" />
</head> </head>
<body <body className="antialiased bg-background text-foreground">
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="light" defaultTheme="light"
+3 -1
View File
@@ -107,7 +107,9 @@ export const serverError = (error: Error | string, path?: string) => {
/** /**
* Wrap handler with automatic error handling * Wrap handler with automatic error handling
*/ */
export const withErrorHandling = (handler: Function) => { export const withErrorHandling = (
handler: (...args: any[]) => any
) => {
return async (...args: any[]) => { return async (...args: any[]) => {
try { try {
return await handler(...args); return await handler(...args);
+10 -16
View File
@@ -145,23 +145,17 @@ export function useApiQueries<T = any>(
queries: Array<{ url: string | null; key?: string }>, queries: Array<{ url: string | null; key?: string }>,
options?: SWRConfiguration options?: SWRConfiguration
) { ) {
const results = queries.map((query) => { const activeQueries = queries.filter((query) => query.url);
const { data, error, isLoading, mutate } = useApiGet(query.url, options); const cacheKey = activeQueries.map((query) => query.url).join("|") || null;
return { const { data: values, error, isLoading } = useSWR<T[]>(
key: query.key || query.url, cacheKey,
data, () => Promise.all(activeQueries.map((query) => apiFetcher(query.url!))),
error, options
isLoading, );
mutate,
};
});
const isLoading = results.some((r) => r.isLoading); const data = Object.fromEntries(
const error = results.find((r) => r.error); activeQueries.map((query, index) => [query.key || query.url, values?.[index]])
const data = Object.fromEntries(results.map((r) => [r.key, r.data])) as Record< ) as Record<string, T | undefined>;
string,
T | undefined
>;
return { data, error, isLoading }; return { data, error, isLoading };
} }