Commit graph

127 commits

Author SHA1 Message Date
9574c91be1 refactor(routines): remove hardcoded grooming actions from system prompt 2026-03-03 20:22:59 +01:00
4627ec70bf refactor(routines): remove examples from inventory management rule to avoid bias 2026-03-03 20:07:13 +01:00
30ebc093bf feat(routines): adjust inventory management prompt to allow opening better suited sealed products 2026-03-03 20:06:38 +01:00
877051cfaf feat(routines): add actives and recent usage tracking to product context 2026-03-03 20:01:39 +01:00
1109d9f397 fix(products): only suggest when real need exists 2026-03-03 19:51:49 +01:00
d1bdfc4993 docs: replace CLAUDE.md with AGENTS.md and add frontend details 2026-03-03 01:27:01 +01:00
098b158b75 feat(frontend): add ESLint and Prettier with Svelte support
- Install eslint, prettier and related plugins
- Add lint and format npm scripts
- Configure eslint.config.js with Svelte + TypeScript rules
- Configure .prettierrc with Svelte plugin
- Fix code to comply with lint rules:
  - Use resolve() for navigation links
  - Use SvelteMap for reactive maps
  - Use writable  instead of  +
  - Remove unused imports and variables

Note: ignoreGoto is set to true due to eslint-plugin-svelte#1327
2026-03-03 01:21:50 +01:00
609995732b feat(routines): add minimize_products option for batch suggestions 2026-03-03 00:50:49 +01:00
40f9a353bb feat(products): add shopping suggestions feature
- Add POST /api/products/suggest endpoint that analyzes skin condition
  and inventory to suggest product types (e.g., 'Salicylic Acid 2% Masque')
- Add MCP tool get_shopping_suggestions() for MCP clients
- Add 'Suggest' button to Products page in frontend
- Add /products/suggest page with suggestion cards
- Include product type, key ingredients, target concerns, why_needed,
  recommended_time, and frequency in suggestions
