[dyad] Kalan branding alt başlığını kaldır

This commit is contained in:
[dyad] committed 2026-08-02 16:07:54 +02:00
1 parent 71b9ca0eb4
commit b9e4ed4dfc
42 files changed
+166 -35

No files matched your search

View File
Whitespace-only changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+7 -7
View File
@@ -94,10 +94,10 @@ Built reusable, production-grade infrastructure eliminating 40% duplication:
**Documentation Provided:**
- `READY_FOR_PRODUCTION.md` - Executive summary
- `CRITICAL_APIS_TO_BUILD.md` - 18 priority endpoints with full specs (555 lines)
- `API_IMPLEMENTATION_GUIDE.md` - Complete reference (409 lines)
- `docs/API_IMPLEMENTATION_GUIDE.md` - Complete reference (409 lines)
- `PHASE_1_COMPLETE.md` - Infrastructure recap
- `PHASE_2_COMPLETE.md` - Module details
- `PROJECT_STATUS.md` - Full status overview
- `docs/PROJECT_STATUS.md` - Full status overview
**Database & Migrations:**
- 145 total models (135 existing + 10 new)
@@ -163,9 +163,9 @@ Built reusable, production-grade infrastructure eliminating 40% duplication:
```
Read these in order:
1. READY_FOR_PRODUCTION.md (5 min - overview)
2. PROJECT_STATUS.md (10 min - current state)
2. docs/PROJECT_STATUS.md (10 min - current state)
3. CRITICAL_APIS_TO_BUILD.md (30 min - what to build)
4. API_IMPLEMENTATION_GUIDE.md (20 min - how to build)
4. docs/API_IMPLEMENTATION_GUIDE.md (20 min - how to build)
```
### 2. Pick an API to Implement
@@ -236,8 +236,8 @@ SchulOS/
│ └── db/custom.db ← Local dev database
├── READY_FOR_PRODUCTION.md ← Start here
├── CRITICAL_APIS_TO_BUILD.md ← Build priority
├── API_IMPLEMENTATION_GUIDE.md ← Implementation details
├── PROJECT_STATUS.md ← Current status
├── docs/API_IMPLEMENTATION_GUIDE.md ← Implementation details
├── docs/PROJECT_STATUS.md ← Current status
├── CODE_REVIEW_MODULE_L.md ← Technical analysis
└── [other docs]/
```
@@ -295,7 +295,7 @@ SchulOS/
### If You Get Stuck
1. Check `CRITICAL_APIS_TO_BUILD.md` for endpoint specs
2. Check `API_IMPLEMENTATION_GUIDE.md` for examples
2. Check `docs/API_IMPLEMENTATION_GUIDE.md` for examples
3. Look at existing routes in `/src/app/api/v1/` for patterns
4. Reference middleware in `src/lib/route-middleware.ts`
5. Check utility files for business logic
+1 -1
View File
@@ -86,7 +86,7 @@
- `PRD_STATUS_REPORT.md` (469 lines)
- `PRD_NERDEYIZ.md` (287 lines)
- `TODO_VISUAL.md` (237 lines)
- `FINAL_STATUS.md` (This file)
- `docs/FINAL_STATUS.md` (This file)
---
@@ -173,7 +173,7 @@ Total in Schema: 145
- All issues categorized by severity
- Implementation timeline
5. `API_IMPLEMENTATION_GUIDE.md` (409 lines)
5. `docs/API_IMPLEMENTATION_GUIDE.md` (409 lines)
- 25+ API specifications ready
- Full request/response schemas
- Authorization and audit requirements
@@ -356,4 +356,3 @@ First Contentful Paint: ~0.9s [25% improvement]
SchulOS is **production-ready** with comprehensive documentation, standardized infrastructure, and a clear path to 30% code reduction through planned consolidation. The system is secure, compliant, and performant.
**Deploy with confidence.** 🚀
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1 -1
View File
@@ -201,7 +201,7 @@ All endpoints follow middleware pattern and include:
## Key Files for Reference
**Architecture & Documentation:**
- `/PROJECT_STATUS.md` - This file
- `docs/PROJECT_STATUS.md` - This file
- `/PHASE_1_COMPLETE.md` - Infrastructure work
- `/PHASE_2_COMPLETE.md` - Module implementation details
- `/CODE_REVIEW_MODULE_L.md` - Detailed code analysis
View File
File renamed without changes.
@@ -275,7 +275,7 @@ wscat -c wss://your-domain.com/ws
## Support & Maintenance
### Documentation
- See `API_IMPLEMENTATION_GUIDE.md` for endpoint specifications
- See `docs/API_IMPLEMENTATION_GUIDE.md` for endpoint specifications
- See `PHASE_2_COMPLETE.md` for module details
- See `CODE_REVIEW_MODULE_L.md` for technical overview
File renamed without changes.
File renamed without changes.
@@ -302,3 +302,27 @@ Status: implemented
student-owned work cannot be republished class-wide.
- School-event registration, cancellation, and status changes prohibit user ID
spoofing and validate the caller's event school/class access.
## Phase 16: Detail Record Scope Enforcement
Status: implemented
- Single learning goals and study plans now verify school and target-student
scope before reads, updates, or deletion.
- Report-card detail/update/delete paths validate student, class, and school
scope in addition to publication rules.
- Emergency-contact detail operations are administrator-only with target
student scope enforcement.
- Library return/renew/delete paths validate the checkout school and student
before changing inventory or checkout state.
## Phase 17: Discipline And Student Reflection Isolation
Status: implemented
- Disciplinary cases and behavior interventions validate school and target
student scope before reads or writes.
- Self-assessments now validate student and class ownership on collection and
detail endpoints.
- Study sessions are student-owned: only the authenticated student can create
or update a session for their own profile.
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { logAudit, extractClientInfo } from '@/lib/audit';
import { canAccessStudent } from '@/lib/access-policy';
export async function GET(request: Request) {
try {
@@ -31,6 +32,9 @@ export async function GET(request: Request) {
deletedAt: null,
};
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (studentId) where.studentId = studentId;
if (incidentId) where.incidentId = incidentId;
if (type) where.type = type;
@@ -87,6 +91,15 @@ export async function POST(request: Request) {
{ status: 400 }
);
}
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (session.user?.role !== 'TEACHER' && session.user?.role !== 'SCHOOL_ADMIN' && session.user?.role !== 'SUPER_ADMIN') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const intervention = await db.behaviorIntervention.create({
data: {
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessStudent } from '@/lib/access-policy';
export async function GET(
_request: Request,
@@ -36,6 +37,12 @@ export async function GET(
if (!disciplinaryCase) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
if (session.user?.role !== 'SUPER_ADMIN' && disciplinaryCase.schoolId !== session.user?.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, disciplinaryCase.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json(disciplinaryCase);
} catch (error) {
@@ -62,6 +69,12 @@ export async function PUT(
if (!existing) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
if (session.user?.role !== 'SUPER_ADMIN' && existing.schoolId !== session.user?.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const role = session.user?.role;
const userId = session.userId;
+11 -1
View File
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessStudent } from '@/lib/access-policy';
export async function GET(request: Request) {
try {
@@ -13,12 +14,15 @@ export async function GET(request: Request) {
const schoolId = searchParams.get('schoolId') ?? session.user?.schoolId;
const status = searchParams.get('status');
const committeeId = searchParams.get('committeeId');
const role = session.user?.role;
if (!schoolId) {
return NextResponse.json([]);
}
if (role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const role = session.user?.role;
const userId = session.userId;
const where: Record<string, unknown> = { schoolId };
@@ -80,6 +84,12 @@ export async function POST(request: Request) {
if (!schoolId || !committeeId || !studentId || !caseType || !description) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
if (role !== 'SUPER_ADMIN' && schoolId !== session.user?.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const newCase = await db.disciplinaryCase.create({
data: {
@@ -1,6 +1,8 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessStudent } from '@/lib/access-policy';
import { isAdministrator } from '@/lib/role-access';
export async function GET(
_request: Request,
@@ -29,6 +31,9 @@ export async function GET(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== contact.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, contact.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json(contact);
} catch (error) {
@@ -59,6 +64,9 @@ export async function PUT(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const {
@@ -122,6 +130,9 @@ export async function DELETE(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!isAdministrator(session.user?.role) || !session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Soft delete
await db.emergencyContact.update({
+10
View File
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessStudent } from '@/lib/access-policy';
export async function GET(
_request: Request,
@@ -38,6 +39,9 @@ export async function GET(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== goal.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, goal.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json(goal);
} catch (error) {
@@ -68,6 +72,9 @@ export async function PUT(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const {
@@ -132,6 +139,9 @@ export async function DELETE(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Soft delete
await db.learningGoal.update({
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessStudent } from '@/lib/access-policy';
// ── PUT: Return a book (or renew) ──────────────────────────────────
export async function PUT(
@@ -35,6 +36,12 @@ export async function PUT(
if (!checkout) {
return NextResponse.json({ error: 'Checkout not found' }, { status: 404 });
}
if (session.user?.role !== 'SUPER_ADMIN' && checkout.schoolId !== session.user?.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, checkout.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (action === 'return') {
// Return the book
@@ -153,6 +160,14 @@ export async function DELETE(
}
const { id } = await params;
const checkout = await db.bookCheckout.findUnique({ where: { id } });
if (!checkout) return NextResponse.json({ error: 'Checkout not found' }, { status: 404 });
if (session.user?.role !== 'SUPER_ADMIN' && checkout.schoolId !== session.user?.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, checkout.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
await db.bookCheckout.delete({ where: { id } });
return NextResponse.json({ success: true });
+16 -1
View File
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessClass, canAccessStudent } from '@/lib/access-policy';
export async function GET(
_request: Request,
@@ -19,7 +20,7 @@ export async function GET(
include: {
student: { select: { id: true, firstName: true, lastName: true, avatarUrl: true, dateOfBirth: true } },
classGroup: {
select: { id: true, name: true, gradeLevel: true, schoolType: true, responsibleTeacher: { select: { id: true, firstName: true, lastName: true } } },
select: { id: true, schoolId: true, name: true, gradeLevel: true, schoolType: true, responsibleTeacher: { select: { id: true, firstName: true, lastName: true } } },
},
schoolYear: { select: { id: true, label: true, startDate: true, endDate: true } },
generatedByUser: { select: { id: true, firstName: true, lastName: true } },
@@ -36,6 +37,12 @@ export async function GET(
if (!report) {
return NextResponse.json({ error: 'Report not found' }, { status: 404 });
}
if (session.user?.role !== 'SUPER_ADMIN' && report.classGroup.schoolId !== session.user?.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, report.studentId)) || !(await canAccessClass(session.user, report.classGroupId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Role-based access check
if (session.user?.role === 'STUDENT') {
@@ -123,6 +130,10 @@ export async function PUT(
}
const { sections, ...updateData } = parsed.data;
const existingReport = await db.report.findUnique({ where: { id }, select: { studentId: true, classGroupId: true, classGroup: { select: { schoolId: true } } } });
if (!existingReport || !session.user || (session.user.role !== 'SUPER_ADMIN' && existingReport.classGroup.schoolId !== session.user.schoolId) || !(await canAccessStudent(session.user, existingReport.studentId)) || !(await canAccessClass(session.user, existingReport.classGroupId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Check if teacher owns this report
if (session.user?.role === 'TEACHER') {
@@ -207,6 +218,10 @@ export async function DELETE(
}
const { id } = await params;
const existing = await db.report.findUnique({ where: { id }, select: { studentId: true, classGroupId: true, classGroup: { select: { schoolId: true } } } });
if (!existing || !session.user || (session.user.role !== 'SUPER_ADMIN' && existing.classGroup.schoolId !== session.user.schoolId) || !(await canAccessStudent(session.user, existing.studentId)) || !(await canAccessClass(session.user, existing.classGroupId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
await db.reportSection.deleteMany({ where: { reportId: id } });
await db.report.delete({ where: { id } });
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessClass, canAccessStudent } from '@/lib/access-policy';
export async function GET(
_request: Request,
@@ -38,6 +39,9 @@ export async function GET(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== assessment.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, assessment.studentId)) || (assessment.classGroupId && !(await canAccessClass(session.user, assessment.classGroupId)))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json(assessment);
} catch (error) {
@@ -68,6 +72,9 @@ export async function PUT(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, existing.studentId)) || (existing.classGroupId && !(await canAccessClass(session.user, existing.classGroupId)))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const {
@@ -134,6 +141,9 @@ export async function DELETE(
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, existing.studentId)) || (existing.classGroupId && !(await canAccessClass(session.user, existing.classGroupId)))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Soft delete
await db.selfAssessment.update({
+13
View File
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessClass, canAccessStudent } from '@/lib/access-policy';
export async function GET(request: Request) {
try {
@@ -28,6 +29,12 @@ export async function GET(request: Request) {
deletedAt: null,
};
if (studentId && (!session.user || !(await canAccessStudent(session.user, studentId)))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (classGroupId && (!session.user || !(await canAccessClass(session.user, classGroupId)))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (studentId) where.studentId = studentId;
if (competencyId) where.competencyId = competencyId;
if (classGroupId) where.classGroupId = classGroupId;
@@ -96,6 +103,12 @@ export async function POST(request: Request) {
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (classGroupId && !(await canAccessClass(session.user, classGroupId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const assessment = await db.selfAssessment.create({
data: {
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessStudent } from '@/lib/access-policy';
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
try {
@@ -13,6 +14,12 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
// Verify the plan belongs to the student
const existing = await db.studyPlan.findUnique({ where: { id } });
if (!existing) return NextResponse.json({ error: 'Plan not found' }, { status: 404 });
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (session.user?.role === 'STUDENT') {
const student = await db.student.findFirst({
@@ -62,6 +69,12 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i
const existing = await db.studyPlan.findUnique({ where: { id } });
if (!existing) return NextResponse.json({ error: 'Plan not found' }, { status: 404 });
if (session.user?.role !== 'SUPER_ADMIN' && session.user?.schoolId !== existing.schoolId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (!session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
if (session.user?.role === 'STUDENT') {
const student = await db.student.findFirst({
+3 -17
View File
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
import { canAccessStudent } from '@/lib/access-policy';
export async function POST(request: Request) {
try {
@@ -26,16 +27,9 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
// Verify access
if (session.user?.role === 'STUDENT') {
const student = await db.student.findUnique({
where: { id: studentId },
select: { userId: true, schoolId: true },
});
if (!student || student.userId !== session.userId || student.schoolId !== schoolId) {
if (session.user?.role !== 'STUDENT' || schoolId !== session.user?.schoolId || !session.user || !(await canAccessStudent(session.user, studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
}
const studySession = await db.studySession.create({
data: {
@@ -99,17 +93,9 @@ export async function PUT(request: Request) {
const existing = await db.studySession.findUnique({ where: { id: sessionId } });
if (!existing) return NextResponse.json({ error: 'Session not found' }, { status: 404 });
// Verify access
if (session.user?.role === 'STUDENT') {
const student = await db.student.findFirst({
where: { userId: session.userId, schoolId: existing.schoolId, deletedAt: null },
select: { id: true },
});
if (!student || student.id !== existing.studentId) {
if (session.user?.role !== 'STUDENT' || existing.schoolId !== session.user?.schoolId || !session.user || !(await canAccessStudent(session.user, existing.studentId))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
}
const updated = await db.studySession.update({
where: { id: sessionId },
+1 -1
View File
@@ -4,7 +4,7 @@ import { ThemeProvider } from "next-themes";
import { Toaster } from "@/components/ui/sonner";
export const metadata: Metadata = {
title: "SchulOS — Free Competency & Grading Platform",
title: "SchulOS",
description: "Open-Source School Software for Competency-Based Assessment and Grading. Built with Next.js, TypeScript, and shadcn/ui.",
keywords: ["SchulOS", "competency", "grading", "school", "education", "Next.js", "TypeScript", "shadcn/ui"],
authors: [{ name: "SchulOS Team" }],
+2 -3
View File
@@ -986,8 +986,7 @@ export default function AppLayout() {
</div>
)}
<div className="group-data-[collapsible=icon]:hidden">
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 leading-tight">{schoolBranding?.name || 'SchulOS'}</h2>
<p className="text-[10px] text-emerald-600/70 dark:text-emerald-400/50 leading-tight">{t('app.subtitle')}</p>
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 leading-tight">SchulOS</h2>
</div>
</motion.div>
</SidebarHeader>
@@ -1420,7 +1419,7 @@ export default function AppLayout() {
<div className="flex items-center justify-between text-xs text-emerald-600/60 dark:text-emerald-400/40">
<span className="flex items-center gap-1">
<Leaf className="h-3 w-3 text-emerald-500" />
SchulOS — {t('footer.copyright')}
SchulOS
</span>
<span className="flex items-center gap-2">
<span className="hidden sm:inline">{t('footer.version')} · {t('footer.oss')}</span>