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:
Till JS 2026-04-12 18:59:44 +02:00
parent c6c19dbc77
commit a91a6076cc
110 changed files with 831 additions and 707 deletions

View file

@ -44,7 +44,7 @@ export enum CreditOperationType {
// Zitare - AI explanations
AI_QUOTE_EXPLANATION = 'ai_quote_explanation',
// Planta - Plant analysis
// Plants - Plant analysis
AI_PLANT_ANALYSIS = 'ai_plant_analysis',
// Traces - City guide generation
@ -243,12 +243,12 @@ export const OPERATION_METADATA: Record<CreditOperationType, OperationMetadata>
app: 'zitare',
},
// Planta
// Plants
[CreditOperationType.AI_PLANT_ANALYSIS]: {
name: 'Plant Analysis',
description: 'Identify and analyze a plant from a photo',
category: CreditCategory.AI,
app: 'planta',
app: 'plants',
},
// Traces

View file

@ -59,8 +59,8 @@ export function getManaFAQs(locale: string): FAQItem[] {
id: 'faq-mana-use',
question: isDE ? 'Wofür wird Mana verwendet?' : 'What is Mana used for?',
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 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 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 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',
order: 92,
language: isDE ? 'de' : 'en',

View file

@ -53,7 +53,7 @@
calc: ['Rechnen ohne Ablenkung', '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'],
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'],
questions: ['Recherche mit System', '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'],
guides: ['Guides that actually work', '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'],
questions: ['Research with structure', 'Open-source & independent', 'Private by design'],
context: ['Your knowledge, organized', 'Open-source & independent', 'Private by design'],

View file

@ -117,7 +117,7 @@ export const APP_ICONS = {
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>`
),
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>`
),
skilltree: svgToDataUrl(
@ -153,7 +153,7 @@ export const APP_ICONS = {
body: svgToDataUrl(
// Dumbbell + heart-pulse hybrid: training (barbell) + body (pulse line).
// Red→orange gradient to set it apart from the green health-adjacent
// modules (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>`
),
firsts: svgToDataUrl(

View file

@ -246,9 +246,9 @@ export const APP_BRANDING: Record<AppId, AppBranding> = {
logoStroke: true,
logoStrokeWidth: 1.5,
},
planta: {
id: 'planta',
name: 'Planta',
plants: {
id: 'plants',
name: 'Plants',
tagline: 'Plant Care Assistant',
primaryColor: '#22c55e',
secondaryColor: '#4ade80',

View file

@ -33,7 +33,7 @@ export {
ClockLogo,
QuestionsLogo,
SkillTreeLogo,
PlantaLogo,
PlantsLogo,
LightWriteLogo,
MusicLogo,
ContextLogo,

View file

@ -10,4 +10,4 @@
let { size = 55, color, class: className = '' }: Props = $props();
</script>
<AppLogo app="planta" {size} {color} class={className} />
<AppLogo app="plants" {size} {color} class={className} />

View file

@ -20,7 +20,7 @@ export { default as InventoryLogo } from './InventoryLogo.svelte';
export { default as ClockLogo } from './ClockLogo.svelte';
export { default as QuestionsLogo } from './QuestionsLogo.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 MusicLogo } from './MusicLogo.svelte';
export { default as ContextLogo } from './ContextLogo.svelte';

View file

@ -531,17 +531,17 @@ export const MANA_APPS: ManaApp[] = [
requiredTier: 'guest',
},
{
id: 'planta',
name: 'Planta',
id: 'plants',
name: 'Plants',
description: {
de: 'Pflanzenpflege',
de: 'Pflanzen',
en: 'Plant Care',
},
longDescription: {
de: 'Verwalte deine Pflanzen mit Gießplänen, Fotos und Pflegeprotokollen.',
en: 'Manage your plants with watering schedules, photos, and care logs.',
},
icon: APP_ICONS.planta,
icon: APP_ICONS.plants,
color: '#22c55e',
comingSoon: false,
status: 'beta',

View file

@ -21,7 +21,7 @@ export type AppId =
| 'inventory'
| 'questions'
| 'skilltree'
| 'planta'
| 'plants'
| 'lightwrite'
| 'context'
| 'music'

View file

@ -28,7 +28,7 @@ Each app gets its own isolated bucket, created automatically by `minio-init`:
| `mail-storage` | Mail | Email attachments |
| `inventory-storage` | Inventory | Product photos |
| `music-storage` | Music | Music tracks, beats, covers |
| `planta-storage` | Planta | Plant photos |
| `plants-storage` | Planta | Plant photos |
| `projectdoc-storage` | ProjectDoc | Document files |
## Usage

View file

@ -32,7 +32,7 @@ const PUBLIC_URL_ENV: Partial<Record<keyof typeof BUCKETS, string>> = {
PICTURE: 'PICTURE_STORAGE_PUBLIC_URL',
STORAGE: 'STORAGE_S3_PUBLIC_URL',
INVENTORY: 'INVENTORY_S3_PUBLIC_URL',
PLANTA: 'PLANTA_STORAGE_PUBLIC_URL',
PLANTS: 'PLANTS_STORAGE_PUBLIC_URL',
};
/**
@ -111,5 +111,5 @@ export const createStorageStorage = (publicUrl?: string) => createStorage('STORA
export const createMailStorage = () => createStorage('MAIL');
export const createInventoryStorage = (publicUrl?: string) => createStorage('INVENTORY', publicUrl);
export const createMusicStorage = () => createStorage('MUSIC');
export const createPlantaStorage = (publicUrl?: string) => createStorage('PLANTA', publicUrl);
export const createPlantsStorage = (publicUrl?: string) => createStorage('PLANTS', publicUrl);
export const createProjectDocStorage = () => createStorage('PROJECTDOC');

View file

@ -17,7 +17,7 @@ export {
createMailStorage,
createInventoryStorage,
createMusicStorage,
createPlantaStorage,
createPlantsStorage,
createProjectDocStorage,
} from './factory';

View file

@ -137,7 +137,7 @@ export const BUCKETS = {
MAIL: 'mail-storage',
INVENTORY: 'inventory-storage',
MUSIC: 'music-storage',
PLANTA: 'planta-storage',
PLANTS: 'plants-storage',
PROJECTDOC: 'projectdoc-storage',
} as const;

View file

@ -35,7 +35,7 @@ export interface DueReminder {
}
export interface ReminderSource {
/** Source identifier (e.g. 'todo', 'calendar', 'planta') */
/** Source identifier (e.g. 'todo', 'calendar', 'plants') */
id: string;
/** Returns reminders that are currently due */
checkDue: () => Promise<DueReminder[]>;

View file

@ -37,7 +37,7 @@ import { z } from 'zod';
//
// History:
// 1 — initial schemas (foods/totalNutrition for nutriphi,
// scientificName/commonNames/etc for planta)
// scientificName/commonNames/etc for plants)
export const AI_SCHEMA_VERSION = '1' as const;
export type AiSchemaVersion = typeof AI_SCHEMA_VERSION;
@ -107,7 +107,7 @@ export type MealAnalysis = z.infer<typeof MealAnalysisSchema>;
export type AnalyzedFood = z.infer<typeof AnalyzedFoodSchema>;
export type NutritionData = z.infer<typeof NutritionDataSchema>;
// ─── Planta: plant photo identification ──────────────────────────
// ─── Plants: plant photo identification ──────────────────────────
export const PlantIdentificationSchema = z.object({
scientificName: z.string().optional().describe('Latin binomial, e.g. "Monstera deliciosa"'),

View file

@ -156,7 +156,7 @@ const track = {
context: createModuleTracker('context'),
skilltree: createModuleTracker('skilltree'),
nutriphi: createModuleTracker('nutriphi'),
planta: createModuleTracker('planta'),
plants: createModuleTracker('plants'),
questions: createModuleTracker('questions'),
photos: createModuleTracker('photos'),
storage: createModuleTracker('storage'),
@ -389,13 +389,13 @@ export const NutriPhiEvents = {
};
/**
* Planta App Events
* Plants App Events
*/
export const PlantaEvents = {
plantAnalyzed: () => track.planta('plant_analyzed'),
plantCreated: () => track.planta('plant_created'),
plantDeleted: () => track.planta('plant_deleted'),
plantWatered: () => track.planta('plant_watered'),
export const PlantsEvents = {
plantAnalyzed: () => track.plants('plant_analyzed'),
plantCreated: () => track.plants('plant_created'),
plantDeleted: () => track.plants('plant_deleted'),
plantWatered: () => track.plants('plant_watered'),
};
/**

View file

@ -193,7 +193,7 @@ export const MANA_APP_INDEX: Record<string, number> = {
inventory: 15,
times: 16,
nutriphi: 17,
planta: 18,
plants: 18,
questions: 19,
moodlit: 20,
uload: 21,