diff --git a/apps/manacore/apps/landing/src/data/ecosystem-health.json b/apps/manacore/apps/landing/src/data/ecosystem-health.json
index 24e029ede..f02bc5a93 100644
--- a/apps/manacore/apps/landing/src/data/ecosystem-health.json
+++ b/apps/manacore/apps/landing/src/data/ecosystem-health.json
@@ -1,11 +1,11 @@
{
- "generatedAt": "2026-03-31T12:04:45.568Z",
- "overallScore": 70,
+ "generatedAt": "2026-03-31T12:12:26.680Z",
+ "overallScore": 72,
"scores": {
"sharedPackages": 90,
"iconConsistency": 89,
"modalConsistency": 19,
- "errorHandling": 20,
+ "errorHandling": 17,
"i18nCoverage": 86,
"localFirst": 93,
"styleConsistency": 87,
@@ -13,7 +13,9 @@
"typescriptStrict": 100,
"testCoverage": 72,
"pwaSupport": 2,
- "maintainability": 0
+ "maintainability": 0,
+ "securityHeaders": 76,
+ "skeletonLoading": 76
},
"weights": {
"sharedPackages": 20,
@@ -26,13 +28,15 @@
"errorBoundaries": 8,
"typescriptStrict": 7,
"testCoverage": 7,
- "pwaSupport": 5,
- "maintainability": 5
+ "pwaSupport": 4,
+ "maintainability": 4,
+ "securityHeaders": 5,
+ "skeletonLoading": 3
},
"details": {
"icons": {
"adoption": 89,
- "phosphorFiles": 354,
+ "phosphorFiles": 347,
"inlineSvgFiles": 45,
"perApp": {
"calc": {
@@ -40,7 +44,7 @@
"inlineSvg": 0
},
"calendar": {
- "phosphor": 35,
+ "phosphor": 28,
"inlineSvg": 0
},
"chat": {
@@ -155,9 +159,9 @@
},
"modals": {
"adoption": 19,
- "total": 64,
+ "total": 63,
"sharedUsage": 12,
- "focusTrapUsage": 7
+ "focusTrapUsage": 6
},
"packages": {
"coreAdoption": 90,
@@ -206,9 +210,9 @@
}
},
"errors": {
- "adoption": 20,
+ "adoption": 17,
"inline": 193,
- "shared": 47
+ "shared": 40
},
"i18n": {
"adoption": 86,
@@ -266,7 +270,7 @@
"e2eAdoption": 14,
"appsWithTests": 21,
"appsWithE2e": 4,
- "totalTestFiles": 111,
+ "totalTestFiles": 110,
"noTests": [
"calc",
"inventar",
@@ -341,12 +345,12 @@
{
"app": "todo",
"file": "lib/components/TaskItem.svelte",
- "lines": 1217
+ "lines": 1194
},
{
"app": "todo",
"file": "lib/components/board-views/ViewEditorModal.svelte",
- "lines": 1190
+ "lines": 1187
},
{
"app": "contacts",
@@ -356,7 +360,7 @@
{
"app": "calendar",
"file": "lib/components/calendar/WeekView.svelte",
- "lines": 1043
+ "lines": 959
},
{
"app": "zitare",
@@ -374,6 +378,26 @@
"lines": 849
}
]
+ },
+ "todos": {
+ "totalCount": 22,
+ "perApp": {
+ "manacore": 13,
+ "contacts": 5,
+ "todo": 2,
+ "chat": 1,
+ "picture": 1
+ }
+ },
+ "securityHeaders": {
+ "adoption": 76,
+ "appsWithHeaders": 22,
+ "missing": ["manavoxel", "moodlit", "news", "playground", "times", "uload", "wisekeep"]
+ },
+ "skeletons": {
+ "adoption": 76,
+ "appsWithSkeletons": 22,
+ "missing": ["citycorners", "manadeck", "manavoxel", "planta", "presi", "times", "zitare"]
}
},
"apps": [
diff --git a/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro b/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro
index d83ca35f0..63453d877 100644
--- a/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro
+++ b/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro
@@ -106,6 +106,18 @@ const categories = [
icon: 'π',
description: 'Dateien unter 500 Zeilen',
},
+ {
+ key: 'securityHeaders',
+ label: 'Security Headers',
+ icon: 'π',
+ description: 'CSP, X-Frame-Options via setSecurityHeaders()',
+ },
+ {
+ key: 'skeletonLoading',
+ label: 'Skeleton Loading',
+ icon: 'π',
+ description: 'Skeleton-Komponenten fΓΌr Loading States',
+ },
];
const packageDetails = details.packages.perPackage;
@@ -268,6 +280,34 @@ const generatedDate = new Date(ecosystemData.generatedAt).toLocaleDateString('de
+
+ {
+ details.todos && details.todos.totalCount > 0 && (
+
+
+ Technical Debt (TODO/FIXME/HACK)
+
+
+
+ Gesamt im Codebase
+ 50 ? 'text-red-500' : details.todos.totalCount > 20 ? 'text-orange-500' : 'text-yellow-500'}`}
+ >
+ {details.todos.totalCount}
+
+
+
+ {Object.entries(details.todos.perApp).map(([app, count]: [string, any]) => (
+
+ {app}: {count}
+
+ ))}
+
+
+
+ )
+ }
+
{
details.fileSizes?.topOffenders?.length > 0 && (
diff --git a/scripts/ecosystem-audit.mjs b/scripts/ecosystem-audit.mjs
index b13f595c3..7628740d2 100644
--- a/scripts/ecosystem-audit.mjs
+++ b/scripts/ecosystem-audit.mjs
@@ -513,6 +513,94 @@ function measureFileSizes() {
};
}
+function measureTodoFixmeCount() {
+ console.log('π Measuring TODO/FIXME Count...');
+ let totalCount = 0;
+ const perApp = {};
+
+ for (const app of WEB_APPS) {
+ const webSrc = join(APPS_DIR, app, 'apps/web/src');
+ if (!existsSync(webSrc)) continue;
+
+ const count =
+ grepOccurrences('TODO', webSrc, '*.svelte') +
+ grepOccurrences('TODO', webSrc, '*.ts') +
+ grepOccurrences('FIXME', webSrc, '*.svelte') +
+ grepOccurrences('FIXME', webSrc, '*.ts') +
+ grepOccurrences('HACK', webSrc, '*.svelte') +
+ grepOccurrences('HACK', webSrc, '*.ts');
+
+ if (count > 0) {
+ perApp[app] = count;
+ totalCount += count;
+ }
+ }
+
+ const sorted = Object.entries(perApp).sort(([, a], [, b]) => b - a);
+ console.log(` Total TODO/FIXME/HACK: ${totalCount}`);
+ if (sorted.length > 0) {
+ sorted.slice(0, 5).forEach(([app, count]) => console.log(` ${app}: ${count}`));
+ }
+ console.log('');
+
+ return { totalCount, perApp: Object.fromEntries(sorted) };
+}
+
+function measureSecurityHeaders() {
+ console.log('π Measuring Security Headers...');
+ let appsWithHeaders = 0;
+ const missing = [];
+
+ for (const app of WEB_APPS) {
+ const webSrc = join(APPS_DIR, app, 'apps/web/src');
+ if (!existsSync(webSrc)) continue;
+
+ const hasHeaders =
+ grepCount('setSecurityHeaders', webSrc, '*.ts') > 0 ||
+ grepCount('Content-Security-Policy', webSrc, '*.ts') > 0 ||
+ grepCount('X-Frame-Options', webSrc, '*.ts') > 0;
+
+ if (hasHeaders) appsWithHeaders++;
+ else missing.push(app);
+ }
+
+ const adoption = Math.round((appsWithHeaders / WEB_APPS.length) * 100);
+ console.log(` Apps with security headers: ${appsWithHeaders}/${WEB_APPS.length} (${adoption}%)`);
+ if (missing.length > 0 && missing.length <= 10) console.log(` Missing: ${missing.join(', ')}`);
+ console.log('');
+
+ return { adoption, appsWithHeaders, missing };
+}
+
+function measureSkeletonLoading() {
+ console.log('π Measuring Skeleton Loading States...');
+ let appsWithSkeletons = 0;
+ const missing = [];
+
+ for (const app of WEB_APPS) {
+ const webSrc = join(APPS_DIR, app, 'apps/web/src');
+ if (!existsSync(webSrc)) continue;
+
+ const hasSkeletons =
+ fileCount('*Skeleton*', webSrc) > 0 ||
+ fileCount('*skeleton*', webSrc) > 0 ||
+ grepCount('Skeleton', webSrc, '*.svelte') > 0 ||
+ grepCount('animate-pulse', webSrc, '*.svelte') > 0;
+
+ if (hasSkeletons) appsWithSkeletons++;
+ else missing.push(app);
+ }
+
+ const adoption = Math.round((appsWithSkeletons / WEB_APPS.length) * 100);
+ console.log(
+ ` Apps with skeleton loading: ${appsWithSkeletons}/${WEB_APPS.length} (${adoption}%)`
+ );
+ if (missing.length > 0 && missing.length <= 10) console.log(` Missing: ${missing.join(', ')}`);
+ console.log('');
+
+ return { adoption, appsWithSkeletons, missing };
+}
+
// ============================================================
// Main
// ============================================================
@@ -532,6 +620,9 @@ const typescript = measureTypeScriptStrictness();
const tests = measureTestCoverage();
const pwa = measurePwaSupport();
const fileSizes = measureFileSizes();
+const todos = measureTodoFixmeCount();
+const securityHeaders = measureSecurityHeaders();
+const skeletons = measureSkeletonLoading();
// Calculate overall scores
const scores = {
@@ -547,6 +638,8 @@ const scores = {
testCoverage: tests.adoption,
pwaSupport: pwa.adoption,
maintainability: fileSizes.adoption,
+ securityHeaders: securityHeaders.adoption,
+ skeletonLoading: skeletons.adoption,
};
// Weighted overall score
@@ -561,8 +654,10 @@ const weights = {
errorBoundaries: 8,
typescriptStrict: 7,
testCoverage: 7,
- pwaSupport: 5,
- maintainability: 5,
+ pwaSupport: 4,
+ maintainability: 4,
+ securityHeaders: 5,
+ skeletonLoading: 3,
};
let totalWeight = 0;
@@ -616,6 +711,9 @@ const output = {
tests,
pwa,
fileSizes,
+ todos,
+ securityHeaders,
+ skeletons,
},
apps: WEB_APPS,
};