diff --git a/apps/manacore/apps/web/src/lib/components/observatory/SeenplatteScene.svelte b/apps/manacore/apps/web/src/lib/components/observatory/SeenplatteScene.svelte
new file mode 100644
index 000000000..6b82157f3
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/SeenplatteScene.svelte
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+ Mana Seenplatte
+
+
Ecosystem Observatory
+
+
+
+
+
+
+ Mature
+
+
+
+ Production
+
+
+
+ Beta
+
+
+
+ Alpha
+
+
+
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/colors.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/colors.ts
new file mode 100644
index 000000000..74856010b
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/data/colors.ts
@@ -0,0 +1,84 @@
+// Natural color palette for the Seenplatte ecosystem
+
+export const sky = {
+ dayTop: '#87CEEB',
+ dayBottom: '#E0F0FF',
+ duskTop: '#2C1654',
+ duskBottom: '#E8956A',
+ nightTop: '#0B1026',
+ nightBottom: '#1A2444',
+} as const;
+
+export const mountains = {
+ far: '#8BA4B8',
+ mid: '#6B8A9E',
+ near: '#4A7085',
+ snow: '#E8F0F4',
+} as const;
+
+export const water = {
+ shallow: '#7EC8D9',
+ mid: '#4BA3B5',
+ deep: '#2A7A8C',
+ veryDeep: '#1A5666',
+ river: '#5BB5C5',
+ highlight: '#A8E4F0',
+ foam: '#E8F6FA',
+} as const;
+
+export const terrain = {
+ meadow: '#7DB86A',
+ meadowLight: '#96CC86',
+ meadowDark: '#5E9A4D',
+ shore: '#C4B48A',
+ shoreDark: '#A89870',
+ path: '#D4C4A0',
+ rock: '#8C8C80',
+ rockLight: '#A8A89C',
+} as const;
+
+export const vegetation = {
+ // Tree health colors based on ManaScore
+ healthyDark: '#2D6B30',
+ healthy: '#3E8B42',
+ healthyLight: '#5AAB5E',
+ moderate: '#7DB86A',
+ warning: '#C4B848',
+ stressed: '#D4944A',
+ critical: '#B85C4A',
+
+ // Specific plant colors
+ trunk: '#6B4E3D',
+ trunkLight: '#8B6E5D',
+ reed: '#7DA868',
+ reedDark: '#5A8548',
+ lilyPad: '#4A8B50',
+ lilyFlower: '#E8B0D0',
+ lilyFlowerCenter: '#F0D060',
+ moss: '#5A8B4A',
+ mossLight: '#78A868',
+ sprout: '#90C880',
+ sproutStake: '#A89070',
+} as const;
+
+/**
+ * Get tree crown color based on ManaScore (0-100)
+ */
+export function getHealthColor(score: number): string {
+ if (score >= 85) return vegetation.healthyDark;
+ if (score >= 70) return vegetation.healthy;
+ if (score >= 55) return vegetation.moderate;
+ if (score >= 40) return vegetation.warning;
+ if (score >= 25) return vegetation.stressed;
+ return vegetation.critical;
+}
+
+/**
+ * Get water clarity based on error rate (0 = clear, 1 = murky)
+ */
+export function getWaterColor(clarity: number): string {
+ if (clarity > 0.8) return water.shallow;
+ if (clarity > 0.5) return water.mid;
+ if (clarity > 0.2) return water.deep;
+ return water.veryDeep;
+}
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/layout.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/layout.ts
new file mode 100644
index 000000000..9775aa896
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/data/layout.ts
@@ -0,0 +1,172 @@
+import type { LakeData, RiverData } from './types';
+
+// Scene dimensions (SVG viewBox)
+export const SCENE = {
+ width: 1600,
+ height: 900,
+ viewBox: '0 0 1600 900',
+} as const;
+
+// Mountain layer paths (3 layers, back to front)
+export const MOUNTAIN_PATHS = {
+ far: 'M0,280 Q200,180 400,240 Q500,210 600,250 Q750,160 900,220 Q1050,180 1200,230 Q1350,170 1500,210 L1600,240 L1600,350 L0,350 Z',
+ mid: 'M0,310 Q150,250 300,290 Q450,240 550,280 Q700,220 850,270 Q1000,230 1150,275 Q1300,240 1450,265 L1600,280 L1600,380 L0,380 Z',
+ near: 'M0,340 Q100,300 250,330 Q400,290 500,320 Q650,280 800,315 Q950,285 1100,320 Q1250,295 1400,310 L1600,320 L1600,400 L0,400 Z',
+} as const;
+
+// Lake definitions with organic SVG paths
+export const LAKES: LakeData[] = [
+ {
+ id: 'auth',
+ name: 'auth',
+ label: 'Zentralsee',
+ path: 'M680,440 Q720,410 790,415 Q860,420 890,445 Q910,470 900,500 Q885,535 850,550 Q800,565 750,558 Q700,550 680,525 Q660,500 665,470 Q668,450 680,440 Z',
+ color: '#4BA3B5',
+ colorDeep: '#2A7A8C',
+ clarity: 1,
+ level: 0.8,
+ position: { x: 790, y: 485 },
+ },
+ {
+ id: 'redis',
+ name: 'redis',
+ label: 'Bergsee',
+ path: 'M280,390 Q310,370 350,375 Q390,380 405,400 Q415,420 405,440 Q390,455 355,460 Q320,462 295,450 Q275,435 270,415 Q268,398 280,390 Z',
+ color: '#7EC8D9',
+ colorDeep: '#4BA3B5',
+ clarity: 1,
+ level: 0.9,
+ position: { x: 340, y: 420 },
+ },
+ {
+ id: 'minio',
+ name: 'minio',
+ label: 'Stausee',
+ path: 'M1180,400 Q1220,375 1280,380 Q1340,385 1370,410 Q1390,435 1380,465 Q1360,490 1320,500 Q1270,510 1220,502 Q1185,492 1170,468 Q1158,445 1165,420 Q1170,405 1180,400 Z',
+ color: '#5BB5C5',
+ colorDeep: '#3A8A9A',
+ clarity: 0.9,
+ level: 0.7,
+ position: { x: 1275, y: 440 },
+ },
+ {
+ id: 'db-left',
+ name: 'postgres-left',
+ label: 'Waldsee West',
+ path: 'M180,580 Q220,555 280,560 Q340,565 365,590 Q380,615 370,640 Q355,665 310,675 Q260,682 215,670 Q180,658 165,635 Q155,612 165,595 Q170,582 180,580 Z',
+ color: '#3A8A9A',
+ colorDeep: '#1A5666',
+ clarity: 0.85,
+ level: 0.75,
+ position: { x: 270, y: 620 },
+ },
+ {
+ id: 'db-center',
+ name: 'postgres-center',
+ label: 'Waldsee Mitte',
+ path: 'M650,620 Q700,595 770,600 Q840,605 870,635 Q890,660 878,690 Q862,718 810,730 Q760,738 710,730 Q660,720 640,695 Q625,670 632,645 Q638,628 650,620 Z',
+ color: '#3A8A9A',
+ colorDeep: '#1A5666',
+ clarity: 0.9,
+ level: 0.8,
+ position: { x: 760, y: 665 },
+ },
+ {
+ id: 'db-right',
+ name: 'postgres-right',
+ label: 'Waldsee Ost',
+ path: 'M1120,590 Q1160,568 1220,572 Q1280,578 1305,600 Q1322,625 1315,652 Q1300,678 1255,690 Q1210,698 1165,688 Q1130,678 1115,655 Q1102,632 1108,610 Q1112,595 1120,590 Z',
+ color: '#3A8A9A',
+ colorDeep: '#1A5666',
+ clarity: 0.85,
+ level: 0.75,
+ position: { x: 1215, y: 630 },
+ },
+];
+
+// Rivers connecting the lakes
+export const RIVERS: RiverData[] = [
+ {
+ id: 'redis-to-auth',
+ from: 'redis',
+ to: 'auth',
+ path: 'M405,430 Q480,435 540,440 Q600,445 665,460',
+ flowSpeed: 0.8,
+ width: 8,
+ },
+ {
+ id: 'minio-to-auth',
+ from: 'minio',
+ to: 'auth',
+ path: 'M1170,450 Q1100,455 1020,460 Q940,465 900,475',
+ flowSpeed: 0.6,
+ width: 8,
+ },
+ {
+ id: 'auth-to-db-left',
+ from: 'auth',
+ to: 'db-left',
+ path: 'M710,555 Q620,570 520,580 Q420,590 365,595',
+ flowSpeed: 0.7,
+ width: 10,
+ },
+ {
+ id: 'auth-to-db-center',
+ from: 'auth',
+ to: 'db-center',
+ path: 'M790,560 Q785,580 778,600 Q770,610 765,620',
+ flowSpeed: 0.7,
+ width: 10,
+ },
+ {
+ id: 'auth-to-db-right',
+ from: 'auth',
+ to: 'db-right',
+ path: 'M870,548 Q960,565 1040,575 Q1080,582 1115,592',
+ flowSpeed: 0.7,
+ width: 10,
+ },
+ {
+ id: 'inlet',
+ from: 'source',
+ to: 'auth',
+ path: 'M790,350 Q792,370 790,390 Q788,410 790,420',
+ flowSpeed: 0.9,
+ width: 6,
+ },
+];
+
+// App positions around the lakes
+export const APP_POSITIONS: Record = {
+ // Around Zentralsee (auth) - core/mature apps
+ manacore: { x: 730, y: 410, lakeId: 'auth' },
+ chat: { x: 860, y: 420, lakeId: 'auth' },
+ picture: { x: 910, y: 480, lakeId: 'auth' },
+ presi: { x: 660, y: 490, lakeId: 'auth' },
+
+ // Around Waldsee West (db-left)
+ calendar: { x: 170, y: 560, lakeId: 'db-left' },
+ todo: { x: 330, y: 555, lakeId: 'db-left' },
+ contacts: { x: 370, y: 630, lakeId: 'db-left' },
+ storage: { x: 160, y: 650, lakeId: 'db-left' },
+
+ // Around Waldsee Mitte (db-center)
+ zitare: { x: 640, y: 600, lakeId: 'db-center' },
+ mukke: { x: 850, y: 610, lakeId: 'db-center' },
+ clock: { x: 880, y: 680, lakeId: 'db-center' },
+ nutriphi: { x: 650, y: 720, lakeId: 'db-center' },
+
+ // Around Waldsee Ost (db-right)
+ photos: { x: 1110, y: 575, lakeId: 'db-right' },
+ skilltree: { x: 1310, y: 590, lakeId: 'db-right' },
+ context: { x: 1320, y: 660, lakeId: 'db-right' },
+ planta: { x: 1115, y: 675, lakeId: 'db-right' },
+
+ // Around Bergsee (redis) - lightweight/cache
+ matrix: { x: 260, y: 375, lakeId: 'redis' },
+ traces: { x: 400, y: 385, lakeId: 'redis' },
+
+ // Around Stausee (minio) - storage-heavy
+ manadeck: { x: 1180, y: 385, lakeId: 'minio' },
+ questions: { x: 1370, y: 400, lakeId: 'minio' },
+};
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/mockData.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/mockData.ts
new file mode 100644
index 000000000..bdb160166
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/data/mockData.ts
@@ -0,0 +1,362 @@
+import type { AppData, AppStatus, PlantType, CategoryScores } from './types';
+import { APP_POSITIONS } from './layout';
+
+interface AppDefinition {
+ id: string;
+ displayName: string;
+ score: number;
+ status: AppStatus;
+ categories: CategoryScores;
+}
+
+function getPlantType(status: AppStatus, score: number): PlantType {
+ if (score >= 85) return 'oak';
+ if (score >= 70) return 'birch';
+ if (status === 'beta' && score >= 55) return 'youngTree';
+ if (status === 'beta') return 'reed';
+ if (status === 'alpha') return 'sprout';
+ if (status === 'prototype') return 'sprout';
+ return 'youngTree';
+}
+
+const APP_DEFINITIONS: AppDefinition[] = [
+ {
+ id: 'calendar',
+ displayName: 'Calendar',
+ score: 97,
+ status: 'mature',
+ categories: {
+ backend: 95,
+ frontend: 96,
+ database: 95,
+ testing: 98,
+ deployment: 100,
+ documentation: 98,
+ security: 95,
+ ux: 98,
+ },
+ },
+ {
+ id: 'todo',
+ displayName: 'Todo',
+ score: 96,
+ status: 'mature',
+ categories: {
+ backend: 94,
+ frontend: 95,
+ database: 95,
+ testing: 97,
+ deployment: 98,
+ documentation: 95,
+ security: 95,
+ ux: 96,
+ },
+ },
+ {
+ id: 'contacts',
+ displayName: 'Contacts',
+ score: 94,
+ status: 'production',
+ categories: {
+ backend: 92,
+ frontend: 90,
+ database: 95,
+ testing: 92,
+ deployment: 95,
+ documentation: 95,
+ security: 95,
+ ux: 94,
+ },
+ },
+ {
+ id: 'manacore',
+ displayName: 'ManaCore',
+ score: 88,
+ status: 'production',
+ categories: {
+ backend: 55,
+ frontend: 90,
+ database: 70,
+ testing: 72,
+ deployment: 90,
+ documentation: 88,
+ security: 80,
+ ux: 92,
+ },
+ },
+ {
+ id: 'presi',
+ displayName: 'Presi',
+ score: 86,
+ status: 'mature',
+ categories: {
+ backend: 90,
+ frontend: 82,
+ database: 85,
+ testing: 80,
+ deployment: 90,
+ documentation: 85,
+ security: 85,
+ ux: 88,
+ },
+ },
+ {
+ id: 'storage',
+ displayName: 'Storage',
+ score: 84,
+ status: 'production',
+ categories: {
+ backend: 88,
+ frontend: 84,
+ database: 82,
+ testing: 78,
+ deployment: 88,
+ documentation: 82,
+ security: 85,
+ ux: 82,
+ },
+ },
+ {
+ id: 'chat',
+ displayName: 'Chat',
+ score: 82,
+ status: 'production',
+ categories: {
+ backend: 90,
+ frontend: 82,
+ database: 80,
+ testing: 68,
+ deployment: 88,
+ documentation: 85,
+ security: 80,
+ ux: 78,
+ },
+ },
+ {
+ id: 'picture',
+ displayName: 'Picture',
+ score: 81,
+ status: 'production',
+ categories: {
+ backend: 90,
+ frontend: 80,
+ database: 75,
+ testing: 65,
+ deployment: 85,
+ documentation: 82,
+ security: 82,
+ ux: 80,
+ },
+ },
+ {
+ id: 'mukke',
+ displayName: 'Mukke',
+ score: 80,
+ status: 'beta',
+ categories: {
+ backend: 90,
+ frontend: 78,
+ database: 80,
+ testing: 60,
+ deployment: 85,
+ documentation: 78,
+ security: 80,
+ ux: 80,
+ },
+ },
+ {
+ id: 'matrix',
+ displayName: 'Matrix',
+ score: 68,
+ status: 'production',
+ categories: {
+ backend: 10,
+ frontend: 78,
+ database: 60,
+ testing: 50,
+ deployment: 85,
+ documentation: 70,
+ security: 75,
+ ux: 72,
+ },
+ },
+ {
+ id: 'nutriphi',
+ displayName: 'NutriPhi',
+ score: 63,
+ status: 'beta',
+ categories: {
+ backend: 78,
+ frontend: 62,
+ database: 65,
+ testing: 42,
+ deployment: 70,
+ documentation: 58,
+ security: 62,
+ ux: 65,
+ },
+ },
+ {
+ id: 'photos',
+ displayName: 'Photos',
+ score: 62,
+ status: 'beta',
+ categories: {
+ backend: 82,
+ frontend: 65,
+ database: 60,
+ testing: 38,
+ deployment: 68,
+ documentation: 55,
+ security: 60,
+ ux: 62,
+ },
+ },
+ {
+ id: 'zitare',
+ displayName: 'Zitare',
+ score: 62,
+ status: 'beta',
+ categories: {
+ backend: 72,
+ frontend: 78,
+ database: 60,
+ testing: 38,
+ deployment: 68,
+ documentation: 55,
+ security: 58,
+ ux: 65,
+ },
+ },
+ {
+ id: 'context',
+ displayName: 'Context',
+ score: 60,
+ status: 'beta',
+ categories: {
+ backend: 75,
+ frontend: 75,
+ database: 55,
+ testing: 32,
+ deployment: 65,
+ documentation: 52,
+ security: 58,
+ ux: 60,
+ },
+ },
+ {
+ id: 'clock',
+ displayName: 'Clock',
+ score: 58,
+ status: 'beta',
+ categories: {
+ backend: 75,
+ frontend: 70,
+ database: 55,
+ testing: 30,
+ deployment: 62,
+ documentation: 48,
+ security: 55,
+ ux: 58,
+ },
+ },
+ {
+ id: 'skilltree',
+ displayName: 'SkillTree',
+ score: 58,
+ status: 'beta',
+ categories: {
+ backend: 65,
+ frontend: 68,
+ database: 55,
+ testing: 35,
+ deployment: 60,
+ documentation: 50,
+ security: 55,
+ ux: 60,
+ },
+ },
+ {
+ id: 'planta',
+ displayName: 'Planta',
+ score: 50,
+ status: 'alpha',
+ categories: {
+ backend: 68,
+ frontend: 58,
+ database: 45,
+ testing: 25,
+ deployment: 55,
+ documentation: 42,
+ security: 48,
+ ux: 50,
+ },
+ },
+ {
+ id: 'manadeck',
+ displayName: 'ManaDeck',
+ score: 48,
+ status: 'alpha',
+ categories: {
+ backend: 50,
+ frontend: 65,
+ database: 42,
+ testing: 28,
+ deployment: 52,
+ documentation: 45,
+ security: 42,
+ ux: 55,
+ },
+ },
+ {
+ id: 'questions',
+ displayName: 'Questions',
+ score: 48,
+ status: 'alpha',
+ categories: {
+ backend: 88,
+ frontend: 62,
+ database: 40,
+ testing: 20,
+ deployment: 45,
+ documentation: 38,
+ security: 40,
+ ux: 42,
+ },
+ },
+ {
+ id: 'traces',
+ displayName: 'Traces',
+ score: 35,
+ status: 'alpha',
+ categories: {
+ backend: 72,
+ frontend: 10,
+ database: 35,
+ testing: 15,
+ deployment: 40,
+ documentation: 30,
+ security: 35,
+ ux: 25,
+ },
+ },
+];
+
+export function createMockEcosystem(): AppData[] {
+ return APP_DEFINITIONS.map((def) => {
+ const pos = APP_POSITIONS[def.id] || { x: 800, y: 500, lakeId: 'auth' };
+ return {
+ id: def.id,
+ name: def.id,
+ displayName: def.displayName,
+ score: def.score,
+ status: def.status,
+ health: 'up' as const,
+ plantType: getPlantType(def.status, def.score),
+ categories: def.categories,
+ trend: 0,
+ lakeId: pos.lakeId,
+ position: { x: pos.x, y: pos.y },
+ };
+ });
+}
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/data/types.ts b/apps/manacore/apps/web/src/lib/components/observatory/data/types.ts
new file mode 100644
index 000000000..51d495acc
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/data/types.ts
@@ -0,0 +1,69 @@
+export type PlantType =
+ | 'oak'
+ | 'birch'
+ | 'youngTree'
+ | 'reed'
+ | 'waterLily'
+ | 'moss'
+ | 'shrub'
+ | 'sprout'
+ | 'stump'
+ | 'swampCluster';
+
+export type AppStatus = 'prototype' | 'alpha' | 'beta' | 'production' | 'mature';
+
+export type HealthStatus = 'up' | 'degraded' | 'down' | 'unknown';
+
+export interface CategoryScores {
+ backend: number;
+ frontend: number;
+ database: number;
+ testing: number;
+ deployment: number;
+ documentation: number;
+ security: number;
+ ux: number;
+}
+
+export interface AppData {
+ id: string;
+ name: string;
+ displayName: string;
+ score: number;
+ status: AppStatus;
+ health: HealthStatus;
+ plantType: PlantType;
+ categories: CategoryScores;
+ trend: number;
+ lakeId: string;
+ position: { x: number; y: number };
+}
+
+export interface LakeData {
+ id: string;
+ name: string;
+ label: string;
+ path: string;
+ color: string;
+ colorDeep: string;
+ clarity: number; // 0-1, 1 = crystal clear
+ level: number; // 0-1, normalized fill level
+ position: { x: number; y: number };
+}
+
+export interface RiverData {
+ id: string;
+ from: string;
+ to: string;
+ path: string;
+ flowSpeed: number; // 0-1
+ width: number;
+}
+
+export interface EcosystemState {
+ apps: AppData[];
+ lakes: LakeData[];
+ rivers: RiverData[];
+ timeOfDay: number; // 0-24
+ systemHealth: 'sunny' | 'cloudy' | 'stormy';
+}
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/MossCluster.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/MossCluster.svelte
new file mode 100644
index 000000000..724fdae38
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/MossCluster.svelte
@@ -0,0 +1,63 @@
+
+
+ e.key === 'Enter' && onclick?.()}
+>
+ {#each patches as patch}
+
+ {/each}
+
+
+
+ {app.displayName}
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/PlantFactory.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/PlantFactory.svelte
new file mode 100644
index 000000000..74fd31aef
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/PlantFactory.svelte
@@ -0,0 +1,25 @@
+
+
+{#if app.plantType === 'oak' || app.plantType === 'birch' || app.plantType === 'youngTree'}
+
+{:else if app.plantType === 'reed'}
+
+{:else if app.plantType === 'waterLily'}
+
+{:else if app.plantType === 'sprout'}
+
+{:else if app.plantType === 'moss'}
+
+{:else}
+
+
+{/if}
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/ReedPlant.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/ReedPlant.svelte
new file mode 100644
index 000000000..ccdfa9dfa
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/ReedPlant.svelte
@@ -0,0 +1,98 @@
+
+
+ e.key === 'Enter' && onclick?.()}
+>
+
+
+
+ {#each stalkData as stalk}
+
+
+
+
+ {#if stalk.hasBulrush}
+
+ {/if}
+ {/each}
+
+
+
+
+
+
+
+
+ {app.displayName}
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/Sprout.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/Sprout.svelte
new file mode 100644
index 000000000..7d6872a75
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/Sprout.svelte
@@ -0,0 +1,81 @@
+
+
+ e.key === 'Enter' && onclick?.()}
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {app.displayName}
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/TreePlant.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/TreePlant.svelte
new file mode 100644
index 000000000..67f108a5f
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/TreePlant.svelte
@@ -0,0 +1,117 @@
+
+
+ e.key === 'Enter' && onclick?.()}
+>
+
+
+
+
+
+
+
+
+
+ {#if isOak}
+
+
+
+
+ {/if}
+
+
+
+ {#each blobs as blob, i}
+ {@const lightness = i % 3 === 0 ? 15 : i % 3 === 1 ? 0 : -10}
+
+ {/each}
+
+
+
+
+
+
+
+
+ {app.displayName}
+
+
+ {app.score}
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/plants/WaterLily.svelte b/apps/manacore/apps/web/src/lib/components/observatory/plants/WaterLily.svelte
new file mode 100644
index 000000000..6d74057ef
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/plants/WaterLily.svelte
@@ -0,0 +1,118 @@
+
+
+ e.key === 'Enter' && onclick?.()}
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if app.score > 20}
+
+
+ {#each petalData as petal}
+ {@const px = Math.cos(petal.angle) * 5 * bloomAmount}
+ {@const py = Math.sin(petal.angle) * 3 * bloomAmount}
+
+ {/each}
+
+
+
+
+ {/if}
+
+
+
+
+ {app.displayName}
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/terrain/Background.svelte b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Background.svelte
new file mode 100644
index 000000000..4744cb934
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Background.svelte
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each Array(60) as _, i}
+ {@const x = i * 28 + 10}
+ {@const baseY = 335 + Math.sin(i * 0.8) * 15}
+ {@const h = 12 + Math.sin(i * 1.3) * 5}
+
+ {/each}
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/terrain/Terrain.svelte b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Terrain.svelte
new file mode 100644
index 000000000..9438136d0
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/terrain/Terrain.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each Array(40) as _, i}
+ {@const x = i * 42 + 15}
+ {@const y = 820 + Math.sin(i * 2.1) * 20}
+
+ {/each}
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/terrain/WaterBody.svelte b/apps/manacore/apps/web/src/lib/components/observatory/terrain/WaterBody.svelte
new file mode 100644
index 000000000..ac7361670
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/terrain/WaterBody.svelte
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {lake.label}
+
+
diff --git a/apps/manacore/apps/web/src/lib/components/observatory/water/RiverFlow.svelte b/apps/manacore/apps/web/src/lib/components/observatory/water/RiverFlow.svelte
new file mode 100644
index 000000000..a1a3f3ec4
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/observatory/water/RiverFlow.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte
index 473329979..7ae8c6663 100644
--- a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte
+++ b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte
@@ -82,6 +82,7 @@
let baseNavItems: PillNavItem[] = [
{ href: '/home', label: 'Home', icon: 'home' },
{ href: '/dashboard', label: 'Dashboard', icon: 'grid' },
+ { href: '/observatory', label: 'Observatory', icon: 'eye' },
{ href: '/credits', label: 'Credits', icon: 'creditCard' },
{ href: '/gifts', label: 'Geschenke', icon: 'gift' },
{ href: '/api-keys', label: 'API Keys', icon: 'key' },
diff --git a/apps/manacore/apps/web/src/routes/(app)/observatory/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/observatory/+page.svelte
new file mode 100644
index 000000000..054187875
--- /dev/null
+++ b/apps/manacore/apps/web/src/routes/(app)/observatory/+page.svelte
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ {#if selectedApp}
+
+
+ Selected: {selectedApp} — Detail panel coming in Phase 5
+
+
+ {/if}
+