From c33339b0cfcef0634951301c5d6b5d50dcf9284a Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 30 Mar 2026 15:44:18 +0200 Subject: [PATCH] rename(taktik): rebrand to Times MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename taktik → times across the entire app: package names (@taktik → @times), appId, localStorage keys, export filenames, type names (TaktikSettings → TimesSettings), monorepo scripts, shared-branding, mana-auth trustedOrigins, docker-compose, and documentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 4 +- ...26-03-30-taktik.md => 2026-03-30-times.md} | 8 +- apps/taktik/package.json | 14 - apps/{taktik => times}/CLAUDE.md | 20 +- apps/{taktik => times}/apps/web/Dockerfile | 14 +- apps/{taktik => times}/apps/web/package.json | 4 +- apps/{taktik => times}/apps/web/src/app.css | 0 apps/{taktik => times}/apps/web/src/app.html | 2 +- .../src/lib/components/ConfirmDialog.svelte | 0 .../web/src/lib/components/EntryForm.svelte | 2 +- .../web/src/lib/components/EntryItem.svelte | 2 +- .../web/src/lib/components/EntryList.svelte | 2 +- .../lib/components/KeyboardShortcuts.svelte | 0 .../web/src/lib/components/QuickStart.svelte | 2 +- .../web/src/lib/components/TimerCard.svelte | 2 +- .../src/lib/components/TimerIndicator.svelte | 2 +- .../apps/web/src/lib/data/guest-seed.ts | 2 +- .../apps/web/src/lib/data/local-store.ts | 20 +- .../apps/web/src/lib/data/queries.test.ts | 2 +- .../apps/web/src/lib/data/queries.ts | 10 +- .../apps/web/src/lib/data/types.test.ts | 4 +- .../apps/web/src/lib/i18n/index.ts | 4 +- .../apps/web/src/lib/i18n/locales/de.json | 2 +- .../apps/web/src/lib/i18n/locales/en.json | 2 +- .../apps/web/src/lib/stores/auth.svelte.ts | 0 .../apps/web/src/lib/stores/navigation.ts | 0 .../apps/web/src/lib/stores/theme.ts | 2 +- .../apps/web/src/lib/stores/timer.svelte.ts | 0 .../src/lib/stores/user-settings.svelte.ts | 2 +- .../apps/web/src/lib/stores/view.svelte.ts | 8 +- .../apps/web/src/lib/utils/export.test.ts | 2 +- .../apps/web/src/lib/utils/export.ts | 4 +- .../apps/web/src/lib/utils/rounding.test.ts | 0 .../apps/web/src/lib/utils/rounding.ts | 2 +- .../apps/web/src/lib/version.ts | 0 .../apps/web/src/routes/(app)/+layout.svelte | 12 +- .../apps/web/src/routes/(app)/+page.svelte | 4 +- .../web/src/routes/(app)/clients/+page.svelte | 6 +- .../routes/(app)/clients/[id]/+page.svelte | 4 +- .../web/src/routes/(app)/entries/+page.svelte | 4 +- .../src/routes/(app)/feedback/+page.svelte | 2 +- .../web/src/routes/(app)/help/+page.svelte | 2 +- .../web/src/routes/(app)/mana/+page.svelte | 2 +- .../web/src/routes/(app)/profile/+page.svelte | 2 +- .../src/routes/(app)/projects/+page.svelte | 6 +- .../routes/(app)/projects/[id]/+page.svelte | 6 +- .../web/src/routes/(app)/reports/+page.svelte | 4 +- .../src/routes/(app)/settings/+page.svelte | 8 +- .../src/routes/(app)/templates/+page.svelte | 4 +- .../web/src/routes/(app)/themes/+page.svelte | 2 +- .../apps/web/src/routes/(auth)/+layout.svelte | 0 .../web/src/routes/(auth)/login/+page.svelte | 6 +- .../apps/web/src/routes/+error.svelte | 0 .../apps/web/src/routes/+layout.svelte | 0 .../apps/web/src/routes/+layout.ts | 0 .../apps/web/src/routes/health/+server.ts | 2 +- .../apps/web/src/routes/offline/+page.svelte | 2 +- .../apps/web/svelte.config.js | 0 apps/{taktik => times}/apps/web/tsconfig.json | 0 .../{taktik => times}/apps/web/vite.config.ts | 4 +- apps/times/package.json | 14 + .../packages/shared/package.json | 2 +- .../packages/shared/src/constants/index.ts | 0 .../packages/shared/src/index.ts | 0 .../packages/shared/src/types/index.ts | 2 +- .../packages/shared/tsconfig.json | 0 docker-compose.macmini.yml | 10 +- docker/matrix/element/config.json | 59 +++ docs/CLUSTER_HARDWARE_ANALYSE.md | 433 ++++++++++++++++++ docs/FIX_COLIMA_MOUNTS.md | 84 ++++ docs/MANA_BOX_HARDWARE.md | 257 +++++++++++ package.json | 6 +- .../mana-core-nestjs-integration/package.json | 57 --- .../src/decorators/current-user.decorator.ts | 24 - .../src/decorators/index.ts | 3 - .../src/decorators/public.decorator.ts | 8 - .../src/decorators/use-credits.decorator.ts | 97 ---- .../insufficient-credits.exception.ts | 22 - .../src/guards/auth.guard.ts | 176 ------- .../src/guards/index.ts | 2 - .../src/guards/optional-auth.guard.ts | 117 ----- .../mana-core-nestjs-integration/src/index.ts | 53 --- .../src/interceptors/credit.interceptor.ts | 195 -------- .../src/interceptors/index.ts | 1 - .../interfaces/mana-core-options.interface.ts | 24 - .../src/mana-core.module.ts | 83 ---- .../src/services/credit-client.service.ts | 243 ---------- .../tsconfig.json | 21 - packages/shared-branding/src/app-icons.ts | 4 +- packages/shared-branding/src/mana-apps.ts | 6 +- .../mana-auth/src/auth/better-auth.config.ts | 2 +- services/mana-sync/CLAUDE.md | 2 +- 92 files changed, 970 insertions(+), 1263 deletions(-) rename apps/manacore/apps/landing/src/content/manascore/{2026-03-30-taktik.md => 2026-03-30-times.md} (90%) delete mode 100644 apps/taktik/package.json rename apps/{taktik => times}/CLAUDE.md (93%) rename apps/{taktik => times}/apps/web/Dockerfile (77%) rename apps/{taktik => times}/apps/web/package.json (96%) rename apps/{taktik => times}/apps/web/src/app.css (100%) rename apps/{taktik => times}/apps/web/src/app.html (96%) rename apps/{taktik => times}/apps/web/src/lib/components/ConfirmDialog.svelte (100%) rename apps/{taktik => times}/apps/web/src/lib/components/EntryForm.svelte (99%) rename apps/{taktik => times}/apps/web/src/lib/components/EntryItem.svelte (99%) rename apps/{taktik => times}/apps/web/src/lib/components/EntryList.svelte (97%) rename apps/{taktik => times}/apps/web/src/lib/components/KeyboardShortcuts.svelte (100%) rename apps/{taktik => times}/apps/web/src/lib/components/QuickStart.svelte (97%) rename apps/{taktik => times}/apps/web/src/lib/components/TimerCard.svelte (99%) rename apps/{taktik => times}/apps/web/src/lib/components/TimerIndicator.svelte (97%) rename apps/{taktik => times}/apps/web/src/lib/data/guest-seed.ts (99%) rename apps/{taktik => times}/apps/web/src/lib/data/local-store.ts (84%) rename apps/{taktik => times}/apps/web/src/lib/data/queries.test.ts (99%) rename apps/{taktik => times}/apps/web/src/lib/data/queries.ts (98%) rename apps/{taktik => times}/apps/web/src/lib/data/types.test.ts (97%) rename apps/{taktik => times}/apps/web/src/lib/i18n/index.ts (89%) rename apps/{taktik => times}/apps/web/src/lib/i18n/locales/de.json (99%) rename apps/{taktik => times}/apps/web/src/lib/i18n/locales/en.json (99%) rename apps/{taktik => times}/apps/web/src/lib/stores/auth.svelte.ts (100%) rename apps/{taktik => times}/apps/web/src/lib/stores/navigation.ts (100%) rename apps/{taktik => times}/apps/web/src/lib/stores/theme.ts (87%) rename apps/{taktik => times}/apps/web/src/lib/stores/timer.svelte.ts (100%) rename apps/{taktik => times}/apps/web/src/lib/stores/user-settings.svelte.ts (97%) rename apps/{taktik => times}/apps/web/src/lib/stores/view.svelte.ts (94%) rename apps/{taktik => times}/apps/web/src/lib/utils/export.test.ts (98%) rename apps/{taktik => times}/apps/web/src/lib/utils/export.ts (91%) rename apps/{taktik => times}/apps/web/src/lib/utils/rounding.test.ts (100%) rename apps/{taktik => times}/apps/web/src/lib/utils/rounding.ts (94%) rename apps/{taktik => times}/apps/web/src/lib/version.ts (100%) rename apps/{taktik => times}/apps/web/src/routes/(app)/+layout.svelte (96%) rename apps/{taktik => times}/apps/web/src/routes/(app)/+page.svelte (96%) rename apps/{taktik => times}/apps/web/src/routes/(app)/clients/+page.svelte (98%) rename apps/{taktik => times}/apps/web/src/routes/(app)/clients/[id]/+page.svelte (97%) rename apps/{taktik => times}/apps/web/src/routes/(app)/entries/+page.svelte (97%) rename apps/{taktik => times}/apps/web/src/routes/(app)/feedback/+page.svelte (88%) rename apps/{taktik => times}/apps/web/src/routes/(app)/help/+page.svelte (89%) rename apps/{taktik => times}/apps/web/src/routes/(app)/mana/+page.svelte (89%) rename apps/{taktik => times}/apps/web/src/routes/(app)/profile/+page.svelte (89%) rename apps/{taktik => times}/apps/web/src/routes/(app)/projects/+page.svelte (98%) rename apps/{taktik => times}/apps/web/src/routes/(app)/projects/[id]/+page.svelte (98%) rename apps/{taktik => times}/apps/web/src/routes/(app)/reports/+page.svelte (98%) rename apps/{taktik => times}/apps/web/src/routes/(app)/settings/+page.svelte (97%) rename apps/{taktik => times}/apps/web/src/routes/(app)/templates/+page.svelte (98%) rename apps/{taktik => times}/apps/web/src/routes/(app)/themes/+page.svelte (88%) rename apps/{taktik => times}/apps/web/src/routes/(auth)/+layout.svelte (100%) rename apps/{taktik => times}/apps/web/src/routes/(auth)/login/+page.svelte (99%) rename apps/{taktik => times}/apps/web/src/routes/+error.svelte (100%) rename apps/{taktik => times}/apps/web/src/routes/+layout.svelte (100%) rename apps/{taktik => times}/apps/web/src/routes/+layout.ts (100%) rename apps/{taktik => times}/apps/web/src/routes/health/+server.ts (89%) rename apps/{taktik => times}/apps/web/src/routes/offline/+page.svelte (97%) rename apps/{taktik => times}/apps/web/svelte.config.js (100%) rename apps/{taktik => times}/apps/web/tsconfig.json (100%) rename apps/{taktik => times}/apps/web/vite.config.ts (96%) create mode 100644 apps/times/package.json rename apps/{taktik => times}/packages/shared/package.json (93%) rename apps/{taktik => times}/packages/shared/src/constants/index.ts (100%) rename apps/{taktik => times}/packages/shared/src/index.ts (100%) rename apps/{taktik => times}/packages/shared/src/types/index.ts (99%) rename apps/{taktik => times}/packages/shared/tsconfig.json (100%) create mode 100644 docker/matrix/element/config.json create mode 100644 docs/CLUSTER_HARDWARE_ANALYSE.md create mode 100644 docs/FIX_COLIMA_MOUNTS.md create mode 100644 docs/MANA_BOX_HARDWARE.md delete mode 100644 packages/mana-core-nestjs-integration/package.json delete mode 100644 packages/mana-core-nestjs-integration/src/decorators/current-user.decorator.ts delete mode 100644 packages/mana-core-nestjs-integration/src/decorators/index.ts delete mode 100644 packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts delete mode 100644 packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts delete mode 100644 packages/mana-core-nestjs-integration/src/exceptions/insufficient-credits.exception.ts delete mode 100644 packages/mana-core-nestjs-integration/src/guards/auth.guard.ts delete mode 100644 packages/mana-core-nestjs-integration/src/guards/index.ts delete mode 100644 packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts delete mode 100644 packages/mana-core-nestjs-integration/src/index.ts delete mode 100644 packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts delete mode 100644 packages/mana-core-nestjs-integration/src/interceptors/index.ts delete mode 100644 packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts delete mode 100644 packages/mana-core-nestjs-integration/src/mana-core.module.ts delete mode 100644 packages/mana-core-nestjs-integration/src/services/credit-client.service.ts delete mode 100644 packages/mana-core-nestjs-integration/tsconfig.json diff --git a/CLAUDE.md b/CLAUDE.md index 0e1da9f6b..9cd9bf3bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/ | **citycorners** | City guide for Konstanz | Web, Landing | | **inventar** | Inventory management | Web | | **traces** | City exploration | Backend, Mobile | -| **taktik** | Time tracking | Web | +| **times** | Time tracking | Web | | **uload** | URL shortener & link management | Server, Web, Landing | | **news** | AI news reader & personal library | Server, Web, Landing | | **wisekeep** | AI transcription & wisdom library | Server, Web, Landing | @@ -585,7 +585,7 @@ Logged in: App → IndexedDB → UI → SyncEngine → mana-sync (Go) → Postg | Photos | albums, albumItems, favorites, tags, photoTags | Done | | SkilltTree | skills, activities, achievements | Done | | CityCorners | locations, favorites | Done | -| Taktik | clients, projects, timeEntries, tags, templates, settings | Done | +| Times | clients, projects, timeEntries, tags, templates, settings | Done | | uLoad | links, tags, folders, linkTags | Done | | Calc | calculations, savedFormulas | Done | | ManaCore | userSettings, dashboardConfigs | Done | diff --git a/apps/manacore/apps/landing/src/content/manascore/2026-03-30-taktik.md b/apps/manacore/apps/landing/src/content/manascore/2026-03-30-times.md similarity index 90% rename from apps/manacore/apps/landing/src/content/manascore/2026-03-30-taktik.md rename to apps/manacore/apps/landing/src/content/manascore/2026-03-30-times.md index beb8988fa..db8aa1a94 100644 --- a/apps/manacore/apps/landing/src/content/manascore/2026-03-30-taktik.md +++ b/apps/manacore/apps/landing/src/content/manascore/2026-03-30-times.md @@ -1,10 +1,10 @@ --- -title: 'Taktik: Production Readiness Audit' +title: 'Times: Production Readiness Audit' description: 'Zeiterfassung mit Live-Timer, Projekten, Kunden, Reports, CSV-Export, Templates und Abrechnungsraten - local-first mit umfassender Dokumentation und solider Testabdeckung' date: 2026-03-30 -app: 'taktik' +app: 'times' author: 'Claude Code' -tags: ['audit', 'taktik', 'production-readiness', 'beta'] +tags: ['audit', 'times', 'production-readiness', 'beta'] score: 55 scores: backend: 5 @@ -39,7 +39,7 @@ stats: ## Zusammenfassung -Taktik ist eine **vollwertige Zeiterfassung** mit Live-Timer, Projekt-/Kunden-Management, Reports mit Charts, CSV-Export und konfigurierbaren Abrechnungsraten. Local-first mit 6 Dexie-Collections, 4 Testdateien und umfassender CLAUDE.md. Feature-komplett für den Produktiveinsatz. +Times ist eine **vollwertige Zeiterfassung** mit Live-Timer, Projekt-/Kunden-Management, Reports mit Charts, CSV-Export und konfigurierbaren Abrechnungsraten. Local-first mit 6 Dexie-Collections, 4 Testdateien und umfassender CLAUDE.md. Feature-komplett für den Produktiveinsatz. ## Backend (5/100) diff --git a/apps/taktik/package.json b/apps/taktik/package.json deleted file mode 100644 index 279d3617b..000000000 --- a/apps/taktik/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "taktik", - "version": "1.0.0", - "private": true, - "description": "Taktik - Zeiterfassung & Timetracking", - "scripts": { - "dev": "pnpm --filter @taktik/web dev", - "dev:web": "pnpm --filter @taktik/web dev" - }, - "devDependencies": { - "typescript": "^5.9.3" - }, - "packageManager": "pnpm@9.15.0" -} diff --git a/apps/taktik/CLAUDE.md b/apps/times/CLAUDE.md similarity index 93% rename from apps/taktik/CLAUDE.md rename to apps/times/CLAUDE.md index 4a5e10bc0..694e36c08 100644 --- a/apps/taktik/CLAUDE.md +++ b/apps/times/CLAUDE.md @@ -1,4 +1,4 @@ -# Taktik +# Times Zeiterfassung & Timetracking - Dein Arbeitsrhythmus, messbar gemacht. @@ -6,7 +6,7 @@ Zeiterfassung & Timetracking - Dein Arbeitsrhythmus, messbar gemacht. ## Project Overview -Taktik is a professional time tracking app with timer, manual entry, projects, clients, reports, templates, and guild (team) integration. Built local-first for offline capability and instant UI. +Times is a professional time tracking app with timer, manual entry, projects, clients, reports, templates, and guild (team) integration. Built local-first for offline capability and instant UI. ### Tech Stack @@ -24,16 +24,16 @@ Taktik is a professional time tracking app with timer, manual entry, projects, c ```bash # From monorepo root -pnpm dev:taktik:web # Start web app on port 5197 -pnpm dev:taktik:full # Start with auth + sync server +pnpm dev:times:web # Start web app on port 5197 +pnpm dev:times:full # Start with auth + sync server # Tests -pnpm --filter @taktik/web test # Run all tests -pnpm --filter @taktik/web test:unit # Run in watch mode +pnpm --filter @times/web test # Run all tests +pnpm --filter @times/web test:unit # Run in watch mode # Type checking -pnpm --filter @taktik/web type-check -pnpm --filter @taktik/shared type-check +pnpm --filter @times/web type-check +pnpm --filter @times/shared type-check ``` ## Key Features @@ -104,7 +104,7 @@ pnpm --filter @taktik/shared type-check ## Project Structure ``` -apps/taktik/ +apps/times/ ├── apps/ │ └── web/ # SvelteKit web client (port 5197) │ ├── src/ @@ -160,7 +160,7 @@ apps/taktik/ │ │ └── version.ts │ └── static/ ├── packages/ -│ └── shared/ # @taktik/shared +│ └── shared/ # @times/shared │ └── src/ │ ├── types/index.ts # All TypeScript types │ ├── constants/index.ts # Currencies, colors, defaults diff --git a/apps/taktik/apps/web/Dockerfile b/apps/times/apps/web/Dockerfile similarity index 77% rename from apps/taktik/apps/web/Dockerfile rename to apps/times/apps/web/Dockerfile index f331eb09e..15f225f24 100644 --- a/apps/taktik/apps/web/Dockerfile +++ b/apps/times/apps/web/Dockerfile @@ -9,15 +9,15 @@ ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-core-auth:3001 ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL # Copy app-specific packages -COPY apps/taktik/packages/shared ./apps/taktik/packages/shared -COPY apps/taktik/apps/web ./apps/taktik/apps/web +COPY apps/times/packages/shared ./apps/times/packages/shared +COPY apps/times/apps/web ./apps/times/apps/web # Install app-specific dependencies RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ pnpm install --no-frozen-lockfile --ignore-scripts # Build the web app -WORKDIR /app/apps/taktik/apps/web +WORKDIR /app/apps/times/apps/web RUN pnpm exec svelte-kit sync RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build @@ -25,17 +25,17 @@ RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build FROM node:20-alpine AS production # Keep same directory structure as builder so pnpm symlinks resolve correctly -WORKDIR /app/apps/taktik/apps/web +WORKDIR /app/apps/times/apps/web # Copy the pnpm store that symlinks point to (at /app/node_modules/.pnpm) COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm # Copy the app's node_modules (contains symlinks to the pnpm store) -COPY --from=builder /app/apps/taktik/apps/web/node_modules ./node_modules +COPY --from=builder /app/apps/times/apps/web/node_modules ./node_modules # Copy built application -COPY --from=builder /app/apps/taktik/apps/web/build ./build -COPY --from=builder /app/apps/taktik/apps/web/package.json ./ +COPY --from=builder /app/apps/times/apps/web/build ./build +COPY --from=builder /app/apps/times/apps/web/package.json ./ # Expose port EXPOSE 5027 diff --git a/apps/taktik/apps/web/package.json b/apps/times/apps/web/package.json similarity index 96% rename from apps/taktik/apps/web/package.json rename to apps/times/apps/web/package.json index 976c4072b..cc1906d2e 100644 --- a/apps/taktik/apps/web/package.json +++ b/apps/times/apps/web/package.json @@ -1,5 +1,5 @@ { - "name": "@taktik/web", + "name": "@times/web", "version": "1.0.0", "private": true, "scripts": { @@ -44,7 +44,7 @@ "@manacore/shared-types": "workspace:*", "@manacore/shared-ui": "workspace:*", "@manacore/shared-utils": "workspace:*", - "@taktik/shared": "workspace:*", + "@times/shared": "workspace:*", "date-fns": "^4.1.0", "svelte-i18n": "^4.0.1" }, diff --git a/apps/taktik/apps/web/src/app.css b/apps/times/apps/web/src/app.css similarity index 100% rename from apps/taktik/apps/web/src/app.css rename to apps/times/apps/web/src/app.css diff --git a/apps/taktik/apps/web/src/app.html b/apps/times/apps/web/src/app.html similarity index 96% rename from apps/taktik/apps/web/src/app.html rename to apps/times/apps/web/src/app.html index a5df47bb5..27fb3a849 100644 --- a/apps/taktik/apps/web/src/app.html +++ b/apps/times/apps/web/src/app.html @@ -9,7 +9,7 @@ - Taktik + Times %sveltekit.head% diff --git a/apps/taktik/apps/web/src/lib/components/ConfirmDialog.svelte b/apps/times/apps/web/src/lib/components/ConfirmDialog.svelte similarity index 100% rename from apps/taktik/apps/web/src/lib/components/ConfirmDialog.svelte rename to apps/times/apps/web/src/lib/components/ConfirmDialog.svelte diff --git a/apps/taktik/apps/web/src/lib/components/EntryForm.svelte b/apps/times/apps/web/src/lib/components/EntryForm.svelte similarity index 99% rename from apps/taktik/apps/web/src/lib/components/EntryForm.svelte rename to apps/times/apps/web/src/lib/components/EntryForm.svelte index 548042100..401b6a0bc 100644 --- a/apps/taktik/apps/web/src/lib/components/EntryForm.svelte +++ b/apps/times/apps/web/src/lib/components/EntryForm.svelte @@ -2,7 +2,7 @@ import { getContext } from 'svelte'; import { _ } from 'svelte-i18n'; import { timeEntryCollection } from '$lib/data/local-store'; - import type { Project, Client } from '@taktik/shared'; + import type { Project, Client } from '@times/shared'; let { visible = false, diff --git a/apps/taktik/apps/web/src/lib/components/EntryItem.svelte b/apps/times/apps/web/src/lib/components/EntryItem.svelte similarity index 99% rename from apps/taktik/apps/web/src/lib/components/EntryItem.svelte rename to apps/times/apps/web/src/lib/components/EntryItem.svelte index 0c5772aa8..68bedabf6 100644 --- a/apps/taktik/apps/web/src/lib/components/EntryItem.svelte +++ b/apps/times/apps/web/src/lib/components/EntryItem.svelte @@ -3,7 +3,7 @@ import { _ } from 'svelte-i18n'; import { timeEntryCollection } from '$lib/data/local-store'; import { formatDurationCompact } from '$lib/data/queries'; - import type { TimeEntry, Project, Client } from '@taktik/shared'; + import type { TimeEntry, Project, Client } from '@times/shared'; import ConfirmDialog from './ConfirmDialog.svelte'; let { diff --git a/apps/taktik/apps/web/src/lib/components/EntryList.svelte b/apps/times/apps/web/src/lib/components/EntryList.svelte similarity index 97% rename from apps/taktik/apps/web/src/lib/components/EntryList.svelte rename to apps/times/apps/web/src/lib/components/EntryList.svelte index 6aed4789a..d1f96cf83 100644 --- a/apps/taktik/apps/web/src/lib/components/EntryList.svelte +++ b/apps/times/apps/web/src/lib/components/EntryList.svelte @@ -2,7 +2,7 @@ import { _ } from 'svelte-i18n'; import EntryItem from './EntryItem.svelte'; import { groupEntriesByDate, getTotalDuration, formatDurationCompact } from '$lib/data/queries'; - import type { TimeEntry } from '@taktik/shared'; + import type { TimeEntry } from '@times/shared'; let { entries }: { entries: TimeEntry[] } = $props(); diff --git a/apps/taktik/apps/web/src/lib/components/KeyboardShortcuts.svelte b/apps/times/apps/web/src/lib/components/KeyboardShortcuts.svelte similarity index 100% rename from apps/taktik/apps/web/src/lib/components/KeyboardShortcuts.svelte rename to apps/times/apps/web/src/lib/components/KeyboardShortcuts.svelte diff --git a/apps/taktik/apps/web/src/lib/components/QuickStart.svelte b/apps/times/apps/web/src/lib/components/QuickStart.svelte similarity index 97% rename from apps/taktik/apps/web/src/lib/components/QuickStart.svelte rename to apps/times/apps/web/src/lib/components/QuickStart.svelte index 0b4e35fa8..1d78a2cc9 100644 --- a/apps/taktik/apps/web/src/lib/components/QuickStart.svelte +++ b/apps/times/apps/web/src/lib/components/QuickStart.svelte @@ -2,7 +2,7 @@ import { getContext } from 'svelte'; import { _ } from 'svelte-i18n'; import { timerStore } from '$lib/stores/timer.svelte'; - import type { TimeEntry, Project } from '@taktik/shared'; + import type { TimeEntry, Project } from '@times/shared'; const allTimeEntries = getContext<{ value: TimeEntry[] }>('timeEntries'); const allProjects = getContext<{ value: Project[] }>('projects'); diff --git a/apps/taktik/apps/web/src/lib/components/TimerCard.svelte b/apps/times/apps/web/src/lib/components/TimerCard.svelte similarity index 99% rename from apps/taktik/apps/web/src/lib/components/TimerCard.svelte rename to apps/times/apps/web/src/lib/components/TimerCard.svelte index 603647aa1..fa64fdc0a 100644 --- a/apps/taktik/apps/web/src/lib/components/TimerCard.svelte +++ b/apps/times/apps/web/src/lib/components/TimerCard.svelte @@ -3,7 +3,7 @@ import { _ } from 'svelte-i18n'; import { timerStore } from '$lib/stores/timer.svelte'; import { formatDuration } from '$lib/data/queries'; - import type { Project, Client } from '@taktik/shared'; + import type { Project, Client } from '@times/shared'; const allProjects = getContext<{ value: Project[] }>('projects'); const allClients = getContext<{ value: Client[] }>('clients'); diff --git a/apps/taktik/apps/web/src/lib/components/TimerIndicator.svelte b/apps/times/apps/web/src/lib/components/TimerIndicator.svelte similarity index 97% rename from apps/taktik/apps/web/src/lib/components/TimerIndicator.svelte rename to apps/times/apps/web/src/lib/components/TimerIndicator.svelte index 4cbb34796..3768c69de 100644 --- a/apps/taktik/apps/web/src/lib/components/TimerIndicator.svelte +++ b/apps/times/apps/web/src/lib/components/TimerIndicator.svelte @@ -3,7 +3,7 @@ import { _ } from 'svelte-i18n'; import { timerStore } from '$lib/stores/timer.svelte'; import { formatDuration } from '$lib/data/queries'; - import type { Project } from '@taktik/shared'; + import type { Project } from '@times/shared'; const allProjects = getContext<{ value: Project[] }>('projects'); diff --git a/apps/taktik/apps/web/src/lib/data/guest-seed.ts b/apps/times/apps/web/src/lib/data/guest-seed.ts similarity index 99% rename from apps/taktik/apps/web/src/lib/data/guest-seed.ts rename to apps/times/apps/web/src/lib/data/guest-seed.ts index ccad98d30..464ced626 100644 --- a/apps/taktik/apps/web/src/lib/data/guest-seed.ts +++ b/apps/times/apps/web/src/lib/data/guest-seed.ts @@ -1,5 +1,5 @@ /** - * Guest seed data for the Taktik app. + * Guest seed data for the Times app. * * Provides demo clients, projects, and time entries for the guest experience. */ diff --git a/apps/taktik/apps/web/src/lib/data/local-store.ts b/apps/times/apps/web/src/lib/data/local-store.ts similarity index 84% rename from apps/taktik/apps/web/src/lib/data/local-store.ts rename to apps/times/apps/web/src/lib/data/local-store.ts index 22e5c759e..6fa0d8fe5 100644 --- a/apps/taktik/apps/web/src/lib/data/local-store.ts +++ b/apps/times/apps/web/src/lib/data/local-store.ts @@ -1,5 +1,5 @@ /** - * Taktik — Local-First Data Layer + * Times — Local-First Data Layer * * IndexedDB (Dexie.js) with sync support for time tracking. * Clients, projects, time entries, tags, templates, and settings. @@ -13,7 +13,7 @@ import { guestTags, guestSettings, } from './guest-seed'; -import type { BillingRate, ProjectVisibility, EntrySourceRef } from '@taktik/shared'; +import type { BillingRate, ProjectVisibility, EntrySourceRef } from '@times/shared'; // ─── Types ────────────────────────────────────────────────── @@ -97,8 +97,8 @@ export interface LocalSettings extends BaseRecord { const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050'; -export const taktikStore = createLocalStore({ - appId: 'taktik', +export const timesStore = createLocalStore({ + appId: 'times', collections: [ { name: 'clients', @@ -145,9 +145,9 @@ export const taktikStore = createLocalStore({ }); // Typed collection accessors -export const clientCollection = taktikStore.collection('clients'); -export const projectCollection = taktikStore.collection('projects'); -export const timeEntryCollection = taktikStore.collection('timeEntries'); -export const tagCollection = taktikStore.collection('tags'); -export const templateCollection = taktikStore.collection('templates'); -export const settingsCollection = taktikStore.collection('settings'); +export const clientCollection = timesStore.collection('clients'); +export const projectCollection = timesStore.collection('projects'); +export const timeEntryCollection = timesStore.collection('timeEntries'); +export const tagCollection = timesStore.collection('tags'); +export const templateCollection = timesStore.collection('templates'); +export const settingsCollection = timesStore.collection('settings'); diff --git a/apps/taktik/apps/web/src/lib/data/queries.test.ts b/apps/times/apps/web/src/lib/data/queries.test.ts similarity index 99% rename from apps/taktik/apps/web/src/lib/data/queries.test.ts rename to apps/times/apps/web/src/lib/data/queries.test.ts index 36244d127..dc7f51ba6 100644 --- a/apps/taktik/apps/web/src/lib/data/queries.test.ts +++ b/apps/times/apps/web/src/lib/data/queries.test.ts @@ -17,7 +17,7 @@ import { getClientById, getProjectsByClient, } from './queries'; -import type { TimeEntry, Project, Client } from '@taktik/shared'; +import type { TimeEntry, Project, Client } from '@times/shared'; // ─── Test Factories ────────────────────────────────────── diff --git a/apps/taktik/apps/web/src/lib/data/queries.ts b/apps/times/apps/web/src/lib/data/queries.ts similarity index 98% rename from apps/taktik/apps/web/src/lib/data/queries.ts rename to apps/times/apps/web/src/lib/data/queries.ts index e9a4d7970..bc9bf0d3f 100644 --- a/apps/taktik/apps/web/src/lib/data/queries.ts +++ b/apps/times/apps/web/src/lib/data/queries.ts @@ -1,5 +1,5 @@ /** - * Reactive Queries & Pure Helpers for Taktik + * Reactive Queries & Pure Helpers for Times * * Uses Dexie liveQuery to automatically re-render when IndexedDB changes * (local writes, sync updates, other tabs). @@ -26,10 +26,10 @@ import type { TimeEntry, Tag, EntryTemplate, - TaktikSettings, + TimesSettings, FilterCriteria, SortOption, -} from '@taktik/shared'; +} from '@times/shared'; // ─── Type Converters ─────────────────────────────────────── @@ -118,7 +118,7 @@ export function toTemplate(local: LocalTemplate): EntryTemplate { }; } -export function toSettings(local: LocalSettings): TaktikSettings { +export function toSettings(local: LocalSettings): TimesSettings { return { id: local.id, defaultBillingRate: local.defaultBillingRate ?? undefined, @@ -178,7 +178,7 @@ export function useSettings() { const locals = await settingsCollection.getAll(); return locals.length > 0 ? toSettings(locals[0]) : null; }, - null as TaktikSettings | null + null as TimesSettings | null ); } diff --git a/apps/taktik/apps/web/src/lib/data/types.test.ts b/apps/times/apps/web/src/lib/data/types.test.ts similarity index 97% rename from apps/taktik/apps/web/src/lib/data/types.test.ts rename to apps/times/apps/web/src/lib/data/types.test.ts index 0021d0b35..f343b145e 100644 --- a/apps/taktik/apps/web/src/lib/data/types.test.ts +++ b/apps/times/apps/web/src/lib/data/types.test.ts @@ -5,11 +5,11 @@ import type { TimeEntry, Tag, EntryTemplate, - TaktikSettings, + TimesSettings, BillingRate, FilterCriteria, SortOption, -} from '@taktik/shared'; +} from '@times/shared'; describe('Shared Types', () => { it('BillingRate has correct shape', () => { diff --git a/apps/taktik/apps/web/src/lib/i18n/index.ts b/apps/times/apps/web/src/lib/i18n/index.ts similarity index 89% rename from apps/taktik/apps/web/src/lib/i18n/index.ts rename to apps/times/apps/web/src/lib/i18n/index.ts index bf8800877..e6ce82614 100644 --- a/apps/taktik/apps/web/src/lib/i18n/index.ts +++ b/apps/times/apps/web/src/lib/i18n/index.ts @@ -11,7 +11,7 @@ register('en', () => import('./locales/en.json')); function getInitialLocale(): SupportedLocale { if (browser) { - const stored = localStorage.getItem('taktik_locale'); + const stored = localStorage.getItem('times_locale'); if (stored && supportedLocales.includes(stored as SupportedLocale)) { return stored as SupportedLocale; } @@ -31,7 +31,7 @@ init({ export function setLocale(newLocale: SupportedLocale) { locale.set(newLocale); if (browser) { - localStorage.setItem('taktik_locale', newLocale); + localStorage.setItem('times_locale', newLocale); } } diff --git a/apps/taktik/apps/web/src/lib/i18n/locales/de.json b/apps/times/apps/web/src/lib/i18n/locales/de.json similarity index 99% rename from apps/taktik/apps/web/src/lib/i18n/locales/de.json rename to apps/times/apps/web/src/lib/i18n/locales/de.json index 27aedbc0f..5d3ae1077 100644 --- a/apps/taktik/apps/web/src/lib/i18n/locales/de.json +++ b/apps/times/apps/web/src/lib/i18n/locales/de.json @@ -1,6 +1,6 @@ { "app": { - "name": "Taktik", + "name": "Times", "loading": "Laden...", "tagline": "Dein Arbeitsrhythmus, messbar gemacht." }, diff --git a/apps/taktik/apps/web/src/lib/i18n/locales/en.json b/apps/times/apps/web/src/lib/i18n/locales/en.json similarity index 99% rename from apps/taktik/apps/web/src/lib/i18n/locales/en.json rename to apps/times/apps/web/src/lib/i18n/locales/en.json index f6adc8157..1f310d620 100644 --- a/apps/taktik/apps/web/src/lib/i18n/locales/en.json +++ b/apps/times/apps/web/src/lib/i18n/locales/en.json @@ -1,6 +1,6 @@ { "app": { - "name": "Taktik", + "name": "Times", "loading": "Loading...", "tagline": "Your work rhythm, made measurable." }, diff --git a/apps/taktik/apps/web/src/lib/stores/auth.svelte.ts b/apps/times/apps/web/src/lib/stores/auth.svelte.ts similarity index 100% rename from apps/taktik/apps/web/src/lib/stores/auth.svelte.ts rename to apps/times/apps/web/src/lib/stores/auth.svelte.ts diff --git a/apps/taktik/apps/web/src/lib/stores/navigation.ts b/apps/times/apps/web/src/lib/stores/navigation.ts similarity index 100% rename from apps/taktik/apps/web/src/lib/stores/navigation.ts rename to apps/times/apps/web/src/lib/stores/navigation.ts diff --git a/apps/taktik/apps/web/src/lib/stores/theme.ts b/apps/times/apps/web/src/lib/stores/theme.ts similarity index 87% rename from apps/taktik/apps/web/src/lib/stores/theme.ts rename to apps/times/apps/web/src/lib/stores/theme.ts index d5ded30af..08689a37b 100644 --- a/apps/taktik/apps/web/src/lib/stores/theme.ts +++ b/apps/times/apps/web/src/lib/stores/theme.ts @@ -1,6 +1,6 @@ import { createThemeStore } from '@manacore/shared-theme'; export const theme = createThemeStore({ - appId: 'taktik', + appId: 'times', defaultVariant: 'ocean', }); diff --git a/apps/taktik/apps/web/src/lib/stores/timer.svelte.ts b/apps/times/apps/web/src/lib/stores/timer.svelte.ts similarity index 100% rename from apps/taktik/apps/web/src/lib/stores/timer.svelte.ts rename to apps/times/apps/web/src/lib/stores/timer.svelte.ts diff --git a/apps/taktik/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/times/apps/web/src/lib/stores/user-settings.svelte.ts similarity index 97% rename from apps/taktik/apps/web/src/lib/stores/user-settings.svelte.ts rename to apps/times/apps/web/src/lib/stores/user-settings.svelte.ts index 73ffe777f..5a96e282b 100644 --- a/apps/taktik/apps/web/src/lib/stores/user-settings.svelte.ts +++ b/apps/times/apps/web/src/lib/stores/user-settings.svelte.ts @@ -12,7 +12,7 @@ function getAuthUrl(): string { } export const userSettings = createUserSettingsStore({ - appId: 'taktik', + appId: 'times', authUrl: getAuthUrl, getAccessToken: () => authStore.getAccessToken(), }); diff --git a/apps/taktik/apps/web/src/lib/stores/view.svelte.ts b/apps/times/apps/web/src/lib/stores/view.svelte.ts similarity index 94% rename from apps/taktik/apps/web/src/lib/stores/view.svelte.ts rename to apps/times/apps/web/src/lib/stores/view.svelte.ts index d0b94a1a9..b00bd0901 100644 --- a/apps/taktik/apps/web/src/lib/stores/view.svelte.ts +++ b/apps/times/apps/web/src/lib/stores/view.svelte.ts @@ -1,9 +1,9 @@ import { browser } from '$app/environment'; -import type { ViewMode, SortOption, FilterCriteria, SavedFilter } from '@taktik/shared'; +import type { ViewMode, SortOption, FilterCriteria, SavedFilter } from '@times/shared'; -const VIEW_KEY = 'taktik_view_mode'; -const SORT_KEY = 'taktik_sort'; -const FILTERS_KEY = 'taktik_saved_filters'; +const VIEW_KEY = 'times_view_mode'; +const SORT_KEY = 'times_sort'; +const FILTERS_KEY = 'times_saved_filters'; function load(key: string, fallback: T): T { if (!browser) return fallback; diff --git a/apps/taktik/apps/web/src/lib/utils/export.test.ts b/apps/times/apps/web/src/lib/utils/export.test.ts similarity index 98% rename from apps/taktik/apps/web/src/lib/utils/export.test.ts rename to apps/times/apps/web/src/lib/utils/export.test.ts index 83bb6aa4e..e22b1ded3 100644 --- a/apps/taktik/apps/web/src/lib/utils/export.test.ts +++ b/apps/times/apps/web/src/lib/utils/export.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import type { TimeEntry, Project, Client } from '@taktik/shared'; +import type { TimeEntry, Project, Client } from '@times/shared'; // We test the CSV generation logic without triggering DOM download. // This mirrors the core logic from export.ts. diff --git a/apps/taktik/apps/web/src/lib/utils/export.ts b/apps/times/apps/web/src/lib/utils/export.ts similarity index 91% rename from apps/taktik/apps/web/src/lib/utils/export.ts rename to apps/times/apps/web/src/lib/utils/export.ts index 588d3be9f..bb3841c2a 100644 --- a/apps/taktik/apps/web/src/lib/utils/export.ts +++ b/apps/times/apps/web/src/lib/utils/export.ts @@ -2,7 +2,7 @@ * CSV Export utility for time entries */ -import type { TimeEntry, Project, Client } from '@taktik/shared'; +import type { TimeEntry, Project, Client } from '@times/shared'; export function exportEntriesToCSV( entries: TimeEntry[], @@ -55,7 +55,7 @@ export function exportEntriesToCSV( const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `taktik-export-${new Date().toISOString().split('T')[0]}.csv`; + a.download = `times-export-${new Date().toISOString().split('T')[0]}.csv`; a.click(); URL.revokeObjectURL(url); } diff --git a/apps/taktik/apps/web/src/lib/utils/rounding.test.ts b/apps/times/apps/web/src/lib/utils/rounding.test.ts similarity index 100% rename from apps/taktik/apps/web/src/lib/utils/rounding.test.ts rename to apps/times/apps/web/src/lib/utils/rounding.test.ts diff --git a/apps/taktik/apps/web/src/lib/utils/rounding.ts b/apps/times/apps/web/src/lib/utils/rounding.ts similarity index 94% rename from apps/taktik/apps/web/src/lib/utils/rounding.ts rename to apps/times/apps/web/src/lib/utils/rounding.ts index 39b3fab21..229542f0f 100644 --- a/apps/taktik/apps/web/src/lib/utils/rounding.ts +++ b/apps/times/apps/web/src/lib/utils/rounding.ts @@ -4,7 +4,7 @@ * Applies rounding based on user settings (increment + method). */ -import type { RoundingMethod } from '@taktik/shared'; +import type { RoundingMethod } from '@times/shared'; /** * Round a duration in seconds based on settings. diff --git a/apps/taktik/apps/web/src/lib/version.ts b/apps/times/apps/web/src/lib/version.ts similarity index 100% rename from apps/taktik/apps/web/src/lib/version.ts rename to apps/times/apps/web/src/lib/version.ts diff --git a/apps/taktik/apps/web/src/routes/(app)/+layout.svelte b/apps/times/apps/web/src/routes/(app)/+layout.svelte similarity index 96% rename from apps/taktik/apps/web/src/routes/(app)/+layout.svelte rename to apps/times/apps/web/src/routes/(app)/+layout.svelte index 18a9f3999..d9e7d31b4 100644 --- a/apps/taktik/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/times/apps/web/src/routes/(app)/+layout.svelte @@ -13,7 +13,7 @@ import { getPillAppItems } from '@manacore/shared-branding'; import { AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui'; import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui'; - import { taktikStore } from '$lib/data/local-store'; + import { timesStore } from '$lib/data/local-store'; import { useAllClients, useAllProjects, @@ -46,17 +46,17 @@ setContext('settings', settings); async function handleAuthReady() { - await taktikStore.initialize(); + await timesStore.initialize(); if (authStore.isAuthenticated) { - taktikStore.startSync(() => authStore.getValidToken()); + timesStore.startSync(() => authStore.getValidToken()); } viewStore.initialize(); await timerStore.initialize(); initialized = true; - if (!authStore.isAuthenticated && shouldShowGuestWelcome('taktik')) { + if (!authStore.isAuthenticated && shouldShowGuestWelcome('times')) { showGuestWelcome = true; } } @@ -105,7 +105,7 @@ /> - Taktik + Times @@ -203,7 +203,7 @@ (showGuestWelcome = false)} onLogin={() => goto('/login')} diff --git a/apps/taktik/apps/web/src/routes/(app)/+page.svelte b/apps/times/apps/web/src/routes/(app)/+page.svelte similarity index 96% rename from apps/taktik/apps/web/src/routes/(app)/+page.svelte rename to apps/times/apps/web/src/routes/(app)/+page.svelte index db82f9b3a..c0e2d0a1b 100644 --- a/apps/taktik/apps/web/src/routes/(app)/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/+page.svelte @@ -1,7 +1,7 @@ - Timer | Taktik + Timer | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/clients/+page.svelte b/apps/times/apps/web/src/routes/(app)/clients/+page.svelte similarity index 98% rename from apps/taktik/apps/web/src/routes/(app)/clients/+page.svelte rename to apps/times/apps/web/src/routes/(app)/clients/+page.svelte index 8494a4163..533a8d157 100644 --- a/apps/taktik/apps/web/src/routes/(app)/clients/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/clients/+page.svelte @@ -3,8 +3,8 @@ import { _ } from 'svelte-i18n'; import { clientCollection } from '$lib/data/local-store'; import { getTotalDuration, formatDurationCompact } from '$lib/data/queries'; - import type { Client, Project, TimeEntry } from '@taktik/shared'; - import { PROJECT_COLORS } from '@taktik/shared/constants'; + import type { Client, Project, TimeEntry } from '@times/shared'; + import { PROJECT_COLORS } from '@times/shared/constants'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; const allClients = getContext<{ value: Client[] }>('clients'); @@ -101,7 +101,7 @@ - {$_('nav.clients')} | Taktik + {$_('nav.clients')} | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/clients/[id]/+page.svelte b/apps/times/apps/web/src/routes/(app)/clients/[id]/+page.svelte similarity index 97% rename from apps/taktik/apps/web/src/routes/(app)/clients/[id]/+page.svelte rename to apps/times/apps/web/src/routes/(app)/clients/[id]/+page.svelte index e13fce126..babc0a262 100644 --- a/apps/taktik/apps/web/src/routes/(app)/clients/[id]/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/clients/[id]/+page.svelte @@ -10,7 +10,7 @@ formatDurationDecimal, } from '$lib/data/queries'; import EntryList from '$lib/components/EntryList.svelte'; - import type { Project, Client, TimeEntry } from '@taktik/shared'; + import type { Project, Client, TimeEntry } from '@times/shared'; const allClients = getContext<{ value: Client[] }>('clients'); const allProjects = getContext<{ value: Project[] }>('projects'); @@ -42,7 +42,7 @@ - {client?.name || 'Kunde'} | Taktik + {client?.name || 'Kunde'} | Times {#if !client} diff --git a/apps/taktik/apps/web/src/routes/(app)/entries/+page.svelte b/apps/times/apps/web/src/routes/(app)/entries/+page.svelte similarity index 97% rename from apps/taktik/apps/web/src/routes/(app)/entries/+page.svelte rename to apps/times/apps/web/src/routes/(app)/entries/+page.svelte index 4d2fccb47..d35215e09 100644 --- a/apps/taktik/apps/web/src/routes/(app)/entries/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/entries/+page.svelte @@ -1,7 +1,7 @@ - {$_('nav.entries')} | Taktik + {$_('nav.entries')} | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/feedback/+page.svelte b/apps/times/apps/web/src/routes/(app)/feedback/+page.svelte similarity index 88% rename from apps/taktik/apps/web/src/routes/(app)/feedback/+page.svelte rename to apps/times/apps/web/src/routes/(app)/feedback/+page.svelte index c71b318f0..728c121cc 100644 --- a/apps/taktik/apps/web/src/routes/(app)/feedback/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/feedback/+page.svelte @@ -3,7 +3,7 @@ - Feedback | Taktik + Feedback | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/help/+page.svelte b/apps/times/apps/web/src/routes/(app)/help/+page.svelte similarity index 89% rename from apps/taktik/apps/web/src/routes/(app)/help/+page.svelte rename to apps/times/apps/web/src/routes/(app)/help/+page.svelte index 14e51499b..026f98656 100644 --- a/apps/taktik/apps/web/src/routes/(app)/help/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/help/+page.svelte @@ -3,7 +3,7 @@ - Hilfe | Taktik + Hilfe | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/mana/+page.svelte b/apps/times/apps/web/src/routes/(app)/mana/+page.svelte similarity index 89% rename from apps/taktik/apps/web/src/routes/(app)/mana/+page.svelte rename to apps/times/apps/web/src/routes/(app)/mana/+page.svelte index 8e964a054..591e3cb50 100644 --- a/apps/taktik/apps/web/src/routes/(app)/mana/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/mana/+page.svelte @@ -3,7 +3,7 @@ - Mana | Taktik + Mana | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/profile/+page.svelte b/apps/times/apps/web/src/routes/(app)/profile/+page.svelte similarity index 89% rename from apps/taktik/apps/web/src/routes/(app)/profile/+page.svelte rename to apps/times/apps/web/src/routes/(app)/profile/+page.svelte index c3a07ad34..fa18b6d5a 100644 --- a/apps/taktik/apps/web/src/routes/(app)/profile/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/profile/+page.svelte @@ -3,7 +3,7 @@ - Profil | Taktik + Profil | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/projects/+page.svelte b/apps/times/apps/web/src/routes/(app)/projects/+page.svelte similarity index 98% rename from apps/taktik/apps/web/src/routes/(app)/projects/+page.svelte rename to apps/times/apps/web/src/routes/(app)/projects/+page.svelte index ddf264dbb..0a1ea835f 100644 --- a/apps/taktik/apps/web/src/routes/(app)/projects/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/projects/+page.svelte @@ -3,8 +3,8 @@ import { _ } from 'svelte-i18n'; import { projectCollection } from '$lib/data/local-store'; import { getTotalDuration, formatDurationCompact } from '$lib/data/queries'; - import type { Project, Client, TimeEntry } from '@taktik/shared'; - import { PROJECT_COLORS } from '@taktik/shared/constants'; + import type { Project, Client, TimeEntry } from '@times/shared'; + import { PROJECT_COLORS } from '@times/shared/constants'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; const allProjects = getContext<{ value: Project[] }>('projects'); @@ -109,7 +109,7 @@ - {$_('nav.projects')} | Taktik + {$_('nav.projects')} | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/projects/[id]/+page.svelte b/apps/times/apps/web/src/routes/(app)/projects/[id]/+page.svelte similarity index 98% rename from apps/taktik/apps/web/src/routes/(app)/projects/[id]/+page.svelte rename to apps/times/apps/web/src/routes/(app)/projects/[id]/+page.svelte index 600ed8913..7db40156c 100644 --- a/apps/taktik/apps/web/src/routes/(app)/projects/[id]/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/projects/[id]/+page.svelte @@ -11,8 +11,8 @@ formatDurationDecimal, } from '$lib/data/queries'; import EntryList from '$lib/components/EntryList.svelte'; - import type { Project, Client, TimeEntry } from '@taktik/shared'; - import { PROJECT_COLORS } from '@taktik/shared/constants'; + import type { Project, Client, TimeEntry } from '@times/shared'; + import { PROJECT_COLORS } from '@times/shared/constants'; const allProjects = getContext<{ value: Project[] }>('projects'); const allClients = getContext<{ value: Client[] }>('clients'); @@ -82,7 +82,7 @@ - {project?.name || 'Projekt'} | Taktik + {project?.name || 'Projekt'} | Times {#if !project} diff --git a/apps/taktik/apps/web/src/routes/(app)/reports/+page.svelte b/apps/times/apps/web/src/routes/(app)/reports/+page.svelte similarity index 98% rename from apps/taktik/apps/web/src/routes/(app)/reports/+page.svelte rename to apps/times/apps/web/src/routes/(app)/reports/+page.svelte index 240c48df3..9b191f869 100644 --- a/apps/taktik/apps/web/src/routes/(app)/reports/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/reports/+page.svelte @@ -1,7 +1,7 @@ - {$_('nav.reports')} | Taktik + {$_('nav.reports')} | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/settings/+page.svelte b/apps/times/apps/web/src/routes/(app)/settings/+page.svelte similarity index 97% rename from apps/taktik/apps/web/src/routes/(app)/settings/+page.svelte rename to apps/times/apps/web/src/routes/(app)/settings/+page.svelte index 719b40c0a..3266bdfde 100644 --- a/apps/taktik/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/settings/+page.svelte @@ -2,10 +2,10 @@ import { getContext } from 'svelte'; import { _ } from 'svelte-i18n'; import { settingsCollection } from '$lib/data/local-store'; - import type { TaktikSettings } from '@taktik/shared'; - import { CURRENCIES, ROUNDING_INCREMENTS } from '@taktik/shared/constants'; + import type { TimesSettings } from '@times/shared'; + import { CURRENCIES, ROUNDING_INCREMENTS } from '@times/shared/constants'; - const settings = getContext<{ value: TaktikSettings | null }>('settings'); + const settings = getContext<{ value: TimesSettings | null }>('settings'); // Local edit state, synced from settings let workingHoursPerDay = $state(8); @@ -49,7 +49,7 @@ - {$_('settings.title')} | Taktik + {$_('settings.title')} | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/templates/+page.svelte b/apps/times/apps/web/src/routes/(app)/templates/+page.svelte similarity index 98% rename from apps/taktik/apps/web/src/routes/(app)/templates/+page.svelte rename to apps/times/apps/web/src/routes/(app)/templates/+page.svelte index 7c04d45da..805040f1a 100644 --- a/apps/taktik/apps/web/src/routes/(app)/templates/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/templates/+page.svelte @@ -3,7 +3,7 @@ import { _ } from 'svelte-i18n'; import { templateCollection, timeEntryCollection } from '$lib/data/local-store'; import { timerStore } from '$lib/stores/timer.svelte'; - import type { EntryTemplate, Project, Client } from '@taktik/shared'; + import type { EntryTemplate, Project, Client } from '@times/shared'; const allTemplates = getContext<{ value: EntryTemplate[] }>('templates'); const allProjects = getContext<{ value: Project[] }>('projects'); @@ -64,7 +64,7 @@ - {$_('nav.templates')} | Taktik + {$_('nav.templates')} | Times
diff --git a/apps/taktik/apps/web/src/routes/(app)/themes/+page.svelte b/apps/times/apps/web/src/routes/(app)/themes/+page.svelte similarity index 88% rename from apps/taktik/apps/web/src/routes/(app)/themes/+page.svelte rename to apps/times/apps/web/src/routes/(app)/themes/+page.svelte index 8a8878ce4..000b1b683 100644 --- a/apps/taktik/apps/web/src/routes/(app)/themes/+page.svelte +++ b/apps/times/apps/web/src/routes/(app)/themes/+page.svelte @@ -3,7 +3,7 @@ - Themes | Taktik + Themes | Times
diff --git a/apps/taktik/apps/web/src/routes/(auth)/+layout.svelte b/apps/times/apps/web/src/routes/(auth)/+layout.svelte similarity index 100% rename from apps/taktik/apps/web/src/routes/(auth)/+layout.svelte rename to apps/times/apps/web/src/routes/(auth)/+layout.svelte diff --git a/apps/taktik/apps/web/src/routes/(auth)/login/+page.svelte b/apps/times/apps/web/src/routes/(auth)/login/+page.svelte similarity index 99% rename from apps/taktik/apps/web/src/routes/(auth)/login/+page.svelte rename to apps/times/apps/web/src/routes/(auth)/login/+page.svelte index 74438d766..b62dd355d 100644 --- a/apps/taktik/apps/web/src/routes/(auth)/login/+page.svelte +++ b/apps/times/apps/web/src/routes/(auth)/login/+page.svelte @@ -96,7 +96,7 @@ - {showRegister ? $_('auth.register') : $_('auth.login')} | Taktik + {showRegister ? $_('auth.register') : $_('auth.login')} | Times
@@ -115,7 +115,7 @@ />
-

