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 f86d025b2..9a65decd9 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -18,7 +18,6 @@ import { Moon, Drop, MoneyWavy, - MapPin, ChatCircle, Clock, Image, @@ -516,40 +515,8 @@ registerApp({ }, }); -registerApp({ - id: 'places', - name: 'Places', - color: '#0EA5E9', - icon: MapPin, - views: { - list: { load: () => import('$lib/modules/places/ListView.svelte') }, - detail: { load: () => import('$lib/modules/places/views/DetailView.svelte') }, - }, - collection: 'places', - paramKey: 'placeId', - dragType: 'place', - acceptsDropFrom: ['contact'], - transformIncoming: { - contact: (source) => ({ - name: `Treffen mit ${[source.firstName, source.lastName].filter(Boolean).join(' ')}`, - latitude: 0, - longitude: 0, - }), - }, - getDisplayData: (item) => ({ - title: (item.name as string) || 'Ort', - subtitle: (item.address as string) ?? undefined, - }), - createItem: async (data) => { - const { placesStore } = await import('$lib/modules/places/stores/places.svelte'); - const place = await placesStore.createPlace({ - name: (data.name as string) ?? 'Neuer Ort', - latitude: (data.latitude as number) ?? 0, - longitude: (data.longitude as number) ?? 0, - }); - return place.id; - }, -}); +// Places-Modul: dekommissioniert 2026-05-19, lebt als viadocu standalone +// auf viadocu-api.mana.how (GPS-Reise-Tracker + Cities/Countries-Stats). registerApp({ id: 'chat', diff --git a/apps/mana/apps/web/src/lib/data/ai/revert/inverse-operations.ts b/apps/mana/apps/web/src/lib/data/ai/revert/inverse-operations.ts index ee6b5c890..71b4ea273 100644 --- a/apps/mana/apps/web/src/lib/data/ai/revert/inverse-operations.ts +++ b/apps/mana/apps/web/src/lib/data/ai/revert/inverse-operations.ts @@ -15,7 +15,6 @@ import { tasksStore } from '$lib/modules/todo/stores/tasks.svelte'; import { eventsStore } from '$lib/modules/calendar/stores/events.svelte'; -import { placesStore } from '$lib/modules/places/stores/places.svelte'; import { drinkStore } from '$lib/modules/drink/stores/drink.svelte'; export type InverseResult = { readonly ok: true } | { readonly ok: false; readonly reason: string }; @@ -61,13 +60,6 @@ registerInverseOperation('CalendarEventCreated', async (payload) => { return { ok: true }; }); -registerInverseOperation('PlaceCreated', async (payload) => { - const placeId = payload.placeId; - if (typeof placeId !== 'string') return { ok: false, reason: 'missing placeId' }; - await placesStore.deletePlace(placeId); - return { ok: true }; -}); - registerInverseOperation('DrinkLogged', async (payload) => { const drinkId = payload.drinkId; if (typeof drinkId !== 'string') return { ok: false, reason: 'missing drinkId' }; diff --git a/apps/mana/apps/web/src/lib/data/crypto/plaintext-allowlist.ts b/apps/mana/apps/web/src/lib/data/crypto/plaintext-allowlist.ts index fd629c6e2..45d2ea951 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/plaintext-allowlist.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/plaintext-allowlist.ts @@ -68,7 +68,6 @@ export const PLAINTEXT_ALLOWLIST: readonly string[] = [ 'periodSymptoms', // TODO: audit 'photoFavorites', // TODO: audit 'photoMediaTags', // TODO: audit - 'placeTags', // TODO: audit 'presiDeckTags', // TODO: audit 'qCollections', // TODO: audit 'questionTags', // TODO: audit 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 2e1577d33..93fd95384 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/registry.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/registry.ts @@ -290,29 +290,6 @@ export const ENCRYPTION_REGISTRY: Record = { // different storage layout. invItems: { enabled: true, fields: ['description'] }, - // ─── Places ────────────────────────────────────────────── - // Location data is GDPR-sensitive PII. The split between the two tables: - // - `places` holds user-named POIs. We encrypt the user-typed text - // (name/description/address) but leave lat/lng plaintext so the - // proximity matcher in tracking.svelte.ts can run without a vault - // unlock during background geolocation logging. lat/lng on a - // handful of saved POIs is far less sensitive than the full - // movement trail in locationLogs below. - // - `locationLogs` IS the movement trail — every coordinate gets - // encrypted. Indexed columns (timestamp, placeId, [placeId+timestamp]) - // stay plaintext for the time-range scans in the log view. - // `name` on `places` IS schema-indexed but no .where('name') call site - // exists (search filters in JS over the decrypted DTO array) — same - // rationale as files.name and plants.name above. - places: { enabled: true, fields: ['name', 'description', 'address'] }, - locationLogs: { - enabled: true, - fields: ['latitude', 'longitude', 'accuracy', 'altitude', 'speed', 'heading'], - }, - // `placeTags` is intentionally NOT in the registry — pure foreign-key - // join table (placeId / tagId), zero user-typed content. Same pattern - // as manaLinks. - // ─── Playground ────────────────────────────────────────── // Saved system-prompt snippets. `name` is the user's label and // `systemPrompt` is the actual prompt body — both are user-typed 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 3497fe9bb..7c9392cf2 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 @@ -225,7 +225,6 @@ describe('module-registry — snapshot', () => { period: ['periods', 'periodDayLogs', 'periodSymptoms'], events: ['socialEvents', 'eventGuests', 'eventInvitations', 'eventItems'], finance: ['transactions', 'financeCategories', 'budgets'], - places: ['places', 'locationLogs', 'placeTags'], playground: ['playgroundSnippets', 'playgroundConversations', 'playgroundMessages'], body: [ 'bodyExercises', 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 fda410bf8..e236125cc 100644 --- a/apps/mana/apps/web/src/lib/data/module-registry.ts +++ b/apps/mana/apps/web/src/lib/data/module-registry.ts @@ -72,7 +72,6 @@ import { dreamsModuleConfig } from '$lib/modules/dreams/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'; import { playgroundModuleConfig } from '$lib/modules/playground/module.config'; import { bodyModuleConfig } from '$lib/modules/body/module.config'; import { firstsModuleConfig } from '$lib/modules/firsts/module.config'; @@ -122,7 +121,6 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [ periodModuleConfig, eventsModuleConfig, financeModuleConfig, - placesModuleConfig, playgroundModuleConfig, bodyModuleConfig, firstsModuleConfig, diff --git a/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts b/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts index 390dd4a7d..834868b47 100644 --- a/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts +++ b/apps/mana/apps/web/src/lib/data/privacy/exposed-records.ts @@ -115,18 +115,6 @@ const TABLES: TableConfig[] = [ return goalStore.setVisibility(id, next); }, }, - { - module: 'places', - collection: 'places', - moduleLabel: 'Orte', - encrypted: true, - title: (r) => asString(r.name), - href: (id) => `/places/place/${id}`, - setVisibility: async (id, next) => { - const { placesStore } = await import('$lib/modules/places/stores/places.svelte'); - return placesStore.setVisibility(id, next); - }, - }, { module: 'recipes', collection: 'recipes', diff --git a/apps/mana/apps/web/src/lib/data/projections/context-document.ts b/apps/mana/apps/web/src/lib/data/projections/context-document.ts index 3aeb2939a..21b085075 100644 --- a/apps/mana/apps/web/src/lib/data/projections/context-document.ts +++ b/apps/mana/apps/web/src/lib/data/projections/context-document.ts @@ -75,14 +75,6 @@ export function generateContextDocument( lines.push(`- Kaffee: ${day.drinks.coffee.count}x (${day.drinks.coffee.ml}ml)`); } - // Places - if (day.places.visitedToday > 0) { - lines.push(`- ${day.places.visitedToday} Orte besucht`); - } - if (day.places.tracking) { - lines.push('- Standort-Tracking aktiv'); - } - // ── Streaks ───────────────────────────────────── const activeStreaks = streaks.filter((s) => s.status === 'active'); const atRisk = streaks.filter((s) => s.status === 'at_risk'); diff --git a/apps/mana/apps/web/src/lib/data/projections/day-snapshot.ts b/apps/mana/apps/web/src/lib/data/projections/day-snapshot.ts index 2a2493f84..05059e7ba 100644 --- a/apps/mana/apps/web/src/lib/data/projections/day-snapshot.ts +++ b/apps/mana/apps/web/src/lib/data/projections/day-snapshot.ts @@ -14,11 +14,8 @@ import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; import { db } from '../database'; import { decryptRecords } from '../crypto'; import { DEFAULT_DAILY_GOAL_ML } from '$lib/modules/drink/types'; -import { trackingStore } from '$lib/modules/places/stores/tracking.svelte'; import type { LocalTask } from '$lib/modules/todo/types'; -import type { LocalEvent } from '$lib/modules/calendar/types'; import type { LocalDrinkEntry } from '$lib/modules/drink/types'; -import type { LocalPlace } from '$lib/modules/places/types'; import type { LocalTimeBlock } from '../time-blocks/types'; import type { DaySnapshot, TaskSummary, EventSummary } from './types'; @@ -36,7 +33,6 @@ function emptySnapshot(date: string): DaySnapshot { coffee: { ml: 0, count: 0 }, total: { ml: 0, count: 0 }, }, - places: { visitedToday: 0, tracking: false }, }; } @@ -47,7 +43,7 @@ async function buildSnapshot(): Promise { const todayEnd = `${today}T23:59:59`; // ── Parallel queries — all modules at once ────── - const [allTasks, blocks, allDrinks, allPlaces] = await Promise.all([ + const [allTasks, blocks, allDrinks] = await Promise.all([ db.table('tasks').toArray(), db .table('timeBlocks') @@ -55,7 +51,6 @@ async function buildSnapshot(): Promise { .between(todayStart, todayEnd + '\uffff') .toArray(), db.table('drinkEntries').toArray(), - db.table('places').toArray(), ]); // ── Parallel decryption ───────────────────────── @@ -115,11 +110,6 @@ async function buildSnapshot(): Promise { } } - // ── Places ────────────────────────────────────── - const visitedToday = allPlaces.filter( - (p) => !p.deletedAt && p.lastVisitedAt && (p.lastVisitedAt as string).startsWith(today) - ).length; - return { date: today, tasks: { @@ -142,10 +132,6 @@ async function buildSnapshot(): Promise { coffee: { ml: coffeeMl, count: coffeeCount }, total: { ml: totalMl, count: totalCount }, }, - places: { - visitedToday, - tracking: trackingStore.isTracking, - }, }; } diff --git a/apps/mana/apps/web/src/lib/data/projections/types.ts b/apps/mana/apps/web/src/lib/data/projections/types.ts index 15625a417..e5681b647 100644 --- a/apps/mana/apps/web/src/lib/data/projections/types.ts +++ b/apps/mana/apps/web/src/lib/data/projections/types.ts @@ -46,11 +46,6 @@ export interface DaySnapshot { coffee: { ml: number; count: number }; total: { ml: number; count: number }; }; - - places: { - visitedToday: number; - tracking: boolean; - }; } // ── Streaks ───────────────────────────────────────── 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 8e3272345..92c5645aa 100644 --- a/apps/mana/apps/web/src/lib/data/tools/init.ts +++ b/apps/mana/apps/web/src/lib/data/tools/init.ts @@ -7,7 +7,6 @@ import { registerTools } from './registry'; import { todoTools } from '$lib/modules/todo/tools'; import { calendarTools } from '$lib/modules/calendar/tools'; import { drinkTools } from '$lib/modules/drink/tools'; -import { placesTools } from '$lib/modules/places/tools'; import { habitsTools } from '$lib/modules/habits/tools'; import { journalTools } from '$lib/modules/journal/tools'; import { notesTools } from '$lib/modules/notes/tools'; @@ -51,7 +50,6 @@ export function initTools(): void { registerTools(todoTools); registerTools(calendarTools); registerTools(drinkTools); - registerTools(placesTools); registerTools(habitsTools); registerTools(journalTools); registerTools(notesTools); diff --git a/apps/mana/apps/web/src/lib/data/unlisted/resolvers.ts b/apps/mana/apps/web/src/lib/data/unlisted/resolvers.ts index 864da1620..e480abcfa 100644 --- a/apps/mana/apps/web/src/lib/data/unlisted/resolvers.ts +++ b/apps/mana/apps/web/src/lib/data/unlisted/resolvers.ts @@ -19,7 +19,6 @@ import { decryptRecord } from '$lib/data/crypto'; import { mediaFileUrl } from '$lib/modules/website/upload'; import type { LocalEvent } from '$lib/modules/calendar/types'; import type { LocalLibraryEntry } from '$lib/modules/library/types'; -import type { LocalPlace } from '$lib/modules/places/types'; import type { LocalTimeBlock } from '$lib/data/time-blocks/types'; import type { LocalAugurEntry } from '$lib/modules/augur/types'; import type { LocalLast } from '$lib/modules/lasts/types'; @@ -52,8 +51,6 @@ export async function buildUnlistedBlob( return buildEventBlob(recordId); case 'libraryEntries': return buildLibraryEntryBlob(recordId); - case 'places': - return buildPlaceBlob(recordId); case 'augurEntries': return buildAugurEntryBlob(recordId); case 'lasts': @@ -158,35 +155,6 @@ async function buildLibraryEntryBlob(recordId: string): Promise> { - const raw = await db.table('places').get(recordId); - if (!raw || raw.deletedAt) { - throw new RecordNotFoundError('places', recordId); - } - - const decrypted = (await decryptRecord('places', { ...raw })) as LocalPlace; - - return { - name: decrypted.name, - address: decrypted.address ?? null, - category: decrypted.category ?? 'other', - }; -} - /** * Augur entry → snapshot blob. * diff --git a/apps/mana/apps/web/src/lib/i18n/locales/places/de.json b/apps/mana/apps/web/src/lib/i18n/locales/places/de.json deleted file mode 100644 index 51d24a074..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/places/de.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "categories": { - "home": "Zuhause", - "work": "Arbeit", - "shopping": "Einkauf", - "transit": "Transit", - "leisure": "Freizeit", - "other": "Sonstiges" - }, - "detail_view": { - "not_found": "Ort nicht gefunden", - "confirm_delete": "Ort wirklich löschen?", - "untitled": "Unbenannt", - "name_placeholder": "Name", - "map_title": "Kartenvorschau", - "label_visibility": "Sichtbarkeit", - "label_share_link": "Link", - "label_category": "Kategorie", - "label_address": "Adresse", - "placeholder_address": "Adresse eingeben...", - "placeholder_address_search": "Adresse suchen...", - "label_coordinates": "Koordinaten", - "placeholder_lat": "Lat", - "placeholder_lng": "Lng", - "resolve_address_title": "Adresse aus Koordinaten ermitteln", - "label_description": "Beschreibung", - "placeholder_description": "Notizen zum Ort...", - "section_tags": "Tags", - "section_recent_visits": "Letzte Besuche", - "meta_visits": "Besuche: {n}", - "meta_last_visit": "Letzter Besuch: {date}", - "meta_created": "Erstellt: {date}", - "meta_updated": "Bearbeitet: {date}" - }, - "geocoding_notice": { - "sensitive_local_unavailable_title": "Diese Suche bleibt bewusst lokal", - "sensitive_local_unavailable_body": "Sensible Suchbegriffe (Arzt, Klinik, …) werden nie an externe Dienste geschickt. Der lokale Index ist gerade nicht erreichbar — versuche es später nochmal oder formuliere allgemeiner.", - "fallback_used_badge": "≈ ungefähr", - "fallback_used_title": "Adress-Suche über öffentliches OSM" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/places/en.json b/apps/mana/apps/web/src/lib/i18n/locales/places/en.json deleted file mode 100644 index f33593bf2..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/places/en.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "categories": { - "home": "Home", - "work": "Work", - "shopping": "Shopping", - "transit": "Transit", - "leisure": "Leisure", - "other": "Other" - }, - "detail_view": { - "not_found": "Place not found", - "confirm_delete": "Really delete place?", - "untitled": "Untitled", - "name_placeholder": "Name", - "map_title": "Map preview", - "label_visibility": "Visibility", - "label_share_link": "Link", - "label_category": "Category", - "label_address": "Address", - "placeholder_address": "Enter address…", - "placeholder_address_search": "Search address…", - "label_coordinates": "Coordinates", - "placeholder_lat": "Lat", - "placeholder_lng": "Lng", - "resolve_address_title": "Resolve address from coordinates", - "label_description": "Description", - "placeholder_description": "Notes about this place…", - "section_tags": "Tags", - "section_recent_visits": "Recent visits", - "meta_visits": "Visits: {n}", - "meta_last_visit": "Last visit: {date}", - "meta_created": "Created: {date}", - "meta_updated": "Edited: {date}" - }, - "geocoding_notice": { - "sensitive_local_unavailable_title": "This search stays local on purpose", - "sensitive_local_unavailable_body": "Sensitive terms (doctor, clinic, …) are never forwarded to public services. The local index is unavailable right now — try again later or use a more general query.", - "fallback_used_badge": "≈ approximate", - "fallback_used_title": "Address lookup via public OSM" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/places/es.json b/apps/mana/apps/web/src/lib/i18n/locales/places/es.json deleted file mode 100644 index 4be4439de..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/places/es.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "categories": { - "home": "Casa", - "work": "Trabajo", - "shopping": "Compras", - "transit": "Tránsito", - "leisure": "Ocio", - "other": "Otro" - }, - "detail_view": { - "not_found": "Lugar no encontrado", - "confirm_delete": "¿Eliminar realmente el lugar?", - "untitled": "Sin título", - "name_placeholder": "Nombre", - "map_title": "Vista previa del mapa", - "label_visibility": "Visibilidad", - "label_share_link": "Enlace", - "label_category": "Categoría", - "label_address": "Dirección", - "placeholder_address": "Introduce una dirección…", - "placeholder_address_search": "Buscar dirección…", - "label_coordinates": "Coordenadas", - "placeholder_lat": "Lat", - "placeholder_lng": "Lng", - "resolve_address_title": "Obtener dirección desde coordenadas", - "label_description": "Descripción", - "placeholder_description": "Notas sobre este lugar…", - "section_tags": "Etiquetas", - "section_recent_visits": "Últimas visitas", - "meta_visits": "Visitas: {n}", - "meta_last_visit": "Última visita: {date}", - "meta_created": "Creado: {date}", - "meta_updated": "Editado: {date}" - }, - "geocoding_notice": { - "sensitive_local_unavailable_title": "Esta búsqueda permanece local a propósito", - "sensitive_local_unavailable_body": "Los términos sensibles (médico, clínica, …) nunca se envían a servicios externos. El índice local no está disponible — vuelve a intentarlo o usa una búsqueda más general.", - "fallback_used_badge": "≈ aprox.", - "fallback_used_title": "Búsqueda de dirección vía OSM público" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/places/fr.json b/apps/mana/apps/web/src/lib/i18n/locales/places/fr.json deleted file mode 100644 index 58506820a..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/places/fr.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "categories": { - "home": "Maison", - "work": "Travail", - "shopping": "Achats", - "transit": "Transit", - "leisure": "Loisirs", - "other": "Autre" - }, - "detail_view": { - "not_found": "Lieu introuvable", - "confirm_delete": "Vraiment supprimer le lieu ?", - "untitled": "Sans titre", - "name_placeholder": "Nom", - "map_title": "Aperçu de la carte", - "label_visibility": "Visibilité", - "label_share_link": "Lien", - "label_category": "Catégorie", - "label_address": "Adresse", - "placeholder_address": "Saisis une adresse…", - "placeholder_address_search": "Rechercher une adresse…", - "label_coordinates": "Coordonnées", - "placeholder_lat": "Lat", - "placeholder_lng": "Lng", - "resolve_address_title": "Trouver l'adresse à partir des coordonnées", - "label_description": "Description", - "placeholder_description": "Notes sur ce lieu…", - "section_tags": "Tags", - "section_recent_visits": "Dernières visites", - "meta_visits": "Visites : {n}", - "meta_last_visit": "Dernière visite : {date}", - "meta_created": "Créé : {date}", - "meta_updated": "Modifié : {date}" - }, - "geocoding_notice": { - "sensitive_local_unavailable_title": "Cette recherche reste locale par principe", - "sensitive_local_unavailable_body": "Les termes sensibles (médecin, clinique, …) ne sont jamais transmis à des services externes. L'index local est indisponible — réessaie plus tard ou utilise une formulation plus générale.", - "fallback_used_badge": "≈ approx.", - "fallback_used_title": "Recherche d'adresse via OSM public" - } -} diff --git a/apps/mana/apps/web/src/lib/i18n/locales/places/it.json b/apps/mana/apps/web/src/lib/i18n/locales/places/it.json deleted file mode 100644 index 484f065bc..000000000 --- a/apps/mana/apps/web/src/lib/i18n/locales/places/it.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "categories": { - "home": "Casa", - "work": "Lavoro", - "shopping": "Shopping", - "transit": "Transito", - "leisure": "Tempo libero", - "other": "Altro" - }, - "detail_view": { - "not_found": "Luogo non trovato", - "confirm_delete": "Eliminare davvero il luogo?", - "untitled": "Senza titolo", - "name_placeholder": "Nome", - "map_title": "Anteprima mappa", - "label_visibility": "Visibilità", - "label_share_link": "Link", - "label_category": "Categoria", - "label_address": "Indirizzo", - "placeholder_address": "Inserisci un indirizzo…", - "placeholder_address_search": "Cerca indirizzo…", - "label_coordinates": "Coordinate", - "placeholder_lat": "Lat", - "placeholder_lng": "Lng", - "resolve_address_title": "Trova l'indirizzo dalle coordinate", - "label_description": "Descrizione", - "placeholder_description": "Note sul luogo…", - "section_tags": "Tag", - "section_recent_visits": "Ultime visite", - "meta_visits": "Visite: {n}", - "meta_last_visit": "Ultima visita: {date}", - "meta_created": "Creato: {date}", - "meta_updated": "Modificato: {date}" - }, - "geocoding_notice": { - "sensitive_local_unavailable_title": "Questa ricerca resta locale di proposito", - "sensitive_local_unavailable_body": "Termini sensibili (medico, clinica, …) non vengono mai inoltrati a servizi esterni. L'indice locale non è disponibile — riprova più tardi o usa una formulazione più generale.", - "fallback_used_badge": "≈ approssimativo", - "fallback_used_title": "Ricerca indirizzo tramite OSM pubblico" - } -} diff --git a/apps/mana/apps/web/src/lib/modules/lasts/inference/scan.ts b/apps/mana/apps/web/src/lib/modules/lasts/inference/scan.ts index 800330502..755e22118 100644 --- a/apps/mana/apps/web/src/lib/modules/lasts/inference/scan.ts +++ b/apps/mana/apps/web/src/lib/modules/lasts/inference/scan.ts @@ -20,10 +20,11 @@ import { scopedForModule } from '$lib/data/scope'; import { decryptRecords } from '$lib/data/crypto'; import { lastsCooldownTable } from '../collections'; import type { LocalLast, LocalLastsCooldown } from '../types'; -import { placesSource } from './sources/places'; import { INFERENCE_DEFAULTS, type InferenceCandidate, type InferenceSource } from './types'; -const SOURCES: InferenceSource[] = [placesSource]; +// `places` inference source dekommissioniert 2026-05-19 mit places-Modul. +// habits/contacts-Sources sind in M3.b geplant — kein aktiver Scanner aktuell. +const SOURCES: InferenceSource[] = []; /** Read all lasts in the active Space (decrypted). */ async function loadExistingLasts(): Promise { diff --git a/apps/mana/apps/web/src/lib/modules/lasts/inference/sources/places.ts b/apps/mana/apps/web/src/lib/modules/lasts/inference/sources/places.ts deleted file mode 100644 index 398a1ef12..000000000 --- a/apps/mana/apps/web/src/lib/modules/lasts/inference/sources/places.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Places inference source. - * - * Heuristic: a Place with `visitCount >= MIN_PRIOR_OCCURRENCES` whose - * `lastVisitedAt` is older than `MIN_SILENCE_DAYS` is a candidate. We - * don't have direct access to per-visit history (would need to scan - * `locationLogs`), so the visit-count + last-visit pair is the proxy - * for "was a regular thing, has stopped". - * - * Category mapping: Place.category → LastCategory by best-effort. Most - * places land in `other` if their PlaceCategory has no clean milestone - * equivalent. - */ - -import { decryptRecords } from '$lib/data/crypto'; -import { scopedForModule } from '$lib/data/scope'; -import type { LocalPlace, PlaceCategory } from '$lib/modules/places/types'; -import { INFERENCE_DEFAULTS, type InferenceCandidate, type InferenceSource } from '../types'; -import type { LastCategory } from '../../types'; - -const PLACE_CATEGORY_MAP: Record = { - home: 'other', - work: 'career', - shopping: 'other', - transit: 'travel', - leisure: 'culture', - other: 'other', -}; - -function daysBetween(a: Date, b: Date): number { - return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24)); -} - -function silenceLabel(days: number): string { - if (days >= 730) return `${Math.floor(days / 365)} Jahren`; - if (days >= 365) return '1 Jahr'; - const months = Math.floor(days / 30); - return `${months} Monaten`; -} - -export const placesSource: InferenceSource = { - id: 'places', - - async scan(now) { - const visible = ( - await scopedForModule('places', 'places').toArray() - ).filter((p) => !p.deletedAt && !p.isArchived); - // Place names are encrypted in the registry — decrypt before use. - const decrypted = await decryptRecords('places', visible); - - const candidates: InferenceCandidate[] = []; - - for (const place of decrypted) { - const visitCount = place.visitCount ?? 0; - if (visitCount < INFERENCE_DEFAULTS.MIN_PRIOR_OCCURRENCES) continue; - if (!place.lastVisitedAt) continue; - - const lastVisit = new Date(place.lastVisitedAt); - if (Number.isNaN(lastVisit.getTime())) continue; - - const silenceDays = daysBetween(now, lastVisit); - if (silenceDays < INFERENCE_DEFAULTS.MIN_SILENCE_DAYS) continue; - - // Span check: createdAt → lastVisitedAt should cover at least - // MIN_PRIOR_SPAN_DAYS so we know it was a sustained habit, not a - // short burst (e.g. a one-week conference visited 5 days running). - if (place.createdAt) { - const created = new Date(place.createdAt); - const spanDays = daysBetween(lastVisit, created); - if (spanDays < INFERENCE_DEFAULTS.MIN_PRIOR_SPAN_DAYS) continue; - } - - const category = PLACE_CATEGORY_MAP[place.category ?? 'other']; - - candidates.push({ - refTable: 'places', - refId: place.id, - title: `Letztes Mal ${place.name}`, - category, - frequencyHint: `${visitCount}× besucht — seit ${silenceLabel(silenceDays)} nicht mehr`, - suggestedDate: place.lastVisitedAt.slice(0, 10), - }); - } - - // Sort by silence desc (longest gap = oldest lastVisitedAt first) and cap. - candidates.sort((a, b) => (a.suggestedDate ?? '').localeCompare(b.suggestedDate ?? '')); - return candidates.slice(0, INFERENCE_DEFAULTS.MAX_CANDIDATES_PER_SOURCE); - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/myday/tools.ts b/apps/mana/apps/web/src/lib/modules/myday/tools.ts index d29784ab1..945645a75 100644 --- a/apps/mana/apps/web/src/lib/modules/myday/tools.ts +++ b/apps/mana/apps/web/src/lib/modules/myday/tools.ts @@ -11,7 +11,6 @@ import { decryptRecords } from '$lib/data/crypto'; import { DEFAULT_DAILY_GOAL_ML } from '$lib/modules/drink/types'; import type { LocalTask } from '$lib/modules/todo/types'; import type { LocalDrinkEntry } from '$lib/modules/drink/types'; -import type { LocalPlace } from '$lib/modules/places/types'; import type { LocalTimeBlock } from '$lib/data/time-blocks/types'; import type { LocalGoal } from '$lib/companion/goals/types'; @@ -33,7 +32,7 @@ export const mydayTools: ModuleTool[] = [ const todayEnd = `${today}T23:59:59`; // ── Parallel queries ──────────────────────── - const [allTasks, blocks, allDrinks, allPlaces, streakStates, goals] = await Promise.all([ + const [allTasks, blocks, allDrinks, streakStates, goals] = await Promise.all([ db.table('tasks').toArray(), db .table('timeBlocks') @@ -41,7 +40,6 @@ export const mydayTools: ModuleTool[] = [ .between(todayStart, todayEnd + '\uffff') .toArray(), db.table('drinkEntries').toArray(), - db.table('places').toArray(), db.table('_streakState').toArray(), db.table('companionGoals').toArray(), ]); @@ -91,11 +89,6 @@ export const mydayTools: ModuleTool[] = [ } } - // ── Places ────────────────────────────────── - const visitedToday = allPlaces.filter( - (p) => !p.deletedAt && p.lastVisitedAt && (p.lastVisitedAt as string).startsWith(today) - ).length; - // ── Streaks ───────────────────────────────── const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); @@ -148,7 +141,6 @@ export const mydayTools: ModuleTool[] = [ coffee: { count: coffeeCount }, total: { ml: totalMl, count: decDrinks.length }, }, - places: { visitedToday }, streaks, goals: activeGoals, }; diff --git a/apps/mana/apps/web/src/lib/modules/places/ListView.svelte b/apps/mana/apps/web/src/lib/modules/places/ListView.svelte deleted file mode 100644 index cb1774864..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/ListView.svelte +++ /dev/null @@ -1,1108 +0,0 @@ - - - -
- -
-
- - {#if trackingStore.currentPosition} -
- {#if editingLocation} -
- setTimeout(cancelEditLocation, 200)} - placeholder="Adresse suchen..." - /> - {#if showLocationSuggestions} -
- {#if locationNotice === 'sensitive_local_unavailable'} -
-
- {$_('places.geocoding_notice.sensitive_local_unavailable_title')} -
-
- {$_('places.geocoding_notice.sensitive_local_unavailable_body')} -
-
- {:else if locationNotice === 'fallback_used'} -
- {$_('places.geocoding_notice.fallback_used_badge')} -
- {/if} - {#each locationSuggestions as result} - - {/each} -
- {/if} -
- {:else} - - {/if} -
- {/if} -
- {#if trackingStore.error} - {trackingStore.error} - {/if} -
- - -
- -
- - - - - -
- - -
- - -
- {#each filtered as place (place.id)} - {@const tags = getTagsByIds(allTags, place.tagIds ?? [])} - - {/each} -
- - - - {#if filtered.length === 0 && !search} -
-

Noch keine Orte gespeichert.

-

Starte das Tracking oder erstelle einen Ort manuell.

-
- {/if} - - {#if filtered.length === 0 && search} -
-

Keine Orte gefunden.

-
- {/if} -
- - diff --git a/apps/mana/apps/web/src/lib/modules/places/SharedPlaceView.svelte b/apps/mana/apps/web/src/lib/modules/places/SharedPlaceView.svelte deleted file mode 100644 index b943c11f6..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/SharedPlaceView.svelte +++ /dev/null @@ -1,137 +0,0 @@ - - - - - {place.name} · Mana - - - - - - - - - - diff --git a/apps/mana/apps/web/src/lib/modules/places/collections.ts b/apps/mana/apps/web/src/lib/modules/places/collections.ts deleted file mode 100644 index 698fe9c57..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/collections.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Places module — collection accessors and guest seed data. - */ - -import { db } from '$lib/data/database'; -import type { LocalPlace, LocalLocationLog } from './types'; - -// ─── Collection Accessors ────────────────────────────────── - -export const placeTable = db.table('places'); -export const locationLogTable = db.table('locationLogs'); - -// ─── Guest Seed ──────────────────────────────────────────── - -export const PLACES_GUEST_SEED = { - places: [ - { - id: 'guest-place-home', - name: 'Zuhause', - latitude: 47.6603, - longitude: 9.1751, - category: 'home' as const, - isFavorite: true, - isArchived: false, - visitCount: 12, - lastVisitedAt: new Date().toISOString(), - }, - { - id: 'guest-place-work', - name: 'Buero', - latitude: 47.6588, - longitude: 9.1753, - category: 'work' as const, - isFavorite: false, - isArchived: false, - visitCount: 8, - }, - ] satisfies LocalPlace[], - locationLogs: [] satisfies LocalLocationLog[], -}; diff --git a/apps/mana/apps/web/src/lib/modules/places/index.ts b/apps/mana/apps/web/src/lib/modules/places/index.ts deleted file mode 100644 index f9d694249..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Places module — barrel exports. - */ - -export { placesStore } from './stores/places.svelte'; -export { trackingStore } from './stores/tracking.svelte'; -export { - useAllPlaces, - useLocationLogs, - toPlace, - toLocationLog, - searchPlaces, - filterFavorites, - filterActive, - getDistanceKm, - findNearestPlace, -} from './queries'; -export { placeTable, locationLogTable, PLACES_GUEST_SEED } from './collections'; -// Geocoding moved to $lib/geocoding (shared across modules). -// Import directly from $lib/geocoding instead of from this barrel. -export type { LocalPlace, LocalLocationLog, Place, LocationLog, PlaceCategory } from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/places/module.config.ts b/apps/mana/apps/web/src/lib/modules/places/module.config.ts deleted file mode 100644 index 2396e2a36..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/module.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ModuleConfig } from '$lib/data/module-registry'; - -export const placesModuleConfig: ModuleConfig = { - appId: 'places', - tables: [{ name: 'places' }, { name: 'locationLogs' }, { name: 'placeTags' }], -}; diff --git a/apps/mana/apps/web/src/lib/modules/places/queries.ts b/apps/mana/apps/web/src/lib/modules/places/queries.ts deleted file mode 100644 index 30cc768df..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/queries.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Reactive queries & pure helpers for Places — uses Dexie liveQuery on the unified DB. - */ - -import { useScopedLiveQuery } from '$lib/data/scope/use-scoped-live-query.svelte'; -import { deriveUpdatedAt } from '$lib/data/sync'; -import { db } from '$lib/data/database'; -import { scopedForModule } from '$lib/data/scope'; -import { decryptRecords } from '$lib/data/crypto'; -import type { LocalPlace, LocalLocationLog, Place, LocationLog } from './types'; - -// ─── Type Converters ───────────────────────────────────── - -export function toPlace(local: LocalPlace): Place { - return { - id: local.id, - name: local.name, - description: local.description || null, - latitude: local.latitude, - longitude: local.longitude, - address: local.address || null, - category: local.category ?? 'other', - isFavorite: local.isFavorite ?? false, - isArchived: local.isArchived ?? false, - visitCount: local.visitCount ?? 0, - lastVisitedAt: local.lastVisitedAt || null, - tagIds: local.tagIds ?? [], - visibility: local.visibility ?? 'space', - unlistedToken: local.unlistedToken ?? '', - unlistedExpiresAt: local.unlistedExpiresAt ?? null, - createdAt: local.createdAt ?? new Date().toISOString(), - updatedAt: deriveUpdatedAt(local), - }; -} - -export function toLocationLog(local: LocalLocationLog): LocationLog { - return { - id: local.id, - latitude: local.latitude, - longitude: local.longitude, - accuracy: local.accuracy ?? null, - altitude: local.altitude ?? null, - speed: local.speed ?? null, - heading: local.heading ?? null, - timestamp: local.timestamp, - placeId: local.placeId || null, - }; -} - -// ─── Live Queries ──────────────────────────────────────── - -export function useAllPlaces() { - return useScopedLiveQuery(async () => { - const locals = await scopedForModule('places', 'places').toArray(); - const visible = locals.filter((p) => !p.deletedAt); - const decrypted = await decryptRecords('places', visible); - return decrypted.map(toPlace); - }, []); -} - -export function useLocationLogs(placeId?: string) { - return useScopedLiveQuery(async () => { - let query = db.table('locationLogs').orderBy('timestamp').reverse(); - const locals = await query.toArray(); - const filtered = placeId ? locals.filter((l) => l.placeId === placeId) : locals; - const decrypted = await decryptRecords('locationLogs', filtered); - return decrypted.map(toLocationLog); - }, []); -} - -// ─── Pure Filter / Search ──────────────────────────────── - -export function searchPlaces(places: Place[], query: string): Place[] { - if (!query.trim()) return places; - const q = query.toLowerCase().trim(); - return places.filter( - (p) => - p.name.toLowerCase().includes(q) || - p.address?.toLowerCase().includes(q) || - p.category.toLowerCase().includes(q) - ); -} - -export function filterFavorites(places: Place[]): Place[] { - return places.filter((p) => p.isFavorite); -} - -export function filterActive(places: Place[]): Place[] { - return places.filter((p) => !p.isArchived); -} - -/** - * Haversine distance between two coordinates in kilometers. - */ -export function getDistanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number { - const R = 6371; - const dLat = ((lat2 - lat1) * Math.PI) / 180; - const dLng = ((lng2 - lng1) * Math.PI) / 180; - const a = - Math.sin(dLat / 2) ** 2 + - Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) ** 2; - return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); -} - -/** - * Find the nearest known place within a given radius (km). - */ -export function findNearestPlace( - places: Place[], - lat: number, - lng: number, - radiusKm = 0.1 -): Place | null { - let nearest: Place | null = null; - let minDist = radiusKm; - for (const p of places) { - const d = getDistanceKm(lat, lng, p.latitude, p.longitude); - if (d < minDist) { - minDist = d; - nearest = p; - } - } - return nearest; -} diff --git a/apps/mana/apps/web/src/lib/modules/places/stores/places.svelte.ts b/apps/mana/apps/web/src/lib/modules/places/stores/places.svelte.ts deleted file mode 100644 index d62c59218..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/stores/places.svelte.ts +++ /dev/null @@ -1,304 +0,0 @@ -/** - * Places Store — Mutation-Only - * - * All reads are handled by liveQuery hooks in queries.ts. - * This store only exposes mutations that write to IndexedDB. - */ - -import { encryptRecord, decryptRecord } from '$lib/data/crypto'; -import { emitDomainEvent } from '$lib/data/events'; -import { getActiveSpace } from '$lib/data/scope'; -import { getEffectiveUserId } from '$lib/data/current-user'; -import { - defaultVisibilityFor, - publishUnlistedSnapshot, - revokeUnlistedSnapshot, - type VisibilityLevel, -} from '@mana/shared-privacy'; -import { buildUnlistedBlob } from '$lib/data/unlisted/resolvers'; -import { authStore } from '$lib/stores/auth.svelte'; -import { getManaApiUrl } from '$lib/api/config'; -import { createBlock } from '$lib/data/time-blocks/service'; -import { placeTable } from '../collections'; -import { toPlace } from '../queries'; -import type { LocalPlace, Place, PlaceCategory } from '../types'; - -export const placesStore = { - async createPlace(data: { - name: string; - latitude: number; - longitude: number; - description?: string; - address?: string; - category?: PlaceCategory; - }) { - const now = new Date().toISOString(); - const newLocal: LocalPlace = { - id: crypto.randomUUID(), - name: data.name, - latitude: data.latitude, - longitude: data.longitude, - description: data.description, - address: data.address, - category: data.category ?? 'other', - isFavorite: false, - isArchived: false, - visitCount: 0, - visibility: defaultVisibilityFor(getActiveSpace()?.type), - createdAt: now, - }; - - // Snapshot the plaintext DTO before encryption mutates the record - // in place — same pattern as the notes/dreams/contacts stores. - const plaintextSnapshot = toPlace({ ...newLocal }); - await encryptRecord('places', newLocal); - await placeTable.add(newLocal); - emitDomainEvent('PlaceCreated', 'places', 'places', newLocal.id, { - placeId: newLocal.id, - name: data.name, - category: data.category, - lat: data.latitude, - lng: data.longitude, - }); - return plaintextSnapshot; - }, - - async updatePlace(id: string, data: Partial & Record) { - const updateData: Partial = {}; - if (data.name !== undefined) updateData.name = data.name; - if (data.description !== undefined) updateData.description = data.description ?? undefined; - if (data.latitude !== undefined) updateData.latitude = data.latitude; - if (data.longitude !== undefined) updateData.longitude = data.longitude; - if (data.address !== undefined) updateData.address = data.address ?? undefined; - if (data.category !== undefined) updateData.category = data.category; - if (data.isFavorite !== undefined) updateData.isFavorite = data.isFavorite; - if (data.isArchived !== undefined) updateData.isArchived = data.isArchived; - - const diff = { - ...updateData, - }; - // encryptRecord mutates the diff in place. Fields not in the - // places allowlist (lat/lng, isFavorite, isArchived, …) pass - // through untouched. - await encryptRecord('places', diff); - await placeTable.update(id, diff); - // Refresh share-snapshot if this place is unlisted. - void this.refreshUnlistedSnapshot(id); - }, - - async deletePlace(id: string) { - const local = await placeTable.get(id); - const decrypted = local ? await decryptRecord('places', { ...local }) : null; - - // Revoke active share-link before tombstone. - if (local?.visibility === 'unlisted' && local.unlistedToken) { - const jwt = await authStore.getValidToken(); - if (jwt) { - try { - await revokeUnlistedSnapshot({ - apiUrl: getManaApiUrl(), - jwt, - collection: 'places', - recordId: id, - }); - } catch (e) { - console.error('[places] revoke on delete failed', e); - } - } - } - - await placeTable.update(id, { - deletedAt: new Date().toISOString(), - }); - emitDomainEvent('PlaceDeleted', 'places', 'places', id, { - placeId: id, - name: (decrypted?.name as string) ?? '', - }); - }, - - async toggleFavorite(id: string) { - const local = await placeTable.get(id); - if (!local) return; - - await placeTable.update(id, { - isFavorite: !local.isFavorite, - }); - }, - - async updateTagIds(id: string, tagIds: string[]) { - await placeTable.update(id, { - tagIds, - }); - }, - - async recordVisit(id: string) { - const local = await placeTable.get(id); - if (!local) return; - - const now = new Date().toISOString(); - const decrypted = await decryptRecord('places', { ...local }); - const placeName = decrypted?.name ?? 'Ort'; - - await placeTable.update(id, { - visitCount: (local.visitCount ?? 0) + 1, - lastVisitedAt: now, - }); - - await createBlock({ - startDate: now, - endDate: now, - kind: 'logged', - type: 'visit', - sourceModule: 'places', - sourceId: id, - title: placeName, - color: '#a855f7', - }); - emitDomainEvent('PlaceVisited', 'places', 'places', id, { - placeId: id, - name: placeName, - visitCount: (local.visitCount ?? 0) + 1, - }); - }, - - /** - * Flip a place's visibility. Coordinates with the server-side - * unlisted-snapshots table — see calendar/eventsStore.setVisibility - * for the full pattern. Server is authoritative for the token. - */ - async setVisibility(id: string, next: VisibilityLevel) { - const existing = await placeTable.get(id); - if (!existing) throw new Error(`Place ${id} not found`); - const before: VisibilityLevel = existing.visibility ?? 'space'; - if (before === next) return; - - const now = new Date().toISOString(); - const patch: Partial = { - visibility: next, - visibilityChangedAt: now, - visibilityChangedBy: getEffectiveUserId(), - }; - - if (next === 'unlisted') { - const blob = await buildUnlistedBlob('places', id); - const jwt = await authStore.getValidToken(); - if (!jwt) throw new Error('Nicht eingeloggt'); - const spaceId = - (existing as unknown as { spaceId?: string }).spaceId ?? getActiveSpace()?.id ?? ''; - const { token } = await publishUnlistedSnapshot({ - apiUrl: getManaApiUrl(), - jwt, - collection: 'places', - recordId: id, - spaceId, - blob, - }); - patch.unlistedToken = token; - patch.unlistedExpiresAt = undefined; - } else if (before === 'unlisted') { - const jwt = await authStore.getValidToken(); - if (jwt) { - await revokeUnlistedSnapshot({ - apiUrl: getManaApiUrl(), - jwt, - collection: 'places', - recordId: id, - }); - } - patch.unlistedToken = undefined; - patch.unlistedExpiresAt = undefined; - } - - await placeTable.update(id, patch); - - emitDomainEvent('VisibilityChanged', 'places', 'places', id, { - recordId: id, - collection: 'places', - before, - after: next, - }); - }, - - async regenerateUnlistedToken(id: string) { - const existing = await placeTable.get(id); - if (!existing || existing.visibility !== 'unlisted') return null; - const jwt = await authStore.getValidToken(); - if (!jwt) return null; - try { - await revokeUnlistedSnapshot({ - apiUrl: getManaApiUrl(), - jwt, - collection: 'places', - recordId: id, - }); - const blob = await buildUnlistedBlob('places', id); - const spaceId = - (existing as unknown as { spaceId?: string }).spaceId ?? getActiveSpace()?.id ?? ''; - const { token } = await publishUnlistedSnapshot({ - apiUrl: getManaApiUrl(), - jwt, - collection: 'places', - recordId: id, - spaceId, - blob, - expiresAt: existing.unlistedExpiresAt ? new Date(existing.unlistedExpiresAt) : undefined, - }); - await placeTable.update(id, { - unlistedToken: token, - }); - return token; - } catch (e) { - console.error('[places] regenerate failed', e); - return null; - } - }, - - async setUnlistedExpiry(id: string, expiresAt: Date | null) { - const existing = await placeTable.get(id); - if (!existing || existing.visibility !== 'unlisted') return; - const jwt = await authStore.getValidToken(); - if (!jwt) return; - try { - const blob = await buildUnlistedBlob('places', id); - const spaceId = - (existing as unknown as { spaceId?: string }).spaceId ?? getActiveSpace()?.id ?? ''; - await publishUnlistedSnapshot({ - apiUrl: getManaApiUrl(), - jwt, - collection: 'places', - recordId: id, - spaceId, - blob, - expiresAt, - }); - await placeTable.update(id, { - unlistedExpiresAt: expiresAt ? expiresAt.toISOString() : undefined, - }); - } catch (e) { - console.error('[places] setUnlistedExpiry failed', e); - } - }, - - async refreshUnlistedSnapshot(id: string) { - const existing = await placeTable.get(id); - if (!existing || existing.visibility !== 'unlisted') return; - try { - const blob = await buildUnlistedBlob('places', id); - const jwt = await authStore.getValidToken(); - if (!jwt) return; - const spaceId = - (existing as unknown as { spaceId?: string }).spaceId ?? getActiveSpace()?.id ?? ''; - await publishUnlistedSnapshot({ - apiUrl: getManaApiUrl(), - jwt, - collection: 'places', - recordId: id, - spaceId, - blob, - expiresAt: existing.unlistedExpiresAt ? new Date(existing.unlistedExpiresAt) : undefined, - }); - } catch (e) { - console.error('[places] refreshUnlistedSnapshot failed', e); - } - }, -}; diff --git a/apps/mana/apps/web/src/lib/modules/places/stores/tracking.svelte.ts b/apps/mana/apps/web/src/lib/modules/places/stores/tracking.svelte.ts deleted file mode 100644 index 54c49d673..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/stores/tracking.svelte.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Tracking Store — Browser Geolocation API wrapper with Svelte 5 runes. - * - * Tracks the user's position via watchPosition and periodically logs - * entries to IndexedDB. Also detects proximity to known places. - */ - -import { decryptRecords, encryptRecord } from '$lib/data/crypto'; -import { emitDomainEvent } from '$lib/data/events'; -import { createBlock } from '$lib/data/time-blocks/service'; -import { locationLogTable, placeTable } from '../collections'; -import { getDistanceKm, findNearestPlace, toPlace } from '../queries'; -import { reverseGeocode, formatAddress } from '$lib/geocoding'; -import type { LocalLocationLog, LocalPlace } from '../types'; - -// ─── State ────────────────────────────────────────────── - -let isTracking = $state(false); -let currentPosition = $state(null); -let error = $state(null); -let permissionState = $state('unknown'); - -let _watchId: number | null = null; -let _lastLogTime = 0; - -/** Minimum seconds between log entries (default: 5 minutes). */ -const LOG_INTERVAL_MS = 5 * 60 * 1000; - -// ─── Permission Check ─────────────────────────────────── - -async function checkPermission(): Promise { - try { - const result = await navigator.permissions.query({ name: 'geolocation' }); - permissionState = result.state; - result.addEventListener('change', () => { - permissionState = result.state; - }); - return result.state; - } catch { - permissionState = 'unknown'; - return 'unknown'; - } -} - -// ─── Core Tracking ────────────────────────────────────── - -function startTracking() { - if (isTracking || !navigator.geolocation) return; - - error = null; - isTracking = true; - emitDomainEvent('TrackingStarted', 'places', 'locationLogs', '', { - timestamp: new Date().toISOString(), - }); - - _watchId = navigator.geolocation.watchPosition( - async (pos) => { - currentPosition = pos; - error = null; - - const now = Date.now(); - if (now - _lastLogTime >= LOG_INTERVAL_MS) { - _lastLogTime = now; - await logPosition(pos); - } - }, - (err) => { - error = err.message; - }, - { - enableHighAccuracy: false, - maximumAge: 60_000, - timeout: 30_000, - } - ); - - checkPermission(); -} - -function stopTracking() { - if (_watchId !== null) { - navigator.geolocation.clearWatch(_watchId); - _watchId = null; - } - isTracking = false; - emitDomainEvent('TrackingStopped', 'places', 'locationLogs', '', { - durationMs: 0, - logCount: 0, - }); -} - -async function getCurrentPosition(): Promise { - if (!navigator.geolocation) { - error = 'Geolocation wird nicht unterstuetzt'; - return null; - } - - return new Promise((resolve) => { - navigator.geolocation.getCurrentPosition( - (pos) => { - currentPosition = pos; - error = null; - resolve(pos); - }, - (err) => { - error = err.message; - resolve(null); - }, - { enableHighAccuracy: false, maximumAge: 60_000, timeout: 15_000 } - ); - }); -} - -// ─── Log to IndexedDB ─────────────────────────────────── - -async function logPosition(pos: GeolocationPosition) { - const lat = pos.coords.latitude; - const lng = pos.coords.longitude; - - // Check proximity to known places. lat/lng on `places` stay plaintext - // (see registry.ts) so the proximity matcher works during background - // geolocation logging even before the vault is unlocked. We still - // decrypt so that nearest.name etc. is usable downstream. - const allLocals = await placeTable.toArray(); - const visible = allLocals.filter((p) => !p.deletedAt); - const decrypted = await decryptRecords('places', visible); - const places = decrypted.map(toPlace); - const nearest = findNearestPlace(places, lat, lng); - - const log: LocalLocationLog = { - id: crypto.randomUUID(), - latitude: lat, - longitude: lng, - accuracy: pos.coords.accuracy ?? undefined, - altitude: pos.coords.altitude ?? undefined, - speed: pos.coords.speed ?? undefined, - heading: pos.coords.heading ?? undefined, - timestamp: new Date(pos.timestamp).toISOString(), - placeId: nearest?.id, - createdAt: new Date().toISOString(), - }; - - await encryptRecord('locationLogs', log); - await locationLogTable.add(log); - emitDomainEvent('LocationLogged', 'places', 'locationLogs', log.id, { - logId: log.id, - lat, - lng, - placeId: nearest?.id, - accuracy: pos.coords.accuracy, - }); - - // Update visit count on the matched place + create TimeBlock - if (nearest) { - const local = await placeTable.get(nearest.id); - if (local) { - const updates: Partial = { - visitCount: (local.visitCount ?? 0) + 1, - lastVisitedAt: log.timestamp, - }; - - // Auto-fill address via reverse geocoding if the place has none - if (!local.address) { - reverseGeocode(lat, lng).then(async (result) => { - if (result) { - const addr = formatAddress(result.address); - if (addr) { - const rec: Partial = { address: addr }; - await encryptRecord('places', rec); - await placeTable.update(nearest.id, { - address: rec.address, - }); - } - } - }); - } - - await placeTable.update(nearest.id, updates); - - await createBlock({ - startDate: log.timestamp, - endDate: log.timestamp, - kind: 'logged', - type: 'visit', - sourceModule: 'places', - sourceId: nearest.id, - title: nearest.name, - color: '#a855f7', - }); - } - } -} - -// ─── Force-Log (ignores interval) ─────────────────────── - -async function logNow() { - if (!currentPosition) { - const pos = await getCurrentPosition(); - if (pos) { - _lastLogTime = Date.now(); - await logPosition(pos); - } - return; - } - _lastLogTime = Date.now(); - await logPosition(currentPosition); -} - -// ─── Exports ──────────────────────────────────────────── - -export const trackingStore = { - get isTracking() { - return isTracking; - }, - get currentPosition() { - return currentPosition; - }, - get error() { - return error; - }, - get permissionState() { - return permissionState; - }, - startTracking, - stopTracking, - getCurrentPosition, - checkPermission, - logNow, -}; diff --git a/apps/mana/apps/web/src/lib/modules/places/tools.ts b/apps/mana/apps/web/src/lib/modules/places/tools.ts deleted file mode 100644 index 534fbb90b..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/tools.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Places Tools — LLM-accessible operations for location tracking. - */ - -import type { ModuleTool } from '$lib/data/tools/types'; -import { placesStore } from './stores/places.svelte'; -import { trackingStore } from './stores/tracking.svelte'; -import { placeTable } from './collections'; -import { decryptRecords } from '$lib/data/crypto'; -import { toPlace } from './queries'; -import type { LocalPlace, PlaceCategory } from './types'; - -export const placesTools: ModuleTool[] = [ - { - name: 'create_place', - module: 'places', - description: 'Erstellt einen neuen Ort', - parameters: [ - { name: 'name', type: 'string', description: 'Name des Ortes', required: true }, - { name: 'latitude', type: 'number', description: 'Breitengrad', required: true }, - { name: 'longitude', type: 'number', description: 'Laengengrad', required: true }, - { - name: 'category', - type: 'string', - description: 'Kategorie', - required: false, - enum: [ - 'home', - 'work', - 'shopping', - 'sport', - 'culture', - 'nature', - 'transport', - 'health', - 'education', - 'nightlife', - 'other', - ], - }, - { name: 'address', type: 'string', description: 'Adresse', required: false }, - ], - async execute(params) { - const place = await placesStore.createPlace({ - name: params.name as string, - latitude: params.latitude as number, - longitude: params.longitude as number, - category: params.category as PlaceCategory | undefined, - address: params.address as string | undefined, - }); - return { success: true, data: place, message: `Ort "${params.name}" erstellt` }; - }, - }, - { - name: 'visit_place', - module: 'places', - description: 'Vermerkt einen Besuch an einem bereits erfassten Ort', - parameters: [{ name: 'placeId', type: 'string', description: 'ID des Ortes', required: true }], - async execute(params) { - await placesStore.recordVisit(params.placeId as string); - return { success: true, message: 'Besuch registriert' }; - }, - }, - { - name: 'get_places', - module: 'places', - description: 'Gibt alle gespeicherten Orte zurueck', - parameters: [], - async execute() { - const all = await placeTable.toArray(); - const active = all.filter((p) => !p.deletedAt && !p.isArchived); - const decrypted = await decryptRecords('places', active); - const places = decrypted.map(toPlace); - return { - success: true, - data: places.map((p) => ({ - id: p.id, - name: p.name, - category: p.category, - visitCount: p.visitCount, - })), - message: `${places.length} Orte gespeichert`, - }; - }, - }, - { - name: 'get_current_location', - module: 'places', - description: 'Gibt die aktuelle GPS-Position zurueck (erfordert Standort-Berechtigung)', - parameters: [], - async execute() { - const pos = await trackingStore.getCurrentPosition(); - if (!pos) { - return { success: false, message: 'Standort nicht verfuegbar' }; - } - return { - success: true, - data: { - latitude: pos.coords.latitude, - longitude: pos.coords.longitude, - accuracy: pos.coords.accuracy, - }, - message: `Standort: ${pos.coords.latitude.toFixed(4)}, ${pos.coords.longitude.toFixed(4)}`, - }; - }, - }, -]; diff --git a/apps/mana/apps/web/src/lib/modules/places/types.ts b/apps/mana/apps/web/src/lib/modules/places/types.ts deleted file mode 100644 index 246a4a9fd..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/types.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Places module types for the unified app. - */ - -import type { BaseRecord } from '@mana/local-store'; -import type { VisibilityLevel } from '@mana/shared-privacy'; - -export type PlaceCategory = 'home' | 'work' | 'shopping' | 'transit' | 'leisure' | 'other'; - -export interface LocalPlace extends BaseRecord { - name: string; - description?: string; - latitude: number; - longitude: number; - address?: string; - category?: PlaceCategory; - isFavorite?: boolean; - isArchived?: boolean; - visitCount?: number; - lastVisitedAt?: string; - tagIds?: string[]; - visibility?: VisibilityLevel; - visibilityChangedAt?: string; - visibilityChangedBy?: string; - unlistedToken?: string; - /** ISO timestamp when the unlisted snapshot expires; absent = never. */ - unlistedExpiresAt?: string; -} - -export interface LocalLocationLog extends BaseRecord { - latitude: number; - longitude: number; - accuracy?: number; - altitude?: number; - speed?: number; - heading?: number; - timestamp: string; - placeId?: string; -} - -// ─── Shared Place Type ────────────────────────────────── - -export interface Place { - id: string; - name: string; - description: string | null; - latitude: number; - longitude: number; - address: string | null; - category: PlaceCategory; - isFavorite: boolean; - isArchived: boolean; - visitCount: number; - lastVisitedAt: string | null; - tagIds: string[]; - visibility: VisibilityLevel; - /** Server-issued share token. Empty when not 'unlisted'. */ - unlistedToken: string; - /** ISO timestamp when the unlisted snapshot expires, or null = never. */ - unlistedExpiresAt: string | null; - createdAt: string; - updatedAt: string; -} - -export interface LocationLog { - id: string; - latitude: number; - longitude: number; - accuracy: number | null; - altitude: number | null; - speed: number | null; - heading: number | null; - timestamp: string; - placeId: string | null; -} diff --git a/apps/mana/apps/web/src/lib/modules/places/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/places/views/DetailView.svelte deleted file mode 100644 index 1e3c6b6c6..000000000 --- a/apps/mana/apps/web/src/lib/modules/places/views/DetailView.svelte +++ /dev/null @@ -1,695 +0,0 @@ - - - - - {#snippet body(place)} -
-
- -
-
- -
- -
- - {#if mapUrl} -
- -
- {/if} - -
-
- {$_('places.detail_view.label_visibility')} - -
- - {#if place.visibility === 'unlisted' && place.unlistedToken && shareUrl} - - {/if} - -
- {$_('places.detail_view.label_category')} - -
- -
- {$_('places.detail_view.label_address')} - -
- - -
- -
-
- - { - if (addressSuggestions.length > 0) showAddressSuggestions = true; - }} - /> -
- {#if showAddressSuggestions} -
- {#each addressSuggestions as result} - - {/each} -
- {/if} -
-
- -
- {$_('places.detail_view.label_coordinates')} -
- - - -
-
- -
- {$_('places.detail_view.label_description')} - -
-
- - {#if placeTags.length > 0} -
- -
- {#each placeTags as tag (tag.id)} - - {/each} -
-
- {/if} - - - - {#if logs.length > 0} -
- -
- {#each logs as log (log.id)} -
- {formatDate(log.timestamp)} - {#if log.accuracy} - ±{Math.round(log.accuracy)}m - {/if} -
- {/each} -
-
- {/if} - -
- {#if (place.visitCount ?? 0) > 0} - {$_('places.detail_view.meta_visits', { values: { n: place.visitCount } })} - {/if} - {#if place.lastVisitedAt} - {$_('places.detail_view.meta_last_visit', { - values: { date: formatDate(place.lastVisitedAt) }, - })} - {/if} - {#if place.createdAt} - {$_('places.detail_view.meta_created', { - values: { date: new Date(place.createdAt).toLocaleDateString(get(locale) ?? 'de') }, - })} - {/if} - {#if place.updatedAt} - {$_('places.detail_view.meta_updated', { - values: { date: new Date(place.updatedAt).toLocaleDateString(get(locale) ?? 'de') }, - })} - {/if} -
- {/snippet} -
- - diff --git a/apps/mana/apps/web/src/lib/modules/website/embeds.ts b/apps/mana/apps/web/src/lib/modules/website/embeds.ts index 58aee76cf..515c36007 100644 --- a/apps/mana/apps/web/src/lib/modules/website/embeds.ts +++ b/apps/mana/apps/web/src/lib/modules/website/embeds.ts @@ -27,7 +27,6 @@ import type { LocalEvent } from '$lib/modules/calendar/types'; import type { LocalTask } from '$lib/modules/todo/types'; import type { LocalTaskTag } from '$lib/modules/todo/types'; import type { LocalGoal } from '$lib/companion/goals/types'; -import type { LocalPlace } from '$lib/modules/places/types'; import type { LocalRecipe } from '$lib/modules/recipes/types'; import type { LocalHabit, LocalHabitLog } from '$lib/modules/habits/types'; import type { LocalQuiz } from '$lib/modules/quiz/types'; @@ -63,9 +62,6 @@ export async function resolveEmbed(props: ModuleEmbedProps): Promise { - let places = await db.table('places').toArray(); - places = places.filter( - (p) => !p.deletedAt && !p.isArchived && canEmbedOnWebsite(p.visibility ?? 'private') - ); - - if (props.filter?.kind) { - places = places.filter((p) => p.category === props.filter?.kind); - } - if (props.filter?.isFavorite === true) { - places = places.filter((p) => p.isFavorite === true); - } - if (props.filter?.tagIds?.length) { - const wanted = new Set(props.filter.tagIds); - places = places.filter((p) => (p.tagIds ?? []).some((t) => wanted.has(t))); - } - - const decrypted = (await decryptRecords('places', places)) as LocalPlace[]; - - // Favourites first, then alphabetical for a stable order. - decrypted.sort((a, b) => { - const favA = a.isFavorite ? 0 : 1; - const favB = b.isFavorite ? 0 : 1; - if (favA !== favB) return favA - favB; - return a.name.localeCompare(b.name); - }); - - return decrypted.map((p) => ({ - title: p.name, - subtitle: p.address ?? undefined, - })); -} - /** * Recipes: "my tested recipes" / "cookbook". Hard-gated on * canEmbedOnWebsite. diff --git a/apps/mana/apps/web/src/routes/(app)/places/+page.svelte b/apps/mana/apps/web/src/routes/(app)/places/+page.svelte deleted file mode 100644 index 109b6d1fd..000000000 --- a/apps/mana/apps/web/src/routes/(app)/places/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - Places - Mana - - - - {}} goBack={() => history.back()} params={{}} /> - diff --git a/apps/mana/apps/web/src/routes/share/[token]/+page.svelte b/apps/mana/apps/web/src/routes/share/[token]/+page.svelte index ef7517ef2..65bbae742 100644 --- a/apps/mana/apps/web/src/routes/share/[token]/+page.svelte +++ b/apps/mana/apps/web/src/routes/share/[token]/+page.svelte @@ -6,7 +6,6 @@