Onboarding

This commit is contained in:
2026-05-08 13:00:30 +02:00
parent d37b49f1f6
commit 9386ae1be7
37 changed files with 5606 additions and 2275 deletions

View File

@@ -47,9 +47,34 @@ const authPost = async (path: string, body: object): Promise<{ userId: string; e
console.warn(`[Auth] ${path} failed:`, response.status, code, msg);
throw new Error(code);
}
return data as any;
};
return data as any;
};
const authDelete = async (path: string, token: string): Promise<void> => {
const backendUrl = getConfiguredBackendRootUrl();
const hasBackendUrl = Boolean(backendUrl);
const url = hasBackendUrl ? `${backendUrl}${path}` : path;
let response: Response;
try {
response = await fetch(url, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
} catch {
if (!hasBackendUrl) {
throw new Error('BACKEND_URL_MISSING');
}
throw new Error('NETWORK_ERROR');
}
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const code = (data as any).code || 'AUTH_ERROR';
const msg = (data as any).message || '';
console.warn(`[Auth] ${path} failed:`, response.status, code, msg);
throw new Error(code);
}
};
const buildSession = (data: { userId: string; email: string; name: string; token: string; isNewUser?: boolean }): AuthSession => {
const localUser = AuthDb.ensureLocalUser(data.email, data.name);
return {
@@ -109,10 +134,22 @@ export const AuthService = {
},
async logout(): Promise<void> {
await clearStoredSession();
},
async updateSessionName(name: string): Promise<void> {
await clearStoredSession();
},
async deleteAccount(): Promise<void> {
const session = await this.getSession();
if (!session) {
await clearStoredSession();
return;
}
await authDelete('/auth/account', session.token);
AuthDb.deleteLocalUser(session.userId);
await clearStoredSession();
await SecureStore.deleteItemAsync('greenlens_first_run_complete');
},
async updateSessionName(name: string): Promise<void> {
const session = await this.getSession();
if (!session) return;
await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify({ ...session, name }));

View File

@@ -34,10 +34,10 @@ const GUEST_TRIAL_CREDITS = 0;
const TRIAL_MONTHLY_CREDITS = 30;
const PRO_MONTHLY_CREDITS = 100;
const SCAN_PRIMARY_COST = 1;
const SCAN_REVIEW_COST = 1;
const SEMANTIC_SEARCH_COST = 2;
const HEALTH_CHECK_COST = 2;
const SCAN_PRIMARY_COST = 1;
const SCAN_REVIEW_COST = 0;
const SEMANTIC_SEARCH_COST = 2;
const HEALTH_CHECK_COST = 2;
const LOW_CONFIDENCE_REVIEW_THRESHOLD = 0.8;
const FREE_SIMULATED_DELAY_MS = 1100;
@@ -505,13 +505,18 @@ const buildMockHealthCheck = (request: HealthCheckRequest, creditsCharged: numbe
'Tag 7: Vergleichsfoto erstellen.',
];
return {
generatedAt: nowIso(),
overallHealthScore: score,
status,
likelyIssues,
actionsNow,
plan7Days,
return {
generatedAt: nowIso(),
overallHealthScore: score,
status,
analysisSummary: status === 'critical'
? 'Die Pflanze zeigt mehrere Stresssignale, die schnell stabilisiert werden sollten. Der wichtigste Verdacht ist zu viel Feuchtigkeit im Wurzelbereich, kombiniert mit schwacher Lichtversorgung. Achte besonders auf weiche gelbe Blaetter, dunkle Stellen am Stiel und Erde, die lange nass bleibt. Wenn diese Zeichen zunehmen, kann die Pflanze innerhalb weniger Tage weiter an Blattspannung verlieren. Die Diagnose ist ein Mock-Ergebnis, aber der Plan ist bewusst konkret. Pruefe zuerst Drainage und Substrat, bevor du Duenger oder einen kompletten Standortwechsel einsetzt.'
: status === 'watch'
? 'Die Pflanze wirkt nicht akut gefaehrdet, zeigt aber erkennbare Pflege-Signale, die beobachtet werden sollten. Wahrscheinlich spielen Giessrhythmus, Licht und leichte Naehrstoffversorgung zusammen. Einzelne gelbliche oder matte Blaetter sind noch kein Notfall, koennen aber ein fruehes Muster anzeigen. Entscheidend ist, ob neue Blaetter stabil bleiben und ob die Erde zwischen den Wassergaben gleichmaessig abtrocknet. Der Plan fokussiert auf konstante Bedingungen statt hektische Eingriffe. Ein Vergleichsfoto nach einer Woche zeigt, ob die Anpassungen wirken.'
: 'Die Pflanze wirkt insgesamt stabil und braucht eher Feintuning als Rettungsmassnahmen. Einzelne Blattreaktionen koennen normale Alterung oder leichte Standortanpassung sein. Der Score spricht dafuer, dass keine akute Ursache dominiert. Beobachte trotzdem neue Flecken, haengende Triebe und Veraenderungen an den unteren Blaettern. Halte die Routine konstant, damit du echte Veraenderungen leichter erkennst. Nutze den naechsten Check als Verlaufskontrolle statt als Notfallmassnahme.',
likelyIssues,
actionsNow,
plan7Days,
creditsCharged,
imageUri: request.imageUri,
};
@@ -604,13 +609,18 @@ const buildMockHealthCheck = (request: HealthCheckRequest, creditsCharged: numbe
'Dia 7: Tomar foto de comparacion.',
];
return {
generatedAt: nowIso(),
overallHealthScore: score,
status,
likelyIssues,
actionsNow,
plan7Days,
return {
generatedAt: nowIso(),
overallHealthScore: score,
status,
analysisSummary: status === 'critical'
? 'La planta muestra varias senales de estres que conviene estabilizar pronto. La sospecha principal es demasiada humedad en la zona de raices, combinada con luz insuficiente. Observa hojas amarillas blandas, manchas oscuras en tallos y sustrato que permanece mojado demasiado tiempo. Si estas senales aumentan, la planta puede perder firmeza en pocos dias. El diagnostico es simulado, pero el plan es concreto. Revisa drenaje y sustrato antes de fertilizar o cambiar toda la ubicacion.'
: status === 'watch'
? 'La planta no parece en peligro inmediato, pero muestra senales que deben observarse. Probablemente influyen el ritmo de riego, la luz y una nutricion ligera. Algunas hojas amarillas o apagadas no son una emergencia, pero pueden indicar un patron temprano. Lo importante es ver si las hojas nuevas se mantienen firmes y si el sustrato seca de forma regular. El plan prioriza condiciones constantes, no cambios bruscos. Una foto comparativa en una semana mostrara si los ajustes funcionan.'
: 'La planta parece estable y necesita pequenos ajustes mas que medidas de rescate. Algunas hojas pueden reflejar envejecimiento normal o adaptacion al lugar. El puntaje indica que no domina una causa urgente. Aun asi, observa manchas nuevas, tallos caidos y cambios en hojas inferiores. Mantén la rutina constante para detectar cambios reales. Usa el proximo chequeo como comparacion de evolucion.',
likelyIssues,
actionsNow,
plan7Days,
creditsCharged,
imageUri: request.imageUri,
};
@@ -702,12 +712,17 @@ const buildMockHealthCheck = (request: HealthCheckRequest, creditsCharged: numbe
'Day 7: Take a comparison photo.',
];
return {
generatedAt: nowIso(),
overallHealthScore: score,
status,
likelyIssues,
actionsNow,
return {
generatedAt: nowIso(),
overallHealthScore: score,
status,
analysisSummary: status === 'critical'
? 'The plant shows multiple stress signals that should be stabilized soon. The main suspicion is excess moisture around the roots, possibly combined with weak light. Watch for soft yellow leaves, dark stem areas, and soil that stays wet too long. If those signs increase, the plant may lose more leaf firmness within a few days. This is a mock diagnosis, but the plan is intentionally concrete. Check drainage and substrate before fertilizing or changing the whole routine.'
: status === 'watch'
? 'The plant does not look like an immediate emergency, but it has visible care signals worth tracking. Watering cadence, light level, and mild nutrition are the most likely levers. A few yellow or dull leaves are not automatically severe, but they can show an early pattern. The key is whether new leaves stay firm and whether soil dries predictably between watering. The plan focuses on stable conditions instead of abrupt changes. A comparison photo after one week will show whether the adjustments are working.'
: 'The plant looks broadly stable and needs fine-tuning rather than rescue care. Minor leaf reactions may reflect normal aging or placement adjustment. The score suggests no urgent single cause is dominating. Still, monitor new spots, drooping stems, and changes on lower leaves. Keep the routine steady so real changes are easier to see. Use the next check as a trend comparison rather than an emergency intervention.',
likelyIssues,
actionsNow,
plan7Days,
creditsCharged,
imageUri: request.imageUri,
@@ -1006,12 +1021,13 @@ export const mockBackendService = {
HEALTH_CHECK_COST,
);
const healthCheck: PlantHealthCheck = {
generatedAt: nowIso(),
overallHealthScore: aiAnalysis.overallHealthScore,
status: aiAnalysis.status,
likelyIssues: aiAnalysis.likelyIssues,
actionsNow: aiAnalysis.actionsNow,
const healthCheck: PlantHealthCheck = {
generatedAt: nowIso(),
overallHealthScore: aiAnalysis.overallHealthScore,
status: aiAnalysis.status,
analysisSummary: aiAnalysis.analysisSummary,
likelyIssues: aiAnalysis.likelyIssues,
actionsNow: aiAnalysis.actionsNow,
plan7Days: aiAnalysis.plan7Days,
creditsCharged,
imageUri: normalizedImageUri,

View File

@@ -9,12 +9,13 @@ export interface OpenAiHealthIssue {
details: string;
}
export interface OpenAiHealthAnalysis {
overallHealthScore: number;
status: 'healthy' | 'watch' | 'critical';
likelyIssues: OpenAiHealthIssue[];
actionsNow: string[];
plan7Days: string[];
export interface OpenAiHealthAnalysis {
overallHealthScore: number;
status: 'healthy' | 'watch' | 'critical';
analysisSummary?: string;
likelyIssues: OpenAiHealthIssue[];
actionsNow: string[];
plan7Days: string[];
}
const OPENAI_API_KEY = (process.env.EXPO_PUBLIC_OPENAI_API_KEY || '').trim();
@@ -203,16 +204,18 @@ const buildHealthPrompt = (
return [
`Analyze this plant photo for real health condition signs with focus on yellowing leaves, watering stress, pests, and light stress.`,
`Return strict JSON only in this shape:`,
`{"overallHealthScore":72,"status":"watch","likelyIssues":[{"title":"...","confidence":0.64,"details":"..."}],"actionsNow":["..."],"plan7Days":["..."]}`,
`Return strict JSON only in this shape:`,
`{"overallHealthScore":72,"status":"watch","analysisSummary":"...","likelyIssues":[{"title":"...","confidence":0.64,"details":"..."}],"actionsNow":["..."],"plan7Days":["..."]}`,
`Rules:`,
`- "overallHealthScore" must be an integer between 0 and 100.`,
`- "status" must be one of: "healthy", "watch", "critical".`,
`- "likelyIssues" must contain 1 to 4 items sorted by confidence descending.`,
`- "analysisSummary" must be 6 to 9 precise sentences. Cover the visible condition, symptom pattern, likely root cause, urgency, what evidence is uncertain, and what the owner should monitor next.`,
`- "likelyIssues" must contain 2 to 4 items sorted by confidence descending.`,
`- "confidence" must be between 0 and 1.`,
`- "title", "details", "actionsNow", and "plan7Days" must be written in ${getLanguageLabel(language)}.`,
`- "actionsNow" should be immediate steps for the next 24 hours.`,
`- "plan7Days" should be short actionable steps for the next week.`,
`- "title", "details", "analysisSummary", "actionsNow", and "plan7Days" must be written in ${getLanguageLabel(language)}.`,
`- Each issue "details" value must be 2 to 4 sentences and explain visual evidence, likely cause, and risk if untreated.`,
`- "actionsNow" must contain 5 to 8 concrete steps for the next 24 to 48 hours.`,
`- "plan7Days" must contain 7 to 10 day-by-day or milestone steps for the next week.`,
`- Do not include markdown, explanations, or extra keys.`,
...contextLines,
].join('\n');
@@ -229,9 +232,10 @@ const buildFallbackHealthAnalysis = (
): OpenAiHealthAnalysis => {
if (language === 'de') {
return {
overallHealthScore: 58,
status: 'watch',
likelyIssues: [
overallHealthScore: 58,
status: 'watch',
analysisSummary: `${plantContext?.name || 'Die Pflanze'} braucht eine erneute Bewertung mit einem scharfen Foto, weil die KI-Antwort nicht stabil genug war. Behandle sie bis dahin vorsichtig als Beobachtungsfall. Vermeide radikale Standortwechsel, grosse Wassermengen und starke Duengung. Pruefe zuerst, ob die auffaelligen Blaetter weich, trocken, fleckig oder eingerollt wirken. Kontrolliere danach die oberen 3 cm Erde und achte auf stehendes Wasser im Uebertopf. Die wichtigsten sichtbaren Signale sollten bei Tageslicht erneut geprueft werden. Wenn sich Blattfarbe oder Spannung innerhalb von 48 Stunden verschlechtern, starte einen neuen Health-Scan mit einem detailreicheren Foto.`,
likelyIssues: [
{
title: 'Eingeschraenkte KI-Analyse',
confidence: 0.42,
@@ -253,9 +257,10 @@ const buildFallbackHealthAnalysis = (
if (language === 'es') {
return {
overallHealthScore: 58,
status: 'watch',
likelyIssues: [
overallHealthScore: 58,
status: 'watch',
analysisSummary: `${plantContext?.name || 'La planta'} necesita una nueva evaluacion con una foto mas nitida porque la respuesta de IA no fue suficientemente estable. Hasta entonces tratala como un caso de observacion. Evita cambios bruscos de ubicacion, exceso de agua y fertilizacion fuerte. Revisa si las hojas afectadas estan blandas, secas, manchadas o enrolladas. Comprueba despues los 3 cm superiores del sustrato y busca agua acumulada. Las senales visibles deben revisarse de nuevo con luz natural. Si el color o la firmeza empeoran en 48 horas, inicia otro health-scan con una foto mas detallada.`,
likelyIssues: [
{
title: 'Analisis de IA limitado',
confidence: 0.42,
@@ -276,9 +281,10 @@ const buildFallbackHealthAnalysis = (
}
return {
overallHealthScore: 58,
status: 'watch',
likelyIssues: [
overallHealthScore: 58,
status: 'watch',
analysisSummary: `${plantContext?.name || 'This plant'} needs another assessment with a sharper photo because the AI response was not stable enough. Until then, treat it as a watch case. Avoid major placement changes, heavy watering, or strong fertilizing. First inspect whether the unusual leaves look soft, dry, spotted, or curled. Then check the top 3 cm of soil and look for standing water in the outer pot. Re-check the visible signals in daylight before making bigger care changes. If color or leaf firmness gets worse within 48 hours, run a new health scan with a more detailed photo.`,
likelyIssues: [
{
title: 'Limited AI analysis',
confidence: 0.42,
@@ -302,11 +308,12 @@ const normalizeHealthAnalysis = (
raw: Record<string, unknown>,
language: Language,
): OpenAiHealthAnalysis | null => {
const scoreRaw = getNumber(raw.overallHealthScore);
const statusRaw = getString(raw.status);
const issuesRaw = raw.likelyIssues;
const actionsNowRaw = getStringArray(raw.actionsNow).slice(0, 6);
const plan7DaysRaw = getStringArray(raw.plan7Days).slice(0, 7);
const scoreRaw = getNumber(raw.overallHealthScore);
const statusRaw = getString(raw.status);
const analysisSummary = getString(raw.analysisSummary);
const issuesRaw = raw.likelyIssues;
const actionsNowRaw = getStringArray(raw.actionsNow).slice(0, 8);
const plan7DaysRaw = getStringArray(raw.plan7Days).slice(0, 10);
if (scoreRaw == null || !statusRaw || !Array.isArray(issuesRaw)) {
return null;
@@ -340,9 +347,10 @@ const normalizeHealthAnalysis = (
? 'La IA no pudo extraer senales de salud estables.'
: 'AI could not extract stable health signals.';
return {
overallHealthScore: Math.round(clamp(scoreRaw, 0, 100)),
status,
likelyIssues: [
overallHealthScore: Math.round(clamp(scoreRaw, 0, 100)),
status,
analysisSummary: analysisSummary || fallbackIssue,
likelyIssues: [
{
title: language === 'de'
? 'Analyse unsicher'
@@ -363,9 +371,10 @@ const normalizeHealthAnalysis = (
}
return {
overallHealthScore: Math.round(clamp(scoreRaw, 0, 100)),
status,
likelyIssues,
overallHealthScore: Math.round(clamp(scoreRaw, 0, 100)),
status,
analysisSummary,
likelyIssues,
actionsNow: actionsNowRaw,
plan7Days: plan7DaysRaw,
};

View File

@@ -148,15 +148,19 @@ export const AuthDb = {
return { id: newUserId };
},
getUserById(id: number): DbUser | null {
const db = getDb();
const user = db.getFirstSync<DbUser>(
'SELECT id, email, name FROM users WHERE id = ?',
[id],
);
return user || null;
},
};
getUserById(id: number): DbUser | null {
const db = getDb();
const user = db.getFirstSync<DbUser>(
'SELECT id, email, name FROM users WHERE id = ?',
[id],
);
return user || null;
},
deleteLocalUser(id: number): void {
getDb().runSync('DELETE FROM users WHERE id = ?', [id]);
},
};
// ─── Settings ──────────────────────────────────────────────────────────────────