diff --git a/apps/docs/src/content/docs/architecture/security.mdx b/apps/docs/src/content/docs/architecture/security.mdx index 31c056a93..c2fba61b2 100644 --- a/apps/docs/src/content/docs/architecture/security.mdx +++ b/apps/docs/src/content/docs/architecture/security.mdx @@ -25,7 +25,7 @@ Mana encrypts user-typed content with **AES-GCM-256** before it touches IndexedD | Dreams | `title`, `content`, `transcript`, `interpretation`, `location` | | Memoro | `title`, `intro`, `transcript` (the largest plaintext blobs in the app) | | Contacts | 16 PII fields (firstName, lastName, email, phone, mobile, birthday, address, social) | -| Cycles | `notes`, `mood` (GDPR Art. 9 sensitive personal data) | +| Period | `notes`, `mood` (GDPR Art. 9 sensitive personal data) | | Finance | `transactions.description`, `transactions.note` | | Cards | `front`, `back`, deck name + description | | Todo | `tasks.title`, `description`, `subtasks`, `metadata` | diff --git a/apps/mana/apps/landing/src/content/devlog/2026-04-07-encryption-phases-1-9-period-dreams-events.md b/apps/mana/apps/landing/src/content/devlog/2026-04-07-encryption-phases-1-9-period-dreams-events.md index f16bab7d8..fb71c9d55 100644 --- a/apps/mana/apps/landing/src/content/devlog/2026-04-07-encryption-phases-1-9-period-dreams-events.md +++ b/apps/mana/apps/landing/src/content/devlog/2026-04-07-encryption-phases-1-9-period-dreams-events.md @@ -1,6 +1,6 @@ --- -title: 'Encryption Phasen 1–9: Vault-Ende-zu-Ende + Dreams, Cycles, Events Module' -description: 'Größter Tag der Woche: AES-GCM-256 Encryption für 27 Tabellen in 9 Phasen ausgerollt, inkl. Zero-Knowledge-Modus mit Recovery-Code. Plus drei neue Module: Dreams (Voice→STT), Cycles (Menstrual-Tracking) und Events (öffentliche RSVP).' +title: 'Encryption Phasen 1–9: Vault-Ende-zu-Ende + Dreams, Period, Events Module' +description: 'Größter Tag der Woche: AES-GCM-256 Encryption für 27 Tabellen in 9 Phasen ausgerollt, inkl. Zero-Knowledge-Modus mit Recovery-Code. Plus drei neue Module: Dreams (Voice→STT), Period (Menstrual-Tracking) und Events (öffentliche RSVP).' date: 2026-04-07 author: 'Till Schneider' category: 'feature' @@ -11,7 +11,7 @@ tags: 'zero-knowledge', 'recovery-code', 'dreams', - 'cycles', + 'period', 'events', 'rsvp', 'mana-stt', @@ -39,7 +39,7 @@ workingHours: - **At-Rest Encryption** in 9 Phasen ausgerollt: AES-GCM-256 für 27 Tabellen - **Zero-Knowledge-Modus** mit User-only Recovery-Code (Mana kann nichts lesen) - **Lock-Screen** mit Recovery-Unlock-Modal -- **Drei neue Module**: Dreams (Traumtagebuch), Cycles (Zyklus-Tracking), Events (öffentliche RSVP) +- **Drei neue Module**: Dreams (Traumtagebuch), Period (Zyklus-Tracking), Events (öffentliche RSVP) - **Data-Layer-Audit Sprints 1–4** abgeschlossen — LWW, retry, atomic cascades, perf, quota, telemetry - **mana-stt Voice-Pipeline** für Dreams + Memoro live - **Pre-Launch Cleanup** — Schema-Collapse, Ghost-API-Clients raus, RLS auf sync_changes @@ -100,7 +100,7 @@ Erstes Modul mit aktiver Encryption: **Notes**. Klein, kontrolliert, low-risk. F ### Phase 5: Rollout auf 6 Module -chat, dreams, memoro, contacts, cycles, finance — alles user-typed Content der eindeutig privat ist. +chat, dreams, memoro, contacts, period, finance — alles user-typed Content der eindeutig privat ist. ### Phase 6: Polish + UI @@ -186,7 +186,7 @@ Während die Encryption durch die Phasen lief, entstanden parallel drei neue Mod - **Mic-Permission UX** auf macOS — wenn Browser den Prompt nicht zeigt, gibt's einen erklärenden Screen + Force-Retry - **Proxy-Toleranz**: octet-stream und invalid form bodies werden vom Voice-Proxy nicht abgewiesen -### Cycles (Menstruelle Zyklus-Tracking) +### Period (Menstruelle Zyklus-Tracking) - **Period Auto-Detect**: Start/Ende werden aus Symptomen + Bleeding-Levels abgeleitet - **Symptom Management UI**: konfigurierbare Symptome mit Severity @@ -331,7 +331,7 @@ Vor dem Production-Launch eine größere Aufräumrunde: - **`PRE_LAUNCH_CLEANUP.md`** — was wurde entfernt vor Launch und warum - **`FILE_BYTES_ENCRYPTION_PLAN.md`** — nächste Encryption-Stufe für Bytes/Bilder - **`docs/postmortems/2026-04-07-stt-tunnel-down.md`** — STT-Ausfall Postmortem -- **`docs/cycles/ROADMAP.md`** — Cycles Feature-Backlog +- **`docs/period/ROADMAP.md`** — Period Feature-Backlog - **`docs/events/PHASE2_ROADMAP.md`** — Events Phase 2 + tech debt - GPU Tunnel Setup, STT env wiring docs @@ -344,7 +344,7 @@ Vor dem Production-Launch eine größere Aufräumrunde: | Encryption Phasen 1–9 | ~22 | 27 Tabellen, ZK-Modus, Recovery-Code, Lock-Screen, Settings, Tests | | Data-Layer Sprints | ~8 | LWW, retry, cascades, perf, quota, telemetry | | Dreams Modul | ~9 | Voice via mana-stt, Symbol-Library, Mic-UX | -| Cycles Modul | ~12 | Phase-Detection, Symptome, Calendar-View, Widget, i18n | +| Period Modul | ~12 | Phase-Detection, Symptome, Calendar-View, Widget, i18n | | Events Modul | ~12 | RSVP-Flow, Bring-List, 35 Tests, Playwright, Phase 2 | | mana-stt | ~3 | Voice-Pipeline, Postmortem, GPU-Tunnel | | Pre-Launch Cleanup | ~7 | Schema-Collapse, RLS, idempotent startup | diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index 5bd8c6fd9..7c37dac1c 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -65,6 +65,7 @@ import { Question, ChatCircleDots, CreditCard, + SquaresFour, } from '@mana/shared-icons'; // ── Apps with entity capabilities ─────────────────────────── @@ -409,12 +410,12 @@ registerApp({ }); registerApp({ - id: 'cycles', - name: 'Cycles', + id: 'period', + name: 'Period', color: '#ec4899', icon: GenderFemale, views: { - list: { load: () => import('$lib/modules/cycles/ListView.svelte') }, + list: { load: () => import('$lib/modules/period/ListView.svelte') }, }, contextMenuActions: [ { @@ -423,18 +424,18 @@ registerApp({ icon: Plus, action: () => window.dispatchEvent( - new CustomEvent('mana:quick-action', { detail: { app: 'cycles', action: 'new' } }) + new CustomEvent('mana:quick-action', { detail: { app: 'period', action: 'new' } }) ), }, ], - collection: 'cycleDayLogs', + collection: 'periodDayLogs', paramKey: 'logId', getDisplayData: (item) => ({ title: (item.logDate as string) || 'Tageseintrag', subtitle: (item.flow as string) ?? undefined, }), createItem: async (data) => { - const { dayLogsStore } = await import('$lib/modules/cycles/stores/dayLogs.svelte'); + const { dayLogsStore } = await import('$lib/modules/period/stores/dayLogs.svelte'); const log = await dayLogsStore.logDay({ logDate: (data.logDate as string) ?? undefined, notes: (data.title as string) ?? null, @@ -957,12 +958,12 @@ registerApp({ }); registerApp({ - id: 'eventstream', + id: 'activity', name: 'Events', color: '#6366F1', icon: Pulse, views: { - list: { load: () => import('$lib/modules/eventstream/ListView.svelte') }, + list: { load: () => import('$lib/modules/activity/ListView.svelte') }, }, }); @@ -1028,6 +1029,16 @@ registerApp({ }, }); +registerApp({ + id: 'complexity', + name: 'Complexity', + color: '#0EA5E9', + icon: SquaresFour, + views: { + list: { load: () => import('$lib/modules/complexity/ListView.svelte') }, + }, +}); + registerApp({ id: 'api-keys', name: 'API Keys', diff --git a/apps/mana/apps/web/src/lib/app-registry/categories.ts b/apps/mana/apps/web/src/lib/app-registry/categories.ts index 3d2346340..0fcd051b7 100644 --- a/apps/mana/apps/web/src/lib/app-registry/categories.ts +++ b/apps/mana/apps/web/src/lib/app-registry/categories.ts @@ -3,7 +3,7 @@ * can find pages by intent rather than scanning an alphabetical list. * * Five categories (Vorschlag C): - * - companion: Companion Brain pages (myday, eventstream, companion, goals) + * - companion: Companion Brain pages (myday, activity, companion, goals) * - life: Personal / wellness / everyday-life tracking * - work: Productivity & planning * - creative: Creative, learning, generation @@ -40,7 +40,7 @@ export const APP_CATEGORIES: CategoryMeta[] = [ export const APP_CATEGORY_MAP: Record = { // Companion Brain myday: 'companion', - eventstream: 'companion', + activity: 'companion', companion: 'companion', goals: 'companion', @@ -50,7 +50,7 @@ export const APP_CATEGORY_MAP: Record = { sleep: 'life', mood: 'life', stretch: 'life', - cycles: 'life', + period: 'life', dreams: 'life', drink: 'life', meditate: 'life', diff --git a/apps/mana/apps/web/src/lib/components/SyncConflictToast.svelte b/apps/mana/apps/web/src/lib/components/SyncConflictToast.svelte index 512a750b7..c73aca4e7 100644 --- a/apps/mana/apps/web/src/lib/components/SyncConflictToast.svelte +++ b/apps/mana/apps/web/src/lib/components/SyncConflictToast.svelte @@ -38,8 +38,8 @@ contacts: 'Kontakt', events: 'Termin', timeBlocks: 'Zeitblock', - cycles: 'Zyklus', - cycleDayLogs: 'Tageseintrag', + period: 'Zyklus', + periodDayLogs: 'Tageseintrag', transactions: 'Transaktion', cards: 'Karte', cardDecks: 'Kartendeck', diff --git a/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts b/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts index 6782c699e..9173940cd 100644 --- a/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts +++ b/apps/mana/apps/web/src/lib/components/dashboard/widget-registry.ts @@ -30,7 +30,7 @@ import RecentContactsWidget from '$lib/modules/core/widgets/RecentContactsWidget import ActiveTimerWidget from '$lib/modules/core/widgets/ActiveTimerWidget.svelte'; import NutritionProgressWidget from '$lib/modules/core/widgets/NutritionProgressWidget.svelte'; import PlantWateringWidget from '$lib/modules/core/widgets/PlantWateringWidget.svelte'; -import CyclesWidget from '$lib/modules/core/widgets/CyclesWidget.svelte'; +import PeriodWidget from '$lib/modules/core/widgets/PeriodWidget.svelte'; import NewsUnreadWidget from '$lib/modules/news/widgets/NewsUnreadWidget.svelte'; import BodyStatsWidget from '$lib/modules/body/widgets/BodyStatsWidget.svelte'; import DayTimelineWidget from './widgets/DayTimelineWidget.svelte'; @@ -59,7 +59,7 @@ export const widgetComponents: Record = { 'plant-watering': PlantWateringWidget, 'day-timeline': DayTimelineWidget, 'activity-feed': ActivityFeedWidget, - cycles: CyclesWidget, + period: PeriodWidget, 'news-unread': NewsUnreadWidget, 'body-stats': BodyStatsWidget, }; diff --git a/apps/mana/apps/web/src/lib/data/crypto/aes.test.ts b/apps/mana/apps/web/src/lib/data/crypto/aes.test.ts index 04cb10c70..c4d668577 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/aes.test.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/aes.test.ts @@ -279,7 +279,7 @@ describe('encryption registry', () => { expect(tables).toContain('notes'); expect(tables).toContain('memos'); expect(tables).toContain('contacts'); - expect(tables).toContain('cycleDayLogs'); + expect(tables).toContain('periodDayLogs'); }); it('every registry entry has a non-empty fields list', () => { diff --git a/apps/mana/apps/web/src/lib/data/crypto/registry.ts b/apps/mana/apps/web/src/lib/data/crypto/registry.ts index b2bfba164..8b33fb596 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/registry.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/registry.ts @@ -119,15 +119,15 @@ export const ENCRYPTION_REGISTRY: Record = { // would still leak via the timeBlocks table. events: { enabled: true, fields: ['title', 'description', 'location'] }, - // ─── Cycles ────────────────────────────────────────────── + // ─── Period ────────────────────────────────────────────── // Health data — GDPR Art. 9 sensitive personal data category. // `symptoms` stays plaintext: it's a string-array of standardised // labels (cramps, headache, ...) used as a Set in the symptom // counter store; encrypting it would break the diff loop in // dayLogsStore.logDay. `mood` is a single enum but with the same // privacy sensitivity as `notes` — encrypt it. - cycles: { enabled: true, fields: ['notes'] }, - cycleDayLogs: { enabled: true, fields: ['notes', 'mood'] }, + periods: { enabled: true, fields: ['notes'] }, + periodDayLogs: { enabled: true, fields: ['notes', 'mood'] }, // ─── Food ──────────────────────────────────────────── // LocalMeal user-typed / AI-generated content → encrypted: @@ -353,7 +353,7 @@ export const ENCRYPTION_REGISTRY: Record = { // plaintext indexes still resolve "which sets did I do" without // leaking how heavy or how many. // - bodyChecks.energy/sleep/soreness/mood: 1-5 mood-style ratings with - // the same sensitivity as cycleDayLogs.mood. + // the same sensitivity as periodDayLogs.mood. // - bodyPhases.startWeight/targetWeight: identical reasoning to // measurement values. // Plaintext (intentional): diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index a242748ad..61102540b 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -236,10 +236,10 @@ db.version(1).stores({ dreamSymbols: 'id, name, count, updatedAt', dreamTags: 'id, dreamId, tagId, [dreamId+tagId]', - // ─── Cycles (appId: 'cycles') ─── - cycles: 'id, startDate, endDate, isPredicted, isArchived, updatedAt', - cycleDayLogs: 'id, logDate, cycleId, flow, [cycleId+logDate]', - cycleSymptoms: 'id, name, category, count, updatedAt', + // ─── Period (appId: 'period') ─── + periods: 'id, startDate, endDate, isPredicted, isArchived, updatedAt', + periodDayLogs: 'id, logDate, periodId, flow, [periodId+logDate]', + periodSymptoms: 'id, name, category, count, updatedAt', // ─── Social Events (appId: 'events') ─── // `socialEvents` is named distinctly to avoid colliding with calendar.events. diff --git a/apps/mana/apps/web/src/lib/data/events/catalog.ts b/apps/mana/apps/web/src/lib/data/events/catalog.ts index 2fd2d38bd..8d006dfe7 100644 --- a/apps/mana/apps/web/src/lib/data/events/catalog.ts +++ b/apps/mana/apps/web/src/lib/data/events/catalog.ts @@ -442,14 +442,14 @@ export interface SocialEventDeletedPayload { export type SocialEventsEventType = 'SocialEventCreated' | 'SocialEventDeleted'; -// ── Cycles ────────────────────────────────────────── +// ── Period ────────────────────────────────────────── -export interface CycleDayLoggedPayload { +export interface PeriodDayLoggedPayload { logId: string; date: string; flow?: string; } -export type CyclesEventType = 'CycleDayLogged'; +export type PeriodEventType = 'PeriodDayLogged'; // ── Firsts ────────────────────────────────────────── @@ -653,7 +653,7 @@ export type ManaEventType = | ChatEventType | MemoroEventType | SkilltreeEventType - | CyclesEventType + | PeriodEventType | FirstsEventType | GuidesEventType | InventoryEventType @@ -746,8 +746,8 @@ export type ManaEvent = // Skilltree | DomainEvent<'SkillXpAdded', SkillXpAddedPayload> | DomainEvent<'SkillCreated', SkillCreatedPayload> - // Cycles - | DomainEvent<'CycleDayLogged', CycleDayLoggedPayload> + // Period + | DomainEvent<'PeriodDayLogged', PeriodDayLoggedPayload> // Firsts | DomainEvent<'FirstCreated', FirstCreatedPayload> // Guides diff --git a/apps/mana/apps/web/src/lib/data/module-registry.test.ts b/apps/mana/apps/web/src/lib/data/module-registry.test.ts index 56eae470f..e53c27555 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.test.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.test.ts @@ -165,7 +165,7 @@ describe('module-registry — pre-refactor snapshot', () => { habits: ['habits', 'habitLogs'], notes: ['notes', 'noteTags'], dreams: ['dreams', 'dreamSymbols', 'dreamTags'], - cycles: ['cycles', 'cycleDayLogs', 'cycleSymptoms'], + period: ['periods', 'periodDayLogs', 'periodSymptoms'], events: ['socialEvents', 'eventGuests', 'eventInvitations', 'eventItems'], finance: ['transactions', 'financeCategories', 'budgets'], places: ['places', 'locationLogs', 'placeTags'], diff --git a/apps/mana/apps/web/src/lib/data/module-registry.ts b/apps/mana/apps/web/src/lib/data/module-registry.ts index 936190c9b..054d5266b 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -79,7 +79,7 @@ import { habitsModuleConfig } from '$lib/modules/habits/module.config'; import { notesModuleConfig } from '$lib/modules/notes/module.config'; import { journalModuleConfig } from '$lib/modules/journal/module.config'; import { dreamsModuleConfig } from '$lib/modules/dreams/module.config'; -import { cyclesModuleConfig } from '$lib/modules/cycles/module.config'; +import { periodModuleConfig } from '$lib/modules/period/module.config'; import { eventsModuleConfig } from '$lib/modules/events/module.config'; import { financeModuleConfig } from '$lib/modules/finance/module.config'; import { placesModuleConfig } from '$lib/modules/places/module.config'; @@ -129,7 +129,7 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ notesModuleConfig, journalModuleConfig, dreamsModuleConfig, - cyclesModuleConfig, + periodModuleConfig, eventsModuleConfig, financeModuleConfig, placesModuleConfig, diff --git a/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts b/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts index 01bcbf356..37b8b5694 100644 --- a/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts +++ b/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts @@ -29,7 +29,7 @@ const TYPE_COLORS: Record = { watering: '#06b6d4', sleep: '#6366f1', practice: '#f97316', - cycle: '#ec4899', + period: '#ec4899', guide: '#14b8a6', visit: '#a855f7', study: '#0ea5e9', @@ -49,7 +49,7 @@ const TYPE_LABELS: Record = { watering: 'Gießen', sleep: 'Schlaf', practice: 'Übung', - cycle: 'Zyklus', + period: 'Periode', guide: 'Guides', visit: 'Besuche', study: 'Lernen', diff --git a/apps/mana/apps/web/src/lib/data/time-blocks/types.ts b/apps/mana/apps/web/src/lib/data/time-blocks/types.ts index ba3bf0095..62beed1c2 100644 --- a/apps/mana/apps/web/src/lib/data/time-blocks/types.ts +++ b/apps/mana/apps/web/src/lib/data/time-blocks/types.ts @@ -23,7 +23,7 @@ export type TimeBlockType = | 'watering' | 'sleep' | 'practice' - | 'cycle' + | 'period' | 'guide' | 'visit' | 'study' @@ -41,7 +41,7 @@ export type TimeBlockSourceModule = | 'plants' | 'dreams' | 'skilltree' - | 'cycles' + | 'period' | 'guides' | 'places' | 'cards' diff --git a/apps/mana/apps/web/src/lib/data/tools/init.ts b/apps/mana/apps/web/src/lib/data/tools/init.ts index d89cc7091..7e7b6821e 100644 --- a/apps/mana/apps/web/src/lib/data/tools/init.ts +++ b/apps/mana/apps/web/src/lib/data/tools/init.ts @@ -24,7 +24,7 @@ import { storageTools } from '$lib/modules/storage/tools'; import { chatTools } from '$lib/modules/chat/tools'; import { memoroTools } from '$lib/modules/memoro/tools'; import { skilltreeTools } from '$lib/modules/skilltree/tools'; -import { cyclesTools } from '$lib/modules/cycles/tools'; +import { periodTools } from '$lib/modules/period/tools'; import { firstsTools } from '$lib/modules/firsts/tools'; import { guidesTools } from '$lib/modules/guides/tools'; import { inventoryTools } from '$lib/modules/inventory/tools'; @@ -59,7 +59,7 @@ export function initTools(): void { registerTools(chatTools); registerTools(memoroTools); registerTools(skilltreeTools); - registerTools(cyclesTools); + registerTools(periodTools); registerTools(firstsTools); registerTools(guidesTools); registerTools(inventoryTools); diff --git a/apps/mana/apps/web/src/lib/i18n/index.ts b/apps/mana/apps/web/src/lib/i18n/index.ts index 993869388..d24690ff5 100644 --- a/apps/mana/apps/web/src/lib/i18n/index.ts +++ b/apps/mana/apps/web/src/lib/i18n/index.ts @@ -48,7 +48,7 @@ function registerLocale(lang: SupportedLocale) { questions, guides, help, - cycles, + period, news, body, ] = await Promise.all([ @@ -84,7 +84,7 @@ function registerLocale(lang: SupportedLocale) { import(`./locales/questions/${lang}.json`), import(`./locales/guides/${lang}.json`), import(`./locales/help/${lang}.json`), - import(`./locales/cycles/${lang}.json`), + import(`./locales/period/${lang}.json`), import(`./locales/news/${lang}.json`), import(`./locales/body/${lang}.json`), ]); @@ -122,7 +122,7 @@ function registerLocale(lang: SupportedLocale) { questions: questions.default, guides: guides.default, help: help.default, - cycles: cycles.default, + period: period.default, news: news.default, body: body.default, }; diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json index ce12edc2b..5874cb550 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/de.json @@ -26,7 +26,7 @@ "citycorners": "Stadtführer", "uload": "uLoad", "calc": "Rechner", - "cycles": "Zyklus", + "period": "Periode", "body": "Körper", "dreams": "Träume", "journal": "Tagebuch", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json index be637ab59..e59077480 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/en.json @@ -26,7 +26,7 @@ "citycorners": "City Guide", "uload": "uLoad", "calc": "Calculator", - "cycles": "Cycles", + "period": "Period", "body": "Body", "dreams": "Dreams", "journal": "Journal", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json index ec4e9d81e..f47ece27e 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/es.json @@ -26,7 +26,7 @@ "citycorners": "Guía urbana", "uload": "uLoad", "calc": "Calculadora", - "cycles": "Ciclo", + "period": "Ciclo", "body": "Cuerpo", "dreams": "Sueños", "journal": "Diario", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json index af790d0d4..22d1e2fd6 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/fr.json @@ -26,7 +26,7 @@ "citycorners": "Guide urbain", "uload": "uLoad", "calc": "Calculatrice", - "cycles": "Cycle", + "period": "Règles", "body": "Corps", "dreams": "Rêves", "journal": "Journal", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json b/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json index 8bee33a56..72d2f1c51 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/apps/it.json @@ -26,7 +26,7 @@ "citycorners": "Guida città", "uload": "uLoad", "calc": "Calcolatrice", - "cycles": "Ciclo", + "period": "Ciclo", "body": "Corpo", "dreams": "Sogni", "journal": "Diario", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json index 562c5cbf0..f071563c1 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/de.json @@ -145,7 +145,7 @@ "description": "Letzte Änderungen über alle Module", "empty": "Noch keine Aktivität" }, - "cycles": { + "period": { "title": "Zyklus", "description": "Aktuelle Phase und Countdown zur nächsten Periode", "empty": "Noch kein Zyklus erfasst.", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json index 7944fef33..4797fc77b 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/en.json @@ -145,7 +145,7 @@ "description": "Recent changes across all modules", "empty": "No activity yet" }, - "cycles": { + "period": { "title": "Cycle", "description": "Current phase and countdown to next period", "empty": "No cycle logged yet.", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json index cd9eca43d..c10b398c8 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/es.json @@ -140,7 +140,7 @@ "description": "Línea temporal cronológica de todas las actividades", "empty": "Nada todavía hoy" }, - "cycles": { + "period": { "title": "Ciclo", "description": "Fase actual y cuenta regresiva hasta el próximo período", "empty": "Ningún ciclo registrado.", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json index 92baf6ae8..451baaf48 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/fr.json @@ -140,7 +140,7 @@ "description": "Chronologie de toutes les activités de la journée", "empty": "Rien encore aujourd'hui" }, - "cycles": { + "period": { "title": "Cycle", "description": "Phase actuelle et compte à rebours jusqu'aux prochaines règles", "empty": "Aucun cycle enregistré.", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json index 26f755ec7..8f323cc3f 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/dashboard/it.json @@ -140,7 +140,7 @@ "description": "Cronologia di tutte le attività della giornata", "empty": "Niente ancora oggi" }, - "cycles": { + "period": { "title": "Ciclo", "description": "Fase attuale e conto alla rovescia per il prossimo ciclo", "empty": "Nessun ciclo registrato.", diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cycles/de.json b/apps/mana/apps/web/src/lib/i18n/locales/period/de.json similarity index 96% rename from apps/mana/apps/web/src/lib/i18n/locales/cycles/de.json rename to apps/mana/apps/web/src/lib/i18n/locales/period/de.json index 178d2a5fa..fab67052c 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/cycles/de.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/period/de.json @@ -1,6 +1,6 @@ { "app": { - "name": "Cycles", + "name": "Period", "tagline": "Menstruationszyklus-Tracking" }, "phase": { @@ -25,7 +25,7 @@ "bad": "Schlecht" }, "label": { - "cycleDay": "Zyklustag", + "periodDay": "Zyklustag", "daysUntilPeriod": "Tage bis zur Periode", "today": "Heute", "predicted": "vorhergesagt", @@ -53,7 +53,7 @@ "avgDays": "Ø Tage", "shortest": "kürzester", "longest": "längster", - "cycles": "Zyklen", + "period": "Zyklen", "nextPeriod": "Nächste Periode:", "fertileWindow": "Fruchtbares Fenster:" }, diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cycles/en.json b/apps/mana/apps/web/src/lib/i18n/locales/period/en.json similarity index 96% rename from apps/mana/apps/web/src/lib/i18n/locales/cycles/en.json rename to apps/mana/apps/web/src/lib/i18n/locales/period/en.json index 4ab7c1d15..ca6de2411 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/cycles/en.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/period/en.json @@ -1,6 +1,6 @@ { "app": { - "name": "Cycles", + "name": "Period", "tagline": "Menstrual Cycle Tracking" }, "phase": { @@ -25,7 +25,7 @@ "bad": "Bad" }, "label": { - "cycleDay": "Cycle day", + "periodDay": "Cycle day", "daysUntilPeriod": "days until period", "today": "Today", "predicted": "predicted", @@ -53,7 +53,7 @@ "avgDays": "Avg days", "shortest": "shortest", "longest": "longest", - "cycles": "cycles", + "period": "period", "nextPeriod": "Next period:", "fertileWindow": "Fertile window:" }, diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cycles/es.json b/apps/mana/apps/web/src/lib/i18n/locales/period/es.json similarity index 96% rename from apps/mana/apps/web/src/lib/i18n/locales/cycles/es.json rename to apps/mana/apps/web/src/lib/i18n/locales/period/es.json index 5b861d6b0..b24b4ce1c 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/cycles/es.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/period/es.json @@ -1,6 +1,6 @@ { "app": { - "name": "Cycles", + "name": "Period", "tagline": "Seguimiento del ciclo menstrual" }, "phase": { @@ -25,7 +25,7 @@ "bad": "Malo" }, "label": { - "cycleDay": "Día del ciclo", + "periodDay": "Día del ciclo", "daysUntilPeriod": "días hasta el período", "today": "Hoy", "predicted": "previsto", @@ -53,7 +53,7 @@ "avgDays": "Prom. días", "shortest": "más corto", "longest": "más largo", - "cycles": "ciclos", + "period": "ciclos", "nextPeriod": "Próximo período:", "fertileWindow": "Ventana fértil:" }, diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cycles/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/period/fr.json similarity index 96% rename from apps/mana/apps/web/src/lib/i18n/locales/cycles/fr.json rename to apps/mana/apps/web/src/lib/i18n/locales/period/fr.json index 71515fbf6..88ba6f0f8 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/cycles/fr.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/period/fr.json @@ -1,6 +1,6 @@ { "app": { - "name": "Cycles", + "name": "Period", "tagline": "Suivi du cycle menstruel" }, "phase": { @@ -25,7 +25,7 @@ "bad": "Mauvais" }, "label": { - "cycleDay": "Jour du cycle", + "periodDay": "Jour du cycle", "daysUntilPeriod": "jours avant les règles", "today": "Aujourd'hui", "predicted": "prévu", @@ -53,7 +53,7 @@ "avgDays": "Moy. jours", "shortest": "plus court", "longest": "plus long", - "cycles": "cycles", + "period": "period", "nextPeriod": "Prochaines règles :", "fertileWindow": "Fenêtre fertile :" }, diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cycles/it.json b/apps/mana/apps/web/src/lib/i18n/locales/period/it.json similarity index 96% rename from apps/mana/apps/web/src/lib/i18n/locales/cycles/it.json rename to apps/mana/apps/web/src/lib/i18n/locales/period/it.json index a5fb3e2ac..0c75360fd 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/cycles/it.json +++ b/apps/mana/apps/web/src/lib/i18n/locales/period/it.json @@ -1,6 +1,6 @@ { "app": { - "name": "Cycles", + "name": "Period", "tagline": "Monitoraggio del ciclo mestruale" }, "phase": { @@ -25,7 +25,7 @@ "bad": "Cattivo" }, "label": { - "cycleDay": "Giorno del ciclo", + "periodDay": "Giorno del ciclo", "daysUntilPeriod": "giorni al ciclo", "today": "Oggi", "predicted": "previsto", @@ -53,7 +53,7 @@ "avgDays": "Media gg", "shortest": "più breve", "longest": "più lungo", - "cycles": "cicli", + "period": "cicli", "nextPeriod": "Prossimo ciclo:", "fertileWindow": "Finestra fertile:" }, diff --git a/apps/mana/apps/web/src/lib/i18n/locales/cycles/parity.test.ts b/apps/mana/apps/web/src/lib/i18n/locales/period/parity.test.ts similarity index 96% rename from apps/mana/apps/web/src/lib/i18n/locales/cycles/parity.test.ts rename to apps/mana/apps/web/src/lib/i18n/locales/period/parity.test.ts index bc133ac42..e07f19d4d 100644 --- a/apps/mana/apps/web/src/lib/i18n/locales/cycles/parity.test.ts +++ b/apps/mana/apps/web/src/lib/i18n/locales/period/parity.test.ts @@ -1,5 +1,5 @@ /** - * i18n parity test for the cycles module. + * i18n parity test for the period module. * * Ensures all 5 locale files (de/en/it/fr/es) have identical key * structure — stub copies of en.json would otherwise drift silently @@ -41,7 +41,7 @@ const locales = { es: es as Dict, }; -describe('cycles i18n parity', () => { +describe('period i18n parity', () => { const referenceKeys = flattenKeys(locales.de); test('de has a non-empty set of keys', () => { diff --git a/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte b/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte index ec876e861..796e0ef7f 100644 --- a/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte +++ b/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte @@ -49,7 +49,7 @@ { type: 'watering', label: 'Gießen', icon: Drop }, { type: 'sleep', label: 'Schlaf', icon: Moon }, { type: 'practice', label: 'Übung', icon: GraduationCap }, - { type: 'cycle', label: 'Zyklus', icon: FlowerLotus }, + { type: 'period', label: 'Periode', icon: FlowerLotus }, { type: 'guide', label: 'Guides', icon: Compass }, { type: 'visit', label: 'Besuche', icon: MapPin }, { type: 'study', label: 'Lernen', icon: BookOpen }, diff --git a/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts b/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts index 83709c290..42a106b70 100644 --- a/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts @@ -35,7 +35,7 @@ let visibleBlockTypes = $state>( 'watering', 'sleep', 'practice', - 'cycle', + 'period', 'guide', 'visit', 'study', diff --git a/apps/mana/apps/web/src/lib/modules/core/widgets/CyclesWidget.svelte b/apps/mana/apps/web/src/lib/modules/core/widgets/PeriodWidget.svelte similarity index 65% rename from apps/mana/apps/web/src/lib/modules/core/widgets/CyclesWidget.svelte rename to apps/mana/apps/web/src/lib/modules/core/widgets/PeriodWidget.svelte index b7b11f2b9..a3b1cdc96 100644 --- a/apps/mana/apps/web/src/lib/modules/core/widgets/CyclesWidget.svelte +++ b/apps/mana/apps/web/src/lib/modules/core/widgets/PeriodWidget.svelte @@ -1,32 +1,32 @@ - +
{#if symptoms.length === 0} -

{$_('cycles.symptomManager.empty')}

+

{$_('period.symptomManager.empty')}

{:else}
    {#each symptoms as sym (sym.id)} @@ -101,15 +101,15 @@ />
    {:else} @@ -120,7 +120,7 @@
    {sym.name} - {$_(`cycles.symptomCategory.${sym.category}`)} + {$_(`period.symptomCategory.${sym.category}`)} {#if sym.count > 0} · {sym.count} {/if} @@ -128,10 +128,10 @@
    {/if} diff --git a/apps/mana/apps/web/src/lib/modules/period/index.ts b/apps/mana/apps/web/src/lib/modules/period/index.ts index 4c5a559a5..5ba13c65a 100644 --- a/apps/mana/apps/web/src/lib/modules/period/index.ts +++ b/apps/mana/apps/web/src/lib/modules/period/index.ts @@ -1,39 +1,44 @@ /** - * Cycles module — barrel exports. + * Periods module — barrel exports. */ // ─── Stores ────────────────────────────────────────────── -export { cyclesStore } from './stores/cycles.svelte'; +export { periodsStore } from './stores/periods.svelte'; export { dayLogsStore } from './stores/dayLogs.svelte'; export { symptomsStore } from './stores/symptoms.svelte'; // ─── Queries ───────────────────────────────────────────── export { - useAllCycles, - useCurrentCycle, + useAllPeriods, + useCurrentPeriod, useAllDayLogs, useDayLog, useAllSymptoms, - toCycle, - toCycleDayLog, - toCycleSymptom, + toPeriod, + toPeriodDayLog, + toPeriodSymptom, groupLogsByMonth, formatLogDate, } from './queries'; export type { RelativeDateLabels } from './queries'; // ─── Utils ─────────────────────────────────────────────── -export { derivePhase, findCycleForDate, getCycleDayNumber, daysBetween } from './utils/phase'; +export { derivePhase, findPeriodForDate, getPeriodDayNumber, daysBetween } from './utils/phase'; export { - averageCycleLength, + averagePeriodLength, predictNextPeriodStart, daysUntilNextPeriod, predictFertileWindow, - computeCycleStats, + computePeriodStats, } from './utils/prediction'; // ─── Collections ───────────────────────────────────────── -export { cycleTable, cycleDayLogTable, cycleSymptomTable, CYCLES_GUEST_SEED } from './collections'; +export { + periodTable, + periodDayLogTable, + periodSymptomTable, + PERIODS_GUEST_SEED, +} from './collections'; // ─── Types & Constants ─────────────────────────────────── export { @@ -44,20 +49,20 @@ export { PHASE_COLORS, PHASE_LABELS, CERVICAL_MUCUS_LABELS, - DEFAULT_CYCLE_LENGTH, DEFAULT_PERIOD_LENGTH, + DEFAULT_BLEEDING_DAYS, DEFAULT_LUTEAL_LENGTH, } from './types'; export type { - LocalCycle, - LocalCycleDayLog, - LocalCycleSymptom, - Cycle, - CycleDayLog, - CycleSymptom, + LocalPeriod, + LocalPeriodDayLog, + LocalPeriodSymptom, + Period, + PeriodDayLog, + PeriodSymptom, Flow, Mood, CervicalMucus, SymptomCategory, - CyclePhase, + PeriodPhase, } from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/period/module.config.ts b/apps/mana/apps/web/src/lib/modules/period/module.config.ts index 7b230a30c..1d39a54ab 100644 --- a/apps/mana/apps/web/src/lib/modules/period/module.config.ts +++ b/apps/mana/apps/web/src/lib/modules/period/module.config.ts @@ -1,6 +1,6 @@ import type { ModuleConfig } from '$lib/data/module-registry'; -export const cyclesModuleConfig: ModuleConfig = { - appId: 'cycles', - tables: [{ name: 'cycles' }, { name: 'cycleDayLogs' }, { name: 'cycleSymptoms' }], +export const periodModuleConfig: ModuleConfig = { + appId: 'period', + tables: [{ name: 'periods' }, { name: 'periodDayLogs' }, { name: 'periodSymptoms' }], }; diff --git a/apps/mana/apps/web/src/lib/modules/period/queries.ts b/apps/mana/apps/web/src/lib/modules/period/queries.ts index 2428ebdc2..974eb4b4a 100644 --- a/apps/mana/apps/web/src/lib/modules/period/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/period/queries.ts @@ -1,22 +1,22 @@ /** - * Reactive Queries & Pure Helpers for Cycles module. + * Reactive Queries & Pure Helpers for Periods module. */ import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; import { db } from '$lib/data/database'; import { decryptRecord, decryptRecords } from '$lib/data/crypto'; import type { - Cycle, - CycleDayLog, - CycleSymptom, - LocalCycle, - LocalCycleDayLog, - LocalCycleSymptom, + Period, + PeriodDayLog, + PeriodSymptom, + LocalPeriod, + LocalPeriodDayLog, + LocalPeriodSymptom, } from './types'; // ─── Type Converters ─────────────────────────────────────── -export function toCycle(local: LocalCycle): Cycle { +export function toPeriod(local: LocalPeriod): Period { return { id: local.id, startDate: local.startDate, @@ -31,11 +31,11 @@ export function toCycle(local: LocalCycle): Cycle { }; } -export function toCycleDayLog(local: LocalCycleDayLog): CycleDayLog { +export function toPeriodDayLog(local: LocalPeriodDayLog): PeriodDayLog { return { id: local.id, logDate: local.logDate, - cycleId: local.cycleId, + periodId: local.periodId, flow: local.flow, mood: local.mood, energy: local.energy, @@ -49,7 +49,7 @@ export function toCycleDayLog(local: LocalCycleDayLog): CycleDayLog { }; } -export function toCycleSymptom(local: LocalCycleSymptom): CycleSymptom { +export function toPeriodSymptom(local: LocalPeriodSymptom): PeriodSymptom { return { id: local.id, name: local.name, @@ -63,65 +63,65 @@ export function toCycleSymptom(local: LocalCycleSymptom): CycleSymptom { // ─── Live Queries ────────────────────────────────────────── -export function useAllCycles() { +export function useAllPeriods() { return useLiveQueryWithDefault(async () => { - const visible = (await db.table('cycles').toArray()).filter( + const visible = (await db.table('periods').toArray()).filter( (c) => !c.deletedAt && !c.isArchived ); - const decrypted = await decryptRecords('cycles', visible); - return decrypted.map(toCycle).sort((a, b) => b.startDate.localeCompare(a.startDate)); - }, [] as Cycle[]); + const decrypted = await decryptRecords('periods', visible); + return decrypted.map(toPeriod).sort((a, b) => b.startDate.localeCompare(a.startDate)); + }, [] as Period[]); } -export function useCurrentCycle() { +export function useCurrentPeriod() { return useLiveQueryWithDefault( async () => { - const locals = await db.table('cycles').toArray(); + const locals = await db.table('periods').toArray(); const real = locals.filter((c) => !c.deletedAt && !c.isArchived && !c.isPredicted); if (real.length === 0) return null; const latest = real.sort((a, b) => b.startDate.localeCompare(a.startDate))[0]; - const decrypted = await decryptRecord('cycles', { ...latest }); - return toCycle(decrypted); + const decrypted = await decryptRecord('periods', { ...latest }); + return toPeriod(decrypted); }, - null as Cycle | null + null as Period | null ); } export function useAllDayLogs() { return useLiveQueryWithDefault(async () => { - const visible = (await db.table('cycleDayLogs').toArray()).filter( + const visible = (await db.table('periodDayLogs').toArray()).filter( (l) => !l.deletedAt ); - const decrypted = await decryptRecords('cycleDayLogs', visible); - return decrypted.map(toCycleDayLog).sort((a, b) => b.logDate.localeCompare(a.logDate)); - }, [] as CycleDayLog[]); + const decrypted = await decryptRecords('periodDayLogs', visible); + return decrypted.map(toPeriodDayLog).sort((a, b) => b.logDate.localeCompare(a.logDate)); + }, [] as PeriodDayLog[]); } export function useDayLog(date: string) { return useLiveQueryWithDefault( async () => { const locals = await db - .table('cycleDayLogs') + .table('periodDayLogs') .where('logDate') .equals(date) .toArray(); const active = locals.find((l) => !l.deletedAt); if (!active) return null; - const decrypted = await decryptRecord('cycleDayLogs', { ...active }); - return toCycleDayLog(decrypted); + const decrypted = await decryptRecord('periodDayLogs', { ...active }); + return toPeriodDayLog(decrypted); }, - null as CycleDayLog | null + null as PeriodDayLog | null ); } export function useAllSymptoms() { return useLiveQueryWithDefault(async () => { - const locals = await db.table('cycleSymptoms').toArray(); + const locals = await db.table('periodSymptoms').toArray(); return locals .filter((s) => !s.deletedAt) - .map(toCycleSymptom) + .map(toPeriodSymptom) .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); - }, [] as CycleSymptom[]); + }, [] as PeriodSymptom[]); } // ─── Pure Helpers ────────────────────────────────────────── @@ -136,10 +136,10 @@ export interface RelativeDateLabels { /** Group day logs by localized month label. */ export function groupLogsByMonth( - logs: CycleDayLog[], + logs: PeriodDayLog[], dateLocale: string = 'de-DE' -): Array<{ label: string; logs: CycleDayLog[] }> { - const groups = new Map(); +): Array<{ label: string; logs: PeriodDayLog[] }> { + const groups = new Map(); for (const l of logs) { const date = new Date(l.logDate); const label = date.toLocaleDateString(dateLocale, { month: 'long', year: 'numeric' }); diff --git a/apps/mana/apps/web/src/lib/modules/period/stores/dayLogs.svelte.ts b/apps/mana/apps/web/src/lib/modules/period/stores/dayLogs.svelte.ts index d5c683a6f..a4c2e7f5e 100644 --- a/apps/mana/apps/web/src/lib/modules/period/stores/dayLogs.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/period/stores/dayLogs.svelte.ts @@ -1,24 +1,24 @@ /** - * Day Logs Store — Mutation-Only Service for daily cycle entries. + * Day Logs Store — Mutation-Only Service for daily period entries. */ -import { cycleDayLogTable, cycleTable } from '../collections'; -import { toCycle, toCycleDayLog } from '../queries'; -import { detectPeriodEnd, shouldStartNewCycle } from '../utils/auto-detect'; -import { cyclesStore } from './cycles.svelte'; +import { periodDayLogTable, periodTable } from '../collections'; +import { toPeriod, toPeriodDayLog } from '../queries'; +import { detectPeriodEnd, shouldStartNewPeriod } from '../utils/auto-detect'; +import { periodsStore } from './periods.svelte'; import { symptomsStore } from './symptoms.svelte'; import { encryptRecord } from '$lib/data/crypto'; -import type { CervicalMucus, Flow, LocalCycle, LocalCycleDayLog, Mood } from '../types'; +import type { CervicalMucus, Flow, LocalPeriod, LocalPeriodDayLog, Mood } from '../types'; function todayIsoDate(): string { return new Date().toISOString().slice(0, 10); } /** Findet den passenden Zyklus für ein Datum (letzter startDate <= date). */ -async function resolveCycleId(date: string): Promise { - const all = await cycleTable.toArray(); +async function resolvePeriodId(date: string): Promise { + const all = await periodTable.toArray(); const candidates = all - .filter((c: LocalCycle) => !c.deletedAt && !c.isPredicted && c.startDate <= date) + .filter((c: LocalPeriod) => !c.deletedAt && !c.isPredicted && c.startDate <= date) .sort((a, b) => b.startDate.localeCompare(a.startDate)); return candidates[0]?.id ?? null; } @@ -42,18 +42,18 @@ export const dayLogsStore = { // ─ Auto-Start: explizites flow + Bedingungen erfüllt → neuen Zyklus VOR dem Schreiben anlegen if (data.flow !== undefined) { - const allCycles = await cycleTable.toArray(); - const visibleCycles = allCycles.filter((c) => !c.deletedAt).map(toCycle); - if (shouldStartNewCycle(logDate, data.flow, visibleCycles)) { - await cyclesStore.createCycle({ startDate: logDate }); + const allPeriods = await periodTable.toArray(); + const visiblePeriods = allPeriods.filter((c) => !c.deletedAt).map(toPeriod); + if (shouldStartNewPeriod(logDate, data.flow, visiblePeriods)) { + await periodsStore.createPeriod({ startDate: logDate }); } } - const existing = (await cycleDayLogTable.where('logDate').equals(logDate).toArray()).find( + const existing = (await periodDayLogTable.where('logDate').equals(logDate).toArray()).find( (l) => !l.deletedAt ); - let result: LocalCycleDayLog; + let result: LocalPeriodDayLog; if (existing) { // Symptom-Counter aktualisieren. if (data.symptoms) { @@ -64,22 +64,22 @@ export const dayLogsStore = { if (added.length) await symptomsStore.touchSymptoms(added, +1); if (removed.length) await symptomsStore.touchSymptoms(removed, -1); } - const updateDiff: Partial = { + const updateDiff: Partial = { ...data, logDate, updatedAt: new Date().toISOString(), }; - await encryptRecord('cycleDayLogs', updateDiff); - await cycleDayLogTable.update(existing.id, updateDiff); + await encryptRecord('periodDayLogs', updateDiff); + await periodDayLogTable.update(existing.id, updateDiff); // `result` keeps the plaintext for the return value — caller // expects to render the input back. result = { ...existing, ...data, logDate }; } else { - const cycleId = await resolveCycleId(logDate); - const newLocal: LocalCycleDayLog = { + const periodId = await resolvePeriodId(logDate); + const newLocal: LocalPeriodDayLog = { id: crypto.randomUUID(), logDate, - cycleId, + periodId, flow: data.flow ?? 'none', mood: data.mood ?? null, energy: data.energy ?? null, @@ -92,52 +92,52 @@ export const dayLogsStore = { // Plaintext copy retained for the return value — what we // write to disk is encrypted. result = { ...newLocal }; - await encryptRecord('cycleDayLogs', newLocal); - await cycleDayLogTable.add(newLocal); + await encryptRecord('periodDayLogs', newLocal); + await periodDayLogTable.add(newLocal); if (result.symptoms.length) { await symptomsStore.touchSymptoms(result.symptoms, +1); } } // ─ Auto-End: Wenn explizit 'none' geloggt wurde, prüfe ob die Periode beendet werden soll - if (data.flow === 'none' && result.cycleId) { - const openCycleLocal = await cycleTable.get(result.cycleId); - if (openCycleLocal && !openCycleLocal.deletedAt && !openCycleLocal.periodEndDate) { - const cycleLogsLocal = await cycleDayLogTable - .where('cycleId') - .equals(result.cycleId) + if (data.flow === 'none' && result.periodId) { + const openPeriodLocal = await periodTable.get(result.periodId); + if (openPeriodLocal && !openPeriodLocal.deletedAt && !openPeriodLocal.periodEndDate) { + const periodLogsLocal = await periodDayLogTable + .where('periodId') + .equals(result.periodId) .toArray(); - const cycleLogs = cycleLogsLocal.filter((l) => !l.deletedAt).map(toCycleDayLog); - const endDate = detectPeriodEnd(logDate, 'none', toCycle(openCycleLocal), cycleLogs); + const periodLogs = periodLogsLocal.filter((l) => !l.deletedAt).map(toPeriodDayLog); + const endDate = detectPeriodEnd(logDate, 'none', toPeriod(openPeriodLocal), periodLogs); if (endDate) { - await cyclesStore.setPeriodEnd(openCycleLocal.id, endDate); + await periodsStore.setPeriodEnd(openPeriodLocal.id, endDate); } } } - return toCycleDayLog(result); + return toPeriodDayLog(result); }, async deleteLog(id: string) { - const existing = await cycleDayLogTable.get(id); + const existing = await periodDayLogTable.get(id); if (existing?.symptoms?.length) { await symptomsStore.touchSymptoms(existing.symptoms, -1); } - await cycleDayLogTable.update(id, { + await periodDayLogTable.update(id, { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); }, /** Hängt nicht zugeordnete Logs an den passenden Zyklus an. */ - async autoAssignCycle() { - const logs = await cycleDayLogTable.toArray(); + async autoAssignPeriod() { + const logs = await periodDayLogTable.toArray(); for (const log of logs) { - if (log.cycleId || log.deletedAt) continue; - const cycleId = await resolveCycleId(log.logDate); - if (cycleId) { - await cycleDayLogTable.update(log.id, { - cycleId, + if (log.periodId || log.deletedAt) continue; + const periodId = await resolvePeriodId(log.logDate); + if (periodId) { + await periodDayLogTable.update(log.id, { + periodId, updatedAt: new Date().toISOString(), }); } diff --git a/apps/mana/apps/web/src/lib/modules/period/stores/cycles.integration.test.ts b/apps/mana/apps/web/src/lib/modules/period/stores/periods.integration.test.ts similarity index 60% rename from apps/mana/apps/web/src/lib/modules/period/stores/cycles.integration.test.ts rename to apps/mana/apps/web/src/lib/modules/period/stores/periods.integration.test.ts index 7bb6c031e..e64c23085 100644 --- a/apps/mana/apps/web/src/lib/modules/period/stores/cycles.integration.test.ts +++ b/apps/mana/apps/web/src/lib/modules/period/stores/periods.integration.test.ts @@ -1,10 +1,10 @@ /** - * Integration tests for cycles stores against a real (fake) IndexedDB. + * Integration tests for periods stores against a real (fake) IndexedDB. * * Covers the complex interactions that pure-function tests cannot: - * - cyclesStore auto-closes the previous open cycle when a new one starts + * - periodsStore auto-closes the previous open period when a new one starts * - dayLogsStore upserts per date (no duplicates) - * - dayLogsStore auto-creates a cycle on first bleeding log + * - dayLogsStore auto-creates a period on first bleeding log * - dayLogsStore auto-sets periodEndDate after 2 dry days * - symptomsStore.touchSymptoms increments/decrements ref counts * - dayLogsStore updates symptom counters when symptoms change on an existing log @@ -27,19 +27,19 @@ import { setKeyProvider, decryptRecords, } from '$lib/data/crypto'; -import { cyclesStore } from './cycles.svelte'; +import { periodsStore } from './periods.svelte'; import { dayLogsStore } from './dayLogs.svelte'; import { symptomsStore } from './symptoms.svelte'; -import type { LocalCycle, LocalCycleDayLog, LocalCycleSymptom } from '../types'; +import type { LocalPeriod, LocalPeriodDayLog, LocalPeriodSymptom } from '../types'; -const cycleTable = () => db.table('cycles'); -const dayLogTable = () => db.table('cycleDayLogs'); -const symptomTable = () => db.table('cycleSymptoms'); +const periodTable = () => db.table('periods'); +const dayLogTable = () => db.table('periodDayLogs'); +const symptomTable = () => db.table('periodSymptoms'); const iso = (dateStr: string) => dateStr; // alias for readability -async function resetCyclesTables() { - await cycleTable().clear(); +async function resetPeriodsTables() { + await periodTable().clear(); await dayLogTable().clear(); await symptomTable().clear(); await db.table('_pendingChanges').clear(); @@ -48,65 +48,65 @@ async function resetCyclesTables() { beforeEach(async () => { setCurrentUserId('test-user'); - // Phase 5 cycles encryption requires an unlocked vault — install a + // Phase 5 periods encryption requires an unlocked vault — install a // real Web Crypto key in a fresh MemoryKeyProvider for each test // run so the dayLogsStore.logDay calls below can encrypt notes/mood. const key = await generateMasterKey(); const provider = new MemoryKeyProvider(); provider.setKey(key); setKeyProvider(provider); - await resetCyclesTables(); + await resetPeriodsTables(); }); -describe('cyclesStore.createCycle', () => { - it('creates a single open cycle when none exists', async () => { - const cycle = await cyclesStore.createCycle({ startDate: iso('2026-01-01') }); - expect(cycle.startDate).toBe('2026-01-01'); - expect(cycle.endDate).toBeNull(); - expect(cycle.length).toBeNull(); +describe('periodsStore.createPeriod', () => { + it('creates a single open period when none exists', async () => { + const period = await periodsStore.createPeriod({ startDate: iso('2026-01-01') }); + expect(period.startDate).toBe('2026-01-01'); + expect(period.endDate).toBeNull(); + expect(period.length).toBeNull(); - const stored = await cycleTable().toArray(); + const stored = await periodTable().toArray(); expect(stored).toHaveLength(1); - expect(stored[0].id).toBe(cycle.id); + expect(stored[0].id).toBe(period.id); }); - it('auto-closes the previous open cycle and computes length', async () => { - const first = await cyclesStore.createCycle({ startDate: iso('2026-01-01') }); - await cyclesStore.createCycle({ startDate: iso('2026-01-29') }); + it('auto-closes the previous open period and computes length', async () => { + const first = await periodsStore.createPeriod({ startDate: iso('2026-01-01') }); + await periodsStore.createPeriod({ startDate: iso('2026-01-29') }); - const firstStored = await cycleTable().get(first.id); + const firstStored = await periodTable().get(first.id); expect(firstStored?.endDate).toBe('2026-01-28'); // day before new start expect(firstStored?.length).toBe(28); }); - it('does not touch cycles whose startDate is >= the new cycle', async () => { - // Backfilling an older cycle should NOT close a future one - const future = await cyclesStore.createCycle({ startDate: iso('2026-03-01') }); - await cyclesStore.createCycle({ startDate: iso('2026-02-01') }); + it('does not touch periods whose startDate is >= the new period', async () => { + // Backfilling an older period should NOT close a future one + const future = await periodsStore.createPeriod({ startDate: iso('2026-03-01') }); + await periodsStore.createPeriod({ startDate: iso('2026-02-01') }); - const futureStored = await cycleTable().get(future.id); + const futureStored = await periodTable().get(future.id); expect(futureStored?.endDate).toBeNull(); expect(futureStored?.length).toBeNull(); }); }); -describe('cyclesStore.setPeriodEnd', () => { +describe('periodsStore.setPeriodEnd', () => { it('sets periodEndDate without affecting endDate', async () => { - const c = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); - await cyclesStore.setPeriodEnd(c.id, '2026-04-05'); + const c = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); + await periodsStore.setPeriodEnd(c.id, '2026-04-05'); - const stored = await cycleTable().get(c.id); + const stored = await periodTable().get(c.id); expect(stored?.periodEndDate).toBe('2026-04-05'); expect(stored?.endDate).toBeNull(); }); }); -describe('cyclesStore.deleteCycle', () => { +describe('periodsStore.deletePeriod', () => { it('soft-deletes via deletedAt', async () => { - const c = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); - await cyclesStore.deleteCycle(c.id); + const c = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); + await periodsStore.deletePeriod(c.id); - const stored = await cycleTable().get(c.id); + const stored = await periodTable().get(c.id); expect(stored?.deletedAt).toBeTruthy(); }); }); @@ -129,110 +129,110 @@ describe('dayLogsStore.logDay — upsert behavior', () => { // Phase 5: `mood` is encrypted on disk — decrypt before asserting // so the test reads the same view the UI does. const raw = (await dayLogTable().toArray()).filter((l) => !l.deletedAt); - const logs = await decryptRecords('cycleDayLogs', raw); + const logs = await decryptRecords('periodDayLogs', raw); expect(logs).toHaveLength(1); expect(logs[0].flow).toBe('light'); expect(logs[0].mood).toBe('good'); expect(logs[0].temperature).toBe(36.6); }); - it('assigns cycleId when a matching cycle exists', async () => { - await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); + it('assigns periodId when a matching period exists', async () => { + await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); await dayLogsStore.logDay({ logDate: '2026-04-05', mood: 'good' }); const log = (await dayLogTable().toArray())[0]; - expect(log.cycleId).toBeTruthy(); + expect(log.periodId).toBeTruthy(); }); - it('leaves cycleId null when no cycle covers the date', async () => { + it('leaves periodId null when no period covers the date', async () => { await dayLogsStore.logDay({ logDate: '2026-04-07', mood: 'good' }); const log = (await dayLogTable().toArray())[0]; - expect(log.cycleId).toBeNull(); + expect(log.periodId).toBeNull(); }); }); -describe('dayLogsStore.logDay — auto-start cycle', () => { - it('creates a new cycle on first bleeding log with no history', async () => { +describe('dayLogsStore.logDay — auto-start period', () => { + it('creates a new period on first bleeding log with no history', async () => { await dayLogsStore.logDay({ logDate: '2026-04-07', flow: 'medium' }); - const cycles = await cycleTable().toArray(); - expect(cycles).toHaveLength(1); - expect(cycles[0].startDate).toBe('2026-04-07'); + const periods = await periodTable().toArray(); + expect(periods).toHaveLength(1); + expect(periods[0].startDate).toBe('2026-04-07'); - // And the log itself is attached to that cycle + // And the log itself is attached to that period const log = (await dayLogTable().toArray())[0]; - expect(log.cycleId).toBe(cycles[0].id); + expect(log.periodId).toBe(periods[0].id); }); - it('does NOT create a new cycle for spotting', async () => { + it('does NOT create a new period for spotting', async () => { await dayLogsStore.logDay({ logDate: '2026-04-07', flow: 'spotting' }); - const cycles = await cycleTable().toArray(); - expect(cycles).toHaveLength(0); + const periods = await periodTable().toArray(); + expect(periods).toHaveLength(0); }); - it('does NOT create a new cycle during an open cycle', async () => { - await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); - // Mid-cycle bleeding should NOT spawn a second cycle + it('does NOT create a new period during an open period', async () => { + await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); + // Mid-period bleeding should NOT spawn a second period await dayLogsStore.logDay({ logDate: '2026-04-10', flow: 'medium' }); - const cycles = await cycleTable().toArray(); - expect(cycles).toHaveLength(1); + const periods = await periodTable().toArray(); + expect(periods).toHaveLength(1); }); - it('creates a new cycle when the previous is closed and far enough apart', async () => { - const first = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); - await cyclesStore.setPeriodEnd(first.id, '2026-04-05'); + it('creates a new period when the previous is closed and far enough apart', async () => { + const first = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); + await periodsStore.setPeriodEnd(first.id, '2026-04-05'); // 15 days after periodEndDate — well beyond MIN_GAP (10) await dayLogsStore.logDay({ logDate: '2026-04-20', flow: 'medium' }); - const cycles = (await cycleTable().toArray()).filter((c) => !c.deletedAt); - expect(cycles).toHaveLength(2); + const periods = (await periodTable().toArray()).filter((c) => !c.deletedAt); + expect(periods).toHaveLength(2); }); - it('does NOT create a new cycle if bleeding is too soon after period end', async () => { - const first = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); - await cyclesStore.setPeriodEnd(first.id, '2026-04-05'); + it('does NOT create a new period if bleeding is too soon after period end', async () => { + const first = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); + await periodsStore.setPeriodEnd(first.id, '2026-04-05'); - // Only 8 days after — treated as mid-cycle spotting + // Only 8 days after — treated as mid-period spotting await dayLogsStore.logDay({ logDate: '2026-04-13', flow: 'medium' }); - const cycles = (await cycleTable().toArray()).filter((c) => !c.deletedAt); - expect(cycles).toHaveLength(1); + const periods = (await periodTable().toArray()).filter((c) => !c.deletedAt); + expect(periods).toHaveLength(1); }); }); describe('dayLogsStore.logDay — auto-end period', () => { it('sets periodEndDate after 2 dry days following bleeding', async () => { - const c = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); + const c = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); await dayLogsStore.logDay({ logDate: '2026-04-01', flow: 'medium' }); await dayLogsStore.logDay({ logDate: '2026-04-02', flow: 'medium' }); await dayLogsStore.logDay({ logDate: '2026-04-03', flow: 'light' }); await dayLogsStore.logDay({ logDate: '2026-04-04', flow: 'none' }); await dayLogsStore.logDay({ logDate: '2026-04-05', flow: 'none' }); - const stored = await cycleTable().get(c.id); + const stored = await periodTable().get(c.id); expect(stored?.periodEndDate).toBe('2026-04-03'); }); it('does NOT set periodEndDate after only 1 dry day', async () => { - const c = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); + const c = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); await dayLogsStore.logDay({ logDate: '2026-04-01', flow: 'medium' }); await dayLogsStore.logDay({ logDate: '2026-04-02', flow: 'none' }); - const stored = await cycleTable().get(c.id); + const stored = await periodTable().get(c.id); expect(stored?.periodEndDate).toBeNull(); }); it('does not overwrite an already-set periodEndDate', async () => { - const c = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); - await cyclesStore.setPeriodEnd(c.id, '2026-04-03'); + const c = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); + await periodsStore.setPeriodEnd(c.id, '2026-04-03'); // Logging more none days should not re-trigger await dayLogsStore.logDay({ logDate: '2026-04-01', flow: 'medium' }); await dayLogsStore.logDay({ logDate: '2026-04-10', flow: 'none' }); - const stored = await cycleTable().get(c.id); + const stored = await periodTable().get(c.id); expect(stored?.periodEndDate).toBe('2026-04-03'); }); }); @@ -296,18 +296,18 @@ describe('dayLogsStore.logDay — symptom counter integration', () => { }); }); -describe('dayLogsStore.autoAssignCycle', () => { - it('retroactively attaches orphan logs to the right cycle', async () => { - // Log something before any cycle exists +describe('dayLogsStore.autoAssignPeriod', () => { + it('retroactively attaches orphan logs to the right period', async () => { + // Log something before any period exists await dayLogsStore.logDay({ logDate: '2026-04-07', mood: 'good' }); const orphan = (await dayLogTable().toArray())[0]; - expect(orphan.cycleId).toBeNull(); + expect(orphan.periodId).toBeNull(); - // Now create a cycle that should claim that day - const cycle = await cyclesStore.createCycle({ startDate: iso('2026-04-01') }); - await dayLogsStore.autoAssignCycle(); + // Now create a period that should claim that day + const period = await periodsStore.createPeriod({ startDate: iso('2026-04-01') }); + await dayLogsStore.autoAssignPeriod(); const reattached = await dayLogTable().get(orphan.id); - expect(reattached?.cycleId).toBe(cycle.id); + expect(reattached?.periodId).toBe(period.id); }); }); diff --git a/apps/mana/apps/web/src/lib/modules/period/stores/cycles.svelte.ts b/apps/mana/apps/web/src/lib/modules/period/stores/periods.svelte.ts similarity index 63% rename from apps/mana/apps/web/src/lib/modules/period/stores/cycles.svelte.ts rename to apps/mana/apps/web/src/lib/modules/period/stores/periods.svelte.ts index ad59ddca5..c8fedaa80 100644 --- a/apps/mana/apps/web/src/lib/modules/period/stores/cycles.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/period/stores/periods.svelte.ts @@ -1,14 +1,14 @@ /** - * Cycles Store — Mutation-Only Service for menstrual cycles. + * Periods Store — Mutation-Only Service for menstrual periods. */ -import { cycleTable } from '../collections'; -import { toCycle } from '../queries'; +import { periodTable } from '../collections'; +import { toPeriod } from '../queries'; import { daysBetween } from '../utils/phase'; import { encryptRecord } from '$lib/data/crypto'; import { emitDomainEvent } from '$lib/data/events'; import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service'; -import type { LocalCycle } from '../types'; +import type { LocalPeriod } from '../types'; function todayIsoDate(): string { return new Date().toISOString().slice(0, 10); @@ -20,13 +20,13 @@ function dayBefore(iso: string): string { return d.toISOString().slice(0, 10); } -export const cyclesStore = { +export const periodsStore = { /** Startet einen neuen Zyklus. Schließt automatisch den vorigen offenen Zyklus. */ - async createCycle(data: { startDate?: string; notes?: string | null }) { + async createPeriod(data: { startDate?: string; notes?: string | null }) { const startDate = data.startDate ?? todayIsoDate(); // Vorigen offenen Zyklus schließen. - const all = await cycleTable.toArray(); + const all = await periodTable.toArray(); const open = all .filter((c) => !c.deletedAt && !c.isPredicted && c.endDate === null) .sort((a, b) => b.startDate.localeCompare(a.startDate)); @@ -34,7 +34,7 @@ export const cyclesStore = { if (prev.startDate >= startDate) continue; const endDate = dayBefore(startDate); const length = daysBetween(startDate, prev.startDate); - await cycleTable.update(prev.id, { + await periodTable.update(prev.id, { endDate, length, updatedAt: new Date().toISOString(), @@ -42,21 +42,21 @@ export const cyclesStore = { } // Create a TimeBlock for the menstruation phase (allDay, open-ended until periodEnd is set) - const cycleId = crypto.randomUUID(); + const periodId = crypto.randomUUID(); const timeBlockId = await createBlock({ startDate: `${startDate}T00:00:00.000Z`, endDate: null, allDay: true, kind: 'logged', - type: 'cycle', - sourceModule: 'cycles', - sourceId: cycleId, + type: 'period', + sourceModule: 'period', + sourceId: periodId, title: 'Periode', color: '#ec4899', }); - const newLocal: LocalCycle = { - id: cycleId, + const newLocal: LocalPeriod = { + id: periodId, startDate, periodEndDate: null, endDate: null, @@ -66,10 +66,10 @@ export const cyclesStore = { notes: data.notes ?? null, timeBlockId, }; - const plaintextSnapshot = toCycle(newLocal); - await encryptRecord('cycles', newLocal); - await cycleTable.add(newLocal); - emitDomainEvent('CycleDayLogged', 'cycles', 'cycleDayLogs', newLocal.id, { + const plaintextSnapshot = toPeriod(newLocal); + await encryptRecord('periods', newLocal); + await periodTable.add(newLocal); + emitDomainEvent('PeriodDayLogged', 'period', 'periodDayLogs', newLocal.id, { logId: newLocal.id, date: newLocal.startDate, flow: null, @@ -77,51 +77,51 @@ export const cyclesStore = { return plaintextSnapshot; }, - async updateCycle( + async updatePeriod( id: string, data: Partial< Pick< - LocalCycle, + LocalPeriod, 'startDate' | 'periodEndDate' | 'endDate' | 'length' | 'notes' | 'isArchived' > > ) { - const diff: Partial = { + const diff: Partial = { ...data, updatedAt: new Date().toISOString(), }; - await encryptRecord('cycles', diff); - await cycleTable.update(id, diff); + await encryptRecord('periods', diff); + await periodTable.update(id, diff); }, /** Markiert das Ende der Blutung (nicht das Ende des Zyklus). */ async setPeriodEnd(id: string, periodEndDate: string | null) { - const cycle = await cycleTable.get(id); - await cycleTable.update(id, { + const period = await periodTable.get(id); + await periodTable.update(id, { periodEndDate, updatedAt: new Date().toISOString(), }); // Update the TimeBlock's endDate to reflect the period duration - if (cycle?.timeBlockId && periodEndDate) { - await updateBlock(cycle.timeBlockId, { + if (period?.timeBlockId && periodEndDate) { + await updateBlock(period.timeBlockId, { endDate: `${periodEndDate}T23:59:59.999Z`, }); } }, - async deleteCycle(id: string) { - const cycle = await cycleTable.get(id); - if (cycle?.timeBlockId) { - await deleteBlock(cycle.timeBlockId); + async deletePeriod(id: string) { + const period = await periodTable.get(id); + if (period?.timeBlockId) { + await deleteBlock(period.timeBlockId); } - await cycleTable.update(id, { + await periodTable.update(id, { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); }, - async archiveCycle(id: string) { - await cycleTable.update(id, { + async archivePeriod(id: string) { + await periodTable.update(id, { isArchived: true, updatedAt: new Date().toISOString(), }); diff --git a/apps/mana/apps/web/src/lib/modules/period/stores/symptoms.svelte.ts b/apps/mana/apps/web/src/lib/modules/period/stores/symptoms.svelte.ts index baa0ee4df..257e17215 100644 --- a/apps/mana/apps/web/src/lib/modules/period/stores/symptoms.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/period/stores/symptoms.svelte.ts @@ -1,35 +1,35 @@ /** - * Symptoms Store — Mutation-Only Service for cycle symptom taxonomy. + * Symptoms Store — Mutation-Only Service for period symptom taxonomy. */ -import { cycleSymptomTable } from '../collections'; -import type { LocalCycleSymptom, SymptomCategory } from '../types'; +import { periodSymptomTable } from '../collections'; +import type { LocalPeriodSymptom, SymptomCategory } from '../types'; export const symptomsStore = { async createSymptom(data: { name: string; category?: SymptomCategory; color?: string | null }) { - const newLocal: LocalCycleSymptom = { + const newLocal: LocalPeriodSymptom = { id: crypto.randomUUID(), name: data.name.trim(), category: data.category ?? 'physical', color: data.color ?? null, count: 0, }; - await cycleSymptomTable.add(newLocal); + await periodSymptomTable.add(newLocal); return newLocal; }, async updateSymptom( id: string, - data: Partial> + data: Partial> ) { - await cycleSymptomTable.update(id, { + await periodSymptomTable.update(id, { ...data, updatedAt: new Date().toISOString(), }); }, async deleteSymptom(id: string) { - await cycleSymptomTable.update(id, { + await periodSymptomTable.update(id, { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); @@ -38,10 +38,10 @@ export const symptomsStore = { /** Inkrementiert/dekrementiert die Verwendungszähler für IDs. */ async touchSymptoms(ids: string[], delta: number) { for (const id of ids) { - const existing = await cycleSymptomTable.get(id); + const existing = await periodSymptomTable.get(id); if (!existing) continue; const next = Math.max(0, (existing.count ?? 0) + delta); - await cycleSymptomTable.update(id, { + await periodSymptomTable.update(id, { count: next, updatedAt: new Date().toISOString(), }); diff --git a/apps/mana/apps/web/src/lib/modules/period/tools.ts b/apps/mana/apps/web/src/lib/modules/period/tools.ts index a672c4047..acd16386c 100644 --- a/apps/mana/apps/web/src/lib/modules/period/tools.ts +++ b/apps/mana/apps/web/src/lib/modules/period/tools.ts @@ -1,8 +1,8 @@ import type { ModuleTool } from '$lib/data/tools/types'; -export const cyclesTools: ModuleTool[] = [ +export const periodTools: ModuleTool[] = [ { - name: 'log_cycle_day', - module: 'cycles', + name: 'log_period_day', + module: 'period', description: 'Loggt einen Zyklus-Tag (Menstruationszyklus)', parameters: [ { @@ -14,8 +14,8 @@ export const cyclesTools: ModuleTool[] = [ }, ], async execute(params) { - const { cyclesStore } = await import('./stores/cycles.svelte'); - const entry = await cyclesStore.createCycle({}); + const { periodsStore } = await import('./stores/periods.svelte'); + const entry = await periodsStore.createPeriod({}); return { success: true, data: entry, message: 'Zyklus-Tag geloggt' }; }, }, diff --git a/apps/mana/apps/web/src/lib/modules/period/types.ts b/apps/mana/apps/web/src/lib/modules/period/types.ts index df50c1369..b7fd3cc50 100644 --- a/apps/mana/apps/web/src/lib/modules/period/types.ts +++ b/apps/mana/apps/web/src/lib/modules/period/types.ts @@ -1,5 +1,5 @@ /** - * Cycles module types — Menstruationszyklus-Tracking. + * Periods module types — Menstruationszyklus-Tracking. */ import type { BaseRecord } from '@mana/local-store'; @@ -8,11 +8,11 @@ export type Flow = 'none' | 'spotting' | 'light' | 'medium' | 'heavy'; export type Mood = 'great' | 'good' | 'neutral' | 'low' | 'bad'; export type CervicalMucus = 'dry' | 'sticky' | 'creamy' | 'watery' | 'eggwhite'; export type SymptomCategory = 'physical' | 'emotional' | 'other'; -export type CyclePhase = 'menstruation' | 'follicular' | 'ovulation' | 'luteal' | 'unknown'; +export type PeriodPhase = 'menstruation' | 'follicular' | 'ovulation' | 'luteal' | 'unknown'; // ─── Local Record Types (Dexie) ─────────────────────────── -export interface LocalCycle extends BaseRecord { +export interface LocalPeriod extends BaseRecord { startDate: string; // ISO YYYY-MM-DD — erster Tag der Periode periodEndDate: string | null; // letzter Tag der Blutung endDate: string | null; // Tag vor dem nächsten Zyklusstart (berechnet) @@ -23,20 +23,20 @@ export interface LocalCycle extends BaseRecord { timeBlockId?: string | null; // link to timeBlocks table (menstruation phase) } -export interface LocalCycleDayLog extends BaseRecord { +export interface LocalPeriodDayLog extends BaseRecord { logDate: string; // ISO YYYY-MM-DD - cycleId: string | null; + periodId: string | null; flow: Flow; mood: Mood | null; energy: number | null; // 1..5 temperature: number | null; // °C, BBT cervicalMucus: CervicalMucus | null; - symptoms: string[]; // cycleSymptom.id refs + symptoms: string[]; // periodSymptom.id refs sexualActivity: boolean | null; notes: string | null; } -export interface LocalCycleSymptom extends BaseRecord { +export interface LocalPeriodSymptom extends BaseRecord { name: string; category: SymptomCategory; color: string | null; @@ -45,7 +45,7 @@ export interface LocalCycleSymptom extends BaseRecord { // ─── Domain Types ───────────────────────────────────────── -export interface Cycle { +export interface Period { id: string; startDate: string; periodEndDate: string | null; @@ -58,10 +58,10 @@ export interface Cycle { updatedAt: string; } -export interface CycleDayLog { +export interface PeriodDayLog { id: string; logDate: string; - cycleId: string | null; + periodId: string | null; flow: Flow; mood: Mood | null; energy: number | null; @@ -74,7 +74,7 @@ export interface CycleDayLog { updatedAt: string; } -export interface CycleSymptom { +export interface PeriodSymptom { id: string; name: string; category: SymptomCategory; @@ -118,7 +118,7 @@ export const MOOD_COLORS: Record = { bad: '#ef4444', }; -export const PHASE_LABELS: Record = { +export const PHASE_LABELS: Record = { menstruation: 'Menstruation', follicular: 'Follikelphase', ovulation: 'Eisprung', @@ -126,7 +126,7 @@ export const PHASE_LABELS: Record = { unknown: 'Unbekannt', }; -export const PHASE_COLORS: Record = { +export const PHASE_COLORS: Record = { menstruation: '#e11d48', follicular: '#f59e0b', ovulation: '#22c55e', @@ -142,6 +142,6 @@ export const CERVICAL_MUCUS_LABELS: Record = { eggwhite: 'Eiweiß', }; -export const DEFAULT_CYCLE_LENGTH = 28; -export const DEFAULT_PERIOD_LENGTH = 5; +export const DEFAULT_PERIOD_LENGTH = 28; +export const DEFAULT_BLEEDING_DAYS = 5; export const DEFAULT_LUTEAL_LENGTH = 14; diff --git a/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.test.ts b/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.test.ts index ddf53d4ba..95124f35a 100644 --- a/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.test.ts +++ b/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.test.ts @@ -3,12 +3,12 @@ import { detectPeriodEnd, DRY_DAYS_FOR_PERIOD_END, isBleedingFlow, - MIN_GAP_FOR_NEW_CYCLE, - shouldStartNewCycle, + MIN_GAP_FOR_NEW_PERIOD, + shouldStartNewPeriod, } from './auto-detect'; -import type { Cycle, CycleDayLog, Flow } from '../types'; +import type { Period, PeriodDayLog, Flow } from '../types'; -function makeCycle(overrides: Partial): Cycle { +function makePeriod(overrides: Partial): Period { return { id: 'c', startDate: '2026-01-01', @@ -24,11 +24,11 @@ function makeCycle(overrides: Partial): Cycle { }; } -function makeLog(logDate: string, flow: Flow): CycleDayLog { +function makeLog(logDate: string, flow: Flow): PeriodDayLog { return { id: `log-${logDate}`, logDate, - cycleId: 'c', + periodId: 'c', flow, mood: null, energy: null, @@ -54,76 +54,76 @@ describe('isBleedingFlow', () => { }); }); -describe('shouldStartNewCycle', () => { +describe('shouldStartNewPeriod', () => { it('returns false for non-bleeding flow', () => { - expect(shouldStartNewCycle('2026-04-07', 'none', [])).toBe(false); - expect(shouldStartNewCycle('2026-04-07', 'spotting', [])).toBe(false); + expect(shouldStartNewPeriod('2026-04-07', 'none', [])).toBe(false); + expect(shouldStartNewPeriod('2026-04-07', 'spotting', [])).toBe(false); }); - it('returns true with no existing cycles and bleeding flow', () => { - expect(shouldStartNewCycle('2026-04-07', 'medium', [])).toBe(true); + it('returns true with no existing periods and bleeding flow', () => { + expect(shouldStartNewPeriod('2026-04-07', 'medium', [])).toBe(true); }); - it('returns false when current cycle is still open (no periodEndDate)', () => { - const cycles = [makeCycle({ startDate: '2026-04-01' })]; - // flow during the open period — not a new cycle - expect(shouldStartNewCycle('2026-04-03', 'heavy', cycles)).toBe(false); + it('returns false when current period is still open (no periodEndDate)', () => { + const periods = [makePeriod({ startDate: '2026-04-01' })]; + // flow during the open period — not a new period + expect(shouldStartNewPeriod('2026-04-03', 'heavy', periods)).toBe(false); }); it('returns false when bleed is too soon after period end', () => { - const cycles = [makeCycle({ startDate: '2026-04-01', periodEndDate: '2026-04-05' })]; - // 9 days after periodEndDate — too soon, probably mid-cycle bleeding - expect(shouldStartNewCycle('2026-04-14', 'medium', cycles)).toBe(false); + const periods = [makePeriod({ startDate: '2026-04-01', periodEndDate: '2026-04-05' })]; + // 9 days after periodEndDate — too soon, probably mid-period bleeding + expect(shouldStartNewPeriod('2026-04-14', 'medium', periods)).toBe(false); }); it('returns true when bleed is at least MIN_GAP days after period end', () => { - const cycles = [makeCycle({ startDate: '2026-04-01', periodEndDate: '2026-04-05' })]; + const periods = [makePeriod({ startDate: '2026-04-01', periodEndDate: '2026-04-05' })]; const newDate = '2026-04-15'; // 10 days after - expect(daysGapForTest(newDate, '2026-04-05')).toBe(MIN_GAP_FOR_NEW_CYCLE); - expect(shouldStartNewCycle(newDate, 'medium', cycles)).toBe(true); + expect(daysGapForTest(newDate, '2026-04-05')).toBe(MIN_GAP_FOR_NEW_PERIOD); + expect(shouldStartNewPeriod(newDate, 'medium', periods)).toBe(true); }); - it('ignores predicted cycles', () => { - const cycles = [ - makeCycle({ id: 'real', startDate: '2026-01-01', periodEndDate: '2026-01-05' }), - makeCycle({ id: 'pred', startDate: '2026-04-01', isPredicted: true }), + it('ignores predicted periods', () => { + const periods = [ + makePeriod({ id: 'real', startDate: '2026-01-01', periodEndDate: '2026-01-05' }), + makePeriod({ id: 'pred', startDate: '2026-04-01', isPredicted: true }), ]; - // 2026-04-15 → with the real cycle far in the past, should start new - expect(shouldStartNewCycle('2026-04-15', 'medium', cycles)).toBe(true); + // 2026-04-15 → with the real period far in the past, should start new + expect(shouldStartNewPeriod('2026-04-15', 'medium', periods)).toBe(true); }); - it('returns false for date before the latest cycle', () => { - const cycles = [makeCycle({ startDate: '2026-04-01', periodEndDate: '2026-04-05' })]; + it('returns false for date before the latest period', () => { + const periods = [makePeriod({ startDate: '2026-04-01', periodEndDate: '2026-04-05' })]; // Backfilling an old date should never auto-create - expect(shouldStartNewCycle('2026-03-10', 'medium', cycles)).toBe(false); + expect(shouldStartNewPeriod('2026-03-10', 'medium', periods)).toBe(false); }); }); describe('detectPeriodEnd', () => { - const openCycle = makeCycle({ id: 'c', startDate: '2026-04-01' }); + const openPeriod = makePeriod({ id: 'c', startDate: '2026-04-01' }); it('returns null for non-none flow', () => { - expect(detectPeriodEnd('2026-04-07', 'light', openCycle, [])).toBeNull(); + expect(detectPeriodEnd('2026-04-07', 'light', openPeriod, [])).toBeNull(); }); - it('returns null without an open cycle', () => { + it('returns null without an open period', () => { expect(detectPeriodEnd('2026-04-07', 'none', null, [])).toBeNull(); }); - it('returns null when cycle already has periodEndDate', () => { - const closed = makeCycle({ id: 'c', startDate: '2026-04-01', periodEndDate: '2026-04-05' }); + it('returns null when period already has periodEndDate', () => { + const closed = makePeriod({ id: 'c', startDate: '2026-04-01', periodEndDate: '2026-04-05' }); expect(detectPeriodEnd('2026-04-07', 'none', closed, [])).toBeNull(); }); - it('returns null when no bleeding day exists in cycle', () => { + it('returns null when no bleeding day exists in period', () => { const logs = [makeLog('2026-04-01', 'none'), makeLog('2026-04-02', 'none')]; - expect(detectPeriodEnd('2026-04-07', 'none', openCycle, logs)).toBeNull(); + expect(detectPeriodEnd('2026-04-07', 'none', openPeriod, logs)).toBeNull(); }); it('returns null when not enough dry days have passed', () => { const logs = [makeLog('2026-04-04', 'medium')]; // logDate = 2026-04-05 → only 1 day after bleeding - expect(detectPeriodEnd('2026-04-05', 'none', openCycle, logs)).toBeNull(); + expect(detectPeriodEnd('2026-04-05', 'none', openPeriod, logs)).toBeNull(); }); it('returns lastBleedingDay after DRY_DAYS_FOR_PERIOD_END', () => { @@ -135,7 +135,7 @@ describe('detectPeriodEnd', () => { ]; // logDate = 2026-04-06 → 2 days after last bleeding (04-04) expect(daysGapForTest('2026-04-06', '2026-04-04')).toBe(DRY_DAYS_FOR_PERIOD_END); - expect(detectPeriodEnd('2026-04-06', 'none', openCycle, logs)).toBe('2026-04-04'); + expect(detectPeriodEnd('2026-04-06', 'none', openPeriod, logs)).toBe('2026-04-04'); }); it('uses the LAST bleeding day, not the first', () => { @@ -144,7 +144,7 @@ describe('detectPeriodEnd', () => { makeLog('2026-04-02', 'medium'), makeLog('2026-04-03', 'light'), ]; - expect(detectPeriodEnd('2026-04-05', 'none', openCycle, logs)).toBe('2026-04-03'); + expect(detectPeriodEnd('2026-04-05', 'none', openPeriod, logs)).toBe('2026-04-03'); }); it('ignores logs after the current logDate (chronology safe)', () => { @@ -155,7 +155,7 @@ describe('detectPeriodEnd', () => { makeLog('2026-04-10', 'medium'), // future log shouldn't affect detection for 04-03 ]; // 2026-04-03 - 2026-04-02 = 1 day → not enough - expect(detectPeriodEnd('2026-04-03', 'none', openCycle, logs)).toBeNull(); + expect(detectPeriodEnd('2026-04-03', 'none', openPeriod, logs)).toBeNull(); }); it('handles spotting as not bleeding (so spotting is not lastBleedingDay)', () => { @@ -164,7 +164,7 @@ describe('detectPeriodEnd', () => { makeLog('2026-04-02', 'spotting'), // not counted as bleeding ]; // 2026-04-03 - 2026-04-01 = 2 → trigger, lastBleedingDay = 04-01 - expect(detectPeriodEnd('2026-04-03', 'none', openCycle, logs)).toBe('2026-04-01'); + expect(detectPeriodEnd('2026-04-03', 'none', openPeriod, logs)).toBe('2026-04-01'); }); }); diff --git a/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.ts b/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.ts index 805cbd702..f2c86163c 100644 --- a/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.ts +++ b/apps/mana/apps/web/src/lib/modules/period/utils/auto-detect.ts @@ -9,7 +9,7 @@ * periodEndDate auf den letzten Bleeding-Tag. */ -import type { Cycle, CycleDayLog, Flow } from '../types'; +import type { Period, PeriodDayLog, Flow } from '../types'; import { daysBetween } from './phase'; /** Welche Flow-Werte zählen als "Blutung" (= Periode)? */ @@ -18,7 +18,7 @@ export function isBleedingFlow(flow: Flow): boolean { } /** Mindestabstand (Tage) zwischen Ende einer Periode und Start eines neuen Zyklus. */ -export const MIN_GAP_FOR_NEW_CYCLE = 10; +export const MIN_GAP_FOR_NEW_PERIOD = 10; /** Wieviele zusammenhängende trockene Tage nach Bleeding für Period-End-Detection. */ export const DRY_DAYS_FOR_PERIOD_END = 2; @@ -28,14 +28,14 @@ export const DRY_DAYS_FOR_PERIOD_END = 2; * Ja, wenn: * - flow ist eine echte Blutung (nicht none/spotting), UND * - es gibt keinen Zyklus, ODER der letzte Zyklus hat eine periodEndDate UND - * logDate liegt mindestens MIN_GAP_FOR_NEW_CYCLE Tage danach. + * logDate liegt mindestens MIN_GAP_FOR_NEW_PERIOD Tage danach. * * Verhindert false positives für Tage innerhalb eines bestehenden Zyklus. */ -export function shouldStartNewCycle(logDate: string, flow: Flow, cycles: Cycle[]): boolean { +export function shouldStartNewPeriod(logDate: string, flow: Flow, periods: Period[]): boolean { if (!isBleedingFlow(flow)) return false; - const real = cycles.filter((c) => !c.isPredicted && !c.isArchived); + const real = periods.filter((c) => !c.isPredicted && !c.isArchived); if (real.length === 0) return true; const latest = [...real].sort((a, b) => b.startDate.localeCompare(a.startDate))[0]; @@ -43,11 +43,11 @@ export function shouldStartNewCycle(logDate: string, flow: Flow, cycles: Cycle[] // logDate vor dem letzten Zyklus → wir bauen keinen "vergangenen" Zyklus auto if (logDate < latest.startDate) return false; - // Aktueller Zyklus läuft noch — Blutung gehört dazu (Mid-Cycle-Spotting o.ä.) + // Aktueller Zyklus läuft noch — Blutung gehört dazu (Mid-Period-Spotting o.ä.) if (!latest.periodEndDate) return false; // Aktueller Zyklus ist abgeschlossen → wenn genug Abstand, ist das eine neue Periode - return daysBetween(logDate, latest.periodEndDate) >= MIN_GAP_FOR_NEW_CYCLE; + return daysBetween(logDate, latest.periodEndDate) >= MIN_GAP_FOR_NEW_PERIOD; } /** @@ -65,14 +65,14 @@ export function shouldStartNewCycle(logDate: string, flow: Flow, cycles: Cycle[] export function detectPeriodEnd( logDate: string, flow: Flow, - openCycle: Cycle | null, - logsInCycle: CycleDayLog[] + openPeriod: Period | null, + logsInPeriod: PeriodDayLog[] ): string | null { if (flow !== 'none') return null; - if (!openCycle || openCycle.periodEndDate) return null; + if (!openPeriod || openPeriod.periodEndDate) return null; // Tage des Zyklus nach Datum sortieren, alle bis einschließlich logDate - const sorted = [...logsInCycle] + const sorted = [...logsInPeriod] .filter((l) => l.logDate <= logDate) .sort((a, b) => a.logDate.localeCompare(b.logDate)); diff --git a/apps/mana/apps/web/src/lib/modules/period/utils/phase.test.ts b/apps/mana/apps/web/src/lib/modules/period/utils/phase.test.ts index 2481fca14..4f99700fe 100644 --- a/apps/mana/apps/web/src/lib/modules/period/utils/phase.test.ts +++ b/apps/mana/apps/web/src/lib/modules/period/utils/phase.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { daysBetween, derivePhase, findCycleForDate, getCycleDayNumber } from './phase'; -import type { Cycle } from '../types'; +import { daysBetween, derivePhase, findPeriodForDate, getPeriodDayNumber } from './phase'; +import type { Period } from '../types'; -function makeCycle(overrides: Partial): Cycle { +function makePeriod(overrides: Partial): Period { return { id: 'c', startDate: '2026-01-01', @@ -33,43 +33,43 @@ describe('daysBetween', () => { }); }); -describe('findCycleForDate', () => { - const cycles: Cycle[] = [ - makeCycle({ id: 'c1', startDate: '2026-01-01' }), - makeCycle({ id: 'c2', startDate: '2026-01-29' }), - makeCycle({ id: 'c3', startDate: '2026-02-26' }), +describe('findPeriodForDate', () => { + const periods: Period[] = [ + makePeriod({ id: 'c1', startDate: '2026-01-01' }), + makePeriod({ id: 'c2', startDate: '2026-01-29' }), + makePeriod({ id: 'c3', startDate: '2026-02-26' }), ]; - it('returns null for date before any cycle', () => { - expect(findCycleForDate('2025-12-31', cycles)).toBeNull(); + it('returns null for date before any period', () => { + expect(findPeriodForDate('2025-12-31', periods)).toBeNull(); }); - it('finds the latest cycle whose startDate <= date', () => { - expect(findCycleForDate('2026-02-15', cycles)?.id).toBe('c2'); + it('finds the latest period whose startDate <= date', () => { + expect(findPeriodForDate('2026-02-15', periods)?.id).toBe('c2'); }); it('matches exact start date', () => { - expect(findCycleForDate('2026-02-26', cycles)?.id).toBe('c3'); + expect(findPeriodForDate('2026-02-26', periods)?.id).toBe('c3'); }); it('returns most recent for late date', () => { - expect(findCycleForDate('2026-12-31', cycles)?.id).toBe('c3'); + expect(findPeriodForDate('2026-12-31', periods)?.id).toBe('c3'); }); }); -describe('getCycleDayNumber', () => { - const cycle = makeCycle({ startDate: '2026-04-01' }); +describe('getPeriodDayNumber', () => { + const period = makePeriod({ startDate: '2026-04-01' }); it('returns 1 on the start date', () => { - expect(getCycleDayNumber('2026-04-01', cycle)).toBe(1); + expect(getPeriodDayNumber('2026-04-01', period)).toBe(1); }); it('returns N+1 N days after start', () => { - expect(getCycleDayNumber('2026-04-08', cycle)).toBe(8); + expect(getPeriodDayNumber('2026-04-08', period)).toBe(8); }); - it('returns null before the cycle', () => { - expect(getCycleDayNumber('2026-03-30', cycle)).toBeNull(); + it('returns null before the period', () => { + expect(getPeriodDayNumber('2026-03-30', period)).toBeNull(); }); }); describe('derivePhase', () => { - const cycles: Cycle[] = [ - makeCycle({ + const periods: Period[] = [ + makePeriod({ id: 'c1', startDate: '2026-04-01', periodEndDate: '2026-04-05', // 5 days of period @@ -77,31 +77,31 @@ describe('derivePhase', () => { }), ]; - it('returns unknown when no cycle covers the date', () => { - expect(derivePhase('2025-12-31', cycles)).toBe('unknown'); + it('returns unknown when no period covers the date', () => { + expect(derivePhase('2025-12-31', periods)).toBe('unknown'); }); it('returns menstruation on day 1', () => { - expect(derivePhase('2026-04-01', cycles)).toBe('menstruation'); + expect(derivePhase('2026-04-01', periods)).toBe('menstruation'); }); it('returns menstruation on the last bleeding day', () => { - expect(derivePhase('2026-04-05', cycles)).toBe('menstruation'); + expect(derivePhase('2026-04-05', periods)).toBe('menstruation'); }); it('returns follicular after period before ovulation', () => { // day 8, ovulation should be day 14 (28 - 14) - expect(derivePhase('2026-04-08', cycles)).toBe('follicular'); + expect(derivePhase('2026-04-08', periods)).toBe('follicular'); }); it('returns ovulation around day 14 (±1)', () => { // 2026-04-14 = day 14 - expect(derivePhase('2026-04-14', cycles)).toBe('ovulation'); - expect(derivePhase('2026-04-13', cycles)).toBe('ovulation'); - expect(derivePhase('2026-04-15', cycles)).toBe('ovulation'); + expect(derivePhase('2026-04-14', periods)).toBe('ovulation'); + expect(derivePhase('2026-04-13', periods)).toBe('ovulation'); + expect(derivePhase('2026-04-15', periods)).toBe('ovulation'); }); it('returns luteal after ovulation', () => { // day 20 - expect(derivePhase('2026-04-20', cycles)).toBe('luteal'); + expect(derivePhase('2026-04-20', periods)).toBe('luteal'); }); it('falls back to default period length when periodEndDate missing', () => { - const c: Cycle[] = [makeCycle({ startDate: '2026-04-01', length: 28 })]; + const c: Period[] = [makePeriod({ startDate: '2026-04-01', length: 28 })]; // default period length = 5, so day 5 is still menstruation expect(derivePhase('2026-04-05', c)).toBe('menstruation'); expect(derivePhase('2026-04-06', c)).toBe('follicular'); diff --git a/apps/mana/apps/web/src/lib/modules/period/utils/phase.ts b/apps/mana/apps/web/src/lib/modules/period/utils/phase.ts index 0cbf98771..f9ad4ca28 100644 --- a/apps/mana/apps/web/src/lib/modules/period/utils/phase.ts +++ b/apps/mana/apps/web/src/lib/modules/period/utils/phase.ts @@ -3,11 +3,11 @@ */ import { - DEFAULT_CYCLE_LENGTH, - DEFAULT_LUTEAL_LENGTH, DEFAULT_PERIOD_LENGTH, - type Cycle, - type CyclePhase, + DEFAULT_LUTEAL_LENGTH, + DEFAULT_BLEEDING_DAYS, + type Period, + type PeriodPhase, } from '../types'; /** Tage zwischen zwei ISO-Daten (a - b) */ @@ -16,10 +16,10 @@ export function daysBetween(a: string, b: string): number { return Math.round(ms / 86_400_000); } -/** Findet den Zyklus, der das gegebene Datum enthält. Cycles müssen nach startDate sortiert sein. */ -export function findCycleForDate(date: string, cycles: Cycle[]): Cycle | null { - const sorted = [...cycles].sort((a, b) => a.startDate.localeCompare(b.startDate)); - let match: Cycle | null = null; +/** Findet den Zyklus, der das gegebene Datum enthält. Periods müssen nach startDate sortiert sein. */ +export function findPeriodForDate(date: string, periods: Period[]): Period | null { + const sorted = [...periods].sort((a, b) => a.startDate.localeCompare(b.startDate)); + let match: Period | null = null; for (const c of sorted) { if (c.startDate <= date) match = c; else break; @@ -28,8 +28,8 @@ export function findCycleForDate(date: string, cycles: Cycle[]): Cycle | null { } /** Tag-Nummer innerhalb des Zyklus (Tag 1 = startDate). null wenn date vor dem Zyklus liegt. */ -export function getCycleDayNumber(date: string, cycle: Cycle): number | null { - const diff = daysBetween(date, cycle.startDate); +export function getPeriodDayNumber(date: string, period: Period): number | null { + const diff = daysBetween(date, period.startDate); if (diff < 0) return null; return diff + 1; } @@ -39,29 +39,29 @@ export function getCycleDayNumber(date: string, cycle: Cycle): number | null { * * Heuristik: * - Periode: Tag 1..periodLength - * - Eisprung: cycleLength - lutealLength (±1 Tag) + * - Eisprung: periodLength - lutealLength (±1 Tag) * - Vorher = Follikelphase, danach = Lutealphase */ export function derivePhase( date: string, - cycles: Cycle[], - avgCycleLength = DEFAULT_CYCLE_LENGTH -): CyclePhase { - const cycle = findCycleForDate(date, cycles); - if (!cycle) return 'unknown'; + periods: Period[], + avgPeriodLength = DEFAULT_PERIOD_LENGTH +): PeriodPhase { + const period = findPeriodForDate(date, periods); + if (!period) return 'unknown'; - const dayNum = getCycleDayNumber(date, cycle); + const dayNum = getPeriodDayNumber(date, period); if (dayNum === null) return 'unknown'; - const periodLength = - cycle.periodEndDate && cycle.periodEndDate >= cycle.startDate - ? daysBetween(cycle.periodEndDate, cycle.startDate) + 1 - : DEFAULT_PERIOD_LENGTH; + const bleedingLength = + period.periodEndDate && period.periodEndDate >= period.startDate + ? daysBetween(period.periodEndDate, period.startDate) + 1 + : DEFAULT_BLEEDING_DAYS; - const cycleLength = cycle.length ?? avgCycleLength; - const ovulationDay = cycleLength - DEFAULT_LUTEAL_LENGTH; + const periodLength = period.length ?? avgPeriodLength; + const ovulationDay = periodLength - DEFAULT_LUTEAL_LENGTH; - if (dayNum <= periodLength) return 'menstruation'; + if (dayNum <= bleedingLength) return 'menstruation'; if (Math.abs(dayNum - ovulationDay) <= 1) return 'ovulation'; if (dayNum < ovulationDay) return 'follicular'; return 'luteal'; diff --git a/apps/mana/apps/web/src/lib/modules/period/utils/prediction.test.ts b/apps/mana/apps/web/src/lib/modules/period/utils/prediction.test.ts index c36498e74..68cd851c2 100644 --- a/apps/mana/apps/web/src/lib/modules/period/utils/prediction.test.ts +++ b/apps/mana/apps/web/src/lib/modules/period/utils/prediction.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from 'vitest'; import { - averageCycleLength, - computeCycleStats, + averagePeriodLength, + computePeriodStats, daysUntilNextPeriod, predictFertileWindow, predictNextPeriodStart, } from './prediction'; -import type { Cycle } from '../types'; +import type { Period } from '../types'; -function cycle(startDate: string, length: number | null = null, isPredicted = false): Cycle { +function period(startDate: string, length: number | null = null, isPredicted = false): Period { return { id: `c-${startDate}`, startDate, @@ -23,50 +23,52 @@ function cycle(startDate: string, length: number | null = null, isPredicted = fa }; } -describe('averageCycleLength', () => { - it('returns default 28 when no cycles', () => { - expect(averageCycleLength([])).toBe(28); +describe('averagePeriodLength', () => { + it('returns default 28 when no periods', () => { + expect(averagePeriodLength([])).toBe(28); }); - it('returns default when no closed cycles (no length)', () => { - expect(averageCycleLength([cycle('2026-01-01')])).toBe(28); + it('returns default when no closed periods (no length)', () => { + expect(averagePeriodLength([period('2026-01-01')])).toBe(28); }); - it('averages closed cycle lengths', () => { + it('averages closed period lengths', () => { expect( - averageCycleLength([ - cycle('2026-01-01', 28), - cycle('2026-02-01', 30), - cycle('2026-03-01', 26), + averagePeriodLength([ + period('2026-01-01', 28), + period('2026-02-01', 30), + period('2026-03-01', 26), ]) ).toBe(28); }); - it('caps to most recent N cycles', () => { - // 7 cycles, but window=6 — oldest (length 100) should be ignored + it('caps to most recent N periods', () => { + // 7 periods, but window=6 — oldest (length 100) should be ignored const c = [ - cycle('2026-01-01', 100), - cycle('2026-02-01', 28), - cycle('2026-03-01', 28), - cycle('2026-04-01', 28), - cycle('2026-05-01', 28), - cycle('2026-06-01', 28), - cycle('2026-07-01', 28), + period('2026-01-01', 100), + period('2026-02-01', 28), + period('2026-03-01', 28), + period('2026-04-01', 28), + period('2026-05-01', 28), + period('2026-06-01', 28), + period('2026-07-01', 28), ]; - expect(averageCycleLength(c, 6)).toBe(28); + expect(averagePeriodLength(c, 6)).toBe(28); }); - it('ignores predicted cycles', () => { - expect(averageCycleLength([cycle('2026-01-01', 28), cycle('2026-02-01', 100, true)])).toBe(28); + it('ignores predicted periods', () => { + expect(averagePeriodLength([period('2026-01-01', 28), period('2026-02-01', 100, true)])).toBe( + 28 + ); }); }); describe('predictNextPeriodStart', () => { - it('returns null with no cycles', () => { + it('returns null with no periods', () => { expect(predictNextPeriodStart([])).toBeNull(); }); it('predicts based on latest start + average length', () => { - const c = [cycle('2026-01-01', 28), cycle('2026-01-29', 28)]; + const c = [period('2026-01-01', 28), period('2026-01-29', 28)]; expect(predictNextPeriodStart(c)).toBe('2026-02-26'); }); - it('uses default length when no closed cycles exist', () => { - const c = [cycle('2026-01-01')]; + it('uses default length when no closed periods exist', () => { + const c = [period('2026-01-01')]; // 2026-01-01 + 28 days = 2026-01-29 expect(predictNextPeriodStart(c)).toBe('2026-01-29'); }); @@ -78,9 +80,9 @@ describe('daysUntilNextPeriod', () => { }); it('returns positive count when prediction is in the future', () => { const todayIso = new Date().toISOString().slice(0, 10); - // Create a cycle that started 14 days ago (default length 28 → next in 14 days) + // Create a period that started 14 days ago (default length 28 → next in 14 days) const start = new Date(Date.now() - 14 * 86_400_000).toISOString().slice(0, 10); - const result = daysUntilNextPeriod([cycle(start)]); + const result = daysUntilNextPeriod([period(start)]); expect(result).toBeGreaterThanOrEqual(13); expect(result).toBeLessThanOrEqual(15); expect(todayIso).toBeTruthy(); @@ -92,8 +94,8 @@ describe('predictFertileWindow', () => { expect(predictFertileWindow([])).toBeNull(); }); it('predicts a 7-day window centred near ovulation', () => { - // Default 28 day cycle, luteal=14, so ovulation = day 14 (= start + 13 days) - const c = [cycle('2026-04-01', 28)]; + // Default 28 day period, luteal=14, so ovulation = day 14 (= start + 13 days) + const c = [period('2026-04-01', 28)]; const window = predictFertileWindow(c); expect(window).not.toBeNull(); // ovulationDay = 28 - 14 = 14, so start = startDate + (14 - 5) = +9 days = 2026-04-10 @@ -103,12 +105,12 @@ describe('predictFertileWindow', () => { }); }); -describe('computeCycleStats', () => { +describe('computePeriodStats', () => { it('returns zeros for empty input', () => { - expect(computeCycleStats([])).toEqual({ total: 0, avg: 0, shortest: 0, longest: 0 }); + expect(computePeriodStats([])).toEqual({ total: 0, avg: 0, shortest: 0, longest: 0 }); }); - it('ignores cycles with no length', () => { - expect(computeCycleStats([cycle('2026-01-01')])).toEqual({ + it('ignores periods with no length', () => { + expect(computePeriodStats([period('2026-01-01')])).toEqual({ total: 0, avg: 0, shortest: 0, @@ -116,7 +118,7 @@ describe('computeCycleStats', () => { }); }); it('computes avg/min/max correctly', () => { - const c = [cycle('2026-01-01', 26), cycle('2026-02-01', 28), cycle('2026-03-01', 30)]; - expect(computeCycleStats(c)).toEqual({ total: 3, avg: 28, shortest: 26, longest: 30 }); + const c = [period('2026-01-01', 26), period('2026-02-01', 28), period('2026-03-01', 30)]; + expect(computePeriodStats(c)).toEqual({ total: 3, avg: 28, shortest: 26, longest: 30 }); }); }); diff --git a/apps/mana/apps/web/src/lib/modules/period/utils/prediction.ts b/apps/mana/apps/web/src/lib/modules/period/utils/prediction.ts index 0318be9f7..8908b4313 100644 --- a/apps/mana/apps/web/src/lib/modules/period/utils/prediction.ts +++ b/apps/mana/apps/web/src/lib/modules/period/utils/prediction.ts @@ -2,44 +2,44 @@ * Prediction — einfache Vorhersagen über gleitenden Mittelwert. */ -import { DEFAULT_CYCLE_LENGTH, DEFAULT_LUTEAL_LENGTH, type Cycle } from '../types'; +import { DEFAULT_PERIOD_LENGTH, DEFAULT_LUTEAL_LENGTH, type Period } from '../types'; import { daysBetween } from './phase'; /** Durchschnittliche Zykluslänge aus den letzten N geschlossenen Zyklen. */ -export function averageCycleLength(cycles: Cycle[], window = 6): number { - const closed = cycles +export function averagePeriodLength(periods: Period[], window = 6): number { + const closed = periods .filter((c) => !c.isPredicted && typeof c.length === 'number' && (c.length ?? 0) > 0) .sort((a, b) => b.startDate.localeCompare(a.startDate)) .slice(0, window); - if (closed.length === 0) return DEFAULT_CYCLE_LENGTH; + if (closed.length === 0) return DEFAULT_PERIOD_LENGTH; const sum = closed.reduce((acc, c) => acc + (c.length ?? 0), 0); return Math.round(sum / closed.length); } /** Vorhergesagter Start der nächsten Periode (ISO-Date). */ -export function predictNextPeriodStart(cycles: Cycle[]): string | null { - const real = cycles.filter((c) => !c.isPredicted); +export function predictNextPeriodStart(periods: Period[]): string | null { + const real = periods.filter((c) => !c.isPredicted); if (real.length === 0) return null; const latest = real.sort((a, b) => b.startDate.localeCompare(a.startDate))[0]; - const avg = averageCycleLength(real); + const avg = averagePeriodLength(real); const start = new Date(latest.startDate); start.setUTCDate(start.getUTCDate() + avg); return start.toISOString().slice(0, 10); } /** Tage bis zur nächsten Periode. Negativ = überfällig. null wenn keine Daten. */ -export function daysUntilNextPeriod(cycles: Cycle[]): number | null { - const next = predictNextPeriodStart(cycles); +export function daysUntilNextPeriod(periods: Period[]): number | null { + const next = predictNextPeriodStart(periods); if (!next) return null; return daysBetween(next, new Date().toISOString().slice(0, 10)); } /** Fruchtbares Fenster für den aktuellen Zyklus (5 Tage vor + Eisprung). */ -export function predictFertileWindow(cycles: Cycle[]): { start: string; end: string } | null { - const real = cycles.filter((c) => !c.isPredicted); +export function predictFertileWindow(periods: Period[]): { start: string; end: string } | null { + const real = periods.filter((c) => !c.isPredicted); if (real.length === 0) return null; const latest = real.sort((a, b) => b.startDate.localeCompare(a.startDate))[0]; - const avg = averageCycleLength(real); + const avg = averagePeriodLength(real); const ovulationDay = avg - DEFAULT_LUTEAL_LENGTH; const start = new Date(latest.startDate); start.setUTCDate(start.getUTCDate() + ovulationDay - 5); @@ -52,8 +52,8 @@ export function predictFertileWindow(cycles: Cycle[]): { start: string; end: str } /** Statistik-Snapshot über alle echten Zyklen. */ -export function computeCycleStats(cycles: Cycle[]) { - const real = cycles.filter((c) => !c.isPredicted && typeof c.length === 'number'); +export function computePeriodStats(periods: Period[]) { + const real = periods.filter((c) => !c.isPredicted && typeof c.length === 'number'); const lengths = real.map((c) => c.length as number); const total = real.length; const avg = lengths.length ? Math.round(lengths.reduce((a, b) => a + b, 0) / lengths.length) : 0; diff --git a/apps/mana/apps/web/src/lib/modules/plants/mutations.test.ts b/apps/mana/apps/web/src/lib/modules/plants/mutations.test.ts index 7b0624824..75ac092a6 100644 --- a/apps/mana/apps/web/src/lib/modules/plants/mutations.test.ts +++ b/apps/mana/apps/web/src/lib/modules/plants/mutations.test.ts @@ -19,7 +19,7 @@ vi.mock('@mana/shared-utils/analytics', () => ({ // Database hooks call into funnel-tracking + trigger registry on every // write. They reach for browser-only globals (localStorage), so stub them -// the same way cycles.integration.test.ts does. +// the same way period.integration.test.ts does. vi.mock('$lib/stores/funnel-tracking', () => ({ trackFirstContent: vi.fn() })); vi.mock('$lib/triggers/registry', () => ({ fire: vi.fn() })); vi.mock('$lib/triggers/inline-suggest', () => ({ diff --git a/apps/mana/apps/web/src/lib/modules/playground/llm.ts b/apps/mana/apps/web/src/lib/modules/playground/llm.ts index a4fd84327..f6df7558f 100644 --- a/apps/mana/apps/web/src/lib/modules/playground/llm.ts +++ b/apps/mana/apps/web/src/lib/modules/playground/llm.ts @@ -4,7 +4,7 @@ * * Lives next to the playground UI rather than in a shared package because * the playground is the only consumer right now. If chat / todo enrichment - * / cycles insights end up calling the same surface in the future, lift + * / period insights end up calling the same surface in the future, lift * this into `$lib/data/llm-client.ts`. * * The chunk parser is hand-rolled rather than pulled from a library: the diff --git a/apps/mana/apps/web/src/lib/types/dashboard.test.ts b/apps/mana/apps/web/src/lib/types/dashboard.test.ts index 5fa9284da..8ad480457 100644 --- a/apps/mana/apps/web/src/lib/types/dashboard.test.ts +++ b/apps/mana/apps/web/src/lib/types/dashboard.test.ts @@ -55,7 +55,7 @@ describe('WIDGET_REGISTRY', () => { 'mana-auth', 'food', 'plants', - 'cycles', + 'period', undefined, ]; for (const widget of WIDGET_REGISTRY) { diff --git a/apps/mana/apps/web/src/lib/types/dashboard.ts b/apps/mana/apps/web/src/lib/types/dashboard.ts index 8c05b6d5c..2a8177034 100644 --- a/apps/mana/apps/web/src/lib/types/dashboard.ts +++ b/apps/mana/apps/web/src/lib/types/dashboard.ts @@ -30,7 +30,7 @@ export type WidgetType = | 'plant-watering' // Plants: plants due for watering | 'day-timeline' // TimeBlocks: chronological day timeline | 'activity-feed' // TimeBlocks: recent activity across modules - | 'cycles' // Cycles: current phase + days until next period + | 'period' // Period: current phase + days until next period | 'news-unread' // News: latest unread curated articles | 'body-stats'; // Body: latest weight + active workout summary @@ -132,7 +132,7 @@ export interface WidgetMeta { | 'times' | 'food' | 'plants' - | 'cycles' + | 'period' | 'body' | 'mana-auth'; } @@ -337,13 +337,13 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [ allowMultiple: false, }, { - type: 'cycles', - nameKey: 'dashboard.widgets.cycles.title', - descriptionKey: 'dashboard.widgets.cycles.description', + type: 'period', + nameKey: 'dashboard.widgets.period.title', + descriptionKey: 'dashboard.widgets.period.description', icon: '🌸', defaultSize: 'small', allowMultiple: false, - requiredBackend: 'cycles', + requiredBackend: 'period', }, { type: 'news-unread', diff --git a/apps/mana/apps/web/src/routes/(app)/admin/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/admin/+layout.svelte index bafc311be..006b709ed 100644 --- a/apps/mana/apps/web/src/routes/(app)/admin/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/admin/+layout.svelte @@ -20,6 +20,7 @@ { href: '/admin/users', label: 'Users', icon: 'users' }, { href: '/admin/user-data', label: 'User Data', icon: 'database' }, { href: '/admin/system', label: 'System', icon: 'server' }, + { href: '/admin/complexity', label: 'Complexity', icon: 'chart' }, ]; const icons: Record = { @@ -27,6 +28,7 @@ users: ``, database: ``, server: ``, + chart: ``, }; function isActive(href: string, pathname: string): boolean { diff --git a/apps/mana/apps/web/src/routes/(app)/period/+page.svelte b/apps/mana/apps/web/src/routes/(app)/period/+page.svelte index cfa133287..a28997f5c 100644 --- a/apps/mana/apps/web/src/routes/(app)/period/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/period/+page.svelte @@ -1,9 +1,9 @@ - Cycles - Mana + Period - Mana {}} goBack={() => history.back()} params={{}} /> diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml index c9d82d39a..81d8812d9 100644 --- a/docker/prometheus/prometheus.yml +++ b/docker/prometheus/prometheus.yml @@ -247,7 +247,7 @@ scrape_configs: - https://mana.how/journal - https://mana.how/dreams - https://mana.how/firsts - - https://mana.how/cycles + - https://mana.how/period - https://mana.how/events - https://mana.how/finance - https://mana.how/places diff --git a/docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md b/docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md index b1a26156b..c9ea3fef5 100644 --- a/docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md +++ b/docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md @@ -1394,7 +1394,7 @@ Phase 1 (Events) ──────┬──> Phase 2 (Projections) | 18 | Chat | 2 | 1 | Batch 4 | | 19 | Memoro | 1 | 1 | Batch 4 | | 20 | Skilltree | 2 | 2 | Batch 4 | -| 21 | Cycles | 1 | 1 | Batch 5 | +| 21 | Period | 1 | 1 | Batch 5 | | 22 | Firsts | 1 | 1 | Batch 5 | | 23 | Guides | 1 | 1 | Batch 5 | | 24 | Inventory | 1 | 1 | Batch 5 | diff --git a/docs/future/MODULE_IDEAS.md b/docs/future/MODULE_IDEAS.md index 8a74fd273..1d14da4e9 100644 --- a/docs/future/MODULE_IDEAS.md +++ b/docs/future/MODULE_IDEAS.md @@ -19,7 +19,7 @@ recommendation. **Productivity:** todo, calendar, contacts, notes, habits, times, timeblocks, events **Knowledge & learning:** cards, zitare, guides, questions, skilltree, memoro, context -**Health & self:** food, cycles, dreams, moodlit, plants +**Health & self:** food, period, dreams, moodlit, plants **Media & creative:** chat, picture, presi, music, photos, storage, uload **Data & tools:** finance, calc, inventory, places, citycorners, who, news, links, tags, playground diff --git a/docs/optimizable/frontend-consistency-improvements.md b/docs/optimizable/frontend-consistency-improvements.md index 67f50b42c..2f819ce2b 100644 --- a/docs/optimizable/frontend-consistency-improvements.md +++ b/docs/optimizable/frontend-consistency-improvements.md @@ -13,7 +13,7 @@ Tracked improvements for UI/styling consistency across the Mana unified app. Module ListViews use two different styling approaches: - **Scoped CSS + `hsl(var(--color-*))` theme tokens** — 27 modules (65%) - - todo, notes, drink, contacts, journal, dreams, habits, firsts, calendar, chat, places, inventory, finance, news, body, calc, events, photos, automations, cycles, uload, picture, recipes + - todo, notes, drink, contacts, journal, dreams, habits, firsts, calendar, chat, places, inventory, finance, news, body, calc, events, photos, automations, period, uload, picture, recipes - **Tailwind utility classes** — 13 modules (35%) - food, plants, moodlit, cards, presi, storage, skilltree, context, guides, memoro, who, music, playground, citycorners, questions, times diff --git a/package.json b/package.json index 037d36ed4..01f6575a1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "check:status": "bash scripts/check-status.sh", "validate:dockerfiles": "node scripts/validate-dockerfiles.mjs", "audit:deps": "node scripts/audit-workspace-deps.mjs", + "audit:modules": "node scripts/audit-modules.mjs", + "audit:coupling": "node scripts/audit-module-coupling.mjs", + "audit:complexity": "node scripts/audit-complexity.mjs", + "audit:map": "node scripts/build-complexity-map.mjs", "generate:dockerfiles": "node scripts/generate-dockerfiles.mjs", "setup:env": "node scripts/generate-env.mjs", "setup:secrets": "node scripts/setup-secrets.mjs", diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts index aead3a8df..b46f515c7 100644 --- a/packages/shared-branding/src/app-icons.ts +++ b/packages/shared-branding/src/app-icons.ts @@ -135,7 +135,7 @@ export const APP_ICONS = { dreams: svgToDataUrl( `` ), - cycles: svgToDataUrl( + period: svgToDataUrl( `` ), finance: svgToDataUrl( @@ -153,7 +153,7 @@ export const APP_ICONS = { body: svgToDataUrl( // Dumbbell + heart-pulse hybrid: training (barbell) + body (pulse line). // Red→orange gradient to set it apart from the green health-adjacent - // modules (plants, food) and the pink cycles icon. + // modules (plants, food) and the pink period icon. `` ), firsts: svgToDataUrl( @@ -200,7 +200,7 @@ export const APP_ICONS = { myday: svgToDataUrl( `` ), - eventstream: svgToDataUrl( + activity: svgToDataUrl( `` ), companion: svgToDataUrl( diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index 6c1dd985f..b0cd561ee 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -667,8 +667,8 @@ export const MANA_APPS: ManaApp[] = [ requiredTier: 'guest', }, { - id: 'cycles', - name: 'Cycles', + id: 'period', + name: 'Periode', description: { de: 'Menstruationszyklus-Tracking', en: 'Menstrual Cycle Tracking', @@ -677,7 +677,7 @@ export const MANA_APPS: ManaApp[] = [ de: 'Tracke deinen Zyklus mit Blutungstagen, Symptomen, Stimmung und Basaltemperatur. Phasen-Erkennung und Vorhersage für die nächste Periode und das fruchtbare Fenster.', en: 'Track your cycle with flow days, symptoms, mood, and basal temperature. Phase detection and prediction of the next period and fertile window.', }, - icon: APP_ICONS.cycles, + icon: APP_ICONS.period, color: '#ec4899', comingSoon: false, status: 'development', @@ -894,14 +894,14 @@ export const MANA_APPS: ManaApp[] = [ requiredTier: 'guest', }, { - id: 'eventstream', - name: 'Events', - description: { de: 'Live Event-Stream', en: 'Live Event Stream' }, + id: 'activity', + name: 'Aktivität', + description: { de: 'Live Aktivitäts-Stream', en: 'Live Activity Stream' }, longDescription: { de: 'Echtzeit-Feed aller Aktionen ueber alle Module: Tasks, Drinks, Termine, Mahlzeiten.', en: 'Real-time feed of all actions across modules: tasks, drinks, events, meals.', }, - icon: APP_ICONS.eventstream ?? '⚡', + icon: APP_ICONS.activity ?? '⚡', color: '#6366F1', comingSoon: false, status: 'development', diff --git a/packages/shared-tailwind/src/themes.css b/packages/shared-tailwind/src/themes.css index 9fe94c159..5aef46816 100644 --- a/packages/shared-tailwind/src/themes.css +++ b/packages/shared-tailwind/src/themes.css @@ -35,7 +35,7 @@ * ✅ color: hsl(var(--color-foreground)); * ✅ background: hsl(var(--color-primary) / 0.12); * - * 4. Brand-literal colors (cycles pink, observatory cosmic scenes, the + * 4. Brand-literal colors (period pink, observatory cosmic scenes, the * automations/spiral indigo→violet ramp, sport/category palettes, the * photo viewer's near-black backdrop, etc.) deliberately stay as * literal hex/rgba/hsl. They are NOT theme intent — they encode brand