diff --git a/apps/manacore/apps/landing/src/data/ecosystem-health.json b/apps/manacore/apps/landing/src/data/ecosystem-health.json index d9561c6cd..24e029ede 100644 --- a/apps/manacore/apps/landing/src/data/ecosystem-health.json +++ b/apps/manacore/apps/landing/src/data/ecosystem-health.json @@ -1,28 +1,38 @@ { - "generatedAt": "2026-03-31T11:53:48.040Z", - "overallScore": 76, + "generatedAt": "2026-03-31T12:04:45.568Z", + "overallScore": 70, "scores": { "sharedPackages": 90, "iconConsistency": 89, - "modalConsistency": 24, + "modalConsistency": 19, "errorHandling": 20, "i18nCoverage": 86, "localFirst": 93, - "styleConsistency": 87 + "styleConsistency": 87, + "errorBoundaries": 54, + "typescriptStrict": 100, + "testCoverage": 72, + "pwaSupport": 2, + "maintainability": 0 }, "weights": { - "sharedPackages": 25, - "iconConsistency": 15, - "modalConsistency": 10, - "errorHandling": 10, - "i18nCoverage": 15, - "localFirst": 10, - "styleConsistency": 15 + "sharedPackages": 20, + "iconConsistency": 10, + "modalConsistency": 5, + "errorHandling": 5, + "i18nCoverage": 10, + "localFirst": 8, + "styleConsistency": 10, + "errorBoundaries": 8, + "typescriptStrict": 7, + "testCoverage": 7, + "pwaSupport": 5, + "maintainability": 5 }, "details": { "icons": { "adoption": 89, - "phosphorFiles": 353, + "phosphorFiles": 354, "inlineSvgFiles": 45, "perApp": { "calc": { @@ -30,7 +40,7 @@ "inlineSvg": 0 }, "calendar": { - "phosphor": 34, + "phosphor": 35, "inlineSvg": 0 }, "chat": { @@ -144,8 +154,8 @@ } }, "modals": { - "adoption": 24, - "total": 51, + "adoption": 19, + "total": 64, "sharedUsage": 12, "focusTrapUsage": 7 }, @@ -212,6 +222,158 @@ "styles": { "themeAdoption": 83, "tailwindAdoption": 90 + }, + "errorBoundaries": { + "adoption": 54, + "errorAdoption": 28, + "offlineAdoption": 79, + "appsWithErrorPage": 8, + "appsWithOfflinePage": 23, + "missing": { + "error": [ + "calc", + "chat", + "citycorners", + "clock", + "context", + "manadeck", + "manavoxel", + "moodlit", + "mukke", + "news", + "nutriphi", + "photos", + "picture", + "planta", + "playground", + "questions", + "skilltree", + "storage", + "uload", + "wisekeep", + "zitare" + ], + "offline": ["manavoxel", "moodlit", "news", "playground", "uload", "wisekeep"] + } + }, + "typescript": { + "adoption": 100, + "strictApps": 29, + "nonStrict": [] + }, + "tests": { + "adoption": 72, + "e2eAdoption": 14, + "appsWithTests": 21, + "appsWithE2e": 4, + "totalTestFiles": 111, + "noTests": [ + "calc", + "inventar", + "manavoxel", + "moodlit", + "news", + "playground", + "uload", + "wisekeep" + ] + }, + "pwa": { + "adoption": 2, + "manifestAdoption": 3, + "swAdoption": 0, + "appsWithManifest": 1, + "appsWithServiceWorker": 0, + "noPwa": [ + "calc", + "calendar", + "chat", + "citycorners", + "clock", + "contacts", + "context", + "inventar", + "manacore", + "manadeck", + "manavoxel", + "matrix", + "moodlit", + "mukke", + "news", + "nutriphi", + "photos", + "picture", + "planta", + "playground", + "presi", + "questions", + "skilltree", + "storage", + "times", + "todo", + "wisekeep", + "zitare" + ] + }, + "fileSizes": { + "adoption": 0, + "totalLargeFiles": 38, + "largestFile": { + "path": "matrix: lib/matrix/store.svelte.ts", + "lines": 2019 + }, + "topOffenders": [ + { + "app": "matrix", + "file": "lib/matrix/store.svelte.ts", + "lines": 2019 + }, + { + "app": "calendar", + "file": "lib/components/event/QuickEventOverlay.svelte", + "lines": 1816 + }, + { + "app": "contacts", + "file": "lib/components/ContactDetailModal.svelte", + "lines": 1511 + }, + { + "app": "todo", + "file": "lib/components/TaskItem.svelte", + "lines": 1217 + }, + { + "app": "todo", + "file": "lib/components/board-views/ViewEditorModal.svelte", + "lines": 1190 + }, + { + "app": "contacts", + "file": "lib/components/NewContactModal.svelte", + "lines": 1130 + }, + { + "app": "calendar", + "file": "lib/components/calendar/WeekView.svelte", + "lines": 1043 + }, + { + "app": "zitare", + "file": "routes/(app)/lists/[id]/+page.svelte", + "lines": 958 + }, + { + "app": "calendar", + "file": "routes/(app)/settings/sync/+page.svelte", + "lines": 898 + }, + { + "app": "matrix", + "file": "routes/(auth)/login/+page.svelte", + "lines": 849 + } + ] } }, "apps": [ diff --git a/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro b/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro index 346d0dea2..d83ca35f0 100644 --- a/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro +++ b/apps/manacore/apps/landing/src/pages/manascore/ecosystem.astro @@ -76,6 +76,36 @@ const categories = [ icon: '๐ŸŽญ', description: 'Theme-Variablen + Tailwind CSS Nutzung', }, + { + key: 'errorBoundaries', + label: 'Error Boundaries', + icon: '๐Ÿ›ก๏ธ', + description: '+error.svelte + Offline Page pro App', + }, + { + key: 'typescriptStrict', + label: 'TypeScript Strict', + icon: '๐Ÿ”’', + description: 'Strict Mode in allen Apps', + }, + { + key: 'testCoverage', + label: 'Test Coverage', + icon: '๐Ÿงช', + description: 'Apps mit mindestens einem Test', + }, + { + key: 'pwaSupport', + label: 'PWA Support', + icon: '๐Ÿ“ฑ', + description: 'Manifest + Service Worker', + }, + { + key: 'maintainability', + label: 'Maintainability', + icon: '๐Ÿ“', + description: 'Dateien unter 500 Zeilen', + }, ]; const packageDetails = details.packages.perPackage; @@ -238,6 +268,62 @@ const generatedDate = new Date(ecosystemData.generatedAt).toLocaleDateString('de + + { + details.fileSizes?.topOffenders?.length > 0 && ( +
+

+ Largest Files (Refactoring-Kandidaten) +

+
+ + + + + + + + + {details.fileSizes.topOffenders.slice(0, 10).map((f: any) => ( + + + + + ))} + +
DateiZeilen
+ {f.app}/ + {f.file} + 1000 ? 'text-red-500' : 'text-orange-500'}`} + > + {f.lines} +
+
+
+ ) + } + + + { + details.tests?.noTests?.length > 0 && ( +
+

Apps ohne Tests

+
+ {details.tests.noTests.map((app: string) => ( + + {app} + + ))} +
+

+ {details.tests.totalTestFiles} Test-Dateien gesamt ·{' '} + {details.tests.appsWithE2e} Apps mit E2E-Tests +

+
+ ) + } +
diff --git a/scripts/ecosystem-audit.mjs b/scripts/ecosystem-audit.mjs index 8128678f5..b13f595c3 100644 --- a/scripts/ecosystem-audit.mjs +++ b/scripts/ecosystem-audit.mjs @@ -296,6 +296,223 @@ function measureStyleConsistency() { return { themeAdoption, tailwindAdoption }; } +function measureErrorBoundaries() { + console.log('๐Ÿ“Š Measuring Error Boundaries...'); + let appsWithErrorPage = 0; + let appsWithOfflinePage = 0; + const missing = { error: [], offline: [] }; + + for (const app of WEB_APPS) { + const webSrc = join(APPS_DIR, app, 'apps/web/src'); + if (!existsSync(webSrc)) continue; + + const hasErrorPage = fileCount('+error.svelte', webSrc) > 0; + const hasOfflinePage = + fileCount('*offline*', webSrc) > 0 || + fileCount('*Offline*', webSrc) > 0 || + grepCount('OfflinePage', webSrc, '*.svelte') > 0; + + if (hasErrorPage) appsWithErrorPage++; + else missing.error.push(app); + if (hasOfflinePage) appsWithOfflinePage++; + else missing.offline.push(app); + } + + const errorAdoption = Math.round((appsWithErrorPage / WEB_APPS.length) * 100); + const offlineAdoption = Math.round((appsWithOfflinePage / WEB_APPS.length) * 100); + const adoption = Math.round((errorAdoption + offlineAdoption) / 2); + + console.log(` +error.svelte: ${appsWithErrorPage}/${WEB_APPS.length} (${errorAdoption}%)`); + console.log(` Offline page: ${appsWithOfflinePage}/${WEB_APPS.length} (${offlineAdoption}%)`); + if (missing.error.length > 0) console.log(` Missing error page: ${missing.error.join(', ')}`); + console.log(` Combined: ${adoption}%\n`); + + return { + adoption, + errorAdoption, + offlineAdoption, + appsWithErrorPage, + appsWithOfflinePage, + missing, + }; +} + +function measureTypeScriptStrictness() { + console.log('๐Ÿ“Š Measuring TypeScript Strictness...'); + let strictApps = 0; + const nonStrict = []; + + for (const app of WEB_APPS) { + const tsconfigPaths = [ + join(APPS_DIR, app, 'apps/web/tsconfig.json'), + join(APPS_DIR, app, 'apps/web/.svelte-kit/tsconfig.json'), + ]; + + let isStrict = false; + for (const p of tsconfigPaths) { + try { + const content = readFileSync(p, 'utf-8'); + if (content.includes('"strict": true') || content.includes('"strict":true')) { + isStrict = true; + break; + } + } catch {} + } + + // SvelteKit apps inherit strict from .svelte-kit/tsconfig.json which extends the user config + // Check if svelte.config.js exists (indicates SvelteKit = inherits strict) + if (!isStrict && existsSync(join(APPS_DIR, app, 'apps/web/svelte.config.js'))) { + isStrict = true; // SvelteKit default is strict + } + + if (isStrict) strictApps++; + else nonStrict.push(app); + } + + const adoption = Math.round((strictApps / WEB_APPS.length) * 100); + console.log(` Strict apps: ${strictApps}/${WEB_APPS.length} (${adoption}%)`); + if (nonStrict.length > 0) console.log(` Non-strict: ${nonStrict.join(', ')}`); + console.log(''); + + return { adoption, strictApps, nonStrict }; +} + +function measureTestCoverage() { + console.log('๐Ÿ“Š Measuring Test Coverage...'); + let appsWithTests = 0; + let appsWithE2e = 0; + let totalTestFiles = 0; + const noTests = []; + + for (const app of WEB_APPS) { + const appBase = join(APPS_DIR, app); + + const unitTests = + fileCount('*.test.ts', appBase) + + fileCount('*.spec.ts', appBase) + + fileCount('*.test.js', appBase); + const e2eTests = + fileCount('*.spec.ts', join(appBase, 'apps/web/e2e')) + + fileCount('*.spec.ts', join(appBase, 'apps/web/tests')); + + totalTestFiles += unitTests + e2eTests; + + if (unitTests > 0 || e2eTests > 0) appsWithTests++; + else noTests.push(app); + if (e2eTests > 0) appsWithE2e++; + } + + const adoption = Math.round((appsWithTests / WEB_APPS.length) * 100); + const e2eAdoption = Math.round((appsWithE2e / WEB_APPS.length) * 100); + + console.log(` Apps with tests: ${appsWithTests}/${WEB_APPS.length} (${adoption}%)`); + console.log(` Apps with E2E: ${appsWithE2e}/${WEB_APPS.length} (${e2eAdoption}%)`); + console.log(` Total test files: ${totalTestFiles}`); + if (noTests.length > 0 && noTests.length <= 10) console.log(` No tests: ${noTests.join(', ')}`); + console.log(''); + + return { adoption, e2eAdoption, appsWithTests, appsWithE2e, totalTestFiles, noTests }; +} + +function measurePwaSupport() { + console.log('๐Ÿ“Š Measuring PWA Support...'); + let appsWithManifest = 0; + let appsWithServiceWorker = 0; + const noPwa = []; + + for (const app of WEB_APPS) { + const webDir = join(APPS_DIR, app, 'apps/web'); + const webSrc = join(webDir, 'src'); + const staticDir = join(webDir, 'static'); + + const hasManifest = + existsSync(join(staticDir, 'manifest.json')) || + existsSync(join(staticDir, 'manifest.webmanifest')) || + grepCount('manifest', webSrc, '*.html') > 0 || + grepCount('manifest', join(webSrc, 'app.html'), '*') > 0; + + const hasSw = + existsSync(join(staticDir, 'sw.js')) || + existsSync(join(staticDir, 'service-worker.js')) || + existsSync(join(webSrc, 'service-worker.ts')) || + existsSync(join(webSrc, 'service-worker.js')) || + grepCount('serviceWorker', webSrc, '*.ts') > 0; + + if (hasManifest) appsWithManifest++; + if (hasSw) appsWithServiceWorker++; + if (!hasManifest && !hasSw) noPwa.push(app); + } + + const manifestAdoption = Math.round((appsWithManifest / WEB_APPS.length) * 100); + const swAdoption = Math.round((appsWithServiceWorker / WEB_APPS.length) * 100); + const adoption = Math.round((manifestAdoption + swAdoption) / 2); + + console.log(` Manifest: ${appsWithManifest}/${WEB_APPS.length} (${manifestAdoption}%)`); + console.log(` Service Worker: ${appsWithServiceWorker}/${WEB_APPS.length} (${swAdoption}%)`); + if (noPwa.length > 0 && noPwa.length <= 10) console.log(` No PWA: ${noPwa.join(', ')}`); + console.log(''); + + return { adoption, manifestAdoption, swAdoption, appsWithManifest, appsWithServiceWorker, noPwa }; +} + +function measureFileSizes() { + console.log('๐Ÿ“Š Measuring File Sizes (Maintainability)...'); + let totalLargeFiles = 0; + let largestFile = { path: '', lines: 0 }; + const largeFiles = []; + + for (const app of WEB_APPS) { + const webSrc = join(APPS_DIR, app, 'apps/web/src'); + if (!existsSync(webSrc)) continue; + + try { + const result = execSync( + `find ${webSrc} -name "*.svelte" -o -name "*.ts" | xargs wc -l 2>/dev/null | sort -rn | head -5`, + { encoding: 'utf-8' } + ); + const lines = result.trim().split('\n'); + for (const line of lines) { + const match = line.trim().match(/^(\d+)\s+(.+)$/); + if (match && !match[2].includes('total')) { + const lineCount = parseInt(match[1]); + const filePath = match[2]; + if (lineCount > 500) { + totalLargeFiles++; + largeFiles.push({ app, file: filePath.replace(`${webSrc}/`, ''), lines: lineCount }); + } + if (lineCount > largestFile.lines) { + largestFile = { + path: `${app}: ${filePath.replace(`${webSrc}/`, '')}`, + lines: lineCount, + }; + } + } + } + } catch {} + } + + // Score: fewer large files = better (100% = no files >500 lines, deduct per large file) + const adoption = Math.max(0, 100 - totalLargeFiles * 3); + + console.log(` Files > 500 lines: ${totalLargeFiles}`); + console.log(` Largest: ${largestFile.path} (${largestFile.lines} lines)`); + if (largeFiles.length > 0) { + console.log(' Top offenders:'); + largeFiles + .sort((a, b) => b.lines - a.lines) + .slice(0, 5) + .forEach((f) => console.log(` ${f.lines} lines โ€” ${f.app}/${f.file}`)); + } + console.log(''); + + return { + adoption, + totalLargeFiles, + largestFile, + topOffenders: largeFiles.sort((a, b) => b.lines - a.lines).slice(0, 10), + }; +} + // ============================================================ // Main // ============================================================ @@ -310,6 +527,11 @@ const errors = measureErrorHandling(); const i18n = measureI18nConsistency(); const localFirst = measureLocalFirstAdoption(); const styles = measureStyleConsistency(); +const errorBoundaries = measureErrorBoundaries(); +const typescript = measureTypeScriptStrictness(); +const tests = measureTestCoverage(); +const pwa = measurePwaSupport(); +const fileSizes = measureFileSizes(); // Calculate overall scores const scores = { @@ -320,17 +542,27 @@ const scores = { i18nCoverage: i18n.adoption, localFirst: localFirst.adoption, styleConsistency: Math.round((styles.themeAdoption + styles.tailwindAdoption) / 2), + errorBoundaries: errorBoundaries.adoption, + typescriptStrict: typescript.adoption, + testCoverage: tests.adoption, + pwaSupport: pwa.adoption, + maintainability: fileSizes.adoption, }; // Weighted overall score const weights = { - sharedPackages: 25, - iconConsistency: 15, - modalConsistency: 10, - errorHandling: 10, - i18nCoverage: 15, - localFirst: 10, - styleConsistency: 15, + sharedPackages: 20, + iconConsistency: 10, + modalConsistency: 5, + errorHandling: 5, + i18nCoverage: 10, + localFirst: 8, + styleConsistency: 10, + errorBoundaries: 8, + typescriptStrict: 7, + testCoverage: 7, + pwaSupport: 5, + maintainability: 5, }; let totalWeight = 0; @@ -344,25 +576,13 @@ const overallScore = Math.round(weightedSum / totalWeight); console.log('='.repeat(50)); console.log('\n๐Ÿ† ECOSYSTEM HEALTH SCORE\n'); console.log(` Overall: ${overallScore}/100\n`); -console.log( - ` Shared Package Adoption: ${scores.sharedPackages}% (weight: ${weights.sharedPackages}%)` -); -console.log( - ` Icon Consistency: ${scores.iconConsistency}% (weight: ${weights.iconConsistency}%)` -); -console.log( - ` Modal Consistency: ${scores.modalConsistency}% (weight: ${weights.modalConsistency}%)` -); -console.log( - ` Error Handling: ${scores.errorHandling}% (weight: ${weights.errorHandling}%)` -); -console.log( - ` i18n Coverage: ${scores.i18nCoverage}% (weight: ${weights.i18nCoverage}%)` -); -console.log(` Local-First: ${scores.localFirst}% (weight: ${weights.localFirst}%)`); -console.log( - ` Style Consistency: ${scores.styleConsistency}% (weight: ${weights.styleConsistency}%)` -); +const pad = (s, n) => s.padEnd(n); +for (const [key, weight] of Object.entries(weights)) { + const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase()); + console.log( + ` ${pad(label + ':', 28)} ${String(scores[key]).padStart(3)}% (weight: ${weight}%)` + ); +} // Generate output JSON const output = { @@ -391,6 +611,11 @@ const output = { i18n, localFirst, styles, + errorBoundaries, + typescript, + tests, + pwa, + fileSizes, }, apps: WEB_APPS, };