- Layout: mobile hamburger + drawer nav (backdrop button + sibling nav),
desktop sidebar hidden on small screens, p-4 md:p-8 main padding
- Products: card list view on mobile, flex-wrap filters
- Lab results: card list view on mobile
- ProductForm: responsive grids (grid-cols-1 sm:grid-cols-2), skin
profile checkboxes 2→3 cols, active ingredient row restructured
(name+✕ in flex row, percent/strength/irritation in 3-col grid),
section headers stack on mobile
- Skin snapshots: date+icons on one row, badges on separate row below
- Product [id] header: back link stacked above title, redundant badge removed
- Routines header: flex-col on mobile, sm:flex-row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove _build_inventory_context; fold pao_months into DOSTĘPNE PRODUKTY entries
- Remove "Otwarte równolegle" duplicate section from prompt
- Rename OSTATNIE RUTYNY (7 dni) → OSTATNIE RUTYNY
- Add _build_day_context and SuggestRoutineRequest.leaving_home (optional bool)
- System prompt: replace unconditional PAO rule with conditional; add SPF factor
selection logic based on KONTEKST DNIA leaving_home value
- Frontend: leaving_home checkbox (AM only) + i18n keys pl/en
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gemini API rejects int-valued enums (StrengthLevel) in response_schema,
raising a validation error before any request is sent. Fix by introducing
AIActiveIngredient (inherits ActiveIngredient, overrides strength_level and
irritation_potential as Optional[int]) and ProductParseLLMResponse used only
as the Gemini schema. The two-step validation converts ints back to StrengthLevel
via Pydantic coercion. Adds a test covering the numeric strength level path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemini-flash-latest resolves to gemini-3-flash-preview which uses
thinking_level instead of the legacy thinking_budget (mixing both
returns HTTP 400). Use LOW to reduce thinking overhead while keeping
basic reasoning, replacing the now-incompatible thinking_budget=0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Gemini stops generation early (e.g. due to safety filters or
thinking-model quirks), finish_reason != STOP but no exception is raised,
causing the caller to receive truncated JSON and a confusing 502 "invalid
JSON" error. Now:
- finish_reason is extracted from candidates[0] and stored in ai_call_logs
- any non-STOP finish_reason raises HTTP 502 with a clear message
- Alembic migration adds the finish_reason column to ai_call_logs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
analyzeSkinPhotos was hardcoding PUBLIC_API_BASE, causing browser-side
requests to hit localhost:8000 directly instead of going through nginx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
priorities was present in the model, API, and LLM prompt but never
surfaced in the frontend — add it to create/edit forms, read view,
AI photo pre-fill, and page.server.ts actions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add include_minoxidil_beard flag to SuggestRoutineRequest and SuggestBatchRequest
- Detect minoxidil products by scanning name, brand, INCI and actives; pass them
to the LLM even though they are medications
- Inject CELE UŻYTKOWNIKA context block into prompts when flag is enabled
- Add _build_objectives_context() returning empty string when flag is off
- Add call_gemini() helper that centralises Gemini API calls and logs every
request/response to a new ai_call_logs table (AICallLog model + /ai-logs router)
- Nginx: raise client_max_body_size to 16 MB for photo uploads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Products in the Add/Edit step dropdowns are now grouped by category
(Cleanser → Serum → Moisturizer → …) using SelectGroup/SelectGroupHeading,
and sorted alphabetically by brand then name within each group.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Batch-load all routine steps in a single query and attach them to each
routine dict, mirroring the detail endpoint pattern. Fixes "0 steps"
shown on the routines list page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Step numbers now use each-block index (i+1) instead of order_index+1,
fixing display when order_index starts from 1 in existing data
- api.ts: browser-side requests use /api (nginx proxy) instead of
PUBLIC_API_BASE (localhost:8000), fixing PATCH/client calls on remote hosts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install svelte-dnd-action v0.9.69
- Use dragHandleZone + dragHandle for per-step ⋮⋮ drag handles
- PATCH only steps whose order_index changed after a drop
- Inline edit mode (✎ button) expands step in-place: product steps show product/dose/region selects; action steps show action_type/notes
- DnD disabled while a step is being edited
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
svelte-kit sync runs as a prepare hook but svelte-kit is a devDep,
not installed during prod install — add --ignore-scripts to silence it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
adapter-node bundles the SvelteKit framework but leaves package.json
`dependencies` (clsx, bits-ui, etc.) external — they must be present in
node_modules on the server at runtime.
- deploy.sh: rsync package.json + pnpm-lock.yaml, run pnpm install --prod
- DEPLOYMENT.md: add pnpm to server setup with explanation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add deploy.sh: builds frontend locally, rsyncs build/ to server,
restarts services via passwordless sudo
- DEPLOYMENT.md: remove pnpm build from server setup (frontend is never
built on the LXC — esbuild hangs on low-resource containers), add rsync
to apt packages, document deploy.sh setup (SSH config, sudoers), replace
manual update steps with ./deploy.sh invocation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install @inlang/paraglide-js v2 with Vite plugin and paraglideMiddleware hook
- Add messages/pl.json and messages/en.json with ~400 translation keys
- Create project.inlang/settings.json (PL as base locale)
- Add LanguageSwitcher component (cookie-based, no URL prefix needed)
- Replace all hardcoded strings across 14 pages/components with m.*() calls
- ProductForm uses derived label maps for all enum types (category, texture, etc.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Steps were numbered from 2 due to off-by-one (order_index + 1).
Product steps showed "Unknown step" because the template checked
step.product (never returned by API) instead of looking up by
step.product_id in the already-loaded products list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without cascade, SQLAlchemy tried to NULL-out foreign keys on child rows
before deleting the parent, hitting NOT NULL constraints in PostgreSQL.
- Routine.steps: cascade="all, delete-orphan" (routine_steps.routine_id)
- MedicationEntry.usage_history: cascade="all, delete-orphan"
(medication_usages.medication_record_id)
Product.inventory already had cascade set correctly.
No DB migration needed — ORM-level only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Make Routine.steps optional in types.ts — list endpoint does not
eager-load steps, so the field is absent from the JSON response
- Guard routine.steps?.length ?? 0 in routines list page to prevent
SSR TypeError when steps is undefined
- Move redirect() outside try/catch in suggest save action; SvelteKit
redirect throws internally and was being caught, causing a 500 error
instead of navigating to the new routine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass response_schema to all three generate_content calls so Gemini
constrains its output to valid enum values and correct JSON structure:
- routines.py: _StepOut.action_type Optional[str] → Optional[GroomingAction]
- skincare.py: add _SkinAnalysisOut(PydanticBase) with OverallSkinState,
SkinType, SkinTexture, BarrierState, SkinConcern enums; add response_schema
- products.py: pass ProductParseResponse directly as response_schema;
remove NaN/Infinity/undefined regex cleanup, markdown-fence extraction,
finish_reason logging, and re import — all now unnecessary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Gemini-powered endpoints and frontend pages for proposing skincare
routines based on skin state, product compatibility, grooming schedule,
and recent history.
Backend (routines.py):
- POST /routines/suggest — single AM/PM routine for a date
- POST /routines/suggest-batch — AM+PM plan for up to 14 days
- Prompt context: skin snapshot, grooming schedule, 7-day history,
filtered product list with effects/incompatibilities/context rules
- Respects retinoid frequency limits, acid/retinoid separation,
grooming-aware safe_after_shaving rules
Frontend:
- /routines/suggest page with tab switcher (single / batch)
- Single tab: date + AM/PM + optional notes → generate → preview → save
- Batch tab: date range + notes → collapsible day cards (AM+PM) → save all
- Loading spinner during Gemini calls; product names resolved from map
- "Zaproponuj rutynę AI" button added to routines list page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New route /routines/grooming-schedule with load + create/update/delete actions
- Entries grouped by day of week with inline editing
- 4 API functions added to api.ts (get/create/update/delete)
- Nav: add Grooming link, fix isActive to not highlight parent when child matches
- Nav: replace ⌂ text char with 🏠 emoji for Dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace category filter dropdown with client-side grouping and a
3-way ownership toggle (All / Owned / Not owned). Products are grouped
by category with header rows as visual dividers, sorted brand → name
within each group. Category column removed (redundant with headings).
Backend: GET /products now returns ProductWithInventory so inventory
data is available for ownership filtering (bulk-loaded in one query).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLAlchemy was nulling out product_id on ProductInventory rows instead
of deleting them. Added cascade="all, delete-orphan" to the ORM
relationship and ondelete="CASCADE" to the FK field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model was emitting "anti_aging" as a valid ingredient function
(e.g. for retinoids, peptides). Add it to the enum and the
parse-text system prompt allowed values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regular generation was hitting MAX_TOKENS at 8192. Constrained decoding with
16384 should be a viable middle ground between the truncation at 8192 and the
timeout at 65536.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Constrained decoding is ~10x slower and consumes hidden tokens for constraint
processing, causing truncation at ~1000 chars even with 8192 max_output_tokens.
The system prompt already instructs the model to output raw minified JSON; our
NaN/markdown-fence sanitisation handles edge cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace truncation-recovery heuristic with a higher token budget.
On JSON parse failure, log finish_reason and 160-char error context
to make the root cause visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pretty-printed JSON wastes 2-3x tokens on indentation/newlines.
Minified output fits more data (e.g. long INCI lists) within the
8192 output token limit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Models sometimes emit JS-style literals for unknown numeric fields.
Replace NaN, Infinity, undefined with null before parsing.
Also add error logging to capture raw response on parse failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Increase max_output_tokens 4096 → 8192 to prevent truncated JSON on
products with long INCI lists
- Return explicit 502 when response.text is None (safety filter blocks)
- Fallback JSON extraction strips markdown fences or leading preamble
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch to Node.js 24 LTS via nvm
- Install uv to /usr/local/bin via UV_INSTALL_DIR for system-wide access
- Install pnpm as standalone binary from GitHub releases (not corepack
shim which breaks when copied out of its nvm directory)
- Add libpq5 to apt deps (psycopg3 requires libpq at runtime)
- Add GEMINI_API_KEY and GEMINI_MODEL to backend .env template
- Add ORIGIN to frontend .env.production (SvelteKit CSRF protection)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add alembic 1.14 to dependencies (uv sync → 1.18.4 installed)
- Configure alembic/env.py: loads DATABASE_URL from env, imports all
SQLModel models so metadata is fully populated for autogenerate
- Generate initial migration (c2d626a2b36c) covering all 9 tables:
products, product_inventory, medication_entries, medication_usages,
lab_results, routines, routine_steps, grooming_schedule,
skin_condition_snapshots — with all indexes and constraints
- Add ExecStartPre to innercontext.service: runs alembic upgrade head
before uvicorn starts (idempotent, safe on every restart)
- Update DEPLOYMENT.md: add migration step to backend setup and update
flow; document alembic stamp head for existing installations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove s.trend from dashboard (field removed from SkinConditionSnapshot)
- Replace trend with texture in SkinPhotoAnalysisResponse (api.ts)
- Use record_id instead of id for LabResult and MedicationEntry keyed each blocks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add backend/innercontext/mcp_server.py with tools covering products,
inventory, routines, skin snapshots, medications, lab results, and
grooming schedule
- Mount MCP app at /mcp in main.py using combine_lifespans
- Fix test isolation: patch app.router.lifespan_context in conftest to
avoid StreamableHTTPSessionManager single-run limitation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the derived `trend` field (better computed from history by the MCP
agent) and add `texture: smooth|rough|flaky|bumpy` which LLM can reliably
assess from photos. Updates model, API, system prompt, tests, and frontend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass `text=` as keyword arg to Part.from_text() and raise max_output_tokens
from 1024 to 2048 to prevent JSON truncation in the notes field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the All/AM/PM filter buttons and part_of_day param from
the routines list page — date-based sorting/filtering is preferred.
Add missing keyed {#each} blocks across all pages to follow
Svelte 5 best practice (dashboard, products, medications,
lab results, routines list and detail, skin).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add POST /skincare/analyze-photos endpoint that accepts 1–3 skin
photos, sends them to Gemini vision, and returns a structured
SkinPhotoAnalysisResponse for pre-filling the snapshot form.
Extract shared Gemini client setup into innercontext/llm.py
(get_gemini_client) so both products and skincare use a single
default model (gemini-flash-latest) and API key check.
Frontend: AI photo card on /skin page with file picker, previews,
and auto-fill of all form fields from the analysis result.
New fields (skin_type, sebum_tzone, sebum_cheeks) added to form
and server action.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>