diff --git a/backend/innercontext/api/products.py b/backend/innercontext/api/products.py index d65b5db..f8f3c77 100644 --- a/backend/innercontext/api/products.py +++ b/backend/innercontext/api/products.py @@ -356,21 +356,12 @@ def parse_product_text(data: ProductParseRequest) -> ProductParseResponse: config=genai_types.GenerateContentConfig( system_instruction=_product_parse_system_prompt(), response_mime_type="application/json", - max_output_tokens=8192, + max_output_tokens=4096, temperature=0.0, ), ) - raw = response.text - if not raw: - raise HTTPException(status_code=502, detail="LLM returned an empty response") - # Fallback: extract JSON object in case the model adds preamble or markdown fences - if not raw.lstrip().startswith("{"): - start = raw.find("{") - end = raw.rfind("}") - if start != -1 and end != -1: - raw = raw[start : end + 1] try: - parsed = json.loads(raw) + parsed = json.loads(response.text) except (json.JSONDecodeError, Exception) as e: raise HTTPException(status_code=502, detail=f"LLM returned invalid JSON: {e}") try: diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 36084bd..cee0073 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -53,43 +53,30 @@ pct enter 200 # or SSH into the container ```bash apt update && apt upgrade -y -apt install -y git nginx curl ca-certificates gnupg lsb-release libpq5 +apt install -y git nginx curl ca-certificates gnupg lsb-release ``` ### Python 3.12+ + uv ```bash apt install -y python3 python3-venv -curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.local/bin/env # or re-login ``` -Installing to `/usr/local/bin` makes `uv` available system-wide (required for `sudo -u innercontext uv sync`). - ### Node.js 24 LTS + pnpm ```bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash . "$HOME/.nvm/nvm.sh" nvm install 24 +corepack enable pnpm ``` -Copy `node` to `/usr/local/bin` so it is accessible system-wide -(required for `sudo -u innercontext` and for systemd). -Symlinking into `/root/.nvm/` won't work — other users can't traverse `/root/`. -Use `--remove-destination` to replace any existing symlink with a real file: +Create a stable symlink so systemd can find `node` at a fixed path: ```bash -cp --remove-destination "$(nvm which current)" /usr/local/bin/node -``` - -Install pnpm as a standalone binary from GitHub releases — self-contained, -no wrapper scripts, works system-wide. Do **not** use `corepack enable pnpm` -(the shim requires its nvm directory structure and breaks when copied/linked): - -```bash -curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linux-x64" \ - -o /usr/local/bin/pnpm -chmod 755 /usr/local/bin/pnpm +ln -sf "$(nvm which current)" /usr/local/bin/node ``` ### Application user @@ -152,8 +139,6 @@ sudo -u innercontext uv sync ```bash cat > /opt/innercontext/backend/.env <<'EOF' DATABASE_URL=postgresql+psycopg://innercontext:change-me@/innercontext -GEMINI_API_KEY=your-gemini-api-key -# GEMINI_MODEL=gemini-flash-latest # optional, this is the default EOF chmod 600 /opt/innercontext/backend/.env chown innercontext:innercontext /opt/innercontext/backend/.env @@ -205,7 +190,6 @@ cd /opt/innercontext/frontend ```bash cat > /opt/innercontext/frontend/.env.production <<'EOF' PUBLIC_API_BASE=http://innercontext.lan/api -ORIGIN=http://innercontext.lan EOF chmod 600 /opt/innercontext/frontend/.env.production chown innercontext:innercontext /opt/innercontext/frontend/.env.production diff --git a/frontend/src/routes/skin/+page.server.ts b/frontend/src/routes/skin/+page.server.ts index 95399cd..f11c131 100644 --- a/frontend/src/routes/skin/+page.server.ts +++ b/frontend/src/routes/skin/+page.server.ts @@ -1,4 +1,4 @@ -import { createSkinSnapshot, deleteSkinSnapshot, getSkinSnapshots, updateSkinSnapshot } from '$lib/api'; +import { createSkinSnapshot, getSkinSnapshots } from '$lib/api'; import { fail } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; @@ -50,59 +50,5 @@ export const actions: Actions = { } catch (e) { return fail(500, { error: (e as Error).message }); } - }, - - update: async ({ request }) => { - const form = await request.formData(); - const id = form.get('id') as string; - const snapshot_date = form.get('snapshot_date') as string; - const overall_state = form.get('overall_state') as string; - const texture = form.get('texture') as string; - const notes = form.get('notes') as string; - const hydration_level = form.get('hydration_level') as string; - const sensitivity_level = form.get('sensitivity_level') as string; - const barrier_state = form.get('barrier_state') as string; - const active_concerns_raw = form.get('active_concerns') as string; - const skin_type = form.get('skin_type') as string; - const sebum_tzone = form.get('sebum_tzone') as string; - const sebum_cheeks = form.get('sebum_cheeks') as string; - - if (!id) return fail(400, { error: 'Missing id' }); - - const active_concerns = active_concerns_raw - ?.split(',') - .map((c) => c.trim()) - .filter(Boolean) ?? []; - - const body: Record = { active_concerns }; - if (snapshot_date) body.snapshot_date = snapshot_date; - if (overall_state) body.overall_state = overall_state; - if (texture) body.texture = texture; - if (notes) body.notes = notes; - if (hydration_level) body.hydration_level = Number(hydration_level); - if (sensitivity_level) body.sensitivity_level = Number(sensitivity_level); - if (barrier_state) body.barrier_state = barrier_state; - if (skin_type) body.skin_type = skin_type; - if (sebum_tzone) body.sebum_tzone = Number(sebum_tzone); - if (sebum_cheeks) body.sebum_cheeks = Number(sebum_cheeks); - - try { - await updateSkinSnapshot(id, body); - return { updated: true }; - } catch (e) { - return fail(500, { error: (e as Error).message }); - } - }, - - delete: async ({ request }) => { - const form = await request.formData(); - const id = form.get('id') as string; - if (!id) return fail(400, { error: 'Missing id' }); - try { - await deleteSkinSnapshot(id); - return { deleted: true }; - } catch (e) { - return fail(500, { error: (e as Error).message }); - } } }; diff --git a/frontend/src/routes/skin/+page.svelte b/frontend/src/routes/skin/+page.svelte index 9f13c18..14b5777 100644 --- a/frontend/src/routes/skin/+page.svelte +++ b/frontend/src/routes/skin/+page.svelte @@ -25,7 +25,7 @@ let showForm = $state(false); - // Create form state (bound to inputs so AI can pre-fill) + // Form state (bound to inputs so AI can pre-fill) let snapshotDate = $state(new Date().toISOString().slice(0, 10)); let overallState = $state(''); let texture = $state(''); @@ -38,36 +38,6 @@ let activeConcernsRaw = $state(''); let notes = $state(''); - // Edit state - let editingId = $state(null); - let editSnapshotDate = $state(''); - let editOverallState = $state(''); - let editTexture = $state(''); - let editBarrierState = $state(''); - let editSkinType = $state(''); - let editHydrationLevel = $state(''); - let editSensitivityLevel = $state(''); - let editSebumTzone = $state(''); - let editSebumCheeks = $state(''); - let editActiveConcernsRaw = $state(''); - let editNotes = $state(''); - - function startEdit(snap: (typeof data.snapshots)[number]) { - editingId = snap.id; - editSnapshotDate = snap.snapshot_date; - editOverallState = snap.overall_state ?? ''; - editTexture = snap.texture ?? ''; - editBarrierState = snap.barrier_state ?? ''; - editSkinType = snap.skin_type ?? ''; - editHydrationLevel = snap.hydration_level != null ? String(snap.hydration_level) : ''; - editSensitivityLevel = snap.sensitivity_level != null ? String(snap.sensitivity_level) : ''; - editSebumTzone = snap.sebum_tzone != null ? String(snap.sebum_tzone) : ''; - editSebumCheeks = snap.sebum_cheeks != null ? String(snap.sebum_cheeks) : ''; - editActiveConcernsRaw = snap.active_concerns?.join(', ') ?? ''; - editNotes = snap.notes ?? ''; - showForm = false; - } - // AI photo analysis state let aiPanelOpen = $state(false); let selectedFiles = $state([]); @@ -131,12 +101,6 @@ {#if form?.created}
Snapshot added.
{/if} - {#if form?.updated} -
Snapshot updated.
- {/if} - {#if form?.deleted} -
Snapshot deleted.
- {/if} {#if showForm} @@ -320,230 +284,52 @@ {#each sortedSnapshots as snap (snap.id)} - {#if editingId === snap.id} - -
async ({ result, update }) => { - await update(); - if (result.type === 'success') editingId = null; - }} - class="grid grid-cols-2 gap-4" - > - -
- - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- {:else} - -
- {snap.snapshot_date} -
- {#if snap.overall_state} - - {snap.overall_state} - - {/if} - {#if snap.texture} - {snap.texture} - {/if} - -
- - -
-
-
-
- {#if snap.hydration_level != null} -
-

Hydration

-

{snap.hydration_level}/5

-
+ {snap.overall_state} + {/if} - {#if snap.sensitivity_level != null} -
-

Sensitivity

-

{snap.sensitivity_level}/5

-
- {/if} - {#if snap.barrier_state} -
-

Barrier

-

{snap.barrier_state.replace(/_/g, ' ')}

-
+ {#if snap.texture} + {snap.texture} {/if}
- {#if snap.active_concerns.length} -
- {#each snap.active_concerns as c (c)} - {c.replace(/_/g, ' ')} - {/each} +
+
+ {#if snap.hydration_level != null} +
+

Hydration

+

{snap.hydration_level}/5

{/if} - {#if snap.notes} -

{snap.notes}

+ {#if snap.sensitivity_level != null} +
+

Sensitivity

+

{snap.sensitivity_level}/5

+
{/if} + {#if snap.barrier_state} +
+

Barrier

+

{snap.barrier_state.replace(/_/g, ' ')}

+
+ {/if} +
+ {#if snap.active_concerns.length} +
+ {#each snap.active_concerns as c (c)} + {c.replace(/_/g, ' ')} + {/each} +
+ {/if} + {#if snap.notes} +

{snap.notes}

{/if}