- Fix stock logic: sealed products now count as available inventory
- Add legend to clarify ✓ (in stock) vs ✗ (not in stock) markers
2026-03-02 22:38:08 +01:00
389ca5ffdc fix(backend): resolve ty check errors across api, mcp, and lifespan typing 2026-03-02 15:51:14 +01:00
679e4e81f4 feat(frontend): responsive design for mobile (RWD)
- 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>
2026-03-02 13:35:25 +01:00
c85ca355df refactor(routines): streamline suggest prompt — merge inventory context, add leaving_home SPF hint
- 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>
2026-03-01 23:47:54 +01:00
258b8c4330 refactor(routines): use SQLAlchemy is_(False) for product filters 2026-03-01 23:23:04 +01:00
d3bd2ff30d feat(skincare): allow HEIC/HEIF uploads in skin analysis 2026-03-01 23:23:04 +01:00
f1acfa21fc feat(routines): add inventory-aware product selection rules 2026-03-01 22:15:47 +01:00
914c6087bd fix(products): work around Gemini int-enum schema rejection in parse-text
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>
2026-03-01 22:00:48 +01:00
921fe3ef61 fix(frontend): pick freshest skin snapshot on dashboard 2026-03-01 21:51:07 +01:00
49c304d06f fix(routines): use system prompt for suggest and dedupe rules 2026-03-01 21:45:31 +01:00
cc657998e8 fix(llm): switch from thinking_budget to thinking_level=LOW for Gemini 3
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>
2026-03-01 20:15:49 +01:00
ada5f2a93b fix(llm): disable Gemini thinking to prevent MAX_TOKENS on structured output
Gemini 2.5 Flash (gemini-flash-latest) enables thinking by default.
Thinking tokens count toward max_output_tokens, leaving ~150 tokens for
actual JSON output and causing MAX_TOKENS truncation. Disable thinking
centrally in call_gemini via ThinkingConfig(thinking_budget=0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 20:12:31 +01:00
092fd87606 fix(llm): log and handle non-STOP finish_reason from Gemini
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>
2026-03-01 20:08:22 +01:00
18683925a1 fix(frontend): use /api proxy for skin photo upload in browser context
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>
2026-03-01 20:01:16 +01:00
17eaa5d1bd fix(frontend): add missing priorities field to skin snapshots UI
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>
2026-03-01 19:58:38 +01:00
75ef1bca56 feat(routines): add minoxidil beard/mustache option to routine suggestions
- 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>
2026-03-01 19:46:07 +01:00
3aa03b412b feat(frontend): group product selector by category in routine step forms
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>
2026-03-01 18:22:51 +01:00
78c67b6179 fix(backend): include steps in list_routines response
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>
2026-03-01 17:39:33 +01:00
d4e3040674 fix(frontend): fix step numbering and client-side API base URL
- 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>
2026-03-01 17:34:00 +01:00
5cb44b2c65 fix(backend): apply black/isort formatting and fix ruff noqa annotations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 17:27:07 +01:00
4b0fedde35 feat(frontend): add drag-and-drop reordering and inline editing for routine steps
- 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>
2026-03-01 17:21:59 +01:00
5e2536138b fix(deploy): suppress prepare script noise with --ignore-scripts
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>
2026-03-01 13:57:46 +01:00
91bc9e86d7 fix(deploy): install frontend prod deps on server after deploy
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>
2026-03-01 13:56:28 +01:00
3428885aaa docs: add deploy.sh and rewrite DEPLOYMENT.md for local-build workflow
- 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>
2026-03-01 13:51:51 +01:00
99584521a1 feat(frontend): add PL/EN i18n using @inlang/paraglide-js v2
- 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>
2026-03-01 13:20:34 +01:00
9524e4df54 fix(frontend): fix routine step numbering and product name display
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>
2026-03-01 01:04:28 +01:00
f72d5ba1b7 fix(models): add cascade delete-orphan to parent-child relationships
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>
2026-03-01 00:59:10 +01:00
832676bcfa fix(frontend): fix routines list crash and AI save redirect
- 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>
2026-03-01 00:54:54 +01:00
81b1cacc5c refactor(llm): use response_schema with typed enums in all Gemini calls
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>
2026-03-01 00:46:23 +01:00
6e7f715ef2 feat: AI-generated skincare routine suggestions (single + batch)
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>
2026-03-01 00:34:43 +01:00
a3b25d5e46 feat(frontend): add grooming schedule CRUD page
- 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>
2026-02-28 23:46:33 +01:00
1b1566e6d7 feat(frontend): group products by category with ownership filter
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>
2026-02-28 23:07:37 +01:00
2691708304 fix(models): cascade delete inventory rows when product is deleted
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>
2026-02-28 22:48:00 +01:00
ef8334b93c feat(frontend): add anti_aging to IngredientFunction type and form selector
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:35:39 +01:00
794650afc6 feat(models): add anti_aging to IngredientFunction enum
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>
2026-02-28 22:32:57 +01:00
a3753d0929 fix(backend): restore response_mime_type=json, raise max_output_tokens to 16384
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>
2026-02-28 22:26:41 +01:00
3fbf6d7041 fix(backend): drop response_mime_type=application/json to avoid constrained decoding
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>
2026-02-28 22:03:49 +01:00
26069f5d66 fix(backend): increase max_output_tokens to 65536, log finish_reason on error
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>
2026-02-28 21:57:12 +01:00
4abdc88286 fix(backend): request minified JSON from Gemini to avoid token truncation
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>
2026-02-28 21:49:26 +01:00
3e85858d41 fix(backend): sanitize NaN/Infinity/undefined in Gemini JSON response
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>
2026-02-28 21:46:47 +01:00
54903a3bed fix(backend): handle invalid/empty JSON from Gemini in product parse endpoint
- 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>
2026-02-28 21:43:39 +01:00
d938c9999b feat(frontend): add edit and delete for skin snapshots
Add inline edit form and delete button to each snapshot card on /skin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:37:15 +01:00