Taktik

+

Times

Zeiterfassung

@@ -256,7 +256,7 @@
{#each getPillAppItems() as app} - {#if app.id !== 'taktik'} + {#if app.id !== 'times'} { return json({ status: 'ok', timestamp: new Date().toISOString(), - service: 'taktik-web', + service: 'times-web', }); }; diff --git a/apps/taktik/apps/web/src/routes/offline/+page.svelte b/apps/times/apps/web/src/routes/offline/+page.svelte similarity index 97% rename from apps/taktik/apps/web/src/routes/offline/+page.svelte rename to apps/times/apps/web/src/routes/offline/+page.svelte index 4c1ddb1d9..fef063e96 100644 --- a/apps/taktik/apps/web/src/routes/offline/+page.svelte +++ b/apps/times/apps/web/src/routes/offline/+page.svelte @@ -3,7 +3,7 @@ - Offline | Taktik + Offline | Times
diff --git a/apps/taktik/apps/web/svelte.config.js b/apps/times/apps/web/svelte.config.js similarity index 100% rename from apps/taktik/apps/web/svelte.config.js rename to apps/times/apps/web/svelte.config.js diff --git a/apps/taktik/apps/web/tsconfig.json b/apps/times/apps/web/tsconfig.json similarity index 100% rename from apps/taktik/apps/web/tsconfig.json rename to apps/times/apps/web/tsconfig.json diff --git a/apps/taktik/apps/web/vite.config.ts b/apps/times/apps/web/vite.config.ts similarity index 96% rename from apps/taktik/apps/web/vite.config.ts rename to apps/times/apps/web/vite.config.ts index 4c9133a84..7692ffa2a 100644 --- a/apps/taktik/apps/web/vite.config.ts +++ b/apps/times/apps/web/vite.config.ts @@ -11,8 +11,8 @@ export default defineConfig({ SvelteKitPWA({ registerType: 'autoUpdate', manifest: { - name: 'Taktik', - short_name: 'Taktik', + name: 'Times', + short_name: 'Times', description: 'Zeiterfassung & Timetracking', theme_color: '#f59e0b', background_color: '#0f172a', diff --git a/apps/times/package.json b/apps/times/package.json new file mode 100644 index 000000000..388ba4e6f --- /dev/null +++ b/apps/times/package.json @@ -0,0 +1,14 @@ +{ + "name": "times", + "version": "1.0.0", + "private": true, + "description": "Times - Zeiterfassung & Timetracking", + "scripts": { + "dev": "pnpm --filter @times/web dev", + "dev:web": "pnpm --filter @times/web dev" + }, + "devDependencies": { + "typescript": "^5.9.3" + }, + "packageManager": "pnpm@9.15.0" +} diff --git a/apps/taktik/packages/shared/package.json b/apps/times/packages/shared/package.json similarity index 93% rename from apps/taktik/packages/shared/package.json rename to apps/times/packages/shared/package.json index c7cf82295..05f0e24f2 100644 --- a/apps/taktik/packages/shared/package.json +++ b/apps/times/packages/shared/package.json @@ -1,5 +1,5 @@ { - "name": "@taktik/shared", + "name": "@times/shared", "version": "1.0.0", "private": true, "type": "module", diff --git a/apps/taktik/packages/shared/src/constants/index.ts b/apps/times/packages/shared/src/constants/index.ts similarity index 100% rename from apps/taktik/packages/shared/src/constants/index.ts rename to apps/times/packages/shared/src/constants/index.ts diff --git a/apps/taktik/packages/shared/src/index.ts b/apps/times/packages/shared/src/index.ts similarity index 100% rename from apps/taktik/packages/shared/src/index.ts rename to apps/times/packages/shared/src/index.ts diff --git a/apps/taktik/packages/shared/src/types/index.ts b/apps/times/packages/shared/src/types/index.ts similarity index 99% rename from apps/taktik/packages/shared/src/types/index.ts rename to apps/times/packages/shared/src/types/index.ts index acdd8d3ce..f81df2d8a 100644 --- a/apps/taktik/packages/shared/src/types/index.ts +++ b/apps/times/packages/shared/src/types/index.ts @@ -110,7 +110,7 @@ export interface EntryTemplate { export type RoundingMethod = 'none' | 'up' | 'down' | 'nearest'; -export interface TaktikSettings { +export interface TimesSettings { id: string; defaultBillingRate?: BillingRate; workingHoursPerDay: number; diff --git a/apps/taktik/packages/shared/tsconfig.json b/apps/times/packages/shared/tsconfig.json similarity index 100% rename from apps/taktik/packages/shared/tsconfig.json rename to apps/times/packages/shared/tsconfig.json diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index ff6d73bd6..1d19115cc 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -287,7 +287,7 @@ services: SMTP_PASS: ${SMTP_PASSWORD} SYNAPSE_OIDC_CLIENT_SECRET: ${SYNAPSE_OIDC_CLIENT_SECRET:-} MAX_DAILY_SIGNUPS: ${MAX_DAILY_SIGNUPS:-0} - CORS_ORIGINS: https://mana.how,https://calendar.mana.how,https://chat.mana.how,https://clock.mana.how,https://contacts.mana.how,https://context.mana.how,https://docs.mana.how,https://element.mana.how,https://inventar.mana.how,https://link.mana.how,https://manadeck.mana.how,https://matrix.mana.how,https://mukke.mana.how,https://nutriphi.mana.how,https://photos.mana.how,https://picture.mana.how,https://planta.mana.how,https://playground.mana.how,https://presi.mana.how,https://questions.mana.how,https://skilltree.mana.how,https://storage.mana.how,https://taktik.mana.how,https://todo.mana.how,https://traces.mana.how,https://zitare.mana.how + CORS_ORIGINS: https://mana.how,https://calendar.mana.how,https://chat.mana.how,https://clock.mana.how,https://contacts.mana.how,https://context.mana.how,https://docs.mana.how,https://element.mana.how,https://inventar.mana.how,https://link.mana.how,https://manadeck.mana.how,https://matrix.mana.how,https://mukke.mana.how,https://nutriphi.mana.how,https://photos.mana.how,https://picture.mana.how,https://planta.mana.how,https://playground.mana.how,https://presi.mana.how,https://questions.mana.how,https://skilltree.mana.how,https://storage.mana.how,https://times.mana.how,https://todo.mana.how,https://traces.mana.how,https://zitare.mana.how ports: - "3001:3001" healthcheck: @@ -1355,12 +1355,12 @@ services: retries: 3 start_period: 20s - taktik-web: + times-web: build: context: . - dockerfile: apps/taktik/apps/web/Dockerfile - image: taktik-web:local - container_name: mana-app-taktik-web + dockerfile: apps/times/apps/web/Dockerfile + image: times-web:local + container_name: mana-app-times-web restart: always mem_limit: 128m depends_on: diff --git a/docker/matrix/element/config.json b/docker/matrix/element/config.json new file mode 100644 index 000000000..4a6718886 --- /dev/null +++ b/docker/matrix/element/config.json @@ -0,0 +1,59 @@ +{ + "default_server_config": { + "m.homeserver": { + "base_url": "https://matrix.mana.how", + "server_name": "mana.how" + }, + "m.identity_server": { + "base_url": "" + } + }, + "brand": "ManaCore Chat", + "integrations_ui_url": "", + "integrations_rest_url": "", + "integrations_widgets_urls": [], + "disable_guests": true, + "disable_3pid_login": true, + "default_country_code": "DE", + "show_labs_settings": false, + "features": { + "feature_video_rooms": true, + "feature_group_calls": true, + "feature_thread": true + }, + "room_directory": { + "servers": ["mana.how"] + }, + "setting_defaults": { + "breadcrumbs": true, + "custom_themes": [], + "UIFeature.e2eeDefault": false, + "FTUE.userOnboardingButton": false, + "analyticsOptIn": false, + "pseudonymousAnalyticsOptIn": false + }, + "ui_features": { + "UIFeature.RoomEncryptionSettings": false + }, + "force_verification": false, + "enable_presence_by_hs_url": {}, + "default_theme": "dark", + "permalink_prefix": "https://element.mana.how", + "terms_and_conditions_links": [], + "sso_redirect_options": { + "immediate": false, + "on_welcome_page": true + }, + "posthog": { + "disabled": true + }, + "sentry": { + "disabled": true + }, + "analytics_owner": "", + "privacy_policy_url": "", + "show_analytics_setting": false, + "bug_report_endpoint_url": "", + "help_url": "https://mana.how/help", + "help_encryption_url": "https://element.io/help#encryption" +} diff --git a/docs/CLUSTER_HARDWARE_ANALYSE.md b/docs/CLUSTER_HARDWARE_ANALYSE.md new file mode 100644 index 000000000..a248358d1 --- /dev/null +++ b/docs/CLUSTER_HARDWARE_ANALYSE.md @@ -0,0 +1,433 @@ +# Hardware-Analyse: 3-Node Home Server Cluster + +**Stand: Maerz 2026** | Anforderung: K3s, YugabyteDB, ~60 Container (Go, Node/Bun, Python, SvelteKit) + +## Referenz: Apple Mac Mini M4 (24 GB) + +| Eigenschaft | Wert | +|---|---| +| CPU | Apple M4, 10-Core (4P + 6E), ARM64 | +| RAM | 24 GB unified (nicht aufruestbar) | +| SSD | 256 GB / 512 GB NVMe (nicht aufruestbar) | +| Netzwerk | 1x Gigabit Ethernet (10 GbE als Option: +30 EUR Thunderbolt-Adapter) | +| Leistungsaufnahme | **3-4 W idle / 25-35 W Last** | +| Preis (DE) | **929 EUR** (Apple Store), ab ~870 EUR (Geizhals, 24 GB / 512 GB) | +| Linux | Asahi Linux (Fedora-basiert), experimentell. macOS + Colima/Docker Desktop funktioniert | +| K3s | Nativ nur via Asahi Linux oder VM. Praxis: Colima + k3s in Lima-VM | + +### Staerken +- Unschlagbare Energieeffizienz (3-4 W idle) +- Hervorragende Single- und Multi-Core-Performance +- Kompaktestes Gehaeuse am Markt (12.7 x 12.7 cm) +- Leise (passiv bei niedrigem Last) + +### Schwaechen +- **Kein natives Linux** (Asahi experimentell, kein offizieller Support) +- **RAM und SSD nicht aufruestbar** (bei Kauf festgelegt) +- K3s laeuft nicht nativ auf macOS -- Virtualisierung noetig +- ARM64: ~20% der Helm Charts liefern nur x86-Images +- Kein 10 GbE onboard + +--- + +## 1. ASUS NUC 14 Pro / Pro+ + +### ASUS NUC 14 Pro (Barebone) + +| Eigenschaft | Wert | +|---|---| +| CPU | Intel Core Ultra 5 125H / Ultra 7 155H / Ultra 9 185H (Meteor Lake) | +| Kerne | 14C/18T (Ultra 7) bzw. 16C/22T (Ultra 9) | +| RAM | Bis 96 GB DDR5-5600 SO-DIMM (2 Slots, aufruestbar) | +| SSD | 1x M.2 2280 PCIe 4.0, 1x M.2 2242 | +| Netzwerk | 1x 2.5 GbE Intel i226-V | +| Leistungsaufnahme | **8-10 W idle / 45-88 W Last** | +| Preis (DE) | Barebone ab **235 EUR** (Geizhals), Komplettsystem ab **879 EUR** | +| Linux | Hervorragend (Ubuntu, Fedora nativ) | +| Verfuegbarkeit | Sofort lieferbar | + +### ASUS NUC 14 Pro+ (Komplettsystem) + +| Eigenschaft | Wert | +|---|---| +| CPU | Intel Core Ultra 7 155H / Ultra 9 185H | +| RAM | 16-32 GB DDR5 (aufruestbar bis 96 GB) | +| Preis (DE) | ab **879 EUR** (Komplettsystem mit 16 GB / 512 GB) | +| Besonderheit | Thunderbolt 4, WiFi 6E, vPro (manche Modelle) | + +**Kalkulation fuer 32 GB Variante:** +- Barebone Ultra 7 155H: ~450 EUR +- 32 GB DDR5 SO-DIMM Kit: ~80 EUR +- 1 TB NVMe SSD: ~70 EUR +- **Gesamt: ~600 EUR pro Node** + +### Bewertung vs. Mac Mini M4 + +| Kriterium | NUC 14 Pro | Mac Mini M4 | +|---|---|---| +| Preis (32 GB) | ~600 EUR | ~929 EUR | +| Idle-Verbrauch | 8-10 W | 3-4 W | +| Multi-Core | Vergleichbar (14C) | Leicht besser (effizienter) | +| RAM aufruestbar | Ja (bis 96 GB) | Nein | +| Linux nativ | Ja | Nein (Asahi experimentell) | +| 10 GbE | Nein (2.5 GbE) | Nein (1 GbE) | + +**Fazit:** Bester Kompromiss aus Preis, Performance und Linux-Kompatibilitaet. Klare Empfehlung. + +--- + +## 2. AMD Mini PCs + +### Minisforum UM890 Pro + +| Eigenschaft | Wert | +|---|---| +| CPU | AMD Ryzen 9 8945HS (8C/16T, Zen 4, bis 5.2 GHz) | +| RAM | Bis 64 GB DDR5-5600 SO-DIMM (2 Slots, aufruestbar) | +| SSD | 2x M.2 2280 PCIe 4.0 | +| Netzwerk | 1x 2.5 GbE | +| Leistungsaufnahme | **7-10 W idle / 80-95 W Last** | +| Preis (DE) | ab **463 EUR** (Geizhals), mit 32 GB/1 TB ca. **550-600 EUR** | +| Linux | Hervorragend (voller AMD-Support) | +| Verfuegbarkeit | Teilweise ausverkauft, Nachlieferung Mai 2026 | + +### Beelink SER8 + +| Eigenschaft | Wert | +|---|---| +| CPU | AMD Ryzen 7 8845HS (8C/16T, Zen 4, bis 5.1 GHz) | +| RAM | 32 GB DDR5-5600 (aufruestbar) | +| SSD | 1 TB PCIe 4.0 | +| Netzwerk | 1x 2.5 GbE | +| Leistungsaufnahme | **8-10 W idle (Linux) / 85 W Last** | +| Preis (DE) | **399-450 EUR** (Amazon.de mit 32 GB/1 TB) | +| Linux | Hervorragend | +| Verfuegbarkeit | Sofort lieferbar | + +### Beelink SER9 (Zen 5) + +| Eigenschaft | Wert | +|---|---| +| CPU | AMD Ryzen AI 9 HX 370 (12C/24T, Zen 5) | +| RAM | Bis 64 GB DDR5 | +| Netzwerk | 1x 2.5 GbE | +| Leistungsaufnahme | ~10 W idle / ~95 W Last | +| Preis (DE) | ab ca. **650-750 EUR** (mit 32 GB) | + +### Geekom A8 Max + +| Eigenschaft | Wert | +|---|---| +| CPU | AMD Ryzen 9 8945HS (8C/16T) oder Ryzen 7 8745HS | +| RAM | Bis 64 GB DDR5 (aufruestbar) | +| SSD | Bis 2 TB | +| Netzwerk | **2x 2.5 GbE** (Dual-LAN!) | +| Leistungsaufnahme | ~8-10 W idle / ~85 W Last | +| Preis (DE) | ab **720 EUR** (mit Code), regulaer **849 EUR** | +| Besonderheit | USB4, Dual 2.5 GbE fuer LAG/Failover | + +### Minisforum MS-A2 (Homelab-Spezialist) + +| Eigenschaft | Wert | +|---|---| +| CPU | AMD Ryzen 9 9955HX (16C/32T, Zen 5) | +| RAM | Bis 96 GB DDR5 (2 Slots) | +| SSD | 3x M.2 (2280 + 22110 + U.2) | +| Netzwerk | **2x 10 GbE SFP+** + 2x 2.5 GbE | +| PCIe | 1x PCIe x16 Slot | +| Leistungsaufnahme | **25-30 W idle / 120+ W Last** | +| Preis (DE) | Barebone ab **839 EUR**, Komplett ab **975 EUR** | +| Linux | Hervorragend | +| Verfuegbarkeit | Teilweise ausverkauft | + +**Fazit AMD:** Der **Beelink SER8** bietet das beste Preis-Leistungs-Verhaeltnis. Die **MS-A2** ist das Schweizer Taschenmesser fuer Homelabs mit 10 GbE, aber teuer und stromhungrig im Idle. + +--- + +## 3. ARM-Alternativen + +### Raspberry Pi 5 (8 GB) + +| Eigenschaft | Wert | +|---|---| +| CPU | Broadcom BCM2712, 4x Cortex-A76 @ 2.4 GHz | +| RAM | 8 GB LPDDR4X (nicht aufruestbar) | +| SSD | Via M.2 HAT (NVMe) oder USB 3.0 SSD | +| Netzwerk | 1x Gigabit Ethernet | +| Leistungsaufnahme | **3-4 W idle / 10-12 W Last** | +| Preis (DE) | ca. **95 EUR** (Board + Gehaeuse + Kuehler + Netzteil) | +| Linux | Hervorragend (Raspberry Pi OS, Ubuntu) | + +**Realistisch fuer diesen Workload? NEIN.** + +- 8 GB RAM reicht nicht fuer YugabyteDB + K3s + Container +- Nur 4 Kerne -- ~60 Container werden sehr eng +- ~20% der Container-Images sind nur x86 +- SD-Karten-IO ist ungeeignet (NVMe-HAT hilft, aber PCIe 2.0 x1) +- Kein ECC, instabil unter Dauerlast +- Muesste 5-6 Nodes statt 3 nutzen = mehr Komplexitaet + +**Einsatz:** Gut als dedizierter K3s-Agent fuer leichte Workloads, nicht als Hauptknoten. + +### Orange Pi 5 Plus / Rock 5B + +| Eigenschaft | Orange Pi 5 Plus | Rock 5B | +|---|---|---| +| CPU | RK3588 8C (4x A76 + 4x A55) | RK3588 8C | +| RAM | 16 GB / 32 GB | 16 GB | +| Netzwerk | 2x 2.5 GbE | 1x 2.5 GbE | +| Preis | ~170 EUR (32 GB) | ~130 EUR (16 GB) | +| Leistungsaufnahme | 5-8 W idle | 5-8 W idle | + +**Bewertung:** Mehr RAM als Pi 5, aber gleiche Grundprobleme: ARM-Image-Kompatibilitaet, begrenzte CPU-Leistung, kein ECC. Fuer 60 Container nicht empfohlen. + +### Ampere Altra + +| Eigenschaft | Wert | +|---|---| +| CPU | Ampere Altra (bis 128 ARM-Kerne) | +| RAM | Server-DDR5 ECC, bis 512 GB | +| Preis | ab **3.000+ EUR** fuer komplettes System | + +**Bewertung:** Overkill und weit ueber Budget. Interessant fuer Datacenter, nicht fuer Home-Cluster. + +--- + +## 4. Refurbished Enterprise Micro Server + +### Lenovo ThinkCentre M75q Gen 5 Tiny + +| Eigenschaft | Wert | +|---|---| +| CPU | AMD Ryzen 7 PRO 8700GE (8C/16T, 35W TDP) | +| RAM | Bis 64 GB DDR5 (2 Slots) | +| SSD | 1x M.2 2280 | +| Netzwerk | 1x 1 GbE (kein 2.5 GbE!) | +| Leistungsaufnahme | ~8-12 W idle / 35-50 W Last | +| Preis (DE, neu) | ab ca. **600-800 EUR** | +| Preis (refurbished) | ab ca. **350-500 EUR** | +| Linux | Hervorragend (Business-Hardware, gut getestet) | + +### HP Elite Mini 800 G9 + +| Eigenschaft | Wert | +|---|---| +| CPU | Intel Core i5-12500T / i7-12700T (12./13. Gen) | +| RAM | Bis 64 GB DDR5 | +| SSD | 1x M.2 NVMe | +| Netzwerk | 1x 1 GbE | +| Leistungsaufnahme | ~10-15 W idle / 35-65 W Last | +| Preis (DE, refurbished) | ab ca. **300-450 EUR** (32 GB, refurbed.de) | +| Linux | Hervorragend | + +### Dell OptiPlex 7010/7020 Micro + +| Eigenschaft | Wert | +|---|---| +| CPU | Intel Core i5-13500T / i7-13700T | +| RAM | Bis 64 GB DDR5 (ab Gen 13) | +| Netzwerk | 1x 1 GbE | +| Preis (DE, refurbished) | ab ca. **280-400 EUR** | +| Linux | Hervorragend | + +**Fazit Refurbished:** Extrem guenstig, aber nur 1 GbE Netzwerk und aeltere CPUs. Gut fuer Budget-Cluster, weniger fuer Zukunftssicherheit. + +--- + +## 5. Ultra-Low-Power Mini PCs + +### Intel N100 Mini PCs (z.B. Beelink S12 Pro, CWWK) + +| Eigenschaft | Wert | +|---|---| +| CPU | Intel N100 (4C/4T, Alder Lake-N, bis 3.4 GHz) | +| RAM | 8-16 GB DDR5 (meistens geloetet, nicht aufruestbar) | +| SSD | 1x M.2 2280 | +| Netzwerk | 1x 2.5 GbE (CWWK: bis 6x 2.5 GbE) | +| Leistungsaufnahme | **6-8 W idle / 25 W Last** | +| Preis (DE) | ab **130-200 EUR** | +| Linux | Hervorragend | + +### Intel N305 Mini PCs (z.B. CWWK F7) + +| Eigenschaft | Wert | +|---|---| +| CPU | Intel i3-N305 (8C/8T, Alder Lake-N, bis 3.8 GHz) | +| RAM | Bis 32 GB DDR5 | +| SSD | 1-2x M.2 NVMe | +| Netzwerk | Bis 6x 2.5 GbE (CWWK/Topton Modelle) | +| Leistungsaufnahme | **10-14 W idle / 34-50 W Last** | +| Preis (DE) | ab **200-350 EUR** (mit 32 GB/1 TB) | +| Linux | Hervorragend | +| Besonderheit | Fanless-Modelle verfuegbar (24/7 Betrieb, lautlos) | + +**Bewertung N100:** Nur 4 Kerne und max 16 GB RAM -- fuer YugabyteDB + 20 Container pro Node zu knapp. + +**Bewertung N305:** 8 Kerne und 32 GB moeglich -- minimal ausreichend. Fanless und extrem stromsparend. Aber Single-Thread-Leistung weit unter Ryzen/Apple Silicon. + +--- + +## Gesamtvergleich + +### Performance und Effizienz + +| System | Kerne | Idle (W) | Last (W) | Multi-Core (rel.) | Perf/Watt | +|---|---|---|---|---|---| +| **Mac Mini M4** | 10 (4P+6E) | 3-4 | 25-35 | 100% (Referenz) | Exzellent | +| ASUS NUC 14 Pro (Ultra 7) | 14 (6P+8E) | 8-10 | 45-88 | ~90-100% | Gut | +| Beelink SER8 (8845HS) | 8 (8P) | 8-10 | 85 | ~85% | Gut | +| Minisforum UM890 Pro | 8 (8P) | 7-10 | 80-95 | ~90% | Gut | +| Minisforum MS-A2 (9955HX) | 16 (16P) | 25-30 | 120+ | ~170% | Maessig | +| Lenovo M75q (8700GE) | 8 (8P) | 8-12 | 35-50 | ~75% | Gut | +| Intel N305 (CWWK) | 8 (8E) | 10-14 | 34-50 | ~35% | Gut (fuer idle) | +| Raspberry Pi 5 | 4 | 3-4 | 10-12 | ~15% | Gut (fuer idle) | + +### Stromkosten ueber 3 Jahre (0,35 EUR/kWh, 3 Nodes) + +| System | Idle (W, 3 Nodes) | Jahreskosten (idle) | 3-Jahres-Strom | +|---|---|---|---| +| **Mac Mini M4** | 9-12 W | 28-37 EUR | **84-110 EUR** | +| ASUS NUC 14 Pro | 24-30 W | 74-92 EUR | **221-276 EUR** | +| Beelink SER8 | 24-30 W | 74-92 EUR | **221-276 EUR** | +| Minisforum MS-A2 | 75-90 W | 230-276 EUR | **690-828 EUR** | +| Intel N305 | 30-42 W | 92-129 EUR | **276-387 EUR** | +| Raspberry Pi 5 | 9-12 W | 28-37 EUR | **84-110 EUR** | + +*Annahme: Server laeuft 24/7, Grossteil der Zeit im Idle/Low-Load.* + +### Total Cost of Ownership (3 Jahre: Hardware + Strom) + +| System | Hardware (3x) | Strom (3 J.) | **TCO Gesamt** | +|---|---|---|---| +| **Mac Mini M4 (24 GB)** | 2.787 EUR | ~100 EUR | **~2.887 EUR** | +| ASUS NUC 14 Pro (32 GB) | 1.800 EUR | ~250 EUR | **~2.050 EUR** | +| Beelink SER8 (32 GB) | 1.200-1.350 EUR | ~250 EUR | **~1.500 EUR** | +| Minisforum UM890 Pro (32 GB) | 1.650 EUR | ~250 EUR | **~1.900 EUR** | +| Minisforum MS-A2 (32 GB) | 2.925 EUR | ~760 EUR | **~3.685 EUR** | +| Lenovo M75q refurb (32 GB) | 1.050-1.500 EUR | ~250 EUR | **~1.500 EUR** | +| Intel N305 CWWK (32 GB) | 750-1.050 EUR | ~330 EUR | **~1.200 EUR** | + +--- + +## ARM vs. x86 Ueberlegungen fuer Docker/K8s + +| Aspekt | ARM64 (Mac Mini, Pi) | x86_64 (Intel, AMD) | +|---|---|---| +| Container-Image-Verfuegbarkeit | ~80% (multi-arch nimmt zu) | 99%+ | +| Offizielle K3s-Unterstuetzung | Ja (ARM64 Builds) | Ja (primaer) | +| YugabyteDB | Offiziell nur x86! ARM experimentell | Voll unterstuetzt | +| Go-Services | Kein Problem (Cross-Compile) | Kein Problem | +| Node.js/Bun | Kein Problem | Kein Problem | +| Python | Kein Problem (manche C-Extensions langsamer) | Kein Problem | +| Helm Charts (Community) | ~80% kompatibel | ~99% kompatibel | + +**Kritisch: YugabyteDB hat keinen offiziellen ARM64-Support.** Das allein disqualifiziert reine ARM-Setups fuer das geplante Setup, es sei denn man baut selbst oder nutzt einen Fork. + +--- + +## Empfehlungen + +### Empfehlung 1: Beelink SER8 (Bestes Preis-Leistungs-Verhaeltnis) + +**3x Beelink SER8 (32 GB DDR5 / 1 TB SSD)** + +| | | +|---|---| +| Preis pro Node | ~400-450 EUR | +| Gesamtpreis (3 Nodes) | ~1.200-1.350 EUR | +| TCO (3 Jahre) | ~1.500 EUR | +| Idle-Verbrauch (3 Nodes) | ~24-30 W | + +- Sofort auf Amazon.de verfuegbar +- Linux-Support perfekt +- YugabyteDB laeuft nativ +- 8C/16T pro Node = 24C/48T gesamt -- mehr als genug fuer 60 Container +- 2.5 GbE Netzwerk +- Aufruestbar auf 64 GB RAM pro Node bei Bedarf +- **Spart ~1.400 EUR gegenueber 3x Mac Mini M4** + +### Empfehlung 2: ASUS NUC 14 Pro (Bester Kompromiss) + +**3x ASUS NUC 14 Pro Barebone (Ultra 7 155H) + 32 GB + 1 TB** + +| | | +|---|---| +| Preis pro Node | ~600 EUR | +| Gesamtpreis (3 Nodes) | ~1.800 EUR | +| TCO (3 Jahre) | ~2.050 EUR | + +- Bewaeehrte NUC-Qualitaet (Intel/ASUS) +- 14 Kerne pro Node = 42 Kerne gesamt +- Aufruestbar bis 96 GB RAM +- Thunderbolt 4 fuer 10 GbE Adapter +- Business-Hardware mit laengerer Verfuegbarkeit von Ersatzteilen + +### Empfehlung 3: Minisforum MS-A2 (Maximum Homelab) + +**Nur wenn 10 GbE zwingend noetig.** + +| | | +|---|---| +| Preis pro Node | ~975 EUR | +| Gesamtpreis (3 Nodes) | ~2.925 EUR | +| TCO (3 Jahre) | ~3.685 EUR | + +- 16 Kerne + 10 GbE SFP+ onboard +- PCIe x16 Slot fuer GPU/NIC-Erweiterung +- Aber: Hoher Idle-Verbrauch (25-30 W), teuer, oft ausverkauft + +### Empfehlung 4: Hybrid (Budget + Zukunftssicherheit) + +**2x Beelink SER8 + 1x ASUS NUC 14 Pro (als Control Plane)** + +| | | +|---|---| +| Gesamtpreis | ~1.500 EUR | +| TCO (3 Jahre) | ~1.750 EUR | + +--- + +## Nicht empfohlen + +| Option | Grund | +|---|---| +| **Mac Mini M4** | Kein natives Linux, YugabyteDB nicht ARM-offiziell, RAM nicht aufruestbar, teuer | +| **Raspberry Pi 5** | Zu wenig RAM (8 GB), zu wenig CPU, ARM-Image-Probleme | +| **Intel N100** | Nur 4 Kerne, max 16 GB RAM -- zu schwach | +| **Ampere Altra** | Weit ueber Budget | +| **Minisforum MS-A2** | Nur wenn 10 GbE Pflicht (sonst zu teuer und zu stromhungrig) | + +--- + +## Fazit + +Fuer den geplanten Cluster mit K3s, YugabyteDB und ~60 Containern ist der **Beelink SER8** die wirtschaftlich beste Wahl: 3 Nodes fuer ~1.300 EUR, perfekter Linux-Support, ausreichend Leistung, und moderate Stromkosten. Die ~150 EUR mehr Stromkosten gegenueber Mac Minis werden durch ~1.400 EUR Hardware-Ersparnis mehr als kompensiert. + +Falls die Anforderungen wachsen (mehr RAM, 10 GbE), ist ein spaeterer Umstieg auf ASUS NUC 14 Pro oder Minisforum MS-A2 Nodes moeglich -- K3s macht das Hinzufuegen und Entfernen von Nodes trivial. + +**Wichtigster Grund gegen den Mac Mini M4:** YugabyteDB hat keinen offiziellen ARM64-Support. Das ist fuer eine produktive Datenbank ein No-Go. + +--- + +## Quellen + +- [Mac Mini M4 Effizienz - Jeff Geerling](https://www.jeffgeerling.com/blog/2024/m4-mac-minis-efficiency-incredible/) +- [Mac Mini M4 Review - NotebookCheck](https://www.notebookcheck.net/Apple-Mac-Mini-M4-review-Smaller-faster-and-louder.918832.0.html) +- [Mac Mini M4 Review - ServeTheHome](https://www.servethehome.com/the-apple-mac-mini-m4-sets-the-mini-computer-standard/3/) +- [ASUS NUC 14 Pro Review - ServeTheHome](https://www.servethehome.com/asus-nuc-14-pro-review-intel-core/4/) +- [ASUS NUC 14 Pro Geizhals](https://geizhals.de/asus-nuc-14-pro-kit-barebone-v158051.html) +- [Minisforum UM890 Pro Review - NotebookCheck](https://www.notebookcheck.net/Minisforum-EliteMini-UM890-Pro-review-A-powerful-mini-PC-with-AMD-Ryzen-9-and-whisper-quiet-cooling.982755.0.html) +- [Minisforum UM890 Pro Review - ServeTheHome](https://www.servethehome.com/minisforum-um890-pro-review-re-architected-amd-ryzen-8945hs-mini-pc/4/) +- [Minisforum MS-A2 Review - ServeTheHome](https://www.servethehome.com/minisforum-ms-a2-review-an-almost-perfect-amd-ryzen-intel-10gbe-homelab-system/4/) +- [Minisforum MS-A2 Geizhals](https://geizhals.de/minisforum-ms-a2-a3504299.html) +- [Beelink SER8 Review - ServeTheHome](https://www.servethehome.com/beelink-ser8-review-amd-ryzen-7-8845hs-powered-mini-pc/3/) +- [Beelink SER8 Review - Hardwareluxx](https://www.hardwareluxx.de/index.php/artikel/hardware/komplettsysteme/63918-mit-ryzen-7-8845hs-und-im-kompakten-geh%C3%A4use-beelink-ser8-im-test.html) +- [Beelink SER7 Amazon.de](https://www.amazon.de/Beelink-Ryzen-Threads-PCIe4-0-Display/dp/B0CH7VJZ94) +- [Geekom A8 Max - NotebookCheck](https://www.notebookcheck.net/Best-value-with-AMD-Ryzen-7-8845HS-The-Geekom-A8-Max-mini-PC-review.1005224.0.html) +- [Geekom DE Shop](https://www.geekom.de/geekom-a8-mini-pc/) +- [HP Elite Mini 800 G9 - refurbed.de](https://www.refurbed.de/p/hp-elite-mini-800-g9/) +- [Lenovo M75q Gen 2 Geizhals](https://geizhals.de/lenovo-thinkcentre-m75q-gen-2-v46509.html) +- [CWWK N305 Benchmarks](https://rovingclimber.com/2025/01/05/cwwk-i3-n305-benchmarks-power-consumption/) +- [N100 vs Pi - Jeff Geerling](https://www.jeffgeerling.com/blog/2025/intel-n100-better-value-raspberry-pi/) +- [N100 vs N305 Home Server](https://www.lowerhomeserver.vip/blog/hardware/intel-n100-vs-n305-home-server-2026) +- [K3s auf Raspberry Pi 5](https://www.picocluster.com/blogs/picocluster-software-engineering/installing-k3s-on-the-raspberry-pi-5-a-step-by-step-guide) diff --git a/docs/FIX_COLIMA_MOUNTS.md b/docs/FIX_COLIMA_MOUNTS.md new file mode 100644 index 000000000..1ea21cfb8 --- /dev/null +++ b/docs/FIX_COLIMA_MOUNTS.md @@ -0,0 +1,84 @@ +# Fix: Colima VirtioFS Mounts nach Stromausfall + +Nach dem `colima delete` + recreate fehlt der Home-Directory Mount. +Dadurch werden alle Bind-Mounts zu Projekt-Dateien als leere Verzeichnisse gemountet +und Container wie Synapse, SearXNG, Alertmanager, VictoriaMetrics, Loki crashen. + +## Schritte (direkt am Mac Mini ausfuehren) + +### 1. Mac Mini neu starten (Power-Button) + +SSH funktioniert nicht mehr — der Server muss physisch neugestartet werden. + +### 2. Terminal oeffnen und Colima stoppen + +```bash +PATH=/opt/homebrew/bin:$PATH colima stop +``` + +### 3. Home-Directory Mount in Colima Config einfuegen + +```bash +sed -i '' '/^mounts:/a\ + - location: /Users/mana\ + writable: true +' ~/.colima/default/colima.yaml +``` + +### 4. Verifizieren dass beide Mounts drin sind + +```bash +grep -A8 'mounts:' ~/.colima/default/colima.yaml +``` + +Erwartete Ausgabe: + +``` +mounts: + - location: /Users/mana + writable: true + - location: /Volumes/ManaData + writable: true +``` + +### 5. Colima starten + +```bash +PATH=/opt/homebrew/bin:$PATH colima start +``` + +### 6. Testen ob File-Mounts funktionieren + +```bash +docker run --rm -v ~/projects/manacore-monorepo/docker/alertmanager/alertmanager.yml:/test.yml:ro alpine head -3 /test.yml +``` + +Sollte YAML-Inhalt zeigen, NICHT "Is a directory". + +### 7. Alle Container starten + +```bash +cd ~/projects/manacore-monorepo +docker compose -f docker-compose.macmini.yml up -d --no-build +``` + +### 8. Pruefen ob die vorher crashenden Container laufen + +```bash +docker ps --format 'table {{.Names}}\t{{.Status}}' | grep -E 'synapse|searxng|alertmanager|vmalert|victoria|loki' +``` + +Alle sollten "Up" und "healthy" zeigen. + +### 9. Memory Baseline messen + +```bash +./scripts/mac-mini/memory-baseline.sh +``` + +## Ursache + +`colima delete` hat die VM komplett geloescht. Beim Neuerstellen mit +`colima start --mount /Volumes/ManaData:w` wurde nur das externe SSD +gemountet, nicht das Home-Directory `/Users/mana`. Ohne diesen Mount +sieht VirtioFS alle Host-Dateien als leere Verzeichnisse. diff --git a/docs/MANA_BOX_HARDWARE.md b/docs/MANA_BOX_HARDWARE.md new file mode 100644 index 000000000..8fcd42c7c --- /dev/null +++ b/docs/MANA_BOX_HARDWARE.md @@ -0,0 +1,257 @@ +# Mana Box — On-Premise Appliance Hardware-Planung + +**Stand:** 2026-03-30 +**Status:** Recherche / Evaluierung + +## Vision + +Jeder Kunde (KMU, Verein, Schule, Agentur etc.) kauft eine **Mana Box** — einen kleinen, vorkonfigurierten Server mit dem gesamten Mana-Stack vorinstalliert. Alle Daten liegen on-premise beim Kunden. Optional kann der Kunde auch Websites, Intranets o.a. darauf hosten. + +### Warum On-Premise? + +- **Datensouveranitat**: Kundendaten verlassen nie das Gebaude +- **DSGVO-Compliance**: Keine Cloud-Abhaengigkeiten, keine Auftragsverarbeitung noetig +- **Unabhaengigkeit**: Funktioniert auch ohne Internet (Offline-First-Architektur) +- **Planbare Kosten**: Einmaliger Hardwarekauf + optionales Support-Abo +- **B2B-Differenzierung**: Kein Vendor-Lock-in, Kunde besitzt die Infrastruktur + +## Architektur: Single-Node Appliance + +Da jede Mana Box ein einzelner Server ist (kein Cluster), vereinfacht sich der Stack erheblich: + +| Komponente | Cluster-Ansatz (verworfen) | Mana Box (Single Node) | +|---|---|---| +| Orchestrierung | K3s (Kubernetes) | **Docker Compose** | +| Datenbank | YugabyteDB (verteilt) | **PostgreSQL** (lokal) | +| Netzwerk | 10 GbE + Headscale Mesh | **1 GbE LAN** | +| High Availability | 3-Node Failover | **Backup-Strategie** | +| Management | kubectl | **Portainer oder CLI** | + +### Stack pro Mana Box + +``` +┌─────────────────────────────────────────────┐ +│ Mana Box │ +├─────────────────────────────────────────────┤ +│ Docker Compose │ +│ ├── mana-auth (Hono/Bun) │ +│ ├── mana-credits (Hono/Bun) │ +│ ├── mana-user (Hono/Bun) │ +│ ├── mana-sync (Go) │ +│ ├── mana-search (Go) │ +│ ├── mana-notify (Go) │ +│ ├── mana-api-gateway (Go) │ +│ ├── mana-media (Hono/Bun) │ +│ ├── App-Backends (NestJS / Hono) │ +│ ├── App-Webs (SvelteKit, static) │ +│ ├── PostgreSQL │ +│ ├── Redis │ +│ ├── MinIO │ +│ ├── Traefik (Reverse Proxy) │ +│ └── Tailscale (Remote-Management) │ +├─────────────────────────────────────────────┤ +│ Debian/Ubuntu Minimal + unattended-upgrades│ +└─────────────────────────────────────────────┘ +``` + +## Hardware-Anforderungen + +### Prioritaeten (anders als bei eigenem Cluster) + +| Anforderung | Prioritaet | Begruendung | +|---|---|---| +| **Stueckpreis** | Kritisch | Bestimmt Verkaufspreis und Marge | +| **Zuverlaessigkeit** | Kritisch | Kein IT-Personal beim Kunden | +| **Lautstaerke** | Sehr hoch | Steht im Buero, nicht im Rack | +| **Stromverbrauch** | Hoch | Kunde zahlt Strom, 24/7 Betrieb | +| **Linux nativ** | Pflicht | Docker Compose direkt auf Host | +| **Kompakte Groesse** | Mittel | Schreibtisch oder Serverschrank | +| **RAM aufruetbar** | Niedrig | Wird vor Versand konfiguriert | + +### RAM-Sizing nach Kundengroesse + +| Tier | User | Typische Apps | RAM-Bedarf | +|---|---|---|---| +| **S** (Klein) | 5-15 | Auth + 3-4 Apps + DB + Redis | 6-8 GB | +| **M** (Mittel) | 15-30 | Auth + 8-10 Apps + DB + Redis + MinIO | 10-14 GB | +| **L** (Gross) | 30-50+ | Alle Apps + alle Services + MinIO | 16-20 GB | + +**Sweet Spot: 16 GB RAM** deckt die grosse Mehrheit der KMU-Kunden ab. +32 GB nur fuer Power-User oder lokale LLM-Inferenz. + +## Hardware-Kandidaten + +### Vergleichstabelle + +| Geraet | CPU | Cores | RAM | Preis (EK) | Luefter | Idle | Linux | Eignung | +|---|---|---|---|---|---|---|---|---| +| **Intel N100 Mini PC** (diverse) | Intel N100 | 4C/4T | 16 GB (aufr.) | 120-180 EUR | Luefterlos | ~6W | Nativ | Budget, nur 4 Cores | +| **Intel N305 Mini PC** (z.B. Beelink EQ14) | Intel N305 | 8C/8T | 16 GB (aufr.) | 200-280 EUR | Luefterlos | ~8W | Nativ | **Bester Kandidat fuer Box S** | +| **Beelink SER8** | Ryzen 7 8845HS | 8C/16T | 32 GB DDR5 (aufr.) | ~680 EUR | Aktiv (leise) | ~8W | Nativ | Overpowered fuer die meisten | +| **Beelink SER9 Pro (H255)** | Ryzen 7 H255 | 8C/16T | 32 GB LPDDR5X (fix) | ab 779 EUR | Aktiv (leise) | ~8W | Nativ | Gut, aber RAM verloetet | +| **Beelink SER9 Pro (HX 370)** | Ryzen AI 9 HX 370 | 12C/24T | 32 GB LPDDR5X (fix) | ab 1.029 EUR | Aktiv (leise) | ~10W | Nativ | Overkill, RAM verloetet | +| **Minisforum UM790 Pro** | Ryzen 9 7940HS | 8C/16T | bis 64 GB (aufr.) | ab 399 EUR | Aktiv (leise) | ~8W | Nativ | Gutes Preis/Leistung | +| **Minisforum UM890 Pro** | Ryzen 9 8945HS | 8C/16T | bis 64 GB (aufr.) | ab 463 EUR | Aktiv (leise) | ~8W | Nativ | Homelab-Favorit | +| **GEEKOM A8** | Ryzen 7 8745HS | 8C/16T | 16 GB DDR5 (aufr.) | ab 659 EUR | Aktiv (leise) | ~8W | Nativ | Dual 2.5 GbE, solide | +| **Lenovo M75q Gen 2** (refurb) | Ryzen 5 PRO 5650GE | 6C/12T | bis 64 GB (aufr.) | 150-250 EUR | Aktiv (leise) | ~10W | Nativ | **Guenstigster Kandidat** | +| **Lenovo M75q Gen 5** (neu) | Ryzen 7 PRO 8700GE | 8C/16T | bis 64 GB DDR5 (aufr.) | 500-600 EUR | Aktiv (leise) | ~10W | Nativ | Premium, Enterprise-Qualitaet | +| **ASUS NUC 14 Pro** (Barebone) | Core Ultra 7 155H | 14C/18T | bis 96 GB (aufr.) | ab 547 EUR | Aktiv (leise) | ~10W | Nativ | Viele Cores, flexibel | +| **HP Elite Mini 800 G9** (refurb) | Core i7-12700 | 12C/20T | 32 GB (aufr.) | ~350-500 EUR | Aktiv (leise) | ~12W | Nativ | Enterprise-refurb | +| **Raspberry Pi 5** | Cortex-A76 | 4C/4T | 8 GB (fix) | ~100 EUR | Passiv moegl. | ~3W | Nativ | Zu wenig RAM, ARM-Limits | + +### Detailbewertung der Top-Kandidaten + +#### Intel N305 Mini PCs (z.B. Beelink EQ14) — Box S + +- **Preis:** 200-280 EUR +- **Vorteile:** Luefterlos, 8W idle, 8 Cores, extrem zuverlaessig, guenstig +- **Nachteile:** Nur E-Cores (kein Performance-Core), max ~16 GB typisch +- **Ideal fuer:** Kleine Kunden (5-15 User), Basic-Setup +- **Luefterlos = keine beweglichen Teile = hoechste Zuverlaessigkeit** + +#### Minisforum UM890 Pro — Box M + +- **Preis:** ab 463 EUR (Barebone), ~600-700 EUR konfiguriert +- **Vorteile:** Ryzen 9, aufruetbar bis 64 GB, sehr leise, bewahrt im Homelab +- **Nachteile:** Aktiver Luefter (wenn auch leise) +- **Ideal fuer:** Mittelgrosse Kunden (15-50 User) + +#### Beelink SER8 — Box L + +- **Preis:** ~680 EUR mit 32 GB/1 TB +- **Vorteile:** Ryzen 7 8845HS, DDR5 SO-DIMM aufruetbar bis 256 GB, 2.5 GbE +- **Nachteile:** Auslaufmodell (Nachfolger SER9 hat verloeteten RAM) +- **Ideal fuer:** Grosse Kunden, LLM-Workloads, Website-Hosting +- **Achtung:** Verfuegbarkeit pruefen — wird vom SER9 abgeloest + +#### Lenovo M75q Gen 2 (refurbished) — Budget-Option + +- **Preis:** 150-250 EUR (refurbished mit 12-24 Monate Garantie) +- **Vorteile:** Enterprise-Qualitaet, extrem guenstig, aufruetbar bis 64 GB +- **Nachteile:** Aeltere CPU (Ryzen 5 5650GE), DDR4, keine USB4 +- **Bezugsquellen DE:** it-goods.de, notebookswieneu.de, thinkstore24.de +- **Ideal fuer:** Budget-bewusste Kunden, Prototyp-Phase + +## Produkt-Tiers + +### Mana Box S — 599-799 EUR (VK) + +| Komponente | Spezifikation | +|---|---| +| Hardware | Intel N305 Mini PC, luefterlos | +| RAM | 16 GB DDR5 | +| Speicher | 512 GB NVMe SSD | +| Netzwerk | 1 GbE | +| Strom | ~8W idle | +| EK Hardware | ~250-300 EUR | +| Zielgruppe | 5-15 User | +| Apps | Auth + 3-5 Kern-Apps | + +### Mana Box M — 999-1.299 EUR (VK) + +| Komponente | Spezifikation | +|---|---| +| Hardware | Minisforum UM890 Pro oder Lenovo M75q Gen 5 | +| RAM | 32 GB DDR5 | +| Speicher | 1 TB NVMe SSD | +| Netzwerk | 2.5 GbE | +| Strom | ~10W idle | +| EK Hardware | ~500-650 EUR | +| Zielgruppe | 15-50 User | +| Apps | Auth + 8-15 Apps + MinIO | + +### Mana Box L — 1.499-1.999 EUR (VK) + +| Komponente | Spezifikation | +|---|---| +| Hardware | Beelink SER8 oder Minisforum MS-A1 | +| RAM | 64 GB DDR5 | +| Speicher | 2 TB NVMe SSD | +| Netzwerk | 2.5 GbE (oder 10 GbE bei MS-A1) | +| Strom | ~12W idle | +| EK Hardware | ~800-1.000 EUR | +| Zielgruppe | 50+ User, LLM lokal | +| Apps | Alle Apps + LLM-Inferenz | + +## Betrieb & Management + +### Provisioning + +1. Debian 12 Minimal-Image mit vorinstalliertem Docker + Mana Stack +2. Image wird auf SSD geflasht (vor Versand oder per USB-Stick) +3. Kunde schliesst Box an Strom + LAN → Box startet automatisch +4. Ersteinrichtung via Web-UI (Admin-Account, Domain, App-Auswahl) + +### Remote-Management + +| Funktion | Technologie | +|---|---| +| VPN-Tunnel | Tailscale (oder WireGuard + eigener Koordinator) | +| Health-Monitoring | Phone-Home an Mana-Zentrale (Heartbeat, Metriken) | +| Remote-Shell | SSH via Tailscale (nur mit Kunden-Zustimmung) | +| Logs | Promtail → zentrale Loki-Instanz (optional, opt-in) | + +### Updates + +| Aspekt | Loesung | +|---|---| +| OS-Updates | unattended-upgrades (Debian) | +| App-Updates | Docker Image Pull + Compose Restart | +| Update-Kanal | Staging → Stable (Kunden koennen waehlen) | +| Rollback | Vorheriges Image bleibt lokal, 1-Click Rollback | +| Zeitfenster | Konfigurierbares Wartungsfenster (z.B. 03:00-05:00) | + +### Backup + +| Aspekt | Loesung | +|---|---| +| Datenbank | pg_dump, taeglich, verschluesselt | +| Dateien | MinIO/Medien-Backup | +| Ziel | Lokal (USB/NAS) und/oder verschluesselt in Mana Cloud | +| Retention | 7 Tage lokal, 30 Tage Cloud | + +## Preiskalkulation (Beispiel Box M) + +| Posten | Kosten | +|---|---| +| Hardware (EK) | ~600 EUR | +| Zusammenbau + Imaging + QA | ~50 EUR | +| Verpackung + Versand | ~30 EUR | +| **Gesamtkosten** | **~680 EUR** | +| **Verkaufspreis** | **999-1.299 EUR** | +| **Marge** | **~320-620 EUR (32-48%)** | + +Zusaetzliche Einnahmen: +- Support-Abo: 29-99 EUR/Monat (Remote-Management, Updates, Monitoring) +- Erweiterungen: Zusaetzliche Apps, mehr Speicher, LLM-Modul + +## Eigene Infrastruktur (weiterhin noetig) + +Die Mana Boxen beim Kunden ersetzen nicht die eigene Infrastruktur. Weiterhin benoetigt: + +| Zweck | Loesung | +|---|---| +| SaaS-Variante fuer Einzelnutzer | Mac Mini Cluster (2-3 Nodes) | +| Update-Server (Docker Registry) | Eigener Registry-Mirror | +| Health-Dashboard | Grafana + VictoriaMetrics | +| Tailscale Coordination | Headscale (self-hosted) | +| Landing Pages + Marketing | Cloudflare Pages | +| CI/CD (Image-Builds) | Forgejo Actions | + +## Offene Fragen + +- [ ] Welche Linux-Distribution? Debian 12 (stabil) vs. Ubuntu LTS (breiter Support)? +- [ ] Branding: Eigenes Gehaeuse/Aufkleber oder neutraler Mini PC? +- [ ] Garantie/Support: 1 Jahr vs. 2 Jahre? Austauschgeraet bei Defekt? +- [ ] App-Lizenzierung: Alle Apps inklusive oder modulares Lizenzmodell? +- [ ] LLM lokal: Ollama als optionales Modul? Welche Modelle passen in 16/32 GB? +- [ ] Backup-Cloud: Eigene S3-Instanz oder Hetzner Storage Box als Backup-Ziel? +- [ ] Pilotprojekt: Welcher Kunde/Anwendungsfall fuer den ersten Prototyp? + +## Naechste Schritte + +1. **Prototyp bauen**: N305 Mini PC oder M75q Gen 2 bestellen, Mana Stack als Docker Compose aufsetzen +2. **Benchmarken**: RAM/CPU-Verbrauch mit 5, 15, 30 simulierten Usern messen +3. **Provisioning-Image**: Reproduzierbares Debian-Image mit Packer oder cloud-init erstellen +4. **Remote-Management**: Tailscale/Headscale Setup testen +5. **Pricing validieren**: Gespraeche mit potenziellen Pilotkunden fuehren diff --git a/package.json b/package.json index 6cef79221..a9dcb95f7 100644 --- a/package.json +++ b/package.json @@ -136,9 +136,9 @@ "dev:tags-test": "./scripts/setup-databases.sh todo && ./scripts/setup-databases.sh calendar && ./scripts/setup-databases.sh contacts && ./scripts/setup-databases.sh auth && concurrently -n auth,todo-be,todo-web,cal-be,cal-web,con-be,con-web -c blue,green,cyan,yellow,magenta,red,white \"pnpm dev:auth\" \"pnpm dev:todo:backend\" \"pnpm dev:todo:web\" \"pnpm dev:calendar:backend\" \"pnpm dev:calendar:web\" \"pnpm dev:contacts:backend\" \"pnpm dev:contacts:web\"", "inventar:dev": "turbo run dev --filter=inventar...", "dev:inventar:web": "pnpm --filter @inventar/web dev", - "taktik:dev": "turbo run dev --filter=taktik...", - "dev:taktik:web": "pnpm --filter @taktik/web dev", - "dev:taktik:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:taktik:web\"", + "times:dev": "turbo run dev --filter=times...", + "dev:times:web": "pnpm --filter @times/web dev", + "dev:times:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:times:web\"", "manavoxel:dev": "turbo run dev --filter=manavoxel...", "dev:manavoxel:web": "pnpm --filter @manavoxel/web dev", "dev:manavoxel:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:manavoxel:web\"", diff --git a/packages/mana-core-nestjs-integration/package.json b/packages/mana-core-nestjs-integration/package.json deleted file mode 100644 index 54a163fb5..000000000 --- a/packages/mana-core-nestjs-integration/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@manacore/nestjs-integration", - "version": "1.0.0", - "private": true, - "description": "NestJS integration package for Mana Core authentication and credits", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.js" - }, - "./guards": { - "types": "./dist/guards/index.d.ts", - "import": "./dist/guards/index.js", - "require": "./dist/guards/index.js" - }, - "./decorators": { - "types": "./dist/decorators/index.d.ts", - "import": "./dist/decorators/index.js", - "require": "./dist/decorators/index.js" - }, - "./interceptors": { - "types": "./dist/interceptors/index.d.ts", - "import": "./dist/interceptors/index.js", - "require": "./dist/interceptors/index.js" - } - }, - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "clean": "rm -rf dist", - "lint": "eslint .", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@manacore/credit-operations": "workspace:*", - "@nestjs/common": "^10.0.0 || ^11.0.0", - "@nestjs/config": "^3.0.0 || ^4.0.0", - "@nestjs/core": "^10.0.0 || ^11.0.0", - "reflect-metadata": "^0.1.13 || ^0.2.0", - "rxjs": "^7.0.0" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "@nestjs/common": "^10.0.0 || ^11.0.0", - "@nestjs/config": "^3.0.0 || ^4.0.0", - "@nestjs/core": "^10.0.0 || ^11.0.0" - }, - "files": [ - "dist" - ] -} diff --git a/packages/mana-core-nestjs-integration/src/decorators/current-user.decorator.ts b/packages/mana-core-nestjs-integration/src/decorators/current-user.decorator.ts deleted file mode 100644 index 708405a8a..000000000 --- a/packages/mana-core-nestjs-integration/src/decorators/current-user.decorator.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createParamDecorator } from '@nestjs/common'; -import type { ExecutionContext } from '@nestjs/common'; - -export interface JwtPayload { - sub: string; - email: string; - role?: string; - app_id?: string; - iat?: number; - exp?: number; -} - -export const CurrentUser = createParamDecorator( - (data: keyof JwtPayload | undefined, ctx: ExecutionContext): JwtPayload | string => { - const request = ctx.switchToHttp().getRequest(); - const user = request.user as JwtPayload; - - if (data) { - return user[data] as string; - } - - return user; - } -); diff --git a/packages/mana-core-nestjs-integration/src/decorators/index.ts b/packages/mana-core-nestjs-integration/src/decorators/index.ts deleted file mode 100644 index b81e276e6..000000000 --- a/packages/mana-core-nestjs-integration/src/decorators/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { CurrentUser, JwtPayload } from './current-user.decorator'; -export { Public, IS_PUBLIC_KEY } from './public.decorator'; -export { UseCredits, CreditOperationConfig, CREDIT_OPERATION_KEY } from './use-credits.decorator'; diff --git a/packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts b/packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts deleted file mode 100644 index 35d552c95..000000000 --- a/packages/mana-core-nestjs-integration/src/decorators/public.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; - -export const IS_PUBLIC_KEY = 'isPublic'; - -/** - * Decorator to mark a route as public (no authentication required) - */ -export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts b/packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts deleted file mode 100644 index 0f44ccbe3..000000000 --- a/packages/mana-core-nestjs-integration/src/decorators/use-credits.decorator.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { SetMetadata, applyDecorators, UseInterceptors } from '@nestjs/common'; -import { CreditInterceptor } from '../interceptors/credit.interceptor'; -import { type CreditOperationType } from '@manacore/credit-operations'; - -/** - * Metadata key for credit operation configuration. - */ -export const CREDIT_OPERATION_KEY = 'credit_operation'; - -/** - * Configuration for credit consumption. - */ -export interface CreditOperationConfig { - /** - * The operation type from the credit-operations package. - */ - operation: CreditOperationType; - - /** - * Custom cost override. If not specified, uses the default from CREDIT_COSTS. - */ - customCost?: number; - - /** - * Whether to consume credits before or after the handler execution. - * - 'before': Validate and reserve credits before execution (default) - * - 'after': Consume credits only after successful execution - */ - consumeMode?: 'before' | 'after'; - - /** - * Optional function to calculate cost dynamically based on request. - * Receives the request object and should return the credit cost. - */ - dynamicCost?: (request: any) => number; - - /** - * Optional function to generate description for the transaction. - * Receives the request object and should return a description string. - */ - descriptionFn?: (request: any) => string; - - /** - * Whether to skip the credit check in development mode. - * Default: false - */ - skipInDev?: boolean; -} - -/** - * Decorator to require credits for an endpoint. - * - * @example Simple usage with operation type: - * ```typescript - * @Post('tasks') - * @UseCredits(CreditOperationType.TASK_CREATE) - * async createTask(@Body() dto: CreateTaskDto) { - * return this.taskService.create(dto); - * } - * ``` - * - * @example With configuration object: - * ```typescript - * @Post('generate') - * @UseCredits({ - * operation: CreditOperationType.AI_IMAGE_GENERATION, - * consumeMode: 'after', - * descriptionFn: (req) => `Generated image: ${req.body.prompt}`, - * }) - * async generateImage(@Body() dto: GenerateDto) { - * return this.imageService.generate(dto); - * } - * ``` - * - * @example With dynamic cost: - * ```typescript - * @Post('bulk-import') - * @UseCredits({ - * operation: CreditOperationType.BULK_IMPORT, - * dynamicCost: (req) => Math.ceil(req.body.items.length / 10) * 0.2, - * }) - * async bulkImport(@Body() dto: BulkImportDto) { - * return this.importService.import(dto); - * } - * ``` - */ -export function UseCredits( - operationOrConfig: CreditOperationType | CreditOperationConfig -): MethodDecorator { - const config: CreditOperationConfig = - typeof operationOrConfig === 'string' ? { operation: operationOrConfig } : operationOrConfig; - - return applyDecorators( - SetMetadata(CREDIT_OPERATION_KEY, config), - UseInterceptors(CreditInterceptor) - ); -} diff --git a/packages/mana-core-nestjs-integration/src/exceptions/insufficient-credits.exception.ts b/packages/mana-core-nestjs-integration/src/exceptions/insufficient-credits.exception.ts deleted file mode 100644 index 61cb89dc0..000000000 --- a/packages/mana-core-nestjs-integration/src/exceptions/insufficient-credits.exception.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; - -export interface InsufficientCreditsDetails { - requiredCredits: number; - availableCredits: number; - creditType: 'user' | 'app'; - operation: string; -} - -export class InsufficientCreditsException extends HttpException { - constructor(details: InsufficientCreditsDetails) { - super( - { - statusCode: HttpStatus.PAYMENT_REQUIRED, - error: 'Insufficient Credits', - message: `Not enough credits for ${details.operation}. Required: ${details.requiredCredits}, Available: ${details.availableCredits}`, - details, - }, - HttpStatus.PAYMENT_REQUIRED - ); - } -} diff --git a/packages/mana-core-nestjs-integration/src/guards/auth.guard.ts b/packages/mana-core-nestjs-integration/src/guards/auth.guard.ts deleted file mode 100644 index f70beccdb..000000000 --- a/packages/mana-core-nestjs-integration/src/guards/auth.guard.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { - Injectable, - CanActivate, - ExecutionContext, - UnauthorizedException, - Inject, - Optional, -} from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { ConfigService } from '@nestjs/config'; -import { MANA_CORE_OPTIONS } from '../mana-core.module'; -import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; -import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; - -interface TokenValidationResponse { - valid: boolean; - payload?: { - sub: string; - email: string; - role: string; - sessionId?: string; - sid?: string; - app_id?: string; - iat?: number; - exp?: number; - }; - error?: string; -} - -// Default development test user ID -const DEFAULT_DEV_USER_ID = '00000000-0000-0000-0000-000000000000'; - -/** - * JWT Authentication Guard for NestJS backends. - * - * Validates JWT tokens by calling the Mana Core Auth service. - * Supports development mode bypass via DEV_BYPASS_AUTH=true. - */ -@Injectable() -export class AuthGuard implements CanActivate { - constructor( - @Optional() - @Inject(MANA_CORE_OPTIONS) - private readonly options?: ManaCoreModuleOptions, - @Optional() - private readonly reflector?: Reflector, - @Optional() - private readonly configService?: ConfigService - ) {} - - async canActivate(context: ExecutionContext): Promise { - // Check if route is marked as public - if (this.reflector) { - const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ - context.getHandler(), - context.getClass(), - ]); - if (isPublic) { - return true; - } - } - - const request = context.switchToHttp().getRequest(); - - // Development mode: bypass auth if DEV_BYPASS_AUTH is set - if (this.shouldBypassAuth()) { - request.user = this.getDevUser(); - return true; - } - - const token = this.extractTokenFromHeader(request); - - if (!token) { - throw new UnauthorizedException('No authorization token provided'); - } - - try { - const userData = await this.validateToken(token); - request.user = userData; - request.accessToken = token; - - if (this.options?.debug) { - console.log('[AuthGuard] User authenticated:', userData.sub); - } - - return true; - } catch (error) { - if (error instanceof UnauthorizedException) { - throw error; - } - if (this.options?.debug) { - console.error('[AuthGuard] Token validation failed:', error); - } - throw new UnauthorizedException('Invalid or expired token'); - } - } - - /** - * Check if auth should be bypassed (development mode) - */ - private shouldBypassAuth(): boolean { - const isDev = - this.configService?.get('NODE_ENV') === 'development' || - process.env.NODE_ENV === 'development'; - const bypassAuth = - this.configService?.get('DEV_BYPASS_AUTH') === 'true' || - process.env.DEV_BYPASS_AUTH === 'true'; - return isDev && bypassAuth; - } - - /** - * Get development user data - */ - private getDevUser() { - const devUserId = - this.configService?.get('DEV_USER_ID') || - process.env.DEV_USER_ID || - DEFAULT_DEV_USER_ID; - return { - sub: devUserId, - email: 'dev@example.com', - role: 'user', - app_id: this.options?.appId, - }; - } - - /** - * Validate token with Mana Core Auth service - */ - private async validateToken(token: string): Promise { - const authUrl = - this.configService?.get('MANA_CORE_AUTH_URL') || - process.env.MANA_CORE_AUTH_URL || - 'http://localhost:3001'; - - const response = await fetch(`${authUrl}/api/v1/auth/validate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token }), - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - if (this.options?.debug) { - console.error('[AuthGuard] Token validation failed:', response.status, errorText); - } - throw new UnauthorizedException('Invalid token'); - } - - const result = (await response.json()) as TokenValidationResponse; - - if (!result.valid || !result.payload) { - throw new UnauthorizedException(result.error || 'Invalid token'); - } - - return { - sub: result.payload.sub, - email: result.payload.email, - role: result.payload.role, - app_id: result.payload.app_id || this.options?.appId, - sessionId: result.payload.sessionId || result.payload.sid, - iat: result.payload.iat, - exp: result.payload.exp, - }; - } - - private extractTokenFromHeader(request: any): string | undefined { - const authHeader = request.headers.authorization; - if (!authHeader) { - return undefined; - } - - const [type, token] = authHeader.split(' '); - return type === 'Bearer' ? token : undefined; - } -} diff --git a/packages/mana-core-nestjs-integration/src/guards/index.ts b/packages/mana-core-nestjs-integration/src/guards/index.ts deleted file mode 100644 index e9e9bb171..000000000 --- a/packages/mana-core-nestjs-integration/src/guards/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AuthGuard } from './auth.guard'; -export { OptionalAuthGuard } from './optional-auth.guard'; diff --git a/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts b/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts deleted file mode 100644 index ee5ba7442..000000000 --- a/packages/mana-core-nestjs-integration/src/guards/optional-auth.guard.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext, Inject, Optional } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { MANA_CORE_OPTIONS } from '../mana-core.module'; -import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; - -interface TokenValidationResponse { - valid: boolean; - payload?: { - sub: string; - email: string; - role: string; - sessionId?: string; - sid?: string; - app_id?: string; - iat?: number; - exp?: number; - }; - error?: string; -} - -/** - * Optional auth guard - allows unauthenticated requests but still validates and extracts user info if token is present - */ -@Injectable() -export class OptionalAuthGuard implements CanActivate { - constructor( - @Optional() - @Inject(MANA_CORE_OPTIONS) - private readonly options?: ManaCoreModuleOptions, - @Optional() - private readonly configService?: ConfigService - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const token = this.extractTokenFromHeader(request); - - if (!token) { - // No token - allow request but user will be undefined - request.user = null; - return true; - } - - try { - const userData = await this.validateToken(token); - - if (userData) { - request.user = userData; - request.accessToken = token; - - if (this.options?.debug) { - console.log('[OptionalAuthGuard] User authenticated:', userData.sub); - } - } else { - request.user = null; - } - } catch (error) { - if (this.options?.debug) { - console.error('[OptionalAuthGuard] Token validation failed:', error); - } - // For optional auth, we allow the request to proceed even if token validation fails - request.user = null; - } - - return true; - } - - /** - * Validate token with Mana Core Auth service - */ - private async validateToken(token: string): Promise { - const authUrl = - this.configService?.get('MANA_CORE_AUTH_URL') || - process.env.MANA_CORE_AUTH_URL || - 'http://localhost:3001'; - - try { - const response = await fetch(`${authUrl}/api/v1/auth/validate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token }), - }); - - if (!response.ok) { - return null; - } - - const result = (await response.json()) as TokenValidationResponse; - - if (!result.valid || !result.payload) { - return null; - } - - return { - sub: result.payload.sub, - email: result.payload.email, - role: result.payload.role, - app_id: result.payload.app_id || this.options?.appId, - sessionId: result.payload.sessionId || result.payload.sid, - iat: result.payload.iat, - exp: result.payload.exp, - }; - } catch { - return null; - } - } - - private extractTokenFromHeader(request: any): string | undefined { - const authHeader = request.headers.authorization; - if (!authHeader) { - return undefined; - } - - const [type, token] = authHeader.split(' '); - return type === 'Bearer' ? token : undefined; - } -} diff --git a/packages/mana-core-nestjs-integration/src/index.ts b/packages/mana-core-nestjs-integration/src/index.ts deleted file mode 100644 index e32c06adb..000000000 --- a/packages/mana-core-nestjs-integration/src/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Module -export { ManaCoreModule, MANA_CORE_OPTIONS } from './mana-core.module'; - -// Interfaces -export { - ManaCoreModuleOptions, - ManaCoreModuleAsyncOptions, - ManaCoreOptionsFactory, -} from './interfaces/mana-core-options.interface'; - -// Guards -export { AuthGuard } from './guards/auth.guard'; -export { OptionalAuthGuard } from './guards/optional-auth.guard'; - -// Decorators -export { CurrentUser, JwtPayload } from './decorators/current-user.decorator'; -export { Public, IS_PUBLIC_KEY } from './decorators/public.decorator'; -export { - UseCredits, - CreditOperationConfig, - CREDIT_OPERATION_KEY, -} from './decorators/use-credits.decorator'; - -// Interceptors -export { CreditInterceptor } from './interceptors/credit.interceptor'; - -// Services -export { - CreditClientService, - CreditValidationResult, - CreditBalance, -} from './services/credit-client.service'; - -// Exceptions -export { - InsufficientCreditsException, - InsufficientCreditsDetails, -} from './exceptions/insufficient-credits.exception'; - -// Re-export credit operations for convenience -export { - CreditOperationType, - CREDIT_COSTS, - CreditCategory, - getCreditCost, - getOperationMetadata, - getOperationsForApp, - formatCreditCost, - getPricingTable, - isFreeOperation, - isMicroCreditOperation, - isAiOperation, -} from '@manacore/credit-operations'; diff --git a/packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts b/packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts deleted file mode 100644 index b68b24abe..000000000 --- a/packages/mana-core-nestjs-integration/src/interceptors/credit.interceptor.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { - Injectable, - NestInterceptor, - ExecutionContext, - CallHandler, - Logger, - Inject, - Optional, -} from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { Observable, tap, catchError, throwError } from 'rxjs'; -import { CreditClientService } from '../services/credit-client.service'; -import { - InsufficientCreditsException, - InsufficientCreditsDetails, -} from '../exceptions/insufficient-credits.exception'; -import { CREDIT_OPERATION_KEY, CreditOperationConfig } from '../decorators/use-credits.decorator'; -import { CREDIT_COSTS, getOperationMetadata } from '@manacore/credit-operations'; -import { MANA_CORE_OPTIONS } from '../mana-core.module'; -import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; - -/** - * Interceptor that handles credit validation and consumption for decorated endpoints. - * - * This interceptor: - * 1. Checks if the user has sufficient credits before executing the handler - * 2. Consumes credits after successful execution (or before, depending on config) - * 3. Throws InsufficientCreditsException if the user doesn't have enough credits - */ -@Injectable() -export class CreditInterceptor implements NestInterceptor { - private readonly logger = new Logger(CreditInterceptor.name); - - constructor( - private readonly reflector: Reflector, - private readonly creditClient: CreditClientService, - @Optional() - @Inject(MANA_CORE_OPTIONS) - private readonly options?: ManaCoreModuleOptions - ) {} - - async intercept(context: ExecutionContext, next: CallHandler): Promise> { - const config = this.reflector.get( - CREDIT_OPERATION_KEY, - context.getHandler() - ); - - // If no config, just proceed (shouldn't happen if decorator is used correctly) - if (!config) { - return next.handle(); - } - - const request = context.switchToHttp().getRequest(); - const user = request.user; - - // Check if user is authenticated - if (!user?.sub) { - this.logger.warn('No authenticated user found for credit operation'); - return next.handle(); - } - - const userId = user.sub; - const operationName = config.operation; - - // Calculate cost - const cost = this.calculateCost(config, request); - const consumeMode = config.consumeMode || 'after'; - - // Skip in development if configured - if (config.skipInDev && this.isDevelopment()) { - this.logger.debug(`Skipping credit check in development for ${operationName}`); - return next.handle(); - } - - // Validate credits before execution - const validation = await this.creditClient.validateCredits(userId, operationName, cost); - - if (!validation.hasCredits) { - const details: InsufficientCreditsDetails = { - requiredCredits: cost, - availableCredits: validation.availableCredits, - creditType: 'user', - operation: operationName, - }; - throw new InsufficientCreditsException(details); - } - - // If consume mode is 'before', consume now - if (consumeMode === 'before') { - const description = this.generateDescription(config, request); - const consumed = await this.creditClient.consumeCredits( - userId, - operationName, - cost, - description, - this.buildMetadata(config, request) - ); - - if (!consumed) { - this.logger.error(`Failed to consume credits for ${operationName}`); - // Still allow the operation to proceed - fail open - } - - return next.handle(); - } - - // If consume mode is 'after', consume on success - return next.handle().pipe( - tap(async () => { - const description = this.generateDescription(config, request); - const consumed = await this.creditClient.consumeCredits( - userId, - operationName, - cost, - description, - this.buildMetadata(config, request) - ); - - if (!consumed) { - this.logger.error(`Failed to consume credits after success for ${operationName}`); - } else if (this.options?.debug) { - this.logger.log(`Consumed ${cost} credits for ${operationName} (user: ${userId})`); - } - }), - catchError((error) => { - // Don't consume credits if the operation failed - this.logger.debug(`Operation ${operationName} failed, credits not consumed`); - return throwError(() => error); - }) - ); - } - - /** - * Calculate the credit cost for the operation. - */ - private calculateCost(config: CreditOperationConfig, request: any): number { - // Dynamic cost takes priority - if (config.dynamicCost) { - return config.dynamicCost(request); - } - - // Custom cost override - if (config.customCost !== undefined) { - return config.customCost; - } - - // Default cost from CREDIT_COSTS - return CREDIT_COSTS[config.operation] || 0; - } - - /** - * Generate a description for the credit transaction. - */ - private generateDescription(config: CreditOperationConfig, request: any): string { - // Custom description function - if (config.descriptionFn) { - return config.descriptionFn(request); - } - - // Default description from operation metadata - const metadata = getOperationMetadata(config.operation); - return metadata?.name || config.operation; - } - - /** - * Build metadata for the credit transaction. - */ - private buildMetadata(config: CreditOperationConfig, request: any): Record { - const metadata: Record = { - operation: config.operation, - path: request.path, - method: request.method, - }; - - // Add app info from operation metadata - const opMeta = getOperationMetadata(config.operation); - if (opMeta) { - metadata.app = opMeta.app; - metadata.category = opMeta.category; - } - - return metadata; - } - - /** - * Check if running in development mode. - */ - private isDevelopment(): boolean { - return ( - this.options?.debug || - process.env.NODE_ENV === 'development' || - process.env.NODE_ENV === 'dev' - ); - } -} diff --git a/packages/mana-core-nestjs-integration/src/interceptors/index.ts b/packages/mana-core-nestjs-integration/src/interceptors/index.ts deleted file mode 100644 index 4e0a4af26..000000000 --- a/packages/mana-core-nestjs-integration/src/interceptors/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CreditInterceptor } from './credit.interceptor'; diff --git a/packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts b/packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts deleted file mode 100644 index 31ddddf8c..000000000 --- a/packages/mana-core-nestjs-integration/src/interfaces/mana-core-options.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { type ModuleMetadata } from '@nestjs/common'; -import type { Type } from '@nestjs/common'; - -export interface ManaCoreModuleOptions { - /** - * @deprecated No longer used - auth URL is read from MANA_CORE_AUTH_URL env variable - */ - manaServiceUrl?: string; - appId: string; - serviceKey?: string; - signupRedirectUrl?: string; - debug?: boolean; -} - -export interface ManaCoreOptionsFactory { - createManaCoreOptions(): Promise | ManaCoreModuleOptions; -} - -export interface ManaCoreModuleAsyncOptions extends Pick { - useExisting?: Type; - useClass?: Type; - useFactory?: (...args: any[]) => Promise | ManaCoreModuleOptions; - inject?: any[]; -} diff --git a/packages/mana-core-nestjs-integration/src/mana-core.module.ts b/packages/mana-core-nestjs-integration/src/mana-core.module.ts deleted file mode 100644 index 28bca0a19..000000000 --- a/packages/mana-core-nestjs-integration/src/mana-core.module.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { DynamicModule, Module, Global, Provider } from '@nestjs/common'; -import { - ManaCoreModuleOptions, - ManaCoreModuleAsyncOptions, - ManaCoreOptionsFactory, -} from './interfaces/mana-core-options.interface'; -import { AuthGuard } from './guards/auth.guard'; -import { CreditClientService } from './services/credit-client.service'; - -export const MANA_CORE_OPTIONS = 'MANA_CORE_OPTIONS'; - -@Global() -@Module({}) -export class ManaCoreModule { - static forRoot(options: ManaCoreModuleOptions): DynamicModule { - return { - module: ManaCoreModule, - providers: [ - { - provide: MANA_CORE_OPTIONS, - useValue: options, - }, - AuthGuard, - CreditClientService, - ], - exports: [MANA_CORE_OPTIONS, AuthGuard, CreditClientService], - }; - } - - static forRootAsync(options: ManaCoreModuleAsyncOptions): DynamicModule { - const asyncProviders = this.createAsyncProviders(options); - - return { - module: ManaCoreModule, - imports: options.imports || [], - providers: [...asyncProviders, AuthGuard, CreditClientService], - exports: [MANA_CORE_OPTIONS, AuthGuard, CreditClientService], - }; - } - - private static createAsyncProviders(options: ManaCoreModuleAsyncOptions): Provider[] { - if (options.useFactory) { - return [ - { - provide: MANA_CORE_OPTIONS, - useFactory: options.useFactory, - inject: options.inject || [], - }, - ]; - } - - const useClass = options.useClass; - const useExisting = options.useExisting; - - if (useClass) { - return [ - { - provide: MANA_CORE_OPTIONS, - useFactory: async (optionsFactory: ManaCoreOptionsFactory) => - await optionsFactory.createManaCoreOptions(), - inject: [useClass], - }, - { - provide: useClass, - useClass, - }, - ]; - } - - if (useExisting) { - return [ - { - provide: MANA_CORE_OPTIONS, - useFactory: async (optionsFactory: ManaCoreOptionsFactory) => - await optionsFactory.createManaCoreOptions(), - inject: [useExisting], - }, - ]; - } - - return []; - } -} diff --git a/packages/mana-core-nestjs-integration/src/services/credit-client.service.ts b/packages/mana-core-nestjs-integration/src/services/credit-client.service.ts deleted file mode 100644 index 1b2b83dad..000000000 --- a/packages/mana-core-nestjs-integration/src/services/credit-client.service.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { Injectable, Inject, Optional, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { MANA_CORE_OPTIONS } from '../mana-core.module'; -import { ManaCoreModuleOptions } from '../interfaces/mana-core-options.interface'; - -export interface CreditValidationResult { - hasCredits: boolean; - availableCredits: number; - requiredCredits?: number; -} - -export interface CreditBalance { - balance: number; - totalEarned: number; - totalSpent: number; -} - -@Injectable() -export class CreditClientService { - private readonly logger = new Logger(CreditClientService.name); - - constructor( - @Optional() - @Inject(MANA_CORE_OPTIONS) - private readonly options?: ManaCoreModuleOptions, - @Optional() - private readonly configService?: ConfigService - ) {} - - private getAuthUrl(): string { - return ( - this.configService?.get('MANA_CORE_AUTH_URL') || - process.env.MANA_CORE_AUTH_URL || - 'http://localhost:3001' - ); - } - - /** - * Get the credits service URL. Uses MANA_CREDITS_URL if available, - * falls back to MANA_CORE_AUTH_URL for backward compatibility. - */ - private getCreditsUrl(): string { - return ( - this.configService?.get('MANA_CREDITS_URL') || - process.env.MANA_CREDITS_URL || - this.getAuthUrl() - ); - } - - private getServiceKey(): string { - return ( - this.options?.serviceKey || - this.configService?.get('MANA_CORE_SERVICE_KEY') || - process.env.MANA_CORE_SERVICE_KEY || - '' - ); - } - - private getAppId(): string { - return ( - this.options?.appId || this.configService?.get('APP_ID') || process.env.APP_ID || '' - ); - } - - async validateCredits( - userId: string, - operation: string, - requiredAmount: number - ): Promise { - try { - const balance = await this.getBalance(userId); - - return { - hasCredits: balance.balance >= requiredAmount, - availableCredits: balance.balance, - requiredCredits: requiredAmount, - }; - } catch (error) { - this.logger.error(`Failed to validate credits for user ${userId}:`, error); - // In case of error, we allow the operation to proceed - // The actual credit deduction will fail if there are no credits - return { - hasCredits: true, - availableCredits: 0, - requiredCredits: requiredAmount, - }; - } - } - - async getBalance(userId: string): Promise { - const creditsUrl = this.getCreditsUrl(); - const serviceKey = this.getServiceKey(); - - if (!serviceKey) { - this.logger.warn('Service key not configured, returning default balance'); - return { - balance: 1000, - totalEarned: 0, - totalSpent: 0, - }; - } - - try { - const response = await fetch(`${creditsUrl}/api/v1/internal/credits/balance/${userId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Service-Key': serviceKey, - 'X-App-Id': this.getAppId(), - }, - }); - - if (!response.ok) { - this.logger.warn(`Credit balance request failed: ${response.status}`); - return this.getDefaultBalance(); - } - - const { - balance = 0, - totalEarned = 0, - totalSpent = 0, - } = (await response.json()) as CreditBalance; - return { - balance, - totalEarned, - totalSpent, - }; - } catch (error) { - this.logger.error(`Failed to get balance for user ${userId}:`, error); - return this.getDefaultBalance(); - } - } - - async consumeCredits( - userId: string, - operation: string, - amount: number, - description: string, - metadata?: Record, - creditSource?: { type: 'personal' } | { type: 'guild'; guildId: string } - ): Promise { - const creditsUrl = this.getCreditsUrl(); - const serviceKey = this.getServiceKey(); - - if (!serviceKey) { - this.logger.warn('Service key not configured, skipping credit consumption'); - return true; - } - - try { - const response = await fetch(`${creditsUrl}/api/v1/internal/credits/use`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Service-Key': serviceKey, - 'X-App-Id': this.getAppId(), - }, - body: JSON.stringify({ - userId, - amount, - appId: this.getAppId(), - description, - metadata: { - operation, - ...metadata, - }, - ...(creditSource && { creditSource }), - }), - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - this.logger.error(`Failed to consume credits: ${response.status} ${errorText}`); - return false; - } - - if (this.options?.debug) { - this.logger.log(`Consumed ${amount} credits for user ${userId}: ${description}`); - } - - return true; - } catch (error) { - this.logger.error(`Failed to consume credits for user ${userId}:`, error); - return false; - } - } - - async refundCredits( - userId: string, - amount: number, - description: string, - metadata?: Record - ): Promise { - const creditsUrl = this.getCreditsUrl(); - const serviceKey = this.getServiceKey(); - - if (!serviceKey) { - this.logger.warn('Service key not configured, skipping credit refund'); - return true; - } - - try { - const response = await fetch(`${creditsUrl}/api/v1/internal/credits/refund`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Service-Key': serviceKey, - 'X-App-Id': this.getAppId(), - }, - body: JSON.stringify({ - userId, - amount, - appId: this.getAppId(), - description, - metadata, - }), - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - this.logger.error(`Failed to refund credits: ${response.status} ${errorText}`); - return false; - } - - if (this.options?.debug) { - this.logger.log(`Refunded ${amount} credits for user ${userId}: ${description}`); - } - - return true; - } catch (error) { - this.logger.error(`Failed to refund credits for user ${userId}:`, error); - return false; - } - } - - private getDefaultBalance(): CreditBalance { - return { - balance: 1000, - totalEarned: 0, - totalSpent: 0, - }; - } -} diff --git a/packages/mana-core-nestjs-integration/tsconfig.json b/packages/mana-core-nestjs-integration/tsconfig.json deleted file mode 100644 index 310fa8950..000000000 --- a/packages/mana-core-nestjs-integration/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "lib": ["ES2021"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts index 3d649282b..c2c2e63d1 100644 --- a/packages/shared-branding/src/app-icons.ts +++ b/packages/shared-branding/src/app-icons.ts @@ -76,7 +76,7 @@ const playgroundSvg = ``; // Taktik icon (clock with play button, amber gradient) -const taktikSvg = ``; +const timesSvg = ``; // Calc icon (calculator with pink gradient) const calcSvg = ``; @@ -112,7 +112,7 @@ export const APP_ICONS = { playground: svgToDataUrl(playgroundSvg), context: svgToDataUrl(contextSvg), citycorners: svgToDataUrl(citycornersSvg), - taktik: svgToDataUrl(taktikSvg), + times: svgToDataUrl(timesSvg), calc: svgToDataUrl(calcSvg), uload: svgToDataUrl( `` diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index fd6cebc5f..dc3aa75d8 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -341,7 +341,7 @@ export const MANA_APPS: ManaApp[] = [ status: 'development', }, { - id: 'taktik', + id: 'times', name: 'Taktik', description: { de: 'Zeiterfassung & Timetracking', @@ -351,7 +351,7 @@ export const MANA_APPS: ManaApp[] = [ de: 'Professionelle Zeiterfassung mit Timer, Projekten, Kunden, Reports und Gilden-Integration.', en: 'Professional time tracking with timer, projects, clients, reports, and guild integration.', }, - icon: APP_ICONS.taktik, + icon: APP_ICONS.times, color: '#f59e0b', comingSoon: false, status: 'development', @@ -527,7 +527,7 @@ export const APP_URLS: Record = { playground: { dev: 'http://localhost:5190', prod: 'https://playground.mana.how' }, context: { dev: 'http://localhost:5192', prod: 'https://context.mana.how' }, citycorners: { dev: 'http://localhost:5196', prod: 'https://citycorners.mana.how' }, - taktik: { dev: 'http://localhost:5197', prod: 'https://taktik.mana.how' }, + times: { dev: 'http://localhost:5197', prod: 'https://times.mana.how' }, uload: { dev: 'http://localhost:5173', prod: 'https://ulo.ad' }, reader: { dev: 'exp://localhost:8081', prod: 'https://reader.mana.how' }, news: { dev: 'http://localhost:5174', prod: 'https://news.mana.how' }, diff --git a/services/mana-auth/src/auth/better-auth.config.ts b/services/mana-auth/src/auth/better-auth.config.ts index 1da537db0..e4f4471fb 100644 --- a/services/mana-auth/src/auth/better-auth.config.ts +++ b/services/mana-auth/src/auth/better-auth.config.ts @@ -264,7 +264,7 @@ export function createBetterAuth(databaseUrl: string) { 'https://questions.mana.how', 'https://skilltree.mana.how', 'https://storage.mana.how', - 'https://taktik.mana.how', + 'https://times.mana.how', 'https://todo.mana.how', 'https://traces.mana.how', 'https://zitare.mana.how', diff --git a/services/mana-sync/CLAUDE.md b/services/mana-sync/CLAUDE.md index 8fb0d2388..8b3c49c39 100644 --- a/services/mana-sync/CLAUDE.md +++ b/services/mana-sync/CLAUDE.md @@ -192,4 +192,4 @@ services/mana-sync/ ## Connected Apps (19) -Todo, Calendar, Clock, Contacts, Chat, Questions, Mukke, Context, Photos, ManaDeck, Picture, Presi, Storage, Zitare, SkillTree, CityCorners, NutriPhi, Planta, Inventar, uLoad, Taktik, Calc +Todo, Calendar, Clock, Contacts, Chat, Questions, Mukke, Context, Photos, ManaDeck, Picture, Presi, Storage, Zitare, SkillTree, CityCorners, NutriPhi, Planta, Inventar, uLoad, Times, Calc