mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
refactor: rename planta → plants, clean up codebase
- Rename planta module to plants everywhere (routes, modules, API, branding, i18n, docker, docs, shared packages) - Fix package name collisions: @mana/credits-service, @mana/subscriptions-service (unblocks turbo) - Extract layout composables: use-ai-tier-items, use-sync-status-items, RouteTierGate (layout 1345→1015 lines) - Create shared DB pool for apps/api (lib/db.ts), migrate 5 modules - Add automations module queries.ts with useAllAutomations/useEnabledAutomations - Remove debug console.log statements from production code - Rename storage display name: Ablage → Speicher Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6c19dbc77
commit
a91a6076cc
110 changed files with 831 additions and 707 deletions
|
|
@ -85,7 +85,7 @@ uploadRoutes.post('/avatar', async (c) => {
|
||||||
|
|
||||||
Only servers that need their own database use Drizzle. Most apps rely on mana-sync for data persistence.
|
Only servers that need their own database use Drizzle. Most apps rely on mana-sync for data persistence.
|
||||||
|
|
||||||
**Servers with Drizzle:** chat, todo, moodlit, context, planta, presi, traces, uload, wisekeep, news
|
**Servers with Drizzle:** chat, todo, moodlit, context, plants, presi, traces, uload, wisekeep, news
|
||||||
|
|
||||||
**Servers without Drizzle (mana-sync only):** calendar, contacts, cards, mukke, nutriphi, picture, questions, storage
|
**Servers without Drizzle (mana-sync only):** calendar, contacts, cards, mukke, nutriphi, picture, questions, storage
|
||||||
|
|
||||||
|
|
|
||||||
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"sessionId":"7b3ce34c-30d3-4f38-8ce5-3d170ce48f50","pid":91068,"acquiredAt":1775918818294}
|
||||||
|
|
@ -16,7 +16,7 @@ Monorepo containing all Mana projects — a self-hosted multi-app ecosystem with
|
||||||
| **cards** | Card/deck management | NestJS backend, Expo mobile, SvelteKit web |
|
| **cards** | Card/deck management | NestJS backend, Expo mobile, SvelteKit web |
|
||||||
| **zitare** | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
|
| **zitare** | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
|
||||||
| **mukke** | Music player | NestJS backend, SvelteKit web |
|
| **mukke** | Music player | NestJS backend, SvelteKit web |
|
||||||
| **planta** | Plant care tracker | NestJS backend, SvelteKit web |
|
| **plants** | Plant care tracker | NestJS backend, SvelteKit web |
|
||||||
| **storage** | Cloud storage | NestJS backend, SvelteKit web |
|
| **storage** | Cloud storage | NestJS backend, SvelteKit web |
|
||||||
| **questions** | Q&A with web search | SvelteKit web |
|
| **questions** | Q&A with web search | SvelteKit web |
|
||||||
| **skilltree** | Skill tree visualization | NestJS backend, SvelteKit web |
|
| **skilltree** | Skill tree visualization | NestJS backend, SvelteKit web |
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { contextRoutes } from './modules/context/routes';
|
||||||
import { pictureRoutes } from './modules/picture/routes';
|
import { pictureRoutes } from './modules/picture/routes';
|
||||||
import { storageRoutes } from './modules/storage/routes';
|
import { storageRoutes } from './modules/storage/routes';
|
||||||
import { todoRoutes } from './modules/todo/routes';
|
import { todoRoutes } from './modules/todo/routes';
|
||||||
import { plantaRoutes } from './modules/planta/routes';
|
import { plantsRoutes } from './modules/plants/routes';
|
||||||
import { nutriphiRoutes } from './modules/nutriphi/routes';
|
import { nutriphiRoutes } from './modules/nutriphi/routes';
|
||||||
import { guidesRoutes } from './modules/guides/routes';
|
import { guidesRoutes } from './modules/guides/routes';
|
||||||
import { moodlitRoutes } from './modules/moodlit/routes';
|
import { moodlitRoutes } from './modules/moodlit/routes';
|
||||||
|
|
@ -56,7 +56,7 @@ app.route('/api/v1/context', contextRoutes);
|
||||||
app.route('/api/v1/picture', pictureRoutes);
|
app.route('/api/v1/picture', pictureRoutes);
|
||||||
app.route('/api/v1/storage', storageRoutes);
|
app.route('/api/v1/storage', storageRoutes);
|
||||||
app.route('/api/v1/todo', todoRoutes);
|
app.route('/api/v1/todo', todoRoutes);
|
||||||
app.route('/api/v1/planta', plantaRoutes);
|
app.route('/api/v1/plants', plantsRoutes);
|
||||||
app.route('/api/v1/nutriphi', nutriphiRoutes);
|
app.route('/api/v1/nutriphi', nutriphiRoutes);
|
||||||
app.route('/api/v1/guides', guidesRoutes);
|
app.route('/api/v1/guides', guidesRoutes);
|
||||||
app.route('/api/v1/moodlit', moodlitRoutes);
|
app.route('/api/v1/moodlit', moodlitRoutes);
|
||||||
|
|
|
||||||
32
apps/api/src/lib/db.ts
Normal file
32
apps/api/src/lib/db.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Shared database connection pool for mana-api.
|
||||||
|
*
|
||||||
|
* All modules share a single connection pool instead of each creating
|
||||||
|
* their own. The pool is created lazily on first call and reused.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```ts
|
||||||
|
* import { getConnection } from '../../lib/db';
|
||||||
|
* import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
*
|
||||||
|
* const db = drizzle(getConnection(), { schema: { ... } });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
const DATABASE_URL =
|
||||||
|
process.env.DATABASE_URL ?? 'postgresql://mana:devpassword@localhost:5432/mana_platform';
|
||||||
|
|
||||||
|
let pool: ReturnType<typeof postgres> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the shared postgres connection pool.
|
||||||
|
* Created lazily with sensible defaults (max 10 connections).
|
||||||
|
*/
|
||||||
|
export function getConnection() {
|
||||||
|
if (!pool) {
|
||||||
|
pool = postgres(DATABASE_URL, { max: 10, idle_timeout: 30 });
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
@ -14,16 +14,12 @@ import { Hono } from 'hono';
|
||||||
import { Readability } from '@mozilla/readability';
|
import { Readability } from '@mozilla/readability';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
import postgres from 'postgres';
|
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
|
import { getConnection } from '../../lib/db';
|
||||||
|
|
||||||
// ─── DB Connection (reads from news.curated_articles) ──────
|
// ─── DB Connection (reads from news.curated_articles) ──────
|
||||||
|
|
||||||
const DATABASE_URL =
|
const db = drizzle(getConnection());
|
||||||
process.env.DATABASE_URL ?? 'postgresql://mana:devpassword@localhost:5432/mana_platform';
|
|
||||||
|
|
||||||
const connection = postgres(DATABASE_URL, { max: 10 });
|
|
||||||
const db = drizzle(connection);
|
|
||||||
|
|
||||||
// ─── Extract Service (Readability fallback for ad-hoc URLs) ─
|
// ─── Extract Service (Readability fallback for ad-hoc URLs) ─
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta module — Photo upload + AI plant identification.
|
* Plants module — Photo upload + AI plant identification.
|
||||||
*
|
*
|
||||||
* CRUD for plants, photos, watering is handled by mana-sync. This
|
* CRUD for plants, photos, watering is handled by mana-sync. This
|
||||||
* module owns the server-only operations: photo upload to mana-media
|
* module owns the server-only operations: photo upload to mana-media
|
||||||
|
|
@ -61,7 +61,7 @@ routes.post('/photos/upload', async (c) => {
|
||||||
try {
|
try {
|
||||||
const { uploadImageToMedia } = await import('../../lib/media');
|
const { uploadImageToMedia } = await import('../../lib/media');
|
||||||
const buffer = await file.arrayBuffer();
|
const buffer = await file.arrayBuffer();
|
||||||
const result = await uploadImageToMedia(buffer, file.name, { app: 'planta', userId });
|
const result = await uploadImageToMedia(buffer, file.name, { app: 'plants', userId });
|
||||||
|
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
|
|
@ -73,7 +73,7 @@ routes.post('/photos/upload', async (c) => {
|
||||||
201
|
201
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('planta.upload_failed', {
|
logger.error('plants.upload_failed', {
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
return c.json({ error: 'Upload failed' }, 500);
|
return c.json({ error: 'Upload failed' }, 500);
|
||||||
|
|
@ -107,11 +107,11 @@ routes.post('/analysis/identify', async (c) => {
|
||||||
});
|
});
|
||||||
return c.json(envelope(object));
|
return c.json(envelope(object));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('planta.analysis_failed', {
|
logger.error('plants.analysis_failed', {
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
return c.json({ error: 'Analysis failed' }, 500);
|
return c.json({ error: 'Analysis failed' }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export { routes as plantaRoutes };
|
export { routes as plantsRoutes };
|
||||||
|
|
@ -12,7 +12,7 @@ import { HTTPException } from 'hono/http-exception';
|
||||||
import { authMiddleware } from '@mana/shared-hono/auth';
|
import { authMiddleware } from '@mana/shared-hono/auth';
|
||||||
import type { AuthVariables } from '@mana/shared-hono';
|
import type { AuthVariables } from '@mana/shared-hono';
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
import postgres from 'postgres';
|
import { getConnection } from '../../lib/db';
|
||||||
import {
|
import {
|
||||||
decks,
|
decks,
|
||||||
slides,
|
slides,
|
||||||
|
|
@ -25,11 +25,7 @@ import {
|
||||||
|
|
||||||
// ─── DB Connection ─────────────────────────────────────────
|
// ─── DB Connection ─────────────────────────────────────────
|
||||||
|
|
||||||
const DATABASE_URL =
|
const db = drizzle(getConnection(), {
|
||||||
process.env.DATABASE_URL ?? 'postgresql://mana:devpassword@localhost:5432/mana_platform';
|
|
||||||
|
|
||||||
const connection = postgres(DATABASE_URL, { max: 5, idle_timeout: 20 });
|
|
||||||
const db = drizzle(connection, {
|
|
||||||
schema: {
|
schema: {
|
||||||
decks,
|
decks,
|
||||||
slides,
|
slides,
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
import postgres from 'postgres';
|
|
||||||
import { pgSchema, uuid, text, timestamp, integer, jsonb } from 'drizzle-orm/pg-core';
|
import { pgSchema, uuid, text, timestamp, integer, jsonb } from 'drizzle-orm/pg-core';
|
||||||
|
import { getConnection } from '../../lib/db';
|
||||||
const DATABASE_URL =
|
|
||||||
process.env.DATABASE_URL ?? 'postgresql://mana:devpassword@localhost:5432/mana_platform';
|
|
||||||
|
|
||||||
export const researchSchema = pgSchema('research');
|
export const researchSchema = pgSchema('research');
|
||||||
|
|
||||||
|
|
@ -58,8 +55,7 @@ export const sources = researchSchema.table('sources', {
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const connection = postgres(DATABASE_URL, { max: 5, idle_timeout: 20 });
|
export const db = drizzle(getConnection(), { schema: { researchResults, sources } });
|
||||||
export const db = drizzle(connection, { schema: { researchResults, sources } });
|
|
||||||
|
|
||||||
export type ResearchResult = typeof researchResults.$inferSelect;
|
export type ResearchResult = typeof researchResults.$inferSelect;
|
||||||
export type Source = typeof sources.$inferSelect;
|
export type Source = typeof sources.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ import { rrulestr } from 'rrule';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { eq, and, asc, sql } from 'drizzle-orm';
|
import { eq, and, asc, sql } from 'drizzle-orm';
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
import postgres from 'postgres';
|
|
||||||
import { serviceAuthMiddleware, type AuthVariables } from '@mana/shared-hono';
|
import { serviceAuthMiddleware, type AuthVariables } from '@mana/shared-hono';
|
||||||
|
import { getConnection } from '../../lib/db';
|
||||||
import {
|
import {
|
||||||
pgSchema,
|
pgSchema,
|
||||||
uuid,
|
uuid,
|
||||||
|
|
@ -33,9 +33,6 @@ import {
|
||||||
|
|
||||||
// ─── DB Schema (minimal, server-only) ──────────────────────
|
// ─── DB Schema (minimal, server-only) ──────────────────────
|
||||||
|
|
||||||
const DATABASE_URL =
|
|
||||||
process.env.DATABASE_URL ?? 'postgresql://mana:devpassword@localhost:5432/mana_platform';
|
|
||||||
|
|
||||||
const todoSchema = pgSchema('todo');
|
const todoSchema = pgSchema('todo');
|
||||||
|
|
||||||
const tasks = todoSchema.table('tasks', {
|
const tasks = todoSchema.table('tasks', {
|
||||||
|
|
@ -88,8 +85,7 @@ const reminders = todoSchema.table(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const connection = postgres(DATABASE_URL, { max: 5, idle_timeout: 20 });
|
const db = drizzle(getConnection(), { schema: { tasks, projects, reminders } });
|
||||||
const db = drizzle(connection, { schema: { tasks, projects, reminders } });
|
|
||||||
|
|
||||||
// ─── Routes ────────────────────────────────────────────────
|
// ─── Routes ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Hono } from 'hono';
|
||||||
import { logger, type AuthVariables } from '@mana/shared-hono';
|
import { logger, type AuthVariables } from '@mana/shared-hono';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
import postgres from 'postgres';
|
import { getConnection } from '../../lib/db';
|
||||||
import {
|
import {
|
||||||
pgSchema,
|
pgSchema,
|
||||||
uuid,
|
uuid,
|
||||||
|
|
@ -23,8 +23,6 @@ import {
|
||||||
|
|
||||||
// ─── DB Schema ──────────────────────────────────────────────
|
// ─── DB Schema ──────────────────────────────────────────────
|
||||||
|
|
||||||
const DATABASE_URL =
|
|
||||||
process.env.DATABASE_URL ?? 'postgresql://mana:devpassword@localhost:5432/mana_platform';
|
|
||||||
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
|
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
|
||||||
|
|
||||||
const tracesSchema = pgSchema('traces');
|
const tracesSchema = pgSchema('traces');
|
||||||
|
|
@ -116,8 +114,7 @@ const guidePois = tracesSchema.table('guide_pois', {
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const connection = postgres(DATABASE_URL, { max: 5, idle_timeout: 20 });
|
const db = drizzle(getConnection(), { schema: { locations, cities, pois, guides, guidePois } });
|
||||||
const db = drizzle(connection, { schema: { locations, cities, pois, guides, guidePois } });
|
|
||||||
|
|
||||||
// ─── Routes ─────────────────────────────────────────────────
|
// ─── Routes ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const APP_SUBDOMAINS = new Set([
|
||||||
'contacts',
|
'contacts',
|
||||||
'zitare',
|
'zitare',
|
||||||
'skilltree',
|
'skilltree',
|
||||||
'planta',
|
'plants',
|
||||||
'cards',
|
'cards',
|
||||||
'storage',
|
'storage',
|
||||||
'presi',
|
'presi',
|
||||||
|
|
|
||||||
|
|
@ -605,13 +605,13 @@ registerApp({
|
||||||
});
|
});
|
||||||
|
|
||||||
registerApp({
|
registerApp({
|
||||||
id: 'planta',
|
id: 'plants',
|
||||||
name: 'Planta',
|
name: 'Plants',
|
||||||
color: '#16A34A',
|
color: '#16A34A',
|
||||||
icon: Plant,
|
icon: Plant,
|
||||||
views: {
|
views: {
|
||||||
list: { load: () => import('$lib/modules/planta/ListView.svelte') },
|
list: { load: () => import('$lib/modules/plants/ListView.svelte') },
|
||||||
detail: { load: () => import('$lib/modules/planta/views/DetailView.svelte') },
|
detail: { load: () => import('$lib/modules/plants/views/DetailView.svelte') },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<!--
|
||||||
|
RouteTierGate — blocks access to routes that require a higher tier
|
||||||
|
than the current user holds. Shows a denial panel with the user's
|
||||||
|
current tier vs. the required tier.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { locale } from 'svelte-i18n';
|
||||||
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
appName: string;
|
||||||
|
userTierLabel: string;
|
||||||
|
requiredTierLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { appName, userTierLabel, requiredTierLabel }: Props = $props();
|
||||||
|
let isDE = $derived(($locale || 'de') === 'de');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex min-h-[60vh] items-center justify-center p-6">
|
||||||
|
<div
|
||||||
|
class="w-full max-w-96 rounded-2xl border px-8 py-10 text-center shadow-sm"
|
||||||
|
style:border-color="hsl(var(--border, 0 0% 90%))"
|
||||||
|
style:background-color="hsl(var(--card, 0 0% 100%))"
|
||||||
|
>
|
||||||
|
<h1 class="mb-4 text-xl font-bold" style:color="hsl(var(--foreground, 0 0% 9%))">
|
||||||
|
{appName}
|
||||||
|
</h1>
|
||||||
|
<div class="mb-4 text-5xl">🔒</div>
|
||||||
|
<p
|
||||||
|
class="mb-6 text-[0.9375rem] leading-relaxed"
|
||||||
|
style:color="hsl(var(--muted-foreground, 0 0% 45%))"
|
||||||
|
>
|
||||||
|
{isDE
|
||||||
|
? 'Diese App ist aktuell in der geschlossenen '
|
||||||
|
: 'This app is currently in closed '}<strong>{requiredTierLabel}</strong>{isDE
|
||||||
|
? '-Phase.'
|
||||||
|
: ' phase.'}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mb-6 flex flex-col gap-2 rounded-xl p-4"
|
||||||
|
style:background-color="hsl(var(--muted, 0 0% 96%))"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span style:color="hsl(var(--muted-foreground, 0 0% 45%))"
|
||||||
|
>{isDE ? 'Dein Zugang:' : 'Your access:'}</span
|
||||||
|
>
|
||||||
|
<span class="font-semibold" style:color="hsl(var(--foreground, 0 0% 9%))"
|
||||||
|
>{userTierLabel}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span style:color="hsl(var(--muted-foreground, 0 0% 45%))"
|
||||||
|
>{isDE ? 'Benötigt:' : 'Required:'}</span
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-violet-500">{requiredTierLabel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<button
|
||||||
|
class="w-full cursor-pointer rounded-lg border-none px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-90"
|
||||||
|
style:background-color="hsl(var(--primary, 239 84% 67%))"
|
||||||
|
style:color="hsl(var(--primary-foreground, 0 0% 100%))"
|
||||||
|
onclick={() => goto('/')}
|
||||||
|
>
|
||||||
|
{isDE ? 'Zur Übersicht' : 'Back to overview'}
|
||||||
|
</button>
|
||||||
|
{#if !authStore.isAuthenticated}
|
||||||
|
<button
|
||||||
|
class="w-full cursor-pointer rounded-lg border px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-90"
|
||||||
|
style:border-color="hsl(var(--border, 0 0% 90%))"
|
||||||
|
style:color="hsl(var(--foreground, 0 0% 9%))"
|
||||||
|
onclick={() => goto('/login')}
|
||||||
|
>
|
||||||
|
{isDE ? 'Anmelden' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
/**
|
||||||
|
* AI Tier Selector — composable for the PillNavigation AI dropdown.
|
||||||
|
*
|
||||||
|
* Manages LLM tier toggles (browser/server/cloud) and STT model
|
||||||
|
* selection. Returns reactive PillDropdownItem arrays + labels.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { PillDropdownItem } from '@mana/shared-ui';
|
||||||
|
import { llmSettingsState, updateLlmSettings, type LlmTier } from '@mana/shared-llm';
|
||||||
|
import { isLocalLlmSupported, getLocalLlmStatus, loadLocalLlm } from '@mana/local-llm';
|
||||||
|
import {
|
||||||
|
getLocalSttStatus,
|
||||||
|
loadLocalStt,
|
||||||
|
isLocalSttSupported,
|
||||||
|
MODELS as STT_MODELS,
|
||||||
|
DEFAULT_MODEL as STT_DEFAULT_MODEL,
|
||||||
|
type ModelKey as SttModelKey,
|
||||||
|
} from '@mana/local-stt';
|
||||||
|
|
||||||
|
const TIER_TOGGLE_LIST: Array<{ tier: LlmTier; shortLabel: string; icon: string }> = [
|
||||||
|
{ tier: 'browser', shortLabel: 'Lokal (Gemma 4)', icon: 'robot' },
|
||||||
|
{ tier: 'mana-server', shortLabel: 'Server (Gemma 4)', icon: 'globe' },
|
||||||
|
{ tier: 'cloud', shortLabel: 'Cloud (Gemini)', icon: 'cloud' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function useAiTierItems() {
|
||||||
|
const webgpuSupported = isLocalLlmSupported();
|
||||||
|
const localLlmStatus = getLocalLlmStatus();
|
||||||
|
const sttSupported = isLocalSttSupported();
|
||||||
|
const localSttStatus = getLocalSttStatus();
|
||||||
|
let selectedSttModel = $state<SttModelKey>(STT_DEFAULT_MODEL);
|
||||||
|
const llmSettings = $derived(llmSettingsState.current);
|
||||||
|
|
||||||
|
function toggleAiTier(tier: LlmTier) {
|
||||||
|
const current = llmSettings.allowedTiers;
|
||||||
|
const next = current.includes(tier)
|
||||||
|
? current.filter((t: LlmTier) => t !== tier)
|
||||||
|
: [...current, tier];
|
||||||
|
updateLlmSettings({ allowedTiers: next });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLlmStatusItem(): PillDropdownItem {
|
||||||
|
const s = localLlmStatus.current;
|
||||||
|
const state = s.state;
|
||||||
|
let label: string;
|
||||||
|
let icon: string;
|
||||||
|
let danger = false;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'ready':
|
||||||
|
label = 'Geladen';
|
||||||
|
icon = 'checkCircle';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'downloading':
|
||||||
|
label = `Lade… ${((s as { progress: number }).progress * 100).toFixed(0)}%`;
|
||||||
|
icon = 'clock';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'loading':
|
||||||
|
label = 'Initialisiere…';
|
||||||
|
icon = 'clock';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'checking':
|
||||||
|
label = 'Prüfe…';
|
||||||
|
icon = 'clock';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
label = 'Fehler — erneut versuchen';
|
||||||
|
icon = 'bell';
|
||||||
|
danger = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
label = 'Modell laden';
|
||||||
|
icon = 'cloud';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'ai-browser-status',
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
group: 'local-llm',
|
||||||
|
danger,
|
||||||
|
disabled,
|
||||||
|
progress: state === 'downloading' ? (s as { progress: number }).progress : undefined,
|
||||||
|
onClick: !disabled ? () => void loadLocalLlm() : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSttStatusItem(): PillDropdownItem {
|
||||||
|
const s = localSttStatus.current;
|
||||||
|
const state = s.state;
|
||||||
|
let label: string;
|
||||||
|
let icon: string;
|
||||||
|
let danger = false;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'ready':
|
||||||
|
label = 'STT bereit';
|
||||||
|
icon = 'checkCircle';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'downloading':
|
||||||
|
label = `STT Lade… ${((s as { progress: number }).progress * 100).toFixed(0)}%`;
|
||||||
|
icon = 'clock';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'loading':
|
||||||
|
label = 'STT lädt…';
|
||||||
|
icon = 'clock';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'checking':
|
||||||
|
label = 'STT prüft…';
|
||||||
|
icon = 'clock';
|
||||||
|
disabled = true;
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
label = 'STT Fehler';
|
||||||
|
icon = 'bell';
|
||||||
|
danger = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
label = 'STT Modell laden';
|
||||||
|
icon = 'mic';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'stt-status',
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
danger,
|
||||||
|
disabled,
|
||||||
|
progress: state === 'downloading' ? (s as { progress: number }).progress : undefined,
|
||||||
|
onClick: !disabled ? () => void loadLocalStt(selectedSttModel) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = $derived<PillDropdownItem[]>([
|
||||||
|
// Tier toggles
|
||||||
|
...TIER_TOGGLE_LIST.filter((t) => t.tier !== 'browser' || webgpuSupported).map((t) => {
|
||||||
|
const isActive = llmSettings.allowedTiers.includes(t.tier);
|
||||||
|
return {
|
||||||
|
id: `ai-tier-${t.tier}`,
|
||||||
|
label: t.shortLabel,
|
||||||
|
icon: isActive ? 'checkCircle' : t.icon,
|
||||||
|
active: isActive,
|
||||||
|
onClick: () => toggleAiTier(t.tier),
|
||||||
|
...(t.tier === 'browser' ? { group: 'local-llm' } : {}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
// Browser model status (grouped with the "Lokal" toggle)
|
||||||
|
...(llmSettings.allowedTiers.includes('browser') && webgpuSupported
|
||||||
|
? [buildLlmStatusItem()]
|
||||||
|
: []),
|
||||||
|
// STT section
|
||||||
|
{ id: 'stt-divider', label: '', divider: true },
|
||||||
|
...(sttSupported
|
||||||
|
? (Object.entries(STT_MODELS) as [SttModelKey, (typeof STT_MODELS)[SttModelKey]][]).map(
|
||||||
|
([key, model]) => {
|
||||||
|
const isSelected = selectedSttModel === key;
|
||||||
|
return {
|
||||||
|
id: `stt-model-${key}`,
|
||||||
|
label: model.displayName,
|
||||||
|
icon: isSelected ? 'checkCircle' : 'mic',
|
||||||
|
active: isSelected,
|
||||||
|
onClick: () => {
|
||||||
|
selectedSttModel = key;
|
||||||
|
void loadLocalStt(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: []),
|
||||||
|
...(sttSupported ? [buildSttStatusItem()] : []),
|
||||||
|
// Settings link
|
||||||
|
{ id: 'ai-divider', label: '', divider: true },
|
||||||
|
{
|
||||||
|
id: 'ai-settings',
|
||||||
|
label: 'KI-Einstellungen',
|
||||||
|
icon: 'settings',
|
||||||
|
onClick: () => goto('/settings#ai-options'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const label = $derived.by(() => {
|
||||||
|
const active = llmSettings.allowedTiers;
|
||||||
|
if (active.length === 0) return 'Aus';
|
||||||
|
const sorted = [...active].sort(
|
||||||
|
(a, b) =>
|
||||||
|
TIER_TOGGLE_LIST.findIndex((t) => t.tier === a) -
|
||||||
|
TIER_TOGGLE_LIST.findIndex((t) => t.tier === b)
|
||||||
|
);
|
||||||
|
const first = TIER_TOGGLE_LIST.find((t) => t.tier === sorted[0]);
|
||||||
|
return first ? first.shortLabel.split(' (')[0] : 'KI';
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = $derived.by(() => {
|
||||||
|
const active = llmSettings.allowedTiers;
|
||||||
|
if (active.length === 0) return 'power';
|
||||||
|
const sorted = [...active].sort(
|
||||||
|
(a, b) =>
|
||||||
|
TIER_TOGGLE_LIST.findIndex((t) => t.tier === a) -
|
||||||
|
TIER_TOGGLE_LIST.findIndex((t) => t.tier === b)
|
||||||
|
);
|
||||||
|
const first = TIER_TOGGLE_LIST.find((t) => t.tier === sorted[0]);
|
||||||
|
return first ? first.icon : 'cpu';
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
get items() {
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
get label() {
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
get icon() {
|
||||||
|
return icon;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* Sync Status Dropdown — composable for the PillNavigation sync pill.
|
||||||
|
*
|
||||||
|
* Returns reactive PillDropdownItems showing sync subscription state
|
||||||
|
* (active / paused / inactive) with links to settings and credits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { PillDropdownItem } from '@mana/shared-ui';
|
||||||
|
import { syncBilling } from '$lib/stores/sync-billing.svelte';
|
||||||
|
|
||||||
|
export function useSyncStatusItems() {
|
||||||
|
const items = $derived.by(() => {
|
||||||
|
const result: PillDropdownItem[] = [];
|
||||||
|
|
||||||
|
if (syncBilling.active) {
|
||||||
|
result.push({
|
||||||
|
id: 'sync-active',
|
||||||
|
label: 'Cloud Sync aktiv',
|
||||||
|
icon: 'cloud',
|
||||||
|
active: true,
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
if (syncBilling.nextChargeAt) {
|
||||||
|
const date = new Date(syncBilling.nextChargeAt).toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
result.push({
|
||||||
|
id: 'sync-next',
|
||||||
|
label: `Nächste Abbuchung: ${date}`,
|
||||||
|
icon: 'calendar',
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (syncBilling.paused) {
|
||||||
|
result.push({
|
||||||
|
id: 'sync-paused',
|
||||||
|
label: 'Sync pausiert — Credits aufladen',
|
||||||
|
icon: 'bell',
|
||||||
|
onClick: () => goto('/credits?tab=packages'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
id: 'sync-inactive',
|
||||||
|
label: 'Sync aktivieren',
|
||||||
|
icon: 'cloud',
|
||||||
|
onClick: () => goto('/settings/sync'),
|
||||||
|
});
|
||||||
|
result.push({
|
||||||
|
id: 'sync-info',
|
||||||
|
label: 'Nur lokal — ab 30 Credits/Monat',
|
||||||
|
icon: 'creditCard',
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({ id: 'sync-divider', label: '', divider: true });
|
||||||
|
result.push({
|
||||||
|
id: 'sync-settings',
|
||||||
|
label: 'Sync-Einstellungen',
|
||||||
|
icon: 'settings',
|
||||||
|
onClick: () => goto('/settings/sync'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = $derived(
|
||||||
|
syncBilling.loading
|
||||||
|
? '...'
|
||||||
|
: syncBilling.active
|
||||||
|
? 'Sync'
|
||||||
|
: syncBilling.paused
|
||||||
|
? 'Pausiert'
|
||||||
|
: 'Lokal'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get items() {
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
get label() {
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -160,7 +160,7 @@ export const APP_POSITIONS: Record<string, { x: number; y: number; lakeId: strin
|
||||||
photos: { x: 1110, y: 575, lakeId: 'db-right' },
|
photos: { x: 1110, y: 575, lakeId: 'db-right' },
|
||||||
skilltree: { x: 1310, y: 590, lakeId: 'db-right' },
|
skilltree: { x: 1310, y: 590, lakeId: 'db-right' },
|
||||||
context: { x: 1320, y: 660, lakeId: 'db-right' },
|
context: { x: 1320, y: 660, lakeId: 'db-right' },
|
||||||
planta: { x: 1115, y: 675, lakeId: 'db-right' },
|
plants: { x: 1115, y: 675, lakeId: 'db-right' },
|
||||||
|
|
||||||
// Around Bergsee (redis) - lightweight/cache
|
// Around Bergsee (redis) - lightweight/cache
|
||||||
traces: { x: 400, y: 385, lakeId: 'redis' },
|
traces: { x: 400, y: 385, lakeId: 'redis' },
|
||||||
|
|
|
||||||
|
|
@ -266,8 +266,8 @@ const APP_DEFINITIONS: AppDefinition[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'planta',
|
id: 'plants',
|
||||||
displayName: 'Planta',
|
displayName: 'Plants',
|
||||||
score: 50,
|
score: 50,
|
||||||
status: 'alpha',
|
status: 'alpha',
|
||||||
categories: {
|
categories: {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
zitare: 'https://zitare.mana.how',
|
zitare: 'https://zitare.mana.how',
|
||||||
mana: 'https://mana.how',
|
mana: 'https://mana.how',
|
||||||
cards: 'https://cards.mana.how',
|
cards: 'https://cards.mana.how',
|
||||||
planta: 'https://planta.mana.how',
|
plants: 'https://plants.mana.how',
|
||||||
playground: 'https://playground.mana.how',
|
playground: 'https://playground.mana.how',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
minio: 'Objekt-Speicher fur Dateien, Bilder und Medien aller Apps.',
|
minio: 'Objekt-Speicher fur Dateien, Bilder und Medien aller Apps.',
|
||||||
'db-left': 'PostgreSQL-Datenbanken fur Calendar, Todo, Contacts, Storage.',
|
'db-left': 'PostgreSQL-Datenbanken fur Calendar, Todo, Contacts, Storage.',
|
||||||
'db-center': 'PostgreSQL-Datenbanken fur Zitare, Music, Clock, NutriPhi.',
|
'db-center': 'PostgreSQL-Datenbanken fur Zitare, Music, Clock, NutriPhi.',
|
||||||
'db-right': 'PostgreSQL-Datenbanken fur Photos, SkillTree, Context, Planta.',
|
'db-right': 'PostgreSQL-Datenbanken fur Photos, SkillTree, Context, Plants.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const lakeIcons: Record<string, string> = {
|
const lakeIcons: Record<string, string> = {
|
||||||
|
|
|
||||||
|
|
@ -151,16 +151,8 @@ export function useLocalStt(options?: { language?: string }): LocalSttHandle {
|
||||||
const capturedRate = sampleRate;
|
const capturedRate = sampleRate;
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[local-stt] Captured',
|
|
||||||
capturedChunks.length,
|
|
||||||
'chunks, sample rate:',
|
|
||||||
capturedRate
|
|
||||||
);
|
|
||||||
|
|
||||||
if (capturedChunks.length === 0) {
|
if (capturedChunks.length === 0) {
|
||||||
error = 'Keine Audiodaten aufgenommen.';
|
error = 'Keine Audiodaten aufgenommen.';
|
||||||
console.warn('[local-stt] No audio chunks captured');
|
|
||||||
state = 'idle';
|
state = 'idle';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -180,25 +172,14 @@ export function useLocalStt(options?: { language?: string }): LocalSttHandle {
|
||||||
// Resample to 16 kHz if needed
|
// Resample to 16 kHz if needed
|
||||||
const audio = capturedRate === 16000 ? merged : resample(merged, capturedRate, 16000);
|
const audio = capturedRate === 16000 ? merged : resample(merged, capturedRate, 16000);
|
||||||
|
|
||||||
const durationSec = audio.length / 16000;
|
|
||||||
console.log('[local-stt] Audio ready:', {
|
|
||||||
originalSamples: merged.length,
|
|
||||||
resampledSamples: audio.length,
|
|
||||||
durationSec: durationSec.toFixed(1),
|
|
||||||
sampleRate: capturedRate,
|
|
||||||
maxAmplitude: Math.max(...Array.from(audio.slice(0, 16000)).map(Math.abs)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await transcribe({
|
const result = await transcribe({
|
||||||
audio,
|
audio,
|
||||||
language: options?.language,
|
language: options?.language,
|
||||||
onChunk: (t: string) => {
|
onChunk: (t: string) => {
|
||||||
partial += t;
|
partial += t;
|
||||||
console.log('[local-stt] Chunk:', t);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[local-stt] Result:', result);
|
|
||||||
text = result.text.trim();
|
text = result.text.trim();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : String(e);
|
error = e instanceof Error ? e.message : String(e);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
| 3 | Client Wire-up: vault-client, record-helpers, layout integration | ✅ | `354cbcb17` |
|
| 3 | Client Wire-up: vault-client, record-helpers, layout integration | ✅ | `354cbcb17` |
|
||||||
| 4 | Pilot: notes table mit 8 End-to-End Tests | ✅ | `bed08a1aa` |
|
| 4 | Pilot: notes table mit 8 End-to-End Tests | ✅ | `bed08a1aa` |
|
||||||
| 5 | Rollout: chat, dreams, memoro, contacts, cycles, finance | ✅ | `af92720a6` |
|
| 5 | Rollout: chat, dreams, memoro, contacts, cycles, finance | ✅ | `af92720a6` |
|
||||||
| 6.1 | Rollout: cards, presi, inventar, planta | ✅ | `73f294b29` |
|
| 6.1 | Rollout: cards, presi, inventar, plants | ✅ | `73f294b29` |
|
||||||
| 6.2 + 6.3 | Settings UI (`/settings/security`) + Encryption Intro Banner | ✅ | `6b8e2c717` |
|
| 6.2 + 6.3 | Settings UI (`/settings/security`) + Encryption Intro Banner | ✅ | `6b8e2c717` |
|
||||||
| Roundup | DATA_LAYER_AUDIT roll-up vor Phase 7 | ✅ | `4bdf4238c` |
|
| Roundup | DATA_LAYER_AUDIT roll-up vor Phase 7 | ✅ | `4bdf4238c` |
|
||||||
| 7.1 | timeBlocks-Hub: tasks + calendar.events + timeBlocks (mit Habits-Coupling) | ✅ | `c875b4e96` |
|
| 7.1 | timeBlocks-Hub: tasks + calendar.events + timeBlocks (mit Habits-Coupling) | ✅ | `c875b4e96` |
|
||||||
|
|
@ -396,7 +396,7 @@ Unlock-Flow (Login auf neuem Gerät):
|
||||||
| presi | `presiDecks` | `title`, `description` | 6.1 |
|
| presi | `presiDecks` | `title`, `description` | 6.1 |
|
||||||
| | `slides` | `content` (SlideContent JSON) | 6.1 |
|
| | `slides` | `content` (SlideContent JSON) | 6.1 |
|
||||||
| inventar | `invItems` | `description` (name + notes-array bleiben plaintext) | 6.1 |
|
| inventar | `invItems` | `description` (name + notes-array bleiben plaintext) | 6.1 |
|
||||||
| planta | `plants` | `name`, `careNotes`, `temperature`, `soilType` | 6.1 |
|
| plants | `plants` | `name`, `careNotes`, `temperature`, `soilType` | 6.1 |
|
||||||
| **todo** | **`tasks`** | **`title`, `description`, `subtasks`, `metadata`** | **7.1** |
|
| **todo** | **`tasks`** | **`title`, `description`, `subtasks`, `metadata`** | **7.1** |
|
||||||
| **calendar** | **`events`** | **`title`, `description`, `location`** | **7.1** |
|
| **calendar** | **`events`** | **`title`, `description`, `location`** | **7.1** |
|
||||||
| **time-blocks** | **`timeBlocks`** | **`title`, `description`** (Cross-Module-Hub für todo/calendar/habits/times) | **7.1** |
|
| **time-blocks** | **`timeBlocks`** | **`title`, `description`** (Cross-Module-Hub für todo/calendar/habits/times) | **7.1** |
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,11 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
|
||||||
// - photoMediaId / photoUrl / photoThumbnailUrl: opaque pointers to
|
// - photoMediaId / photoUrl / photoThumbnailUrl: opaque pointers to
|
||||||
// mana-media; the URL alone is not PII (anyone with the URL
|
// mana-media; the URL alone is not PII (anyone with the URL
|
||||||
// already has the bytes), and CAS-deduped media IDs leak no user
|
// already has the bytes), and CAS-deduped media IDs leak no user
|
||||||
// content. Same rationale planta uses for plantPhotos.
|
// content. Same rationale plants uses for plantPhotos.
|
||||||
// - confidence (float 0-1): pure metadata about the AI run.
|
// - confidence (float 0-1): pure metadata about the AI run.
|
||||||
meals: { enabled: true, fields: ['description', 'portionSize', 'foods'] },
|
meals: { enabled: true, fields: ['description', 'portionSize', 'foods'] },
|
||||||
|
|
||||||
// ─── Planta ──────────────────────────────────────────────
|
// ─── Plants ──────────────────────────────────────────────
|
||||||
// `name` is NOT in the schema index for plants (only isActive +
|
// `name` is NOT in the schema index for plants (only isActive +
|
||||||
// healthStatus), so encrypting it is safe. LocalPlant uses
|
// healthStatus), so encrypting it is safe. LocalPlant uses
|
||||||
// `careNotes` (no separate `notes`) plus the user-typed metadata.
|
// `careNotes` (no separate `notes`) plus the user-typed metadata.
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ db.version(1).stores({
|
||||||
nutriFavorites: 'id, mealType, usageCount',
|
nutriFavorites: 'id, mealType, usageCount',
|
||||||
mealTags: 'id, mealId, tagId, [mealId+tagId]',
|
mealTags: 'id, mealId, tagId, [mealId+tagId]',
|
||||||
|
|
||||||
// ─── Planta (appId: 'planta') ───
|
// ─── Plants (appId: 'plants') ───
|
||||||
plants: 'id, isActive, healthStatus',
|
plants: 'id, isActive, healthStatus',
|
||||||
plantPhotos: 'id, plantId, isPrimary, [plantId+isPrimary]',
|
plantPhotos: 'id, plantId, isPrimary, [plantId+isPrimary]',
|
||||||
wateringSchedules: 'id, plantId, nextWateringAt',
|
wateringSchedules: 'id, plantId, nextWateringAt',
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ describe('module-registry — pre-refactor snapshot', () => {
|
||||||
context: ['contextSpaces', 'documents', 'documentTags'],
|
context: ['contextSpaces', 'documents', 'documentTags'],
|
||||||
questions: ['qCollections', 'questions', 'answers', 'questionTags'],
|
questions: ['qCollections', 'questions', 'answers', 'questionTags'],
|
||||||
nutriphi: ['meals', 'goals', 'nutriFavorites', 'mealTags'],
|
nutriphi: ['meals', 'goals', 'nutriFavorites', 'mealTags'],
|
||||||
planta: ['plants', 'plantPhotos', 'wateringSchedules', 'wateringLogs', 'plantTags'],
|
plants: ['plants', 'plantPhotos', 'wateringSchedules', 'wateringLogs', 'plantTags'],
|
||||||
uload: ['links', 'uloadTags', 'uloadFolders', 'linkTags'],
|
uload: ['links', 'uloadTags', 'uloadFolders', 'linkTags'],
|
||||||
calc: ['calculations', 'savedFormulas'],
|
calc: ['calculations', 'savedFormulas'],
|
||||||
moodlit: ['moods', 'sequences', 'moodTags'],
|
moodlit: ['moods', 'sequences', 'moodTags'],
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ import { timesModuleConfig } from '$lib/modules/times/module.config';
|
||||||
import { contextModuleConfig } from '$lib/modules/context/module.config';
|
import { contextModuleConfig } from '$lib/modules/context/module.config';
|
||||||
import { questionsModuleConfig } from '$lib/modules/questions/module.config';
|
import { questionsModuleConfig } from '$lib/modules/questions/module.config';
|
||||||
import { nutriphiModuleConfig } from '$lib/modules/nutriphi/module.config';
|
import { nutriphiModuleConfig } from '$lib/modules/nutriphi/module.config';
|
||||||
import { plantaModuleConfig } from '$lib/modules/planta/module.config';
|
import { plantsModuleConfig } from '$lib/modules/plants/module.config';
|
||||||
import { uloadModuleConfig } from '$lib/modules/uload/module.config';
|
import { uloadModuleConfig } from '$lib/modules/uload/module.config';
|
||||||
import { calcModuleConfig } from '$lib/modules/calc/module.config';
|
import { calcModuleConfig } from '$lib/modules/calc/module.config';
|
||||||
import { moodlitModuleConfig } from '$lib/modules/moodlit/module.config';
|
import { moodlitModuleConfig } from '$lib/modules/moodlit/module.config';
|
||||||
|
|
@ -113,7 +113,7 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [
|
||||||
contextModuleConfig,
|
contextModuleConfig,
|
||||||
questionsModuleConfig,
|
questionsModuleConfig,
|
||||||
nutriphiModuleConfig,
|
nutriphiModuleConfig,
|
||||||
plantaModuleConfig,
|
plantsModuleConfig,
|
||||||
uloadModuleConfig,
|
uloadModuleConfig,
|
||||||
calcModuleConfig,
|
calcModuleConfig,
|
||||||
moodlitModuleConfig,
|
moodlitModuleConfig,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import { SKILLTREE_GUEST_SEED } from '$lib/modules/skilltree/collections';
|
||||||
import { TODO_GUEST_SEED } from '$lib/modules/todo/collections';
|
import { TODO_GUEST_SEED } from '$lib/modules/todo/collections';
|
||||||
import { NOTES_GUEST_SEED } from '$lib/modules/notes/collections';
|
import { NOTES_GUEST_SEED } from '$lib/modules/notes/collections';
|
||||||
import { TIMES_GUEST_SEED } from '$lib/modules/times/collections';
|
import { TIMES_GUEST_SEED } from '$lib/modules/times/collections';
|
||||||
import { PLANTA_GUEST_SEED } from '$lib/modules/planta/collections';
|
import { PLANTS_GUEST_SEED } from '$lib/modules/plants/collections';
|
||||||
import { DRINK_GUEST_SEED } from '$lib/modules/drink/collections';
|
import { DRINK_GUEST_SEED } from '$lib/modules/drink/collections';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,7 +60,7 @@ register(SKILLTREE_GUEST_SEED);
|
||||||
register(TODO_GUEST_SEED);
|
register(TODO_GUEST_SEED);
|
||||||
register(NOTES_GUEST_SEED);
|
register(NOTES_GUEST_SEED);
|
||||||
register(TIMES_GUEST_SEED);
|
register(TIMES_GUEST_SEED);
|
||||||
register(PLANTA_GUEST_SEED);
|
register(PLANTS_GUEST_SEED);
|
||||||
register(DRINK_GUEST_SEED);
|
register(DRINK_GUEST_SEED);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export type TimeBlockSourceModule =
|
||||||
| 'habits'
|
| 'habits'
|
||||||
| 'events'
|
| 'events'
|
||||||
| 'body'
|
| 'body'
|
||||||
| 'planta'
|
| 'plants'
|
||||||
| 'dreams'
|
| 'dreams'
|
||||||
| 'skilltree'
|
| 'skilltree'
|
||||||
| 'cycles'
|
| 'cycles'
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ function registerLocale(lang: SupportedLocale) {
|
||||||
inventory,
|
inventory,
|
||||||
photos,
|
photos,
|
||||||
nutriphi,
|
nutriphi,
|
||||||
planta,
|
plants,
|
||||||
skilltree,
|
skilltree,
|
||||||
citycorners,
|
citycorners,
|
||||||
calc,
|
calc,
|
||||||
|
|
@ -77,7 +77,7 @@ function registerLocale(lang: SupportedLocale) {
|
||||||
import(`./locales/inventory/${lang}.json`),
|
import(`./locales/inventory/${lang}.json`),
|
||||||
import(`./locales/photos/${lang}.json`),
|
import(`./locales/photos/${lang}.json`),
|
||||||
import(`./locales/nutriphi/${lang}.json`),
|
import(`./locales/nutriphi/${lang}.json`),
|
||||||
import(`./locales/planta/${lang}.json`),
|
import(`./locales/plants/${lang}.json`),
|
||||||
import(`./locales/skilltree/${lang}.json`),
|
import(`./locales/skilltree/${lang}.json`),
|
||||||
import(`./locales/citycorners/${lang}.json`),
|
import(`./locales/citycorners/${lang}.json`),
|
||||||
import(`./locales/calc/${lang}.json`),
|
import(`./locales/calc/${lang}.json`),
|
||||||
|
|
@ -115,7 +115,7 @@ function registerLocale(lang: SupportedLocale) {
|
||||||
inventory: inventory.default,
|
inventory: inventory.default,
|
||||||
photos: photos.default,
|
photos: photos.default,
|
||||||
nutriphi: nutriphi.default,
|
nutriphi: nutriphi.default,
|
||||||
planta: planta.default,
|
plants: plants.default,
|
||||||
skilltree: skilltree.default,
|
skilltree: skilltree.default,
|
||||||
citycorners: citycorners.default,
|
citycorners: citycorners.default,
|
||||||
calc: calc.default,
|
calc: calc.default,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
"picture": "Bilder",
|
"picture": "Bilder",
|
||||||
"music": "Musik",
|
"music": "Musik",
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"storage": "Ablage",
|
"storage": "Speicher",
|
||||||
"nutriphi": "NutriPhi",
|
"nutriphi": "NutriPhi",
|
||||||
"planta": "Planta",
|
"plants": "Pflanzen",
|
||||||
"presi": "Presi",
|
"presi": "Presi",
|
||||||
"inventory": "Inventar",
|
"inventory": "Inventar",
|
||||||
"memoro": "Memoro",
|
"memoro": "Memoro",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"storage": "Storage",
|
"storage": "Storage",
|
||||||
"nutriphi": "NutriPhi",
|
"nutriphi": "NutriPhi",
|
||||||
"planta": "Planta",
|
"plants": "Plants",
|
||||||
"presi": "Presi",
|
"presi": "Presi",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"memoro": "Memoro",
|
"memoro": "Memoro",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"photos": "Fotos",
|
"photos": "Fotos",
|
||||||
"storage": "Almacén",
|
"storage": "Almacén",
|
||||||
"nutriphi": "NutriPhi",
|
"nutriphi": "NutriPhi",
|
||||||
"planta": "Planta",
|
"plants": "Plantas",
|
||||||
"presi": "Presi",
|
"presi": "Presi",
|
||||||
"inventory": "Inventario",
|
"inventory": "Inventario",
|
||||||
"memoro": "Memoro",
|
"memoro": "Memoro",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"photos": "Photos",
|
"photos": "Photos",
|
||||||
"storage": "Stockage",
|
"storage": "Stockage",
|
||||||
"nutriphi": "NutriPhi",
|
"nutriphi": "NutriPhi",
|
||||||
"planta": "Planta",
|
"plants": "Plantes",
|
||||||
"presi": "Presi",
|
"presi": "Presi",
|
||||||
"inventory": "Inventaire",
|
"inventory": "Inventaire",
|
||||||
"memoro": "Memoro",
|
"memoro": "Memoro",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"photos": "Foto",
|
"photos": "Foto",
|
||||||
"storage": "Archivio",
|
"storage": "Archivio",
|
||||||
"nutriphi": "NutriPhi",
|
"nutriphi": "NutriPhi",
|
||||||
"planta": "Planta",
|
"plants": "Piante",
|
||||||
"presi": "Presi",
|
"presi": "Presi",
|
||||||
"inventory": "Inventario",
|
"inventory": "Inventario",
|
||||||
"memoro": "Memoro",
|
"memoro": "Memoro",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"name": "Planta",
|
"name": "Plants",
|
||||||
"loading": "Laden...",
|
"loading": "Laden...",
|
||||||
"tagline": "Pflanzenpflege leicht gemacht"
|
"tagline": "Pflanzenpflege leicht gemacht"
|
||||||
},
|
},
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"name": "Planta",
|
"name": "Plants",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"tagline": "Plant care made easy"
|
"tagline": "Plant care made easy"
|
||||||
},
|
},
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"name": "Planta",
|
"name": "Plants",
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
"tagline": "Cuidado de plantas simplificado"
|
"tagline": "Cuidado de plantas simplificado"
|
||||||
},
|
},
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"name": "Planta",
|
"name": "Plants",
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"tagline": "L'entretien des plantes simplifié"
|
"tagline": "L'entretien des plantes simplifié"
|
||||||
},
|
},
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"name": "Planta",
|
"name": "Plants",
|
||||||
"loading": "Caricamento...",
|
"loading": "Caricamento...",
|
||||||
"tagline": "Cura delle piante semplificata"
|
"tagline": "Cura delle piante semplificata"
|
||||||
},
|
},
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
export { automationsStore } from './stores/automations.svelte';
|
export { automationsStore } from './stores/automations.svelte';
|
||||||
export { automationTable } from './collections';
|
export { automationTable } from './collections';
|
||||||
|
export { useAllAutomations, useEnabledAutomations, toAutomation } from './queries';
|
||||||
|
export type { Automation } from './queries';
|
||||||
export type { LocalAutomation, SourceOption, ActionOption } from './types';
|
export type { LocalAutomation, SourceOption, ActionOption } from './types';
|
||||||
export { SOURCE_OPTIONS, ACTION_OPTIONS, CONDITION_OPS } from './types';
|
export { SOURCE_OPTIONS, ACTION_OPTIONS, CONDITION_OPS } from './types';
|
||||||
|
|
|
||||||
63
apps/mana/apps/web/src/lib/modules/automations/queries.ts
Normal file
63
apps/mana/apps/web/src/lib/modules/automations/queries.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* Reactive Queries for Automations module.
|
||||||
|
*
|
||||||
|
* Uses useLiveQueryWithDefault on the unified database.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
|
||||||
|
import { automationTable } from './collections';
|
||||||
|
import type { LocalAutomation } from './types';
|
||||||
|
|
||||||
|
// ─── Type Converter ──────────────────────────────────────
|
||||||
|
|
||||||
|
export interface Automation {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
sourceApp: string;
|
||||||
|
sourceCollection: string;
|
||||||
|
sourceOp: 'insert' | 'update';
|
||||||
|
conditionField?: string;
|
||||||
|
conditionOp?: string;
|
||||||
|
conditionValue?: string;
|
||||||
|
targetApp: string;
|
||||||
|
targetAction: string;
|
||||||
|
targetParams?: Record<string, string>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toAutomation(local: LocalAutomation): Automation {
|
||||||
|
return {
|
||||||
|
id: local.id,
|
||||||
|
name: local.name,
|
||||||
|
enabled: local.enabled,
|
||||||
|
sourceApp: local.sourceApp,
|
||||||
|
sourceCollection: local.sourceCollection,
|
||||||
|
sourceOp: local.sourceOp,
|
||||||
|
conditionField: local.conditionField,
|
||||||
|
conditionOp: local.conditionOp,
|
||||||
|
conditionValue: local.conditionValue,
|
||||||
|
targetApp: local.targetApp,
|
||||||
|
targetAction: local.targetAction,
|
||||||
|
targetParams: local.targetParams,
|
||||||
|
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||||
|
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Live Queries ────────────────────────────────────────
|
||||||
|
|
||||||
|
export function useAllAutomations() {
|
||||||
|
return useLiveQueryWithDefault(async () => {
|
||||||
|
const locals = await automationTable.toArray();
|
||||||
|
return locals.filter((a) => !a.deletedAt).map(toAutomation);
|
||||||
|
}, [] as Automation[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEnabledAutomations() {
|
||||||
|
return useLiveQueryWithDefault(async () => {
|
||||||
|
const locals = await automationTable.toArray();
|
||||||
|
return locals.filter((a) => !a.deletedAt && a.enabled).map(toAutomation);
|
||||||
|
}, [] as Automation[]);
|
||||||
|
}
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
<div class="mb-2 text-3xl">🌱</div>
|
<div class="mb-2 text-3xl">🌱</div>
|
||||||
<p class="text-sm text-muted-foreground">Noch keine Pflanzen angelegt.</p>
|
<p class="text-sm text-muted-foreground">Noch keine Pflanzen angelegt.</p>
|
||||||
<a
|
<a
|
||||||
href="/planta"
|
href="/plants"
|
||||||
class="mt-3 inline-block rounded-lg bg-primary/10 px-4 py-2 text-sm font-medium text-primary hover:bg-primary/20"
|
class="mt-3 inline-block rounded-lg bg-primary/10 px-4 py-2 text-sm font-medium text-primary hover:bg-primary/20"
|
||||||
>
|
>
|
||||||
Pflanze hinzufügen
|
Pflanze hinzufügen
|
||||||
|
|
@ -140,7 +140,7 @@
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each plantsToWater as plant (plant.id)}
|
{#each plantsToWater as plant (plant.id)}
|
||||||
<a
|
<a
|
||||||
href="/planta"
|
href="/plants"
|
||||||
class="flex items-center gap-3 rounded-lg p-2 transition-colors hover:bg-surface-hover"
|
class="flex items-center gap-3 rounded-lg p-2 transition-colors hover:bg-surface-hover"
|
||||||
>
|
>
|
||||||
<span class="text-lg">{@html getHealthIcon(plant.healthStatus)}</span>
|
<span class="text-lg">{@html getHealthIcon(plant.healthStatus)}</span>
|
||||||
|
|
@ -157,7 +157,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/planta" class="mt-3 block text-center text-sm text-primary hover:underline">
|
<a href="/plants" class="mt-3 block text-center text-sm text-primary hover:underline">
|
||||||
Alle Pflanzen anzeigen
|
Alle Pflanzen anzeigen
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
// Database hooks call into funnel-tracking + trigger registry on every write.
|
// Database hooks call into funnel-tracking + trigger registry on every write.
|
||||||
// They reach for browser-only globals (localStorage), so stub them the same
|
// They reach for browser-only globals (localStorage), so stub them the same
|
||||||
// way the planta tests do.
|
// way the plants tests do.
|
||||||
vi.mock('$lib/stores/funnel-tracking', () => ({ trackFirstContent: vi.fn() }));
|
vi.mock('$lib/stores/funnel-tracking', () => ({ trackFirstContent: vi.fn() }));
|
||||||
vi.mock('$lib/triggers/registry', () => ({ fire: vi.fn() }));
|
vi.mock('$lib/triggers/registry', () => ({ fire: vi.fn() }));
|
||||||
vi.mock('$lib/triggers/inline-suggest', () => ({
|
vi.mock('$lib/triggers/inline-suggest', () => ({
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function useAllMeals() {
|
||||||
/**
|
/**
|
||||||
* Look up a single meal by id and decrypt it. Used by the detail page,
|
* Look up a single meal by id and decrypt it. Used by the detail page,
|
||||||
* which inlines its own useLiveQueryWithDefault wrapper so the querier
|
* which inlines its own useLiveQueryWithDefault wrapper so the querier
|
||||||
* can capture the route param directly (matches planta DetailView pattern).
|
* can capture the route param directly (matches plants DetailView pattern).
|
||||||
*/
|
*/
|
||||||
export async function loadMealById(id: string): Promise<MealWithNutrition | null> {
|
export async function loadMealById(id: string): Promise<MealWithNutrition | null> {
|
||||||
const local = await db.table<LocalMeal>('meals').get(id);
|
const local = await db.table<LocalMeal>('meals').get(id);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!--
|
<!--
|
||||||
Planta — Workbench ListView
|
Plants — Workbench ListView
|
||||||
Plant overview with watering schedule.
|
Plant overview with watering schedule.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseListView items={plants} getKey={(p) => p.id} emptyTitle={$_('planta.list.empty')}>
|
<BaseListView items={plants} getKey={(p) => p.id} emptyTitle={$_('plants.list.empty')}>
|
||||||
{#snippet toolbar()}
|
{#snippet toolbar()}
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
|
|
@ -78,8 +78,8 @@
|
||||||
onclick={() => (creating = !creating)}
|
onclick={() => (creating = !creating)}
|
||||||
>
|
>
|
||||||
{creating
|
{creating
|
||||||
? $_('planta.create.cancel', { default: 'Abbrechen' })
|
? $_('plants.create.cancel', { default: 'Abbrechen' })
|
||||||
: $_('planta.create.new', { default: '+ Neue Pflanze' })}
|
: $_('plants.create.new', { default: '+ Neue Pflanze' })}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,14 +88,14 @@
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newName}
|
bind:value={newName}
|
||||||
placeholder={$_('planta.create.namePlaceholder', { default: 'Name (z. B. Monstera)' })}
|
placeholder={$_('plants.create.namePlaceholder', { default: 'Name (z. B. Monstera)' })}
|
||||||
required
|
required
|
||||||
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none"
|
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newScientific}
|
bind:value={newScientific}
|
||||||
placeholder={$_('planta.create.scientificPlaceholder', {
|
placeholder={$_('plants.create.scientificPlaceholder', {
|
||||||
default: 'Botanischer Name (optional)',
|
default: 'Botanischer Name (optional)',
|
||||||
})}
|
})}
|
||||||
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none"
|
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none"
|
||||||
|
|
@ -105,22 +105,22 @@
|
||||||
class="rounded-md bg-green-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
|
class="rounded-md bg-green-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
disabled={!newName.trim()}
|
disabled={!newName.trim()}
|
||||||
>
|
>
|
||||||
{$_('planta.create.save', { default: 'Pflanze anlegen' })}
|
{$_('plants.create.save', { default: 'Pflanze anlegen' })}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet header()}
|
{#snippet header()}
|
||||||
<span>{$_('planta.list.count', { values: { count: plants.length } })}</span>
|
<span>{$_('plants.list.count', { values: { count: plants.length } })}</span>
|
||||||
{#if dueForWatering.length > 0}
|
{#if dueForWatering.length > 0}
|
||||||
<span class="text-blue-400"
|
<span class="text-blue-400"
|
||||||
>{$_('planta.list.dueWatering', { values: { count: dueForWatering.length } })}</span
|
>{$_('plants.list.dueWatering', { values: { count: dueForWatering.length } })}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if needsAttention.length > 0}
|
{#if needsAttention.length > 0}
|
||||||
<span class="text-amber-400"
|
<span class="text-amber-400"
|
||||||
>{$_('planta.list.needsCare', { values: { count: needsAttention.length } })}</span
|
>{$_('plants.list.needsCare', { values: { count: needsAttention.length } })}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
</div>
|
</div>
|
||||||
{#if schedule}
|
{#if schedule}
|
||||||
<p class="mt-1 text-xs text-white/30">
|
<p class="mt-1 text-xs text-white/30">
|
||||||
{$_('planta.list.everyXDays', { values: { days: schedule.frequencyDays } })}
|
{$_('plants.list.everyXDays', { values: { days: schedule.frequencyDays } })}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta — server-only API client
|
* Plants — server-only API client
|
||||||
*
|
*
|
||||||
* CRUD lives in IndexedDB + sync. This module talks to mana-api for the
|
* CRUD lives in IndexedDB + sync. This module talks to mana-api for the
|
||||||
* two server-only operations: photo upload (S3 via mana-media) and AI
|
* two server-only operations: photo upload (S3 via mana-media) and AI
|
||||||
|
|
@ -53,7 +53,7 @@ export async function uploadPlantPhoto(file: File, plantId: string): Promise<Upl
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('plantId', plantId);
|
formData.append('plantId', plantId);
|
||||||
|
|
||||||
const res = await fetch(`${getManaApiUrl()}/api/v1/planta/photos/upload`, {
|
const res = await fetch(`${getManaApiUrl()}/api/v1/plants/photos/upload`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: await authHeader(),
|
headers: await authHeader(),
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
@ -69,7 +69,7 @@ export async function uploadPlantPhoto(file: File, plantId: string): Promise<Upl
|
||||||
|
|
||||||
/** Run AI identification on a previously uploaded photo URL. */
|
/** Run AI identification on a previously uploaded photo URL. */
|
||||||
export async function identifyPlant(photoUrl: string): Promise<IdentifyResult> {
|
export async function identifyPlant(photoUrl: string): Promise<IdentifyResult> {
|
||||||
const res = await fetch(`${getManaApiUrl()}/api/v1/planta/analysis/identify`, {
|
const res = await fetch(`${getManaApiUrl()}/api/v1/plants/analysis/identify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta module — collection accessors and guest seed data.
|
* Plants module — collection accessors and guest seed data.
|
||||||
*
|
*
|
||||||
* Tables are already defined in the unified database (database.ts):
|
* Tables are already defined in the unified database (database.ts):
|
||||||
* plants, plantPhotos, wateringSchedules, wateringLogs
|
* plants, plantPhotos, wateringSchedules, wateringLogs
|
||||||
|
|
@ -19,7 +19,7 @@ export const wateringLogTable = db.table<LocalWateringLog>('wateringLogs');
|
||||||
|
|
||||||
const DEMO_PLANT_ID = 'demo-monstera';
|
const DEMO_PLANT_ID = 'demo-monstera';
|
||||||
|
|
||||||
export const PLANTA_GUEST_SEED = {
|
export const PLANTS_GUEST_SEED = {
|
||||||
plants: [
|
plants: [
|
||||||
{
|
{
|
||||||
id: DEMO_PLANT_ID,
|
id: DEMO_PLANT_ID,
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta module — barrel exports.
|
* Plants module — barrel exports.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Collections & seed data
|
// Collections & seed data
|
||||||
|
|
@ -8,7 +8,7 @@ export {
|
||||||
plantPhotoTable,
|
plantPhotoTable,
|
||||||
wateringScheduleTable,
|
wateringScheduleTable,
|
||||||
wateringLogTable,
|
wateringLogTable,
|
||||||
PLANTA_GUEST_SEED,
|
PLANTS_GUEST_SEED,
|
||||||
} from './collections';
|
} from './collections';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
@ -65,4 +65,4 @@ export {
|
||||||
type ParsedPlant,
|
type ParsedPlant,
|
||||||
type ParsedPlantWithIds,
|
type ParsedPlantWithIds,
|
||||||
} from './utils/plant-parser';
|
} from './utils/plant-parser';
|
||||||
export { PLANTA_SYNTAX } from './utils/syntax-help';
|
export { PLANTS_SYNTAX } from './utils/syntax-help';
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ModuleConfig } from '$lib/data/module-registry';
|
import type { ModuleConfig } from '$lib/data/module-registry';
|
||||||
|
|
||||||
export const plantaModuleConfig: ModuleConfig = {
|
export const plantsModuleConfig: ModuleConfig = {
|
||||||
appId: 'planta',
|
appId: 'plants',
|
||||||
tables: [
|
tables: [
|
||||||
{ name: 'plants' },
|
{ name: 'plants' },
|
||||||
{ name: 'plantPhotos' },
|
{ name: 'plantPhotos' },
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Integration tests for planta mutations against a real (fake) IndexedDB.
|
* Integration tests for plants mutations against a real (fake) IndexedDB.
|
||||||
*
|
*
|
||||||
* Focus: wateringMutations.logWatering — the most consequential planta
|
* Focus: wateringMutations.logWatering — the most consequential plants
|
||||||
* write because it (a) appends a log and (b) re-anchors the schedule's
|
* write because it (a) appends a log and (b) re-anchors the schedule's
|
||||||
* nextWateringAt, which drives every "needs water" badge in the UI.
|
* nextWateringAt, which drives every "needs water" badge in the UI.
|
||||||
*/
|
*/
|
||||||
|
|
@ -10,7 +10,7 @@ import 'fake-indexeddb/auto';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
vi.mock('@mana/shared-utils/analytics', () => ({
|
vi.mock('@mana/shared-utils/analytics', () => ({
|
||||||
PlantaEvents: {
|
PlantsEvents: {
|
||||||
plantCreated: vi.fn(),
|
plantCreated: vi.fn(),
|
||||||
plantDeleted: vi.fn(),
|
plantDeleted: vi.fn(),
|
||||||
plantWatered: vi.fn(),
|
plantWatered: vi.fn(),
|
||||||
|
|
@ -37,7 +37,7 @@ const wateringSchedules = () => db.table<LocalWateringSchedule>('wateringSchedul
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
setCurrentUserId('test-user');
|
setCurrentUserId('test-user');
|
||||||
// Planta `plants` table is encrypted; install a real Web Crypto key
|
// Plants `plants` table is encrypted; install a real Web Crypto key
|
||||||
// so any incidental reads/writes to it succeed. Watering tables
|
// so any incidental reads/writes to it succeed. Watering tables
|
||||||
// themselves are plaintext, but the test harness still requires the
|
// themselves are plaintext, but the test harness still requires the
|
||||||
// vault to be unlocked because shared hooks call into the provider.
|
// vault to be unlocked because shared hooks call into the provider.
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta — Mutation Helpers (Local-First)
|
* Plants — Mutation Helpers (Local-First)
|
||||||
*
|
*
|
||||||
* All writes go to IndexedDB first, sync handles the rest. Mutations throw
|
* All writes go to IndexedDB first, sync handles the rest. Mutations throw
|
||||||
* on failure so UI callers can surface errors via toasts.
|
* on failure so UI callers can surface errors via toasts.
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import { db } from '$lib/data/database';
|
import { db } from '$lib/data/database';
|
||||||
import { toPlant, toWateringSchedule } from './queries';
|
import { toPlant, toWateringSchedule } from './queries';
|
||||||
import { PlantaEvents } from '@mana/shared-utils/analytics';
|
import { PlantsEvents } from '@mana/shared-utils/analytics';
|
||||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||||
import { createBlock } from '$lib/data/time-blocks/service';
|
import { createBlock } from '$lib/data/time-blocks/service';
|
||||||
import { uploadPlantPhoto, identifyPlant, type IdentifyResult } from './api';
|
import { uploadPlantPhoto, identifyPlant, type IdentifyResult } from './api';
|
||||||
|
|
@ -45,7 +45,7 @@ export const plantMutations = {
|
||||||
const plaintextSnapshot = toPlant(newLocal);
|
const plaintextSnapshot = toPlant(newLocal);
|
||||||
await encryptRecord('plants', newLocal);
|
await encryptRecord('plants', newLocal);
|
||||||
await db.table('plants').add(newLocal);
|
await db.table('plants').add(newLocal);
|
||||||
PlantaEvents.plantCreated();
|
PlantsEvents.plantCreated();
|
||||||
return plaintextSnapshot;
|
return plaintextSnapshot;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ export const plantMutations = {
|
||||||
deletedAt: new Date().toISOString(),
|
deletedAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
PlantaEvents.plantDeleted();
|
PlantsEvents.plantDeleted();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -139,7 +139,7 @@ export const wateringMutations = {
|
||||||
endDate: now,
|
endDate: now,
|
||||||
kind: 'logged',
|
kind: 'logged',
|
||||||
type: 'watering',
|
type: 'watering',
|
||||||
sourceModule: 'planta',
|
sourceModule: 'plants',
|
||||||
sourceId: logEntry.id,
|
sourceId: logEntry.id,
|
||||||
title: `${plantName} gegossen`,
|
title: `${plantName} gegossen`,
|
||||||
color: '#06b6d4',
|
color: '#06b6d4',
|
||||||
|
|
@ -159,7 +159,7 @@ export const wateringMutations = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
PlantaEvents.plantWatered();
|
PlantsEvents.plantWatered();
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateSchedule(plantId: string, frequencyDays: number): Promise<void> {
|
async updateSchedule(plantId: string, frequencyDays: number): Promise<void> {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Pure-function tests for planta queries.
|
* Pure-function tests for plants queries.
|
||||||
*
|
*
|
||||||
* Covers the watering date math that drives every "needs water" badge in
|
* Covers the watering date math that drives every "needs water" badge in
|
||||||
* the UI — getting this wrong silently causes bad reminders, so it's worth
|
* the UI — getting this wrong silently causes bad reminders, so it's worth
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Reactive Queries & Pure Helpers for Planta
|
* Reactive Queries & Pure Helpers for Plants
|
||||||
*
|
*
|
||||||
* Uses Dexie liveQuery to automatically re-render when IndexedDB changes
|
* Uses Dexie liveQuery to automatically re-render when IndexedDB changes
|
||||||
* (local writes, sync updates, other tabs). Components call these hooks
|
* (local writes, sync updates, other tabs). Components call these hooks
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta QuickInputBar Adapter
|
* Plants QuickInputBar Adapter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta Tags — Uses shared global tags + module-specific junction table.
|
* Plants Tags — Uses shared global tags + module-specific junction table.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db } from '$lib/data/database';
|
import { db } from '$lib/data/database';
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Planta module types for the unified app.
|
* Plants module types for the unified app.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BaseRecord } from '@mana/local-store';
|
import type { BaseRecord } from '@mana/local-store';
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Plant Parser for Planta Module
|
* Plant Parser for Plants Module
|
||||||
*
|
*
|
||||||
* Extends the base parser with plant-specific patterns:
|
* Extends the base parser with plant-specific patterns:
|
||||||
* - Scientific names (italic Latin names)
|
* - Scientific names (italic Latin names)
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Planta-specific syntax help patterns
|
* Plants-specific syntax help patterns
|
||||||
*/
|
*/
|
||||||
import type { SyntaxGroup } from '@mana/shared-ui';
|
import type { SyntaxGroup } from '@mana/shared-ui';
|
||||||
|
|
||||||
export const PLANTA_SYNTAX: SyntaxGroup[] = [
|
export const PLANTS_SYNTAX: SyntaxGroup[] = [
|
||||||
{
|
{
|
||||||
title: 'Pflanzen',
|
title: 'Pflanzen',
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!--
|
<!--
|
||||||
Planta — DetailView (inline editable overlay)
|
Plants — DetailView (inline editable overlay)
|
||||||
All fields are always editable. Changes auto-save on blur.
|
All fields are always editable. Changes auto-save on blur.
|
||||||
Includes photo upload + AI plant identification.
|
Includes photo upload + AI plant identification.
|
||||||
-->
|
-->
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
showTagPicker = false;
|
showTagPicker = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('add tag failed:', err);
|
console.error('add tag failed:', err);
|
||||||
toast.error($_('planta.errors.saveFailed'));
|
toast.error($_('plants.errors.saveFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@
|
||||||
await plantTagOps.removeTag(plantId, tagId);
|
await plantTagOps.removeTag(plantId, tagId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('remove tag failed:', err);
|
console.error('remove tag failed:', err);
|
||||||
toast.error($_('planta.errors.saveFailed'));
|
toast.error($_('plants.errors.saveFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
detail.blur();
|
detail.blur();
|
||||||
try {
|
try {
|
||||||
await plantMutations.update(plantId, {
|
await plantMutations.update(plantId, {
|
||||||
name: editName.trim() || detail.entity?.name || $_('planta.plant.unnamed'),
|
name: editName.trim() || detail.entity?.name || $_('plants.plant.unnamed'),
|
||||||
scientificName: editScientificName.trim() || undefined,
|
scientificName: editScientificName.trim() || undefined,
|
||||||
careNotes: editCareNotes.trim() || undefined,
|
careNotes: editCareNotes.trim() || undefined,
|
||||||
lightRequirements: editLightRequirements || undefined,
|
lightRequirements: editLightRequirements || undefined,
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('plant save failed:', err);
|
console.error('plant save failed:', err);
|
||||||
toast.error($_('planta.errors.saveFailed'));
|
toast.error($_('plants.errors.saveFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('plant select save failed:', err);
|
console.error('plant select save failed:', err);
|
||||||
toast.error($_('planta.errors.saveFailed'));
|
toast.error($_('plants.errors.saveFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,10 +161,10 @@
|
||||||
uploading = true;
|
uploading = true;
|
||||||
try {
|
try {
|
||||||
await photoMutations.upload(plantId, file);
|
await photoMutations.upload(plantId, file);
|
||||||
toast.success($_('planta.success.photoUploaded'));
|
toast.success($_('plants.success.photoUploaded'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('photo upload failed:', err);
|
console.error('photo upload failed:', err);
|
||||||
toast.error($_('planta.errors.uploadFailed'));
|
toast.error($_('plants.errors.uploadFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
uploading = false;
|
uploading = false;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
async function handleIdentify() {
|
async function handleIdentify() {
|
||||||
const primary = photos[0];
|
const primary = photos[0];
|
||||||
if (!primary) {
|
if (!primary) {
|
||||||
toast.error($_('planta.errors.identifyFailed'));
|
toast.error($_('plants.errors.identifyFailed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
identifying = true;
|
identifying = true;
|
||||||
|
|
@ -182,10 +182,10 @@
|
||||||
try {
|
try {
|
||||||
const result = await photoMutations.identify(primary.id);
|
const result = await photoMutations.identify(primary.id);
|
||||||
identifyResult = result;
|
identifyResult = result;
|
||||||
toast.success($_('planta.success.identified'));
|
toast.success($_('plants.success.identified'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('identify failed:', err);
|
console.error('identify failed:', err);
|
||||||
toast.error($_('planta.errors.identifyFailed'));
|
toast.error($_('plants.errors.identifyFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
identifying = false;
|
identifying = false;
|
||||||
}
|
}
|
||||||
|
|
@ -195,11 +195,11 @@
|
||||||
if (!identifyResult) return;
|
if (!identifyResult) return;
|
||||||
try {
|
try {
|
||||||
await plantMutations.applyIdentification(plantId, identifyResult, { overwrite: false });
|
await plantMutations.applyIdentification(plantId, identifyResult, { overwrite: false });
|
||||||
toast.success($_('planta.success.plantSaved'));
|
toast.success($_('plants.success.plantSaved'));
|
||||||
identifyResult = null;
|
identifyResult = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('apply identification failed:', err);
|
console.error('apply identification failed:', err);
|
||||||
toast.error($_('planta.errors.saveFailed'));
|
toast.error($_('plants.errors.saveFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +208,7 @@
|
||||||
await photoMutations.setPrimary(plantId, photoId);
|
await photoMutations.setPrimary(plantId, photoId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('set primary failed:', err);
|
console.error('set primary failed:', err);
|
||||||
toast.error($_('planta.errors.saveFailed'));
|
toast.error($_('plants.errors.saveFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,7 +217,7 @@
|
||||||
await photoMutations.remove(photoId);
|
await photoMutations.remove(photoId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('remove photo failed:', err);
|
console.error('remove photo failed:', err);
|
||||||
toast.error($_('planta.errors.deleteFailed'));
|
toast.error($_('plants.errors.deleteFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -225,14 +225,14 @@
|
||||||
<DetailViewShell
|
<DetailViewShell
|
||||||
entity={detail.entity}
|
entity={detail.entity}
|
||||||
loading={detail.loading}
|
loading={detail.loading}
|
||||||
notFoundLabel={$_('planta.plant.notFound')}
|
notFoundLabel={$_('plants.plant.notFound')}
|
||||||
confirmDelete={detail.confirmDelete}
|
confirmDelete={detail.confirmDelete}
|
||||||
onAskDelete={detail.askDelete}
|
onAskDelete={detail.askDelete}
|
||||||
onCancelDelete={detail.cancelDelete}
|
onCancelDelete={detail.cancelDelete}
|
||||||
confirmDeleteLabel={$_('planta.plant.confirmDelete')}
|
confirmDeleteLabel={$_('plants.plant.confirmDelete')}
|
||||||
onConfirmDelete={() =>
|
onConfirmDelete={() =>
|
||||||
detail.deleteWithUndo({
|
detail.deleteWithUndo({
|
||||||
label: $_('planta.success.plantDeleted'),
|
label: $_('plants.success.plantDeleted'),
|
||||||
delete: deletePlant,
|
delete: deletePlant,
|
||||||
goBack,
|
goBack,
|
||||||
})}
|
})}
|
||||||
|
|
@ -243,75 +243,75 @@
|
||||||
bind:value={editName}
|
bind:value={editName}
|
||||||
onfocus={detail.focus}
|
onfocus={detail.focus}
|
||||||
onblur={saveField}
|
onblur={saveField}
|
||||||
placeholder={$_('planta.plant.namePlaceholder')}
|
placeholder={$_('plants.plant.namePlaceholder')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="properties">
|
<div class="properties">
|
||||||
<div class="prop-row">
|
<div class="prop-row">
|
||||||
<span class="prop-label">{$_('planta.plant.scientificName')}</span>
|
<span class="prop-label">{$_('plants.plant.scientificName')}</span>
|
||||||
<input
|
<input
|
||||||
class="prop-input"
|
class="prop-input"
|
||||||
bind:value={editScientificName}
|
bind:value={editScientificName}
|
||||||
onfocus={detail.focus}
|
onfocus={detail.focus}
|
||||||
onblur={saveField}
|
onblur={saveField}
|
||||||
placeholder={$_('planta.common.none')}
|
placeholder={$_('plants.common.none')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prop-row">
|
<div class="prop-row">
|
||||||
<span class="prop-label">{$_('planta.plant.species')}</span>
|
<span class="prop-label">{$_('plants.plant.species')}</span>
|
||||||
<input
|
<input
|
||||||
class="prop-input"
|
class="prop-input"
|
||||||
bind:value={editSpecies}
|
bind:value={editSpecies}
|
||||||
onfocus={detail.focus}
|
onfocus={detail.focus}
|
||||||
onblur={saveField}
|
onblur={saveField}
|
||||||
placeholder={$_('planta.common.none')}
|
placeholder={$_('plants.common.none')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prop-row">
|
<div class="prop-row">
|
||||||
<span class="prop-label">{$_('planta.plant.state')}</span>
|
<span class="prop-label">{$_('plants.plant.state')}</span>
|
||||||
<select
|
<select
|
||||||
class="prop-select health-{editHealthStatus}"
|
class="prop-select health-{editHealthStatus}"
|
||||||
bind:value={editHealthStatus}
|
bind:value={editHealthStatus}
|
||||||
onchange={handleSelectChange}
|
onchange={handleSelectChange}
|
||||||
>
|
>
|
||||||
<option value="healthy">{$_('planta.health.healthy')}</option>
|
<option value="healthy">{$_('plants.health.healthy')}</option>
|
||||||
<option value="needs_attention">{$_('planta.health.needsAttention')}</option>
|
<option value="needs_attention">{$_('plants.health.needsAttention')}</option>
|
||||||
<option value="sick">{$_('planta.health.sick')}</option>
|
<option value="sick">{$_('plants.health.sick')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prop-row">
|
<div class="prop-row">
|
||||||
<span class="prop-label">{$_('planta.plant.light')}</span>
|
<span class="prop-label">{$_('plants.plant.light')}</span>
|
||||||
<select
|
<select
|
||||||
class="prop-select"
|
class="prop-select"
|
||||||
bind:value={editLightRequirements}
|
bind:value={editLightRequirements}
|
||||||
onchange={handleSelectChange}
|
onchange={handleSelectChange}
|
||||||
>
|
>
|
||||||
<option value="">{$_('planta.common.none')}</option>
|
<option value="">{$_('plants.common.none')}</option>
|
||||||
<option value="low">{$_('planta.light.low')}</option>
|
<option value="low">{$_('plants.light.low')}</option>
|
||||||
<option value="medium">{$_('planta.light.medium')}</option>
|
<option value="medium">{$_('plants.light.medium')}</option>
|
||||||
<option value="bright">{$_('planta.light.bright')}</option>
|
<option value="bright">{$_('plants.light.bright')}</option>
|
||||||
<option value="direct">{$_('planta.light.direct')}</option>
|
<option value="direct">{$_('plants.light.direct')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prop-row">
|
<div class="prop-row">
|
||||||
<span class="prop-label">{$_('planta.plant.wateringDays')}</span>
|
<span class="prop-label">{$_('plants.plant.wateringDays')}</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="prop-input"
|
class="prop-input"
|
||||||
bind:value={editWateringFrequencyDays}
|
bind:value={editWateringFrequencyDays}
|
||||||
onfocus={detail.focus}
|
onfocus={detail.focus}
|
||||||
onblur={saveField}
|
onblur={saveField}
|
||||||
placeholder={$_('planta.common.none')}
|
placeholder={$_('plants.common.none')}
|
||||||
min="1"
|
min="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prop-row">
|
<div class="prop-row">
|
||||||
<span class="prop-label">{$_('planta.plant.acquired')}</span>
|
<span class="prop-label">{$_('plants.plant.acquired')}</span>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
class="prop-input"
|
class="prop-input"
|
||||||
|
|
@ -323,20 +323,20 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<span class="section-label">{$_('planta.plant.careNotes')}</span>
|
<span class="section-label">{$_('plants.plant.careNotes')}</span>
|
||||||
<textarea
|
<textarea
|
||||||
class="description-input"
|
class="description-input"
|
||||||
bind:value={editCareNotes}
|
bind:value={editCareNotes}
|
||||||
onfocus={detail.focus}
|
onfocus={detail.focus}
|
||||||
onblur={saveField}
|
onblur={saveField}
|
||||||
placeholder={$_('planta.plant.notesPlaceholder')}
|
placeholder={$_('plants.plant.notesPlaceholder')}
|
||||||
rows={3}
|
rows={3}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-label">{$_('planta.photo.section')}</span>
|
<span class="section-label">{$_('plants.photo.section')}</span>
|
||||||
<div class="photo-actions">
|
<div class="photo-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -344,7 +344,7 @@
|
||||||
onclick={() => fileInput?.click()}
|
onclick={() => fileInput?.click()}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
>
|
>
|
||||||
{uploading ? $_('planta.photo.uploading') : $_('planta.photo.upload')}
|
{uploading ? $_('plants.photo.uploading') : $_('plants.photo.upload')}
|
||||||
</button>
|
</button>
|
||||||
{#if photos.length > 0}
|
{#if photos.length > 0}
|
||||||
<button
|
<button
|
||||||
|
|
@ -353,7 +353,7 @@
|
||||||
onclick={handleIdentify}
|
onclick={handleIdentify}
|
||||||
disabled={identifying}
|
disabled={identifying}
|
||||||
>
|
>
|
||||||
{identifying ? $_('planta.identify.analyzing') : $_('planta.identify.button')}
|
{identifying ? $_('plants.identify.analyzing') : $_('plants.identify.button')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -367,7 +367,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if photos.length === 0}
|
{#if photos.length === 0}
|
||||||
<p class="empty">{$_('planta.photo.noPhotos')}</p>
|
<p class="empty">{$_('plants.photo.noPhotos')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="photo-grid">
|
<div class="photo-grid">
|
||||||
{#each photos as photo (photo.id)}
|
{#each photos as photo (photo.id)}
|
||||||
|
|
@ -379,7 +379,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="photo-btn"
|
class="photo-btn"
|
||||||
onclick={() => handleSetPrimary(photo.id)}
|
onclick={() => handleSetPrimary(photo.id)}
|
||||||
title={$_('planta.photo.primary')}
|
title={$_('plants.photo.primary')}
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -388,7 +388,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="photo-btn danger"
|
class="photo-btn danger"
|
||||||
onclick={() => handleRemovePhoto(photo.id)}
|
onclick={() => handleRemovePhoto(photo.id)}
|
||||||
title={$_('planta.common.delete')}
|
title={$_('plants.common.delete')}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -401,14 +401,14 @@
|
||||||
{#if identifyResult}
|
{#if identifyResult}
|
||||||
<div class="identify-result">
|
<div class="identify-result">
|
||||||
<div class="identify-header">
|
<div class="identify-header">
|
||||||
<span class="identify-title">{$_('planta.identify.resultTitle')}</span>
|
<span class="identify-title">{$_('plants.identify.resultTitle')}</span>
|
||||||
<button type="button" class="action-btn primary" onclick={applyIdentification}>
|
<button type="button" class="action-btn primary" onclick={applyIdentification}>
|
||||||
{$_('planta.identify.applyResult')}
|
{$_('plants.identify.applyResult')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if identifyResult.scientificName}
|
{#if identifyResult.scientificName}
|
||||||
<p>
|
<p>
|
||||||
<strong>{$_('planta.plant.scientificName')}:</strong>
|
<strong>{$_('plants.plant.scientificName')}:</strong>
|
||||||
{identifyResult.scientificName}
|
{identifyResult.scientificName}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -417,7 +417,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if identifyResult.confidence !== undefined}
|
{#if identifyResult.confidence !== undefined}
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
{$_('planta.identify.confidence')}: {Math.round(identifyResult.confidence * 100)}%
|
{$_('plants.identify.confidence')}: {Math.round(identifyResult.confidence * 100)}%
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if identifyResult.wateringAdvice}
|
{#if identifyResult.wateringAdvice}
|
||||||
|
|
@ -441,7 +441,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="tag-remove"
|
class="tag-remove"
|
||||||
onclick={() => handleRemoveTag(tag.id)}
|
onclick={() => handleRemoveTag(tag.id)}
|
||||||
aria-label={$_('planta.common.delete')}
|
aria-label={$_('plants.common.delete')}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -468,7 +468,7 @@
|
||||||
<!-- Watering history -->
|
<!-- Watering history -->
|
||||||
{#if wateringLogs.length > 0}
|
{#if wateringLogs.length > 0}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<span class="section-label">{$_('planta.watering.lastWatered')}</span>
|
<span class="section-label">{$_('plants.watering.lastWatered')}</span>
|
||||||
<ul class="watering-history">
|
<ul class="watering-history">
|
||||||
{#each wateringLogs as log (log.id)}
|
{#each wateringLogs as log (log.id)}
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -484,10 +484,10 @@
|
||||||
|
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span
|
<span
|
||||||
>{$_('planta.plant.created')}: {new Date(plant.createdAt ?? '').toLocaleDateString()}</span
|
>{$_('plants.plant.created')}: {new Date(plant.createdAt ?? '').toLocaleDateString()}</span
|
||||||
>
|
>
|
||||||
{#if plant.updatedAt}
|
{#if plant.updatedAt}
|
||||||
<span>{$_('planta.plant.edited')}: {new Date(plant.updatedAt).toLocaleDateString()}</span>
|
<span>{$_('plants.plant.edited')}: {new Date(plant.updatedAt).toLocaleDateString()}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
@ -12,7 +12,7 @@ const registry = new Map<string, () => Promise<AdapterModule>>([
|
||||||
['/calendar', () => import('$lib/modules/calendar/quick-input-adapter')],
|
['/calendar', () => import('$lib/modules/calendar/quick-input-adapter')],
|
||||||
['/contacts', () => import('$lib/modules/contacts/quick-input-adapter')],
|
['/contacts', () => import('$lib/modules/contacts/quick-input-adapter')],
|
||||||
['/times', () => import('$lib/modules/times/quick-input-adapter')],
|
['/times', () => import('$lib/modules/times/quick-input-adapter')],
|
||||||
['/planta', () => import('$lib/modules/planta/quick-input-adapter')],
|
['/plants', () => import('$lib/modules/plants/quick-input-adapter')],
|
||||||
['/nutriphi', () => import('$lib/modules/nutriphi/quick-input-adapter')],
|
['/nutriphi', () => import('$lib/modules/nutriphi/quick-input-adapter')],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const SPLIT_APP_ID_LIST = [
|
||||||
'context',
|
'context',
|
||||||
'questions',
|
'questions',
|
||||||
'nutriphi',
|
'nutriphi',
|
||||||
'planta',
|
'plants',
|
||||||
'uload',
|
'uload',
|
||||||
'calc',
|
'calc',
|
||||||
'moodlit',
|
'moodlit',
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export async function loadAutomations(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active.length > 0) {
|
if (import.meta.env.DEV && active.length > 0) {
|
||||||
console.log(`[Triggers] Loaded ${active.length} automation(s)`);
|
console.log(`[Triggers] Loaded ${active.length} automation(s)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ describe('WIDGET_REGISTRY', () => {
|
||||||
'context',
|
'context',
|
||||||
'mana-auth',
|
'mana-auth',
|
||||||
'nutriphi',
|
'nutriphi',
|
||||||
'planta',
|
'plants',
|
||||||
'cycles',
|
'cycles',
|
||||||
undefined,
|
undefined,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export type WidgetType =
|
||||||
| 'context-docs' // Context: recent documents & spaces
|
| 'context-docs' // Context: recent documents & spaces
|
||||||
| 'active-timer' // Times: running timer
|
| 'active-timer' // Times: running timer
|
||||||
| 'nutrition-progress' // NutriPhi: today's calorie progress
|
| 'nutrition-progress' // NutriPhi: today's calorie progress
|
||||||
| 'plant-watering' // Planta: plants due for watering
|
| 'plant-watering' // Plants: plants due for watering
|
||||||
| 'day-timeline' // TimeBlocks: chronological day timeline
|
| 'day-timeline' // TimeBlocks: chronological day timeline
|
||||||
| 'activity-feed' // TimeBlocks: recent activity across modules
|
| 'activity-feed' // TimeBlocks: recent activity across modules
|
||||||
| 'cycles' // Cycles: current phase + days until next period
|
| 'cycles' // Cycles: current phase + days until next period
|
||||||
|
|
@ -131,7 +131,7 @@ export interface WidgetMeta {
|
||||||
| 'context'
|
| 'context'
|
||||||
| 'times'
|
| 'times'
|
||||||
| 'nutriphi'
|
| 'nutriphi'
|
||||||
| 'planta'
|
| 'plants'
|
||||||
| 'cycles'
|
| 'cycles'
|
||||||
| 'body'
|
| 'body'
|
||||||
| 'mana-auth';
|
| 'mana-auth';
|
||||||
|
|
@ -318,7 +318,7 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [
|
||||||
icon: '🌱',
|
icon: '🌱',
|
||||||
defaultSize: 'small',
|
defaultSize: 'small',
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
requiredBackend: 'planta',
|
requiredBackend: 'plants',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'day-timeline',
|
type: 'day-timeline',
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,9 @@
|
||||||
import { linkLocalStore, linkMutations } from '@mana/shared-links';
|
import { linkLocalStore, linkMutations } from '@mana/shared-links';
|
||||||
import { manaStore } from '$lib/data/local-store';
|
import { manaStore } from '$lib/data/local-store';
|
||||||
import { startLlmQueue, stopLlmQueue } from '$lib/llm-queue';
|
import { startLlmQueue, stopLlmQueue } from '$lib/llm-queue';
|
||||||
import { llmSettingsState, updateLlmSettings, tierLabel, type LlmTier } from '@mana/shared-llm';
|
import { useAiTierItems } from '$lib/components/layout/use-ai-tier-items.svelte';
|
||||||
import { isLocalLlmSupported, getLocalLlmStatus, loadLocalLlm } from '@mana/local-llm';
|
import { useSyncStatusItems } from '$lib/components/layout/use-sync-status-items.svelte';
|
||||||
import {
|
import RouteTierGate from '$lib/components/layout/RouteTierGate.svelte';
|
||||||
getLocalSttStatus,
|
|
||||||
loadLocalStt,
|
|
||||||
isLocalSttSupported,
|
|
||||||
MODELS as STT_MODELS,
|
|
||||||
DEFAULT_MODEL as STT_DEFAULT_MODEL,
|
|
||||||
type ModelKey as SttModelKey,
|
|
||||||
} from '@mana/local-stt';
|
|
||||||
import { useLocalStt } from '$lib/components/voice/use-local-stt.svelte';
|
import { useLocalStt } from '$lib/components/voice/use-local-stt.svelte';
|
||||||
import { Microphone, Stop } from '@mana/shared-icons';
|
import { Microphone, Stop } from '@mana/shared-icons';
|
||||||
import {
|
import {
|
||||||
|
|
@ -186,274 +179,10 @@
|
||||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||||
|
|
||||||
// ── AI Tier Selector (PillNav dropdown) ─────────────────
|
// ── AI Tier Selector (PillNav dropdown) ─────────────────
|
||||||
const webgpuSupported = isLocalLlmSupported();
|
const aiTier = useAiTierItems();
|
||||||
const localLlmStatus = getLocalLlmStatus();
|
|
||||||
const sttSupported = isLocalSttSupported();
|
|
||||||
const localSttStatus = getLocalSttStatus();
|
|
||||||
let selectedSttModel = $state<SttModelKey>(STT_DEFAULT_MODEL);
|
|
||||||
const llmSettings = $derived(llmSettingsState.current);
|
|
||||||
|
|
||||||
function toggleAiTier(tier: LlmTier) {
|
|
||||||
const current = llmSettings.allowedTiers;
|
|
||||||
const next = current.includes(tier)
|
|
||||||
? current.filter((t: LlmTier) => t !== tier)
|
|
||||||
: [...current, tier];
|
|
||||||
updateLlmSettings({ allowedTiers: next });
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIER_TOGGLE_LIST: Array<{ tier: LlmTier; shortLabel: string; icon: string }> = [
|
|
||||||
{ tier: 'browser', shortLabel: 'Lokal (Gemma 4)', icon: 'robot' },
|
|
||||||
{ tier: 'mana-server', shortLabel: 'Server (Gemma 4)', icon: 'globe' },
|
|
||||||
{ tier: 'cloud', shortLabel: 'Cloud (Gemini)', icon: 'cloud' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let aiTierItems = $derived<PillDropdownItem[]>([
|
|
||||||
// Tier toggles — browser tier item and its model-status buddy share a
|
|
||||||
// group so PillDropdownBar renders them as a paired pill.
|
|
||||||
...TIER_TOGGLE_LIST.filter((t) => t.tier !== 'browser' || webgpuSupported).map((t) => {
|
|
||||||
const isActive = llmSettings.allowedTiers.includes(t.tier);
|
|
||||||
return {
|
|
||||||
id: `ai-tier-${t.tier}`,
|
|
||||||
label: t.shortLabel,
|
|
||||||
icon: isActive ? 'checkCircle' : t.icon,
|
|
||||||
active: isActive,
|
|
||||||
onClick: () => toggleAiTier(t.tier),
|
|
||||||
...(t.tier === 'browser' ? { group: 'local-llm' } : {}),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
// Browser model status / load button (grouped with the "Lokal" toggle).
|
|
||||||
// Handles all LoadingStatus states so the user sees feedback during
|
|
||||||
// download, initialization, and on error (e.g. worker crash).
|
|
||||||
...(llmSettings.allowedTiers.includes('browser') && webgpuSupported
|
|
||||||
? [
|
|
||||||
(() => {
|
|
||||||
const s = localLlmStatus.current;
|
|
||||||
const state = s.state;
|
|
||||||
let label: string;
|
|
||||||
let icon: string;
|
|
||||||
let danger = false;
|
|
||||||
let disabled = false;
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 'ready':
|
|
||||||
label = 'Geladen';
|
|
||||||
icon = 'checkCircle';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'downloading':
|
|
||||||
label = `Lade… ${((s as { progress: number }).progress * 100).toFixed(0)}%`;
|
|
||||||
icon = 'clock';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'loading':
|
|
||||||
label = 'Initialisiere…';
|
|
||||||
icon = 'clock';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'checking':
|
|
||||||
label = 'Prüfe…';
|
|
||||||
icon = 'clock';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
label = 'Fehler — erneut versuchen';
|
|
||||||
icon = 'bell';
|
|
||||||
danger = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
label = 'Modell laden';
|
|
||||||
icon = 'cloud';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'ai-browser-status',
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
group: 'local-llm',
|
|
||||||
danger,
|
|
||||||
disabled,
|
|
||||||
progress: state === 'downloading' ? (s as { progress: number }).progress : undefined,
|
|
||||||
onClick: !disabled ? () => void loadLocalLlm() : undefined,
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
// ── STT section ──────────────────────────────────
|
|
||||||
{ id: 'stt-divider', label: '', divider: true },
|
|
||||||
// STT model selector — each model is a pill, active = currently selected
|
|
||||||
...(sttSupported
|
|
||||||
? (Object.entries(STT_MODELS) as [SttModelKey, (typeof STT_MODELS)[SttModelKey]][]).map(
|
|
||||||
([key, model]) => {
|
|
||||||
const isSelected = selectedSttModel === key;
|
|
||||||
return {
|
|
||||||
id: `stt-model-${key}`,
|
|
||||||
label: model.displayName,
|
|
||||||
icon: isSelected ? 'checkCircle' : 'mic',
|
|
||||||
active: isSelected,
|
|
||||||
onClick: () => {
|
|
||||||
selectedSttModel = key;
|
|
||||||
void loadLocalStt(key);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: []),
|
|
||||||
// STT model status (grouped with selected model)
|
|
||||||
...(sttSupported
|
|
||||||
? [
|
|
||||||
(() => {
|
|
||||||
const s = localSttStatus.current;
|
|
||||||
const state = s.state;
|
|
||||||
let label: string;
|
|
||||||
let icon: string;
|
|
||||||
let danger = false;
|
|
||||||
let disabled = false;
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 'ready':
|
|
||||||
label = 'STT bereit';
|
|
||||||
icon = 'checkCircle';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'downloading':
|
|
||||||
label = `STT Lade… ${((s as { progress: number }).progress * 100).toFixed(0)}%`;
|
|
||||||
icon = 'clock';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'loading':
|
|
||||||
label = 'STT lädt…';
|
|
||||||
icon = 'clock';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'checking':
|
|
||||||
label = 'STT prüft…';
|
|
||||||
icon = 'clock';
|
|
||||||
disabled = true;
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
label = 'STT Fehler';
|
|
||||||
icon = 'bell';
|
|
||||||
danger = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
label = 'STT Modell laden';
|
|
||||||
icon = 'mic';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'stt-status',
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
danger,
|
|
||||||
disabled,
|
|
||||||
progress: state === 'downloading' ? (s as { progress: number }).progress : undefined,
|
|
||||||
onClick: !disabled ? () => void loadLocalStt(selectedSttModel) : undefined,
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
// Divider + settings link
|
|
||||||
{ id: 'ai-divider', label: '', divider: true },
|
|
||||||
{
|
|
||||||
id: 'ai-settings',
|
|
||||||
label: 'KI-Einstellungen',
|
|
||||||
icon: 'settings',
|
|
||||||
onClick: () => goto('/settings#ai-options'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let currentAiTierLabel = $derived.by(() => {
|
|
||||||
const active = llmSettings.allowedTiers;
|
|
||||||
if (active.length === 0) return 'Aus';
|
|
||||||
// Show the first (privacy-sorted) tier's short name
|
|
||||||
const sorted = [...active].sort(
|
|
||||||
(a, b) =>
|
|
||||||
TIER_TOGGLE_LIST.findIndex((t) => t.tier === a) -
|
|
||||||
TIER_TOGGLE_LIST.findIndex((t) => t.tier === b)
|
|
||||||
);
|
|
||||||
const first = TIER_TOGGLE_LIST.find((t) => t.tier === sorted[0]);
|
|
||||||
return first ? first.shortLabel.split(' (')[0] : 'KI';
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentAiTierIcon = $derived.by(() => {
|
|
||||||
const active = llmSettings.allowedTiers;
|
|
||||||
if (active.length === 0) return 'power';
|
|
||||||
const sorted = [...active].sort(
|
|
||||||
(a, b) =>
|
|
||||||
TIER_TOGGLE_LIST.findIndex((t) => t.tier === a) -
|
|
||||||
TIER_TOGGLE_LIST.findIndex((t) => t.tier === b)
|
|
||||||
);
|
|
||||||
const first = TIER_TOGGLE_LIST.find((t) => t.tier === sorted[0]);
|
|
||||||
return first ? first.icon : 'cpu';
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Sync status dropdown ────────────────────────────────
|
// ── Sync status dropdown ────────────────────────────────
|
||||||
let syncStatusItems = $derived.by(() => {
|
const syncStatus = useSyncStatusItems();
|
||||||
const items: import('@mana/shared-ui').PillDropdownItem[] = [];
|
|
||||||
|
|
||||||
if (syncBilling.active) {
|
|
||||||
items.push({
|
|
||||||
id: 'sync-active',
|
|
||||||
label: 'Cloud Sync aktiv',
|
|
||||||
icon: 'cloud',
|
|
||||||
active: true,
|
|
||||||
disabled: true,
|
|
||||||
});
|
|
||||||
if (syncBilling.nextChargeAt) {
|
|
||||||
const date = new Date(syncBilling.nextChargeAt).toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
});
|
|
||||||
items.push({
|
|
||||||
id: 'sync-next',
|
|
||||||
label: `Nächste Abbuchung: ${date}`,
|
|
||||||
icon: 'calendar',
|
|
||||||
disabled: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (syncBilling.paused) {
|
|
||||||
items.push({
|
|
||||||
id: 'sync-paused',
|
|
||||||
label: 'Sync pausiert — Credits aufladen',
|
|
||||||
icon: 'bell',
|
|
||||||
onClick: () => goto('/credits?tab=packages'),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.push({
|
|
||||||
id: 'sync-inactive',
|
|
||||||
label: 'Sync aktivieren',
|
|
||||||
icon: 'cloud',
|
|
||||||
onClick: () => goto('/settings/sync'),
|
|
||||||
});
|
|
||||||
items.push({
|
|
||||||
id: 'sync-info',
|
|
||||||
label: 'Nur lokal — ab 30 Credits/Monat',
|
|
||||||
icon: 'creditCard',
|
|
||||||
disabled: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({ id: 'sync-divider', label: '', divider: true });
|
|
||||||
items.push({
|
|
||||||
id: 'sync-settings',
|
|
||||||
label: 'Sync-Einstellungen',
|
|
||||||
icon: 'settings',
|
|
||||||
onClick: () => goto('/settings/sync'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentSyncLabel = $derived(
|
|
||||||
syncBilling.loading
|
|
||||||
? '...'
|
|
||||||
: syncBilling.active
|
|
||||||
? 'Sync'
|
|
||||||
: syncBilling.paused
|
|
||||||
? 'Pausiert'
|
|
||||||
: 'Lokal'
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── User / Guest awareness ──────────────────────────────
|
// ── User / Guest awareness ──────────────────────────────
|
||||||
let userEmail = $derived(
|
let userEmail = $derived(
|
||||||
|
|
@ -817,11 +546,10 @@
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const t = localStt.text;
|
const t = localStt.text;
|
||||||
const e = localStt.error;
|
const e = localStt.error;
|
||||||
if (e) {
|
if (import.meta.env.DEV && e) {
|
||||||
console.warn('[layout-stt] Error:', e);
|
console.warn('[layout-stt] Error:', e);
|
||||||
}
|
}
|
||||||
if (t) {
|
if (t) {
|
||||||
console.log('[layout-stt] Transcribed text:', t);
|
|
||||||
sttInjectedText = t;
|
sttInjectedText = t;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1082,12 +810,12 @@
|
||||||
primaryColor="#6366f1"
|
primaryColor="#6366f1"
|
||||||
showAppSwitcher={true}
|
showAppSwitcher={true}
|
||||||
showAiTierSelector={true}
|
showAiTierSelector={true}
|
||||||
{aiTierItems}
|
aiTierItems={aiTier.items}
|
||||||
{currentAiTierLabel}
|
currentAiTierLabel={aiTier.label}
|
||||||
{currentAiTierIcon}
|
currentAiTierIcon={aiTier.icon}
|
||||||
showSyncStatus={authStore.isAuthenticated}
|
showSyncStatus={authStore.isAuthenticated}
|
||||||
{syncStatusItems}
|
syncStatusItems={syncStatus.items}
|
||||||
{currentSyncLabel}
|
currentSyncLabel={syncStatus.label}
|
||||||
{appItems}
|
{appItems}
|
||||||
{userEmail}
|
{userEmail}
|
||||||
settingsHref="/settings"
|
settingsHref="/settings"
|
||||||
|
|
@ -1121,70 +849,11 @@
|
||||||
>
|
>
|
||||||
<div class="mx-auto max-w-7xl px-3 py-2 sm:px-6 sm:py-3 lg:px-8">
|
<div class="mx-auto max-w-7xl px-3 py-2 sm:px-6 sm:py-3 lg:px-8">
|
||||||
{#if routeBlocked && routeAppId}
|
{#if routeBlocked && routeAppId}
|
||||||
<!-- Per-route tier gate. The wrapping AuthGate only fires
|
<RouteTierGate
|
||||||
onMount + only for authenticated users, so this is the
|
appName={routeAppId.name}
|
||||||
only place that catches direct URL navigation into a
|
userTierLabel={routeTierLabels.user}
|
||||||
gated module by a guest or under-tier user. -->
|
requiredTierLabel={routeTierLabels.required}
|
||||||
<div class="flex min-h-[60vh] items-center justify-center p-6">
|
/>
|
||||||
<div
|
|
||||||
class="w-full max-w-96 rounded-2xl border px-8 py-10 text-center shadow-sm"
|
|
||||||
style:border-color="hsl(var(--border, 0 0% 90%))"
|
|
||||||
style:background-color="hsl(var(--card, 0 0% 100%))"
|
|
||||||
>
|
|
||||||
<h1 class="mb-4 text-xl font-bold" style:color="hsl(var(--foreground, 0 0% 9%))">
|
|
||||||
{routeAppId.name}
|
|
||||||
</h1>
|
|
||||||
<div class="mb-4 text-5xl">🔒</div>
|
|
||||||
<p
|
|
||||||
class="mb-6 text-[0.9375rem] leading-relaxed"
|
|
||||||
style:color="hsl(var(--muted-foreground, 0 0% 45%))"
|
|
||||||
>
|
|
||||||
{($locale || 'de') === 'de'
|
|
||||||
? 'Diese App ist aktuell in der geschlossenen '
|
|
||||||
: 'This app is currently in closed '}<strong>{routeTierLabels.required}</strong
|
|
||||||
>{($locale || 'de') === 'de' ? '-Phase.' : ' phase.'}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="mb-6 flex flex-col gap-2 rounded-xl p-4"
|
|
||||||
style:background-color="hsl(var(--muted, 0 0% 96%))"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between text-sm">
|
|
||||||
<span style:color="hsl(var(--muted-foreground, 0 0% 45%))"
|
|
||||||
>{($locale || 'de') === 'de' ? 'Dein Zugang:' : 'Your access:'}</span
|
|
||||||
>
|
|
||||||
<span class="font-semibold" style:color="hsl(var(--foreground, 0 0% 9%))"
|
|
||||||
>{routeTierLabels.user}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between text-sm">
|
|
||||||
<span style:color="hsl(var(--muted-foreground, 0 0% 45%))"
|
|
||||||
>{($locale || 'de') === 'de' ? 'Benötigt:' : 'Required:'}</span
|
|
||||||
>
|
|
||||||
<span class="font-semibold text-violet-500">{routeTierLabels.required}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<button
|
|
||||||
class="w-full cursor-pointer rounded-lg border-none px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-90"
|
|
||||||
style:background-color="hsl(var(--primary, 239 84% 67%))"
|
|
||||||
style:color="hsl(var(--primary-foreground, 0 0% 100%))"
|
|
||||||
onclick={() => goto('/')}
|
|
||||||
>
|
|
||||||
{($locale || 'de') === 'de' ? 'Zur Übersicht' : 'Back to overview'}
|
|
||||||
</button>
|
|
||||||
{#if !authStore.isAuthenticated}
|
|
||||||
<button
|
|
||||||
class="w-full cursor-pointer rounded-lg border px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-90"
|
|
||||||
style:border-color="hsl(var(--border, 0 0% 90%))"
|
|
||||||
style:color="hsl(var(--foreground, 0 0% 9%))"
|
|
||||||
onclick={() => goto('/login')}
|
|
||||||
>
|
|
||||||
{($locale || 'de') === 'de' ? 'Anmelden' : 'Sign in'}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
cards: 'Cards',
|
cards: 'Cards',
|
||||||
nutriphi: 'NutriPhi',
|
nutriphi: 'NutriPhi',
|
||||||
picture: 'Picture',
|
picture: 'Picture',
|
||||||
planta: 'Planta',
|
plants: 'Plants',
|
||||||
presi: 'Presi',
|
presi: 'Presi',
|
||||||
questions: 'Questions',
|
questions: 'Questions',
|
||||||
skilltree: 'SkillTree',
|
skilltree: 'SkillTree',
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
import { ArrowLeft, Trash } from '@mana/shared-icons';
|
import { ArrowLeft, Trash } from '@mana/shared-icons';
|
||||||
|
|
||||||
// Inline the live query so the closure captures page.params.id directly
|
// Inline the live query so the closure captures page.params.id directly
|
||||||
// (matches the planta DetailView pattern).
|
// (matches the plants DetailView pattern).
|
||||||
const mealQuery = useLiveQueryWithDefault(
|
const mealQuery = useLiveQueryWithDefault(
|
||||||
() => (page.params.id ? loadMealById(page.params.id) : Promise.resolve(null)),
|
() => (page.params.id ? loadMealById(page.params.id) : Promise.resolve(null)),
|
||||||
null as MealWithNutrition | null
|
null as MealWithNutrition | null
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<!--
|
<!--
|
||||||
Planta routes layout
|
Plants routes layout
|
||||||
|
|
||||||
Provides live-query contexts to all child routes (/planta, /planta/[id],
|
Provides live-query contexts to all child routes (/plants, /plants/[id],
|
||||||
/planta/add, /planta/tags). The contexts are referenced via getContext()
|
/plants/add, /plants/tags). The contexts are referenced via getContext()
|
||||||
in the page files; without this layout the legacy routes would crash at
|
in the page files; without this layout the legacy routes would crash at
|
||||||
runtime with "Cannot read properties of undefined".
|
runtime with "Cannot read properties of undefined".
|
||||||
-->
|
-->
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
toWateringSchedule,
|
toWateringSchedule,
|
||||||
toWateringLog,
|
toWateringLog,
|
||||||
useAllTags,
|
useAllTags,
|
||||||
} from '$lib/modules/planta/queries';
|
} from '$lib/modules/plants/queries';
|
||||||
import type {
|
import type {
|
||||||
LocalPlant,
|
LocalPlant,
|
||||||
LocalPlantPhoto,
|
LocalPlantPhoto,
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
PlantPhoto,
|
PlantPhoto,
|
||||||
WateringSchedule,
|
WateringSchedule,
|
||||||
WateringLog,
|
WateringLog,
|
||||||
} from '$lib/modules/planta/types';
|
} from '$lib/modules/plants/types';
|
||||||
|
|
||||||
let { children }: { children: Snippet } = $props();
|
let { children }: { children: Snippet } = $props();
|
||||||
|
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { toast } from '$lib/stores/toast.svelte';
|
import { toast } from '$lib/stores/toast.svelte';
|
||||||
import { wateringMutations } from '$lib/modules/planta/mutations';
|
import { wateringMutations } from '$lib/modules/plants/mutations';
|
||||||
import {
|
import {
|
||||||
getActivePlants,
|
getActivePlants,
|
||||||
getPrimaryPhoto,
|
getPrimaryPhoto,
|
||||||
getScheduleForPlant,
|
getScheduleForPlant,
|
||||||
getDaysUntilWatering,
|
getDaysUntilWatering,
|
||||||
isWateringOverdue,
|
isWateringOverdue,
|
||||||
} from '$lib/modules/planta/queries';
|
} from '$lib/modules/plants/queries';
|
||||||
import type { Plant, PlantPhoto, WateringSchedule } from '$lib/modules/planta/types';
|
import type { Plant, PlantPhoto, WateringSchedule } from '$lib/modules/plants/types';
|
||||||
|
|
||||||
const allPlants: { readonly value: Plant[] } = getContext('plants');
|
const allPlants: { readonly value: Plant[] } = getContext('plants');
|
||||||
const allPlantPhotos: { readonly value: PlantPhoto[] } = getContext('plantPhotos');
|
const allPlantPhotos: { readonly value: PlantPhoto[] } = getContext('plantPhotos');
|
||||||
|
|
@ -35,9 +35,9 @@
|
||||||
if (!schedule) return '';
|
if (!schedule) return '';
|
||||||
const days = getDaysUntilWatering(schedule);
|
const days = getDaysUntilWatering(schedule);
|
||||||
if (days === null) return '';
|
if (days === null) return '';
|
||||||
if (days < 0) return $_('planta.watering.overdue');
|
if (days < 0) return $_('plants.watering.overdue');
|
||||||
if (days === 0) return $_('planta.watering.today');
|
if (days === 0) return $_('plants.watering.today');
|
||||||
return $_('planta.watering.daysUntil', { values: { days } });
|
return $_('plants.watering.daysUntil', { values: { days } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowWaterButton(plantId: string): boolean {
|
function shouldShowWaterButton(plantId: string): boolean {
|
||||||
|
|
@ -51,30 +51,30 @@
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
await wateringMutations.logWatering(plantId);
|
await wateringMutations.logWatering(plantId);
|
||||||
toast.success($_('planta.success.plantWatered'));
|
toast.success($_('plants.success.plantWatered'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('logWatering failed:', err);
|
console.error('logWatering failed:', err);
|
||||||
toast.error($_('planta.errors.wateringFailed'));
|
toast.error($_('plants.errors.wateringFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{$_('planta.nav.plants')} - Planta</title>
|
<title>{$_('plants.nav.plants')} - Plants</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-2xl font-bold">{$_('planta.nav.plants')}</h1>
|
<h1 class="text-2xl font-bold">{$_('plants.nav.plants')}</h1>
|
||||||
<a href="/planta/add" class="btn btn-success">{$_('planta.plant.add')}</a>
|
<a href="/plants/add" class="btn btn-success">{$_('plants.plant.add')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if plants.length === 0}
|
{#if plants.length === 0}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<div class="text-6xl mb-4">🌱</div>
|
<div class="text-6xl mb-4">🌱</div>
|
||||||
<h2 class="text-xl font-semibold mb-2">{$_('planta.plant.noPlants')}</h2>
|
<h2 class="text-xl font-semibold mb-2">{$_('plants.plant.noPlants')}</h2>
|
||||||
<p class="text-muted-foreground mb-4">{$_('planta.app.tagline')}</p>
|
<p class="text-muted-foreground mb-4">{$_('plants.app.tagline')}</p>
|
||||||
<a href="/planta/add" class="btn btn-success">{$_('planta.plant.addFirst')}</a>
|
<a href="/plants/add" class="btn btn-success">{$_('plants.plant.addFirst')}</a>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||||
|
|
@ -87,11 +87,11 @@
|
||||||
role="link"
|
role="link"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="card plant-card text-left"
|
class="card plant-card text-left"
|
||||||
onclick={() => goto(`/planta/${plant.id}`)}
|
onclick={() => goto(`/plants/${plant.id}`)}
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
goto(`/planta/${plant.id}`);
|
goto(`/plants/${plant.id}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -118,9 +118,9 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute top-2 right-2 rounded-full bg-blue-500 px-3 py-1 text-xs text-white hover:bg-blue-600"
|
class="absolute top-2 right-2 rounded-full bg-blue-500 px-3 py-1 text-xs text-white hover:bg-blue-600"
|
||||||
onclick={(e) => handleWater(plant.id, e)}
|
onclick={(e) => handleWater(plant.id, e)}
|
||||||
title={$_('planta.watering.water')}
|
title={$_('plants.watering.water')}
|
||||||
>
|
>
|
||||||
{$_('planta.watering.water')}
|
{$_('plants.watering.water')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -4,14 +4,14 @@
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { toast } from '$lib/stores/toast.svelte';
|
import { toast } from '$lib/stores/toast.svelte';
|
||||||
import { plantMutations, wateringMutations } from '$lib/modules/planta/mutations';
|
import { plantMutations, wateringMutations } from '$lib/modules/plants/mutations';
|
||||||
import {
|
import {
|
||||||
getPlantById,
|
getPlantById,
|
||||||
getPhotosForPlant,
|
getPhotosForPlant,
|
||||||
getScheduleForPlant,
|
getScheduleForPlant,
|
||||||
getLogsForPlant,
|
getLogsForPlant,
|
||||||
} from '$lib/modules/planta/queries';
|
} from '$lib/modules/plants/queries';
|
||||||
import type { Plant, PlantPhoto, WateringSchedule, WateringLog } from '$lib/modules/planta/types';
|
import type { Plant, PlantPhoto, WateringSchedule, WateringLog } from '$lib/modules/plants/types';
|
||||||
|
|
||||||
const allPlants: { readonly value: Plant[] } = getContext('plants');
|
const allPlants: { readonly value: Plant[] } = getContext('plants');
|
||||||
const allPlantPhotos: { readonly value: PlantPhoto[] } = getContext('plantPhotos');
|
const allPlantPhotos: { readonly value: PlantPhoto[] } = getContext('plantPhotos');
|
||||||
|
|
@ -36,10 +36,10 @@
|
||||||
watering = true;
|
watering = true;
|
||||||
try {
|
try {
|
||||||
await wateringMutations.logWatering(plant.id);
|
await wateringMutations.logWatering(plant.id);
|
||||||
toast.success($_('planta.success.plantWatered'));
|
toast.success($_('plants.success.plantWatered'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('logWatering failed:', err);
|
console.error('logWatering failed:', err);
|
||||||
toast.error($_('planta.errors.wateringFailed'));
|
toast.error($_('plants.errors.wateringFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
watering = false;
|
watering = false;
|
||||||
}
|
}
|
||||||
|
|
@ -47,15 +47,15 @@
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (!plant) return;
|
if (!plant) return;
|
||||||
if (!confirm($_('planta.plant.confirmDelete'))) return;
|
if (!confirm($_('plants.plant.confirmDelete'))) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await plantMutations.delete(plant.id);
|
await plantMutations.delete(plant.id);
|
||||||
toast.success($_('planta.success.plantDeleted'));
|
toast.success($_('plants.success.plantDeleted'));
|
||||||
goto('/planta');
|
goto('/plants');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('delete plant failed:', err);
|
console.error('delete plant failed:', err);
|
||||||
toast.error($_('planta.errors.deleteFailed'));
|
toast.error($_('plants.errors.deleteFailed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,48 +75,48 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHealthText(status: string | null | undefined): string {
|
function getHealthText(status: string | null | undefined): string {
|
||||||
if (status === 'needs_attention') return $_('planta.health.needsAttention');
|
if (status === 'needs_attention') return $_('plants.health.needsAttention');
|
||||||
if (status === 'sick') return $_('planta.health.sick');
|
if (status === 'sick') return $_('plants.health.sick');
|
||||||
return $_('planta.health.healthy');
|
return $_('plants.health.healthy');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLightText(light: string | null | undefined): string {
|
function getLightText(light: string | null | undefined): string {
|
||||||
switch (light) {
|
switch (light) {
|
||||||
case 'low':
|
case 'low':
|
||||||
return $_('planta.light.low');
|
return $_('plants.light.low');
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return $_('planta.light.medium');
|
return $_('plants.light.medium');
|
||||||
case 'bright':
|
case 'bright':
|
||||||
return $_('planta.light.bright');
|
return $_('plants.light.bright');
|
||||||
case 'direct':
|
case 'direct':
|
||||||
return $_('planta.light.direct');
|
return $_('plants.light.direct');
|
||||||
default:
|
default:
|
||||||
return $_('planta.common.none');
|
return $_('plants.common.none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHumidityText(humidity: string | null | undefined): string {
|
function getHumidityText(humidity: string | null | undefined): string {
|
||||||
switch (humidity) {
|
switch (humidity) {
|
||||||
case 'low':
|
case 'low':
|
||||||
return $_('planta.humidity.low');
|
return $_('plants.humidity.low');
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return $_('planta.humidity.medium');
|
return $_('plants.humidity.medium');
|
||||||
case 'high':
|
case 'high':
|
||||||
return $_('planta.humidity.high');
|
return $_('plants.humidity.high');
|
||||||
default:
|
default:
|
||||||
return $_('planta.common.none');
|
return $_('plants.common.none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{plant?.name || $_('planta.app.name')} - Planta</title>
|
<title>{plant?.name || $_('plants.app.name')} - Plants</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if !plant}
|
{#if !plant}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<p class="text-lg">{$_('planta.plant.notFound')}</p>
|
<p class="text-lg">{$_('plants.plant.notFound')}</p>
|
||||||
<a href="/planta" class="btn btn-primary mt-4">{$_('planta.nav.plants')}</a>
|
<a href="/plants" class="btn btn-primary mt-4">{$_('plants.nav.plants')}</a>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|
@ -153,28 +153,28 @@
|
||||||
|
|
||||||
<!-- Care Info -->
|
<!-- Care Info -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="font-semibold mb-4">{$_('planta.plant.careNotes')}</h2>
|
<h2 class="font-semibold mb-4">{$_('plants.plant.careNotes')}</h2>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-muted-foreground">{$_('planta.plant.light')}</p>
|
<p class="text-sm text-muted-foreground">{$_('plants.plant.light')}</p>
|
||||||
<p class="font-medium">{getLightText(plant.lightRequirements)}</p>
|
<p class="font-medium">{getLightText(plant.lightRequirements)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-muted-foreground">{$_('planta.watering.water')}</p>
|
<p class="text-sm text-muted-foreground">{$_('plants.watering.water')}</p>
|
||||||
<p class="font-medium">
|
<p class="font-medium">
|
||||||
{plant.wateringFrequencyDays
|
{plant.wateringFrequencyDays
|
||||||
? $_('planta.list.everyXDays', { values: { days: plant.wateringFrequencyDays } })
|
? $_('plants.list.everyXDays', { values: { days: plant.wateringFrequencyDays } })
|
||||||
: $_('planta.common.none')}
|
: $_('plants.common.none')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-muted-foreground">{$_('planta.humidity.medium')}</p>
|
<p class="text-sm text-muted-foreground">{$_('plants.humidity.medium')}</p>
|
||||||
<p class="font-medium">{getHumidityText(plant.humidity)}</p>
|
<p class="font-medium">{getHumidityText(plant.humidity)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if plant.careNotes}
|
{#if plant.careNotes}
|
||||||
<div class="mt-4 pt-4 border-t">
|
<div class="mt-4 pt-4 border-t">
|
||||||
<p class="text-sm text-muted-foreground mb-1">{$_('planta.plant.careNotes')}</p>
|
<p class="text-sm text-muted-foreground mb-1">{$_('plants.plant.careNotes')}</p>
|
||||||
<p class="text-sm whitespace-pre-line">{plant.careNotes}</p>
|
<p class="text-sm whitespace-pre-line">{plant.careNotes}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -183,14 +183,14 @@
|
||||||
<!-- Watering Schedule -->
|
<!-- Watering Schedule -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="font-semibold">{$_('planta.nav.watering')}</h2>
|
<h2 class="font-semibold">{$_('plants.nav.watering')}</h2>
|
||||||
<button type="button" class="btn btn-success" onclick={handleWater} disabled={watering}>
|
<button type="button" class="btn btn-success" onclick={handleWater} disabled={watering}>
|
||||||
{#if watering}
|
{#if watering}
|
||||||
<span
|
<span
|
||||||
class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-r-transparent"
|
class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-r-transparent"
|
||||||
></span>
|
></span>
|
||||||
{:else}
|
{:else}
|
||||||
{$_('planta.watering.water')}
|
{$_('plants.watering.water')}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -198,11 +198,11 @@
|
||||||
{#if wateringSchedule}
|
{#if wateringSchedule}
|
||||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-muted-foreground">{$_('planta.watering.lastWatered')}</p>
|
<p class="text-sm text-muted-foreground">{$_('plants.watering.lastWatered')}</p>
|
||||||
<p class="font-medium">{formatDate(wateringSchedule.lastWateredAt)}</p>
|
<p class="font-medium">{formatDate(wateringSchedule.lastWateredAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-muted-foreground">{$_('planta.watering.nextWatering')}</p>
|
<p class="text-sm text-muted-foreground">{$_('plants.watering.nextWatering')}</p>
|
||||||
<p class="font-medium">{formatDate(wateringSchedule.nextWateringAt)}</p>
|
<p class="font-medium">{formatDate(wateringSchedule.nextWateringAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -210,11 +210,11 @@
|
||||||
|
|
||||||
{#if wateringHistory.length > 0}
|
{#if wateringHistory.length > 0}
|
||||||
<div class="border-t pt-4">
|
<div class="border-t pt-4">
|
||||||
<p class="text-sm text-muted-foreground mb-2">{$_('planta.watering.watered')}</p>
|
<p class="text-sm text-muted-foreground mb-2">{$_('plants.watering.watered')}</p>
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
{#each wateringHistory.slice(0, 5) as log (log.id)}
|
{#each wateringHistory.slice(0, 5) as log (log.id)}
|
||||||
<li class="text-sm flex justify-between">
|
<li class="text-sm flex justify-between">
|
||||||
<span>{$_('planta.watering.watered')}</span>
|
<span>{$_('plants.watering.watered')}</span>
|
||||||
<span class="text-muted-foreground">{formatDate(log.wateredAt)}</span>
|
<span class="text-muted-foreground">{formatDate(log.wateredAt)}</span>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -225,9 +225,9 @@
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a href="/planta" class="btn flex-1 bg-muted text-foreground">{$_('planta.nav.plants')}</a>
|
<a href="/plants" class="btn flex-1 bg-muted text-foreground">{$_('plants.nav.plants')}</a>
|
||||||
<button type="button" class="btn bg-destructive text-white" onclick={handleDelete}>
|
<button type="button" class="btn bg-destructive text-white" onclick={handleDelete}>
|
||||||
{$_('planta.plant.delete')}
|
{$_('plants.plant.delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { toast } from '$lib/stores/toast.svelte';
|
import { toast } from '$lib/stores/toast.svelte';
|
||||||
import { plantMutations } from '$lib/modules/planta/mutations';
|
import { plantMutations } from '$lib/modules/plants/mutations';
|
||||||
|
|
||||||
let plantName = $state('');
|
let plantName = $state('');
|
||||||
let scientificName = $state('');
|
let scientificName = $state('');
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
async function savePlant() {
|
async function savePlant() {
|
||||||
if (!plantName.trim()) {
|
if (!plantName.trim()) {
|
||||||
error = $_('planta.errors.saveFailed');
|
error = $_('plants.errors.saveFailed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,23 +25,23 @@
|
||||||
scientificName: scientificName.trim() || undefined,
|
scientificName: scientificName.trim() || undefined,
|
||||||
commonName: commonName.trim() || undefined,
|
commonName: commonName.trim() || undefined,
|
||||||
});
|
});
|
||||||
toast.success($_('planta.success.plantAdded'));
|
toast.success($_('plants.success.plantAdded'));
|
||||||
goto(`/planta/${plant.id}`);
|
goto(`/plants/${plant.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to create plant:', err);
|
console.error('Failed to create plant:', err);
|
||||||
error = $_('planta.errors.saveFailed');
|
error = $_('plants.errors.saveFailed');
|
||||||
toast.error($_('planta.errors.saveFailed'));
|
toast.error($_('plants.errors.saveFailed'));
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{$_('planta.plant.add')} - Planta</title>
|
<title>{$_('plants.plant.add')} - Plants</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="max-w-2xl mx-auto space-y-6">
|
<div class="max-w-2xl mx-auto space-y-6">
|
||||||
<h1 class="text-2xl font-bold">{$_('planta.plant.add')}</h1>
|
<h1 class="text-2xl font-bold">{$_('plants.plant.add')}</h1>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
<div class="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||||
|
|
@ -52,40 +52,40 @@
|
||||||
<div class="card p-6 space-y-4">
|
<div class="card p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="plant-name" class="block text-sm font-medium mb-2">
|
<label for="plant-name" class="block text-sm font-medium mb-2">
|
||||||
{$_('planta.plant.name')} *
|
{$_('plants.plant.name')} *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="plant-name"
|
id="plant-name"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={plantName}
|
bind:value={plantName}
|
||||||
class="input w-full"
|
class="input w-full"
|
||||||
placeholder={$_('planta.plant.namePlaceholder')}
|
placeholder={$_('plants.plant.namePlaceholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="scientific-name" class="block text-sm font-medium mb-2">
|
<label for="scientific-name" class="block text-sm font-medium mb-2">
|
||||||
{$_('planta.plant.scientificName')}
|
{$_('plants.plant.scientificName')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="scientific-name"
|
id="scientific-name"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={scientificName}
|
bind:value={scientificName}
|
||||||
class="input w-full"
|
class="input w-full"
|
||||||
placeholder={$_('planta.common.none')}
|
placeholder={$_('plants.common.none')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="common-name" class="block text-sm font-medium mb-2">
|
<label for="common-name" class="block text-sm font-medium mb-2">
|
||||||
{$_('planta.plant.species')}
|
{$_('plants.plant.species')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="common-name"
|
id="common-name"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={commonName}
|
bind:value={commonName}
|
||||||
class="input w-full"
|
class="input w-full"
|
||||||
placeholder={$_('planta.common.none')}
|
placeholder={$_('plants.common.none')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -95,14 +95,14 @@
|
||||||
class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-r-transparent"
|
class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-r-transparent"
|
||||||
></span>
|
></span>
|
||||||
{:else}
|
{:else}
|
||||||
{$_('planta.common.save')}
|
{$_('plants.common.save')}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="/planta" class="text-sm text-muted-foreground hover:text-foreground">
|
<a href="/plants" class="text-sm text-muted-foreground hover:text-foreground">
|
||||||
{$_('planta.nav.plants')}
|
{$_('plants.nav.plants')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -7,17 +7,17 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Tags | Planta</title>
|
<title>Tags | Plants</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="tags-page">
|
<div class="tags-page">
|
||||||
<h1>Tags</h1>
|
<h1>Tags</h1>
|
||||||
<p class="text-sm text-muted-foreground mb-4">
|
<p class="text-sm text-muted-foreground mb-4">
|
||||||
{$_('planta.app.tagline')}
|
{$_('plants.app.tagline')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if tagsCtx.value.length === 0}
|
{#if tagsCtx.value.length === 0}
|
||||||
<p>{$_('planta.list.empty')}</p>
|
<p>{$_('plants.list.empty')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
{#each tagsCtx.value as tag}
|
{#each tagsCtx.value as tag}
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
# Planta — consolidated into the unified Mana app
|
# Plants — consolidated into the unified Mana app
|
||||||
|
|
||||||
This product was migrated into the unified Mana monorepo. The legacy
|
This product was migrated into the unified Mana monorepo. The legacy
|
||||||
per-product `apps/planta/apps/backend/` and `apps/planta/apps/web/`
|
per-product `apps/plants/apps/backend/` and `apps/plants/apps/web/`
|
||||||
directories have been removed. Active code now lives in:
|
directories have been removed. Active code now lives in:
|
||||||
|
|
||||||
- **Backend compute routes**: [`apps/api/src/modules/planta/routes.ts`](../api/src/modules/planta/routes.ts) (Gemini Vision plant analysis, S3 upload)
|
- **Backend compute routes**: [`apps/api/src/modules/plants/routes.ts`](../api/src/modules/plants/routes.ts) (Gemini Vision plant analysis, S3 upload)
|
||||||
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/planta/`](../mana/apps/web/src/lib/modules/planta/)
|
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/plants/`](../mana/apps/web/src/lib/modules/plants/)
|
||||||
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/planta/`](../mana/apps/web/src/routes/(app)/planta/)
|
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/plants/`](../mana/apps/web/src/routes/(app)/plants/)
|
||||||
|
|
||||||
For monorepo-wide patterns (auth, sync, encryption, services), see the
|
For monorepo-wide patterns (auth, sync, encryption, services), see the
|
||||||
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
|
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
|
||||||
|
|
||||||
The previous standalone "Planta Project Guide" describing a per-product
|
The previous standalone "Plants Project Guide" describing a per-product
|
||||||
backend with its own database, schema, and watering scheduler was deleted
|
backend with its own database, schema, and watering scheduler was deleted
|
||||||
in the audit cleanup of 2026-04-09 — it had been inaccurate since the
|
in the audit cleanup of 2026-04-09 — it had been inaccurate since the
|
||||||
consolidation. Pre-consolidation reference is in git history.
|
consolidation. Pre-consolidation reference is in git history.
|
||||||
|
|
||||||
> **Note:** The orphaned `apps/planta/packages/shared/` package was
|
> **Note:** The orphaned `apps/plants/packages/shared/` package was
|
||||||
> removed on 2026-04-09 — it had zero consumers across the repo. The
|
> removed on 2026-04-09 — it had zero consumers across the repo. The
|
||||||
> remaining `apps/planta/package.json` is kept as a placeholder so the
|
> remaining `apps/plants/package.json` is kept as a placeholder so the
|
||||||
> directory still surfaces in IDEs and as a doc anchor.
|
> directory still surfaces in IDEs and as a doc anchor.
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "planta",
|
"name": "plants",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Planta - Plant Documentation & Care App",
|
"description": "Planta - Plant Documentation & Care App",
|
||||||
|
|
@ -54,7 +54,7 @@ ingress:
|
||||||
service: http://localhost:5000
|
service: http://localhost:5000
|
||||||
- hostname: skilltree.mana.how
|
- hostname: skilltree.mana.how
|
||||||
service: http://localhost:5000
|
service: http://localhost:5000
|
||||||
- hostname: planta.mana.how
|
- hostname: plants.mana.how
|
||||||
service: http://localhost:5000
|
service: http://localhost:5000
|
||||||
- hostname: cards.mana.how
|
- hostname: cards.mana.how
|
||||||
service: http://localhost:5000
|
service: http://localhost:5000
|
||||||
|
|
|
||||||
|
|
@ -108,12 +108,12 @@ services:
|
||||||
mc mb --ignore-existing myminio/contacts-storage;
|
mc mb --ignore-existing myminio/contacts-storage;
|
||||||
mc mb --ignore-existing myminio/storage-storage;
|
mc mb --ignore-existing myminio/storage-storage;
|
||||||
mc mb --ignore-existing myminio/inventory-storage;
|
mc mb --ignore-existing myminio/inventory-storage;
|
||||||
mc mb --ignore-existing myminio/planta-storage;
|
mc mb --ignore-existing myminio/plants-storage;
|
||||||
mc mb --ignore-existing myminio/projectdoc-storage;
|
mc mb --ignore-existing myminio/projectdoc-storage;
|
||||||
mc mb --ignore-existing myminio/music-storage;
|
mc mb --ignore-existing myminio/music-storage;
|
||||||
mc anonymous set download myminio/mana-storage;
|
mc anonymous set download myminio/mana-storage;
|
||||||
mc anonymous set download myminio/picture-storage;
|
mc anonymous set download myminio/picture-storage;
|
||||||
mc anonymous set download myminio/planta-storage;
|
mc anonymous set download myminio/plants-storage;
|
||||||
mc anonymous set download myminio/inventory-storage;
|
mc anonymous set download myminio/inventory-storage;
|
||||||
mc ilm rule add --expire-days 90 myminio/chat-storage --prefix 'tmp/' 2>/dev/null || true;
|
mc ilm rule add --expire-days 90 myminio/chat-storage --prefix 'tmp/' 2>/dev/null || true;
|
||||||
mc ilm rule add --expire-days 30 myminio/calendar-storage --prefix 'tmp/' 2>/dev/null || true;
|
mc ilm rule add --expire-days 30 myminio/calendar-storage --prefix 'tmp/' 2>/dev/null || true;
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
||||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
||||||
MINIO_API_CORS_ALLOW_ORIGIN: https://music.mana.how,https://mana.how,https://picture.mana.how,https://storage.mana.how,https://planta.mana.how,https://contacts.mana.how,https://chat.mana.how,https://nutriphi.mana.how,https://photos.mana.how
|
MINIO_API_CORS_ALLOW_ORIGIN: https://music.mana.how,https://mana.how,https://picture.mana.how,https://storage.mana.how,https://plants.mana.how,https://contacts.mana.how,https://chat.mana.how,https://nutriphi.mana.how,https://photos.mana.how
|
||||||
volumes:
|
volumes:
|
||||||
- /Volumes/ManaData/minio:/data
|
- /Volumes/ManaData/minio:/data
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -178,12 +178,12 @@ services:
|
||||||
mc mb --ignore-existing myminio/storage-storage;
|
mc mb --ignore-existing myminio/storage-storage;
|
||||||
mc mb --ignore-existing myminio/inventory-storage;
|
mc mb --ignore-existing myminio/inventory-storage;
|
||||||
mc mb --ignore-existing myminio/music-storage;
|
mc mb --ignore-existing myminio/music-storage;
|
||||||
mc mb --ignore-existing myminio/planta-storage;
|
mc mb --ignore-existing myminio/plants-storage;
|
||||||
mc mb --ignore-existing myminio/projectdoc-storage;
|
mc mb --ignore-existing myminio/projectdoc-storage;
|
||||||
mc mb --ignore-existing myminio/mail-storage;
|
mc mb --ignore-existing myminio/mail-storage;
|
||||||
mc anonymous set download myminio/mana-storage;
|
mc anonymous set download myminio/mana-storage;
|
||||||
mc anonymous set download myminio/picture-storage;
|
mc anonymous set download myminio/picture-storage;
|
||||||
mc anonymous set download myminio/planta-storage;
|
mc anonymous set download myminio/plants-storage;
|
||||||
mc anonymous set download myminio/inventory-storage;
|
mc anonymous set download myminio/inventory-storage;
|
||||||
mc ilm rule add --expire-days 90 myminio/chat-storage --prefix 'tmp/' 2>/dev/null || true;
|
mc ilm rule add --expire-days 90 myminio/chat-storage --prefix 'tmp/' 2>/dev/null || true;
|
||||||
mc ilm rule add --expire-days 30 myminio/calendar-storage --prefix 'tmp/' 2>/dev/null || true;
|
mc ilm rule add --expire-days 30 myminio/calendar-storage --prefix 'tmp/' 2>/dev/null || true;
|
||||||
|
|
@ -314,7 +314,7 @@ services:
|
||||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
||||||
STRIPE_WEBHOOK_SECRET: ${STRIPE_CREDITS_WEBHOOK_SECRET:-}
|
STRIPE_WEBHOOK_SECRET: ${STRIPE_CREDITS_WEBHOOK_SECRET:-}
|
||||||
BASE_URL: https://credits.mana.how
|
BASE_URL: https://credits.mana.how
|
||||||
CORS_ORIGINS: https://mana.how,https://chat.mana.how,https://picture.mana.how,https://todo.mana.how,https://zitare.mana.how,https://calendar.mana.how,https://clock.mana.how,https://contacts.mana.how,https://cards.mana.how,https://presi.mana.how,https://storage.mana.how,https://nutriphi.mana.how,https://planta.mana.how,https://music.mana.how,https://context.mana.how,https://photos.mana.how,https://questions.mana.how,https://calc.mana.how
|
CORS_ORIGINS: https://mana.how,https://chat.mana.how,https://picture.mana.how,https://todo.mana.how,https://zitare.mana.how,https://calendar.mana.how,https://clock.mana.how,https://contacts.mana.how,https://cards.mana.how,https://presi.mana.how,https://storage.mana.how,https://nutriphi.mana.how,https://plants.mana.how,https://music.mana.how,https://context.mana.how,https://photos.mana.how,https://questions.mana.how,https://calc.mana.how
|
||||||
ports:
|
ports:
|
||||||
- "3002:3002"
|
- "3002:3002"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
@ -412,7 +412,7 @@ services:
|
||||||
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/mana_platform
|
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/mana_platform
|
||||||
MANA_AUTH_URL: http://mana-auth:3001
|
MANA_AUTH_URL: http://mana-auth:3001
|
||||||
MANA_SERVICE_KEY: ${MANA_SERVICE_KEY}
|
MANA_SERVICE_KEY: ${MANA_SERVICE_KEY}
|
||||||
CORS_ORIGINS: https://mana.how,https://calc.mana.how,https://calendar.mana.how,https://chat.mana.how,https://clock.mana.how,https://contacts.mana.how,https://context.mana.how,https://cards.mana.how,https://music.mana.how,https://nutriphi.mana.how,https://photos.mana.how,https://picture.mana.how,https://planta.mana.how,https://presi.mana.how,https://questions.mana.how,https://storage.mana.how,https://todo.mana.how,https://zitare.mana.how
|
CORS_ORIGINS: https://mana.how,https://calc.mana.how,https://calendar.mana.how,https://chat.mana.how,https://clock.mana.how,https://contacts.mana.how,https://context.mana.how,https://cards.mana.how,https://music.mana.how,https://nutriphi.mana.how,https://photos.mana.how,https://picture.mana.how,https://plants.mana.how,https://presi.mana.how,https://questions.mana.how,https://storage.mana.how,https://todo.mana.how,https://zitare.mana.how
|
||||||
ports:
|
ports:
|
||||||
- "3062:3062"
|
- "3062:3062"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
|
||||||
|
|
@ -661,7 +661,7 @@
|
||||||
"type": "prometheus",
|
"type": "prometheus",
|
||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"expr": "up{job=\"planta-backend\"}",
|
"expr": "up{job=\"plants-backend\"}",
|
||||||
"legendFormat": "Planta",
|
"legendFormat": "Planta",
|
||||||
"refId": "Q"
|
"refId": "Q"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -615,7 +615,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
"expr": "up{job=\"planta-backend\"}",
|
"expr": "up{job=\"plants-backend\"}",
|
||||||
"legendFormat": "Planta",
|
"legendFormat": "Planta",
|
||||||
"refId": "Q"
|
"refId": "Q"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ scrape_configs:
|
||||||
scrape_interval: 30s
|
scrape_interval: 30s
|
||||||
|
|
||||||
# NOTE: Individual app backends (chat, todo, calendar, contacts, storage,
|
# NOTE: Individual app backends (chat, todo, calendar, contacts, storage,
|
||||||
# nutriphi, music, planta, picture) have been REMOVED — all migrated to
|
# nutriphi, music, plants, picture) have been REMOVED — all migrated to
|
||||||
# local-first architecture. Only uload-server and memoro-server remain.
|
# local-first architecture. Only uload-server and memoro-server remain.
|
||||||
|
|
||||||
# Mana LLM Gateway (Ollama + Google Fallback)
|
# Mana LLM Gateway (Ollama + Google Fallback)
|
||||||
|
|
@ -227,7 +227,7 @@ scrape_configs:
|
||||||
- https://mana.how/storage
|
- https://mana.how/storage
|
||||||
- https://mana.how/presi
|
- https://mana.how/presi
|
||||||
- https://mana.how/nutriphi
|
- https://mana.how/nutriphi
|
||||||
- https://mana.how/planta
|
- https://mana.how/plants
|
||||||
- https://mana.how/calc
|
- https://mana.how/calc
|
||||||
- https://mana.how/zitare
|
- https://mana.how/zitare
|
||||||
- https://mana.how/cards
|
- https://mana.how/cards
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ Legende: LOCAL = Self-hosted | CLOUD = Externer Dienst | FREE = Kostenlos
|
||||||
- `picture-storage` - AI-generierte Bilder
|
- `picture-storage` - AI-generierte Bilder
|
||||||
- `chat-storage` - User-Uploads
|
- `chat-storage` - User-Uploads
|
||||||
- `cards-storage` - Card/Deck Assets
|
- `cards-storage` - Card/Deck Assets
|
||||||
- `planta-storage` - Pflanzenfotos
|
- `plants-storage` - Pflanzenfotos
|
||||||
- `nutriphi-storage` - Mahlzeitenfotos
|
- `nutriphi-storage` - Mahlzeitenfotos
|
||||||
- `presi-storage` - Präsentationsfolien
|
- `presi-storage` - Präsentationsfolien
|
||||||
- `calendar-storage` - Kalender-Anhänge
|
- `calendar-storage` - Kalender-Anhänge
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ pnpm setup:db:uload # Setup uload database + schema
|
||||||
pnpm setup:db:context # Setup context database + schema
|
pnpm setup:db:context # Setup context database + schema
|
||||||
pnpm setup:db:storage # Setup storage database + schema
|
pnpm setup:db:storage # Setup storage database + schema
|
||||||
pnpm setup:db:mukke # Setup mukke database + schema
|
pnpm setup:db:mukke # Setup mukke database + schema
|
||||||
pnpm setup:db:planta # Setup planta database + schema
|
pnpm setup:db:plants # Setup plants database + schema
|
||||||
pnpm setup:db:nutriphi # Setup nutriphi database + schema
|
pnpm setup:db:nutriphi # Setup nutriphi database + schema
|
||||||
pnpm setup:db:questions # Setup questions database + schema
|
pnpm setup:db:questions # Setup questions database + schema
|
||||||
pnpm setup:db:traces # Setup traces database + schema
|
pnpm setup:db:traces # Setup traces database + schema
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ All monitoring tools are publicly accessible - no login required (except GlitchT
|
||||||
| Photos | https://stats.mana.how/share/dc201d685f784716a0b8587376eca7a1/photos-webapp |
|
| Photos | https://stats.mana.how/share/dc201d685f784716a0b8587376eca7a1/photos-webapp |
|
||||||
| Storage | https://stats.mana.how/share/392ff51d11f14f0c9d556af1402a3ee6/storage-webapp |
|
| Storage | https://stats.mana.how/share/392ff51d11f14f0c9d556af1402a3ee6/storage-webapp |
|
||||||
| NutriPhi | https://stats.mana.how/share/33dfae72f8e24aaa8008cbbceeaf072d/nutriphi-webapp |
|
| NutriPhi | https://stats.mana.how/share/33dfae72f8e24aaa8008cbbceeaf072d/nutriphi-webapp |
|
||||||
| Planta | https://stats.mana.how/share/1e83a8a67fa84d3995455c21dedbe3a2/planta-webapp |
|
| Planta | https://stats.mana.how/share/1e83a8a67fa84d3995455c21dedbe3a2/plants-webapp |
|
||||||
| Presi | https://stats.mana.how/share/a1eb8d1fa4d543e6b97ac41351fe1c6f/presi-webapp |
|
| Presi | https://stats.mana.how/share/a1eb8d1fa4d543e6b97ac41351fe1c6f/presi-webapp |
|
||||||
| Skilltree | https://stats.mana.how/share/5de13e0895ae4a69aa2a834f985be14d/skilltree-webapp |
|
| Skilltree | https://stats.mana.how/share/5de13e0895ae4a69aa2a834f985be14d/skilltree-webapp |
|
||||||
| Cards | https://stats.mana.how/share/1c1d54c4782943e58dde0a6db7c86ec6/cards-webapp |
|
| Cards | https://stats.mana.how/share/1c1d54c4782943e58dde0a6db7c86ec6/cards-webapp |
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ Pure CRUD apps use mana-sync directly.
|
||||||
| 3036 | cards-server | Hono/Bun | AI card generation |
|
| 3036 | cards-server | Hono/Bun | AI card generation |
|
||||||
| 3037 | mukke-server | Hono/Bun | Audio processing, BPM, ID3 tags |
|
| 3037 | mukke-server | Hono/Bun | Audio processing, BPM, ID3 tags |
|
||||||
| 3038 | nutriphi-server | Hono/Bun | Gemini meal analysis |
|
| 3038 | nutriphi-server | Hono/Bun | Gemini meal analysis |
|
||||||
| 3039 | planta-server | Hono/Bun | Gemini plant analysis |
|
| 3039 | plants-server | Hono/Bun | Gemini plant analysis |
|
||||||
| 3040 | presi-server | Hono/Bun | Share links |
|
| 3040 | presi-server | Hono/Bun | Share links |
|
||||||
| 3041-3059 | *(reserved)* | | |
|
| 3041-3059 | *(reserved)* | | |
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ Pure CRUD apps use mana-sync directly.
|
||||||
| 5025 | inventory-web | *(local-first only)* |
|
| 5025 | inventory-web | *(local-first only)* |
|
||||||
| 5026 | context-web | *(local-first only)* |
|
| 5026 | context-web | *(local-first only)* |
|
||||||
| 5027 | questions-web | *(local-first only)* |
|
| 5027 | questions-web | *(local-first only)* |
|
||||||
| 5028 | planta-web | 3039 planta-server |
|
| 5028 | plants-web | 3039 plants-server |
|
||||||
| 5029 | moodlit-web | *(future)* |
|
| 5029 | moodlit-web | *(future)* |
|
||||||
| 5030-5049 | *(reserved)* | |
|
| 5030-5049 | *(reserved)* | |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ needed them.
|
||||||
removed.
|
removed.
|
||||||
- `~/.cloudflared/config.yml` on the Mac Mini lost its 16 dead ingress
|
- `~/.cloudflared/config.yml` on the Mac Mini lost its 16 dead ingress
|
||||||
routes (`chat-api`, `todo-api`, `calendar-api`, `clock-api`, `clock-bot`,
|
routes (`chat-api`, `todo-api`, `calendar-api`, `clock-api`, `clock-bot`,
|
||||||
`contacts-api`, `zitare-api`, `skilltree-api`, `planta-api`, `cards-api`,
|
`contacts-api`, `zitare-api`, `skilltree-api`, `plants-api`, `cards-api`,
|
||||||
`storage-api`, `presi-api`, `nutriphi-api`, `photos-api`, `mukke-api`,
|
`storage-api`, `presi-api`, `nutriphi-api`, `photos-api`, `mukke-api`,
|
||||||
`picture-api`). The tunnel was reloaded via `kill -HUP <pid>`.
|
`picture-api`). The tunnel was reloaded via `kill -HUP <pid>`.
|
||||||
- After reload, every former 502 returns 404 from the Cloudflare edge
|
- After reload, every former 502 returns 404 from the Cloudflare edge
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ nicht zentralisiert.
|
||||||
|
|
||||||
### 2. ✅ 8× Recursive Turbo Anti-Pattern fixen
|
### 2. ✅ 8× Recursive Turbo Anti-Pattern fixen
|
||||||
|
|
||||||
**Wo:** `apps/{uload,context,moodlit,planta,storage,news,questions}/package.json`
|
**Wo:** `apps/{uload,context,moodlit,plants,storage,news,questions}/package.json`
|
||||||
+ `games/arcade/package.json` (im Audit-Sweep zusätzlich gefunden)
|
+ `games/arcade/package.json` (im Audit-Sweep zusätzlich gefunden)
|
||||||
|
|
||||||
**Problem:** Diese package.json enthielten `"dev": "turbo run dev"`.
|
**Problem:** Diese package.json enthielten `"dev": "turbo run dev"`.
|
||||||
|
|
@ -49,9 +49,9 @@ gefundene Dead-Code:
|
||||||
auf nicht-existierende `@context/web` / `@context/server` Packages —
|
auf nicht-existierende `@context/web` / `@context/server` Packages —
|
||||||
entfernt, nur `dev:mobile` bleibt (das einzige real existierende
|
entfernt, nur `dev:mobile` bleibt (das einzige real existierende
|
||||||
Sub-Package).
|
Sub-Package).
|
||||||
- `apps/planta/package.json` hatte `dev:web`, `dev:server`, `db:push`,
|
- `apps/plants/package.json` hatte `dev:web`, `dev:server`, `db:push`,
|
||||||
`db:studio`, `db:seed` Filter auf nicht-existierende `@planta/web` /
|
`db:studio`, `db:seed` Filter auf nicht-existierende `@plants/web` /
|
||||||
`@planta/server` Packages — entfernt. `apps/planta/` enthält nur ein
|
`@plants/server` Packages — entfernt. `apps/plants/` enthält nur ein
|
||||||
leeres `packages/shared/` (siehe Audit-Item #11/#18).
|
leeres `packages/shared/` (siehe Audit-Item #11/#18).
|
||||||
- Analog ist `apps/storage/` und `apps/questions/` nur ein Stub mit
|
- Analog ist `apps/storage/` und `apps/questions/` nur ein Stub mit
|
||||||
`packages/shared/` bzw. nichts — die CLAUDE.md-Files referenzieren
|
`packages/shared/` bzw. nichts — die CLAUDE.md-Files referenzieren
|
||||||
|
|
@ -86,7 +86,7 @@ hatten die Infrastruktur bereits — sie wurde nur nicht benutzt.
|
||||||
- `packages/shared-hono/src/index.ts`: Re-exportiert `logger` aus
|
- `packages/shared-hono/src/index.ts`: Re-exportiert `logger` aus
|
||||||
`@mana/shared-logger`, damit Module ohne extra dependency darauf
|
`@mana/shared-logger`, damit Module ohne extra dependency darauf
|
||||||
zugreifen können.
|
zugreifen können.
|
||||||
- 7 `console.error` Aufrufe in `apps/api/src/modules/{guides,planta,
|
- 7 `console.error` Aufrufe in `apps/api/src/modules/{guides,plants,
|
||||||
nutriphi,traces}/routes.ts` durch
|
nutriphi,traces}/routes.ts` durch
|
||||||
`logger.error('module.event_name', { error: ... })` ersetzt. Event-Namen
|
`logger.error('module.event_name', { error: ... })` ersetzt. Event-Namen
|
||||||
folgen `<module>.<event>` Konvention für Filterbarkeit in Sentry/JSON-Logs.
|
folgen `<module>.<event>` Konvention für Filterbarkeit in Sentry/JSON-Logs.
|
||||||
|
|
@ -373,7 +373,7 @@ nicht mehr existiert.
|
||||||
`apps/uload/apps/server`. Beide deployed via
|
`apps/uload/apps/server`. Beide deployed via
|
||||||
`docker-compose.macmini.yml`.
|
`docker-compose.macmini.yml`.
|
||||||
- Alle anderen 17 (`calendar`, `chat`, `todo`, `contacts`, `picture`,
|
- Alle anderen 17 (`calendar`, `chat`, `todo`, `contacts`, `picture`,
|
||||||
`cards`, `planta`, `nutriphi`, `news`, `traces`, `presi`, `storage`,
|
`cards`, `plants`, `nutriphi`, `news`, `traces`, `presi`, `storage`,
|
||||||
`music`, `moodlit`, `context`, `guides`, `questions`) wurden zu
|
`music`, `moodlit`, `context`, `guides`, `questions`) wurden zu
|
||||||
`apps/api/src/modules/{name}/routes.ts` migriert. Ihre `apps/server/`
|
`apps/api/src/modules/{name}/routes.ts` migriert. Ihre `apps/server/`
|
||||||
Verzeichnisse existieren nicht mehr.
|
Verzeichnisse existieren nicht mehr.
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ Jeder einzelne Backend-Service:
|
||||||
| Kategorie | Backends | Source Files | Empfehlung |
|
| Kategorie | Backends | Source Files | Empfehlung |
|
||||||
|-----------|----------|-------------|------------|
|
|-----------|----------|-------------|------------|
|
||||||
| **Komplex (behalten)** | Chat, Todo, Calendar, Contacts, Storage, Mukke | 60-89 pro Backend | Eigenstaendige Services |
|
| **Komplex (behalten)** | Chat, Todo, Calendar, Contacts, Storage, Mukke | 60-89 pro Backend | Eigenstaendige Services |
|
||||||
| **AI-spezialisiert (behalten)** | Picture, Planta, NutriPhi | 30-50 pro Backend | Eigene AI-Pipelines |
|
| **AI-spezialisiert (behalten)** | Picture, Plants, NutriPhi | 30-50 pro Backend | Eigene AI-Pipelines |
|
||||||
| **Triviales CRUD (konsolidieren)** | Zitare (20!), Clock (31), Presi, CityCorners, Questions, Context | 20-35 pro Backend | -> 1 Content Service |
|
| **Triviales CRUD (konsolidieren)** | Zitare (20!), Clock (31), Presi, CityCorners, Questions, Context | 20-35 pro Backend | -> 1 Content Service |
|
||||||
| **Eliminieren** | Photos (mana-media Proxy), Skilltree (Frontend-only) | minimal | Backend loeschen |
|
| **Eliminieren** | Photos (mana-media Proxy), Skilltree (Frontend-only) | minimal | Backend loeschen |
|
||||||
|
|
||||||
|
|
@ -558,7 +558,7 @@ Diese Technologie-Entscheidungen sind gut und sollten beibehalten werden:
|
||||||
|
|
||||||
### Apps (24)
|
### Apps (24)
|
||||||
|
|
||||||
calendar, chat, citycorners, clock, contacts, context, docs, inventar, mana, cards, matrix, mukke, nutriphi, photos, picture, planta, playground, presi, questions, skilltree, storage, todo, traces, zitare
|
calendar, chat, citycorners, clock, contacts, context, docs, inventar, mana, cards, matrix, mukke, nutriphi, photos, picture, plants, playground, presi, questions, skilltree, storage, todo, traces, zitare
|
||||||
|
|
||||||
### Services (17)
|
### Services (17)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ recommendation.
|
||||||
|
|
||||||
**Productivity:** todo, calendar, contacts, notes, habits, times, timeblocks, events
|
**Productivity:** todo, calendar, contacts, notes, habits, times, timeblocks, events
|
||||||
**Knowledge & learning:** cards, zitare, guides, questions, skilltree, memoro, context
|
**Knowledge & learning:** cards, zitare, guides, questions, skilltree, memoro, context
|
||||||
**Health & self:** nutriphi, cycles, dreams, moodlit, planta
|
**Health & self:** nutriphi, cycles, dreams, moodlit, plants
|
||||||
**Media & creative:** chat, picture, presi, music, photos, storage, uload
|
**Media & creative:** chat, picture, presi, music, photos, storage, uload
|
||||||
**Data & tools:** finance, calc, inventory, places, citycorners, who, news, links, tags, playground
|
**Data & tools:** finance, calc, inventory, places, citycorners, who, news, links, tags, playground
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ recommendation.
|
||||||
|
|
||||||
- **kids** *(ZK)* — Milestones, illnesses, growth, photos
|
- **kids** *(ZK)* — Milestones, illnesses, growth, photos
|
||||||
- **pets** — Vet appointments, vaccinations, feeding, weight
|
- **pets** — Vet appointments, vaccinations, feeding, weight
|
||||||
- **plants-care** — Extension of `planta`: watering plan, fertilizing, repotting
|
- **plants-care** — Extension of `plants`: watering plan, fertilizing, repotting
|
||||||
|
|
||||||
## Health & Body (additional)
|
## Health & Body (additional)
|
||||||
|
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -155,10 +155,10 @@
|
||||||
"dev:citycorners:app": "pnpm dev:citycorners:web",
|
"dev:citycorners:app": "pnpm dev:citycorners:web",
|
||||||
"dev:citycorners:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:citycorners:web\"",
|
"dev:citycorners:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:citycorners:web\"",
|
||||||
"deploy:landing:citycorners": "pnpm --filter @citycorners/landing build && npx wrangler pages deploy apps/citycorners/apps/landing/dist --project-name=citycorners-landing",
|
"deploy:landing:citycorners": "pnpm --filter @citycorners/landing build && npx wrangler pages deploy apps/citycorners/apps/landing/dist --project-name=citycorners-landing",
|
||||||
"planta:dev": "turbo run dev --filter=planta...",
|
"plants:dev": "turbo run dev --filter=plants...",
|
||||||
"dev:planta:web": "pnpm --filter @planta/web dev",
|
"dev:plants:web": "pnpm --filter @plants/web dev",
|
||||||
"dev:planta:app": "concurrently -n api,web -c yellow,cyan \"pnpm dev:api\" \"pnpm dev:planta:web\"",
|
"dev:plants:app": "concurrently -n api,web -c yellow,cyan \"pnpm dev:api\" \"pnpm dev:plants:web\"",
|
||||||
"dev:planta:full": "concurrently -n auth,sync,api -c blue,magenta,yellow \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:plants:full": "concurrently -n auth,sync,api -c blue,magenta,yellow \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
"nutriphi:dev": "turbo run dev --filter=nutriphi...",
|
"nutriphi:dev": "turbo run dev --filter=nutriphi...",
|
||||||
"dev:nutriphi:web": "pnpm --filter @nutriphi/web dev",
|
"dev:nutriphi:web": "pnpm --filter @nutriphi/web dev",
|
||||||
"dev:nutriphi:landing": "pnpm --filter @nutriphi/landing dev",
|
"dev:nutriphi:landing": "pnpm --filter @nutriphi/landing dev",
|
||||||
|
|
@ -237,7 +237,7 @@
|
||||||
"dev:music:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:music:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
"dev:nutriphi:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:nutriphi:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
"dev:picture:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:picture:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
"dev:planta:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:plants:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
"dev:questions:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:questions:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
"dev:storage:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:storage:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
"dev:presi:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
"dev:presi:local": "concurrently -n sync,api -c magenta,yellow \"pnpm dev:sync\" \"pnpm dev:api\"",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export enum CreditOperationType {
|
||||||
// Zitare - AI explanations
|
// Zitare - AI explanations
|
||||||
AI_QUOTE_EXPLANATION = 'ai_quote_explanation',
|
AI_QUOTE_EXPLANATION = 'ai_quote_explanation',
|
||||||
|
|
||||||
// Planta - Plant analysis
|
// Plants - Plant analysis
|
||||||
AI_PLANT_ANALYSIS = 'ai_plant_analysis',
|
AI_PLANT_ANALYSIS = 'ai_plant_analysis',
|
||||||
|
|
||||||
// Traces - City guide generation
|
// Traces - City guide generation
|
||||||
|
|
@ -243,12 +243,12 @@ export const OPERATION_METADATA: Record<CreditOperationType, OperationMetadata>
|
||||||
app: 'zitare',
|
app: 'zitare',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Planta
|
// Plants
|
||||||
[CreditOperationType.AI_PLANT_ANALYSIS]: {
|
[CreditOperationType.AI_PLANT_ANALYSIS]: {
|
||||||
name: 'Plant Analysis',
|
name: 'Plant Analysis',
|
||||||
description: 'Identify and analyze a plant from a photo',
|
description: 'Identify and analyze a plant from a photo',
|
||||||
category: CreditCategory.AI,
|
category: CreditCategory.AI,
|
||||||
app: 'planta',
|
app: 'plants',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Traces
|
// Traces
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ export function getManaFAQs(locale: string): FAQItem[] {
|
||||||
id: 'faq-mana-use',
|
id: 'faq-mana-use',
|
||||||
question: isDE ? 'Wofür wird Mana verwendet?' : 'What is Mana used for?',
|
question: isDE ? 'Wofür wird Mana verwendet?' : 'What is Mana used for?',
|
||||||
answer: isDE
|
answer: isDE
|
||||||
? '<p>Mana wird für Premium-Funktionen innerhalb der Mana-Apps verwendet:</p><ul><li><strong>Chat</strong>: Cloud-KI-Modelle (Claude, GPT, DeepSeek) — lokale Modelle sind kostenlos</li><li><strong>Picture</strong>: KI-Bildgenerierungen (nach 3 kostenlosen Generierungen)</li><li><strong>Context</strong>: KI-Textgenerierung und -analyse</li><li><strong>Weitere Apps</strong>: KI-gestützte Features in Planta, Questions, etc.</li></ul><p>Basis-Funktionen wie Aufgaben, Kalender, Kontakte, Dateien und Chats mit lokalen Modellen sind <strong>immer kostenlos</strong>.</p>'
|
? '<p>Mana wird für Premium-Funktionen innerhalb der Mana-Apps verwendet:</p><ul><li><strong>Chat</strong>: Cloud-KI-Modelle (Claude, GPT, DeepSeek) — lokale Modelle sind kostenlos</li><li><strong>Picture</strong>: KI-Bildgenerierungen (nach 3 kostenlosen Generierungen)</li><li><strong>Context</strong>: KI-Textgenerierung und -analyse</li><li><strong>Weitere Apps</strong>: KI-gestützte Features in Plants, Questions, etc.</li></ul><p>Basis-Funktionen wie Aufgaben, Kalender, Kontakte, Dateien und Chats mit lokalen Modellen sind <strong>immer kostenlos</strong>.</p>'
|
||||||
: '<p>Mana is used for premium features within Mana apps:</p><ul><li><strong>Chat</strong>: Cloud AI models (Claude, GPT, DeepSeek) — local models are free</li><li><strong>Picture</strong>: AI image generations (after 3 free generations)</li><li><strong>Context</strong>: AI text generation and analysis</li><li><strong>More apps</strong>: AI-powered features in Planta, Questions, etc.</li></ul><p>Core features like tasks, calendar, contacts, files, and chats with local models are <strong>always free</strong>.</p>',
|
: '<p>Mana is used for premium features within Mana apps:</p><ul><li><strong>Chat</strong>: Cloud AI models (Claude, GPT, DeepSeek) — local models are free</li><li><strong>Picture</strong>: AI image generations (after 3 free generations)</li><li><strong>Context</strong>: AI text generation and analysis</li><li><strong>More apps</strong>: AI-powered features in Plants, Questions, etc.</li></ul><p>Core features like tasks, calendar, contacts, files, and chats with local models are <strong>always free</strong>.</p>',
|
||||||
category: 'billing',
|
category: 'billing',
|
||||||
order: 92,
|
order: 92,
|
||||||
language: isDE ? 'de' : 'en',
|
language: isDE ? 'de' : 'en',
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
calc: ['Rechnen ohne Ablenkung', 'Quelloffen & unabhängig', 'Privat by Design'],
|
calc: ['Rechnen ohne Ablenkung', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||||
guides: ['Anleitungen, die funktionieren', 'Quelloffen & unabhängig', 'Privat by Design'],
|
guides: ['Anleitungen, die funktionieren', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||||
citycorners: ['Entdecke deine Stadt', 'Quelloffen & unabhängig', 'Privat by Design'],
|
citycorners: ['Entdecke deine Stadt', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||||
planta: ['Pflanzenpflege leicht gemacht', 'Quelloffen & unabhängig', 'Privat by Design'],
|
plants: ['Pflanzenpflege leicht gemacht', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||||
photos: ['Deine Fotos, deine Galerie', 'Quelloffen & unabhängig', 'Privat by Design'],
|
photos: ['Deine Fotos, deine Galerie', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||||
questions: ['Recherche mit System', 'Quelloffen & unabhängig', 'Privat by Design'],
|
questions: ['Recherche mit System', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||||
context: ['Dein Wissen, strukturiert', 'Quelloffen & unabhängig', 'Privat by Design'],
|
context: ['Dein Wissen, strukturiert', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
calc: ['Calculate without distraction', 'Open-source & independent', 'Private by design'],
|
calc: ['Calculate without distraction', 'Open-source & independent', 'Private by design'],
|
||||||
guides: ['Guides that actually work', 'Open-source & independent', 'Private by design'],
|
guides: ['Guides that actually work', 'Open-source & independent', 'Private by design'],
|
||||||
citycorners: ['Discover your city', 'Open-source & independent', 'Private by design'],
|
citycorners: ['Discover your city', 'Open-source & independent', 'Private by design'],
|
||||||
planta: ['Plant care made simple', 'Open-source & independent', 'Private by design'],
|
plants: ['Plant care made simple', 'Open-source & independent', 'Private by design'],
|
||||||
photos: ['Your photos, your gallery', 'Open-source & independent', 'Private by design'],
|
photos: ['Your photos, your gallery', 'Open-source & independent', 'Private by design'],
|
||||||
questions: ['Research with structure', 'Open-source & independent', 'Private by design'],
|
questions: ['Research with structure', 'Open-source & independent', 'Private by design'],
|
||||||
context: ['Your knowledge, organized', 'Open-source & independent', 'Private by design'],
|
context: ['Your knowledge, organized', 'Open-source & independent', 'Private by design'],
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export const APP_ICONS = {
|
||||||
photos: svgToDataUrl(
|
photos: svgToDataUrl(
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="ph" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#8b5cf6"/><stop offset="100%" style="stop-color:#7c3aed"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#ph)"/><rect x="20" y="28" width="60" height="44" rx="5" stroke="white" stroke-width="4" fill="none"/><circle cx="40" cy="44" r="6" stroke="white" stroke-width="3" fill="none"/><path d="M20 60l16-14 12 10 14-12 18 16" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="ph" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#8b5cf6"/><stop offset="100%" style="stop-color:#7c3aed"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#ph)"/><rect x="20" y="28" width="60" height="44" rx="5" stroke="white" stroke-width="4" fill="none"/><circle cx="40" cy="44" r="6" stroke="white" stroke-width="3" fill="none"/><path d="M20 60l16-14 12 10 14-12 18 16" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`
|
||||||
),
|
),
|
||||||
planta: svgToDataUrl(
|
plants: svgToDataUrl(
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="pl" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#22c55e"/><stop offset="100%" style="stop-color:#16a34a"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#pl)"/><path d="M50 72V42" stroke="white" stroke-width="4" stroke-linecap="round"/><path d="M50 52c-12-2-20-14-18-24 10 0 20 8 18 24z" fill="white" fill-opacity="0.9"/><path d="M50 42c10-4 22-2 24 10-10 2-22-2-24-10z" fill="white" fill-opacity="0.7"/><path d="M38 72h24" stroke="white" stroke-width="4" stroke-linecap="round"/></svg>`
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="pl" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#22c55e"/><stop offset="100%" style="stop-color:#16a34a"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#pl)"/><path d="M50 72V42" stroke="white" stroke-width="4" stroke-linecap="round"/><path d="M50 52c-12-2-20-14-18-24 10 0 20 8 18 24z" fill="white" fill-opacity="0.9"/><path d="M50 42c10-4 22-2 24 10-10 2-22-2-24-10z" fill="white" fill-opacity="0.7"/><path d="M38 72h24" stroke="white" stroke-width="4" stroke-linecap="round"/></svg>`
|
||||||
),
|
),
|
||||||
skilltree: svgToDataUrl(
|
skilltree: svgToDataUrl(
|
||||||
|
|
@ -153,7 +153,7 @@ export const APP_ICONS = {
|
||||||
body: svgToDataUrl(
|
body: svgToDataUrl(
|
||||||
// Dumbbell + heart-pulse hybrid: training (barbell) + body (pulse line).
|
// Dumbbell + heart-pulse hybrid: training (barbell) + body (pulse line).
|
||||||
// Red→orange gradient to set it apart from the green health-adjacent
|
// Red→orange gradient to set it apart from the green health-adjacent
|
||||||
// modules (planta, nutriphi) and the pink cycles icon.
|
// modules (plants, nutriphi) and the pink cycles icon.
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="bd" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#ef4444"/><stop offset="100%" style="stop-color:#f97316"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#bd)"/><rect x="18" y="42" width="6" height="16" rx="2" fill="white"/><rect x="76" y="42" width="6" height="16" rx="2" fill="white"/><rect x="24" y="46" width="4" height="8" rx="1" fill="white" fill-opacity="0.85"/><rect x="72" y="46" width="4" height="8" rx="1" fill="white" fill-opacity="0.85"/><rect x="28" y="48" width="44" height="4" rx="2" fill="white"/><path d="M30 70h12l4-8 6 16 4-10 6 6h12" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="bd" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#ef4444"/><stop offset="100%" style="stop-color:#f97316"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#bd)"/><rect x="18" y="42" width="6" height="16" rx="2" fill="white"/><rect x="76" y="42" width="6" height="16" rx="2" fill="white"/><rect x="24" y="46" width="4" height="8" rx="1" fill="white" fill-opacity="0.85"/><rect x="72" y="46" width="4" height="8" rx="1" fill="white" fill-opacity="0.85"/><rect x="28" y="48" width="44" height="4" rx="2" fill="white"/><path d="M30 70h12l4-8 6 16 4-10 6 6h12" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`
|
||||||
),
|
),
|
||||||
firsts: svgToDataUrl(
|
firsts: svgToDataUrl(
|
||||||
|
|
|
||||||
|
|
@ -246,9 +246,9 @@ export const APP_BRANDING: Record<AppId, AppBranding> = {
|
||||||
logoStroke: true,
|
logoStroke: true,
|
||||||
logoStrokeWidth: 1.5,
|
logoStrokeWidth: 1.5,
|
||||||
},
|
},
|
||||||
planta: {
|
plants: {
|
||||||
id: 'planta',
|
id: 'plants',
|
||||||
name: 'Planta',
|
name: 'Plants',
|
||||||
tagline: 'Plant Care Assistant',
|
tagline: 'Plant Care Assistant',
|
||||||
primaryColor: '#22c55e',
|
primaryColor: '#22c55e',
|
||||||
secondaryColor: '#4ade80',
|
secondaryColor: '#4ade80',
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export {
|
||||||
ClockLogo,
|
ClockLogo,
|
||||||
QuestionsLogo,
|
QuestionsLogo,
|
||||||
SkillTreeLogo,
|
SkillTreeLogo,
|
||||||
PlantaLogo,
|
PlantsLogo,
|
||||||
LightWriteLogo,
|
LightWriteLogo,
|
||||||
MusicLogo,
|
MusicLogo,
|
||||||
ContextLogo,
|
ContextLogo,
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,4 @@
|
||||||
let { size = 55, color, class: className = '' }: Props = $props();
|
let { size = 55, color, class: className = '' }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppLogo app="planta" {size} {color} class={className} />
|
<AppLogo app="plants" {size} {color} class={className} />
|
||||||
|
|
@ -20,7 +20,7 @@ export { default as InventoryLogo } from './InventoryLogo.svelte';
|
||||||
export { default as ClockLogo } from './ClockLogo.svelte';
|
export { default as ClockLogo } from './ClockLogo.svelte';
|
||||||
export { default as QuestionsLogo } from './QuestionsLogo.svelte';
|
export { default as QuestionsLogo } from './QuestionsLogo.svelte';
|
||||||
export { default as SkillTreeLogo } from './SkillTreeLogo.svelte';
|
export { default as SkillTreeLogo } from './SkillTreeLogo.svelte';
|
||||||
export { default as PlantaLogo } from './PlantaLogo.svelte';
|
export { default as PlantsLogo } from './PlantsLogo.svelte';
|
||||||
export { default as LightWriteLogo } from './LightWriteLogo.svelte';
|
export { default as LightWriteLogo } from './LightWriteLogo.svelte';
|
||||||
export { default as MusicLogo } from './MusicLogo.svelte';
|
export { default as MusicLogo } from './MusicLogo.svelte';
|
||||||
export { default as ContextLogo } from './ContextLogo.svelte';
|
export { default as ContextLogo } from './ContextLogo.svelte';
|
||||||
|
|
|
||||||
|
|
@ -531,17 +531,17 @@ export const MANA_APPS: ManaApp[] = [
|
||||||
requiredTier: 'guest',
|
requiredTier: 'guest',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'planta',
|
id: 'plants',
|
||||||
name: 'Planta',
|
name: 'Plants',
|
||||||
description: {
|
description: {
|
||||||
de: 'Pflanzenpflege',
|
de: 'Pflanzen',
|
||||||
en: 'Plant Care',
|
en: 'Plant Care',
|
||||||
},
|
},
|
||||||
longDescription: {
|
longDescription: {
|
||||||
de: 'Verwalte deine Pflanzen mit Gießplänen, Fotos und Pflegeprotokollen.',
|
de: 'Verwalte deine Pflanzen mit Gießplänen, Fotos und Pflegeprotokollen.',
|
||||||
en: 'Manage your plants with watering schedules, photos, and care logs.',
|
en: 'Manage your plants with watering schedules, photos, and care logs.',
|
||||||
},
|
},
|
||||||
icon: APP_ICONS.planta,
|
icon: APP_ICONS.plants,
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
comingSoon: false,
|
comingSoon: false,
|
||||||
status: 'beta',
|
status: 'beta',
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export type AppId =
|
||||||
| 'inventory'
|
| 'inventory'
|
||||||
| 'questions'
|
| 'questions'
|
||||||
| 'skilltree'
|
| 'skilltree'
|
||||||
| 'planta'
|
| 'plants'
|
||||||
| 'lightwrite'
|
| 'lightwrite'
|
||||||
| 'context'
|
| 'context'
|
||||||
| 'music'
|
| 'music'
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ Each app gets its own isolated bucket, created automatically by `minio-init`:
|
||||||
| `mail-storage` | Mail | Email attachments |
|
| `mail-storage` | Mail | Email attachments |
|
||||||
| `inventory-storage` | Inventory | Product photos |
|
| `inventory-storage` | Inventory | Product photos |
|
||||||
| `music-storage` | Music | Music tracks, beats, covers |
|
| `music-storage` | Music | Music tracks, beats, covers |
|
||||||
| `planta-storage` | Planta | Plant photos |
|
| `plants-storage` | Planta | Plant photos |
|
||||||
| `projectdoc-storage` | ProjectDoc | Document files |
|
| `projectdoc-storage` | ProjectDoc | Document files |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue