diff --git a/CLAUDE.md b/AGENTS.md similarity index 63% rename from CLAUDE.md rename to AGENTS.md index 6634b64..6eb1aa6 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -1,17 +1,21 @@ -# CLAUDE.md +# AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to AI coding agents when working with code in this repository. -## Repository structure +## Repository Structure -This is a monorepo. The backend lives in `backend/`; a frontend will be added in the future. +This is a monorepo with **backend** and **frontend** directories. + +## Commit Guidelines + +This repository uses Conventional Commits (e.g., `feat(api): ...`, `fix(frontend): ...`, `test(models): ...`). Always format commit messages accordingly and ensure you include the correct scope to indicate which part of the monorepo is affected. ## Commands -Run all backend commands from the `backend/` directory: +Run the backend from the `backend/` directory: ```bash -# Run scripts +# Backend cd backend && uv run python main.py # Linting / formatting @@ -20,11 +24,27 @@ cd backend && uv run black . cd backend && uv run isort . ``` -No test suite exists yet. +Run the frontend from the `frontend/` directory: + +```bash +# Frontend +cd frontend && pnpm dev + +# Type checking / linting / formatting +cd frontend && pnpm check +cd frontend && pnpm lint +cd frontend && pnpm format +``` + +No test suite exists yet (backend has some test files but they're not integrated into CI). ## Architecture -**innercontext** collects personal health and skincare data and exposes it via MCP to an LLM agent. Stack: Python 3.12, SQLModel (0.0.37) + SQLAlchemy, Pydantic v2, FastAPI, PostgreSQL (psycopg3). +**innercontext** collects personal health and skincare data and exposes it to an LLM agent. + +**Backend Stack:** Python 3.12, SQLModel (0.0.37) + SQLAlchemy, Pydantic v2, FastAPI, PostgreSQL (psycopg3). + +**Frontend Stack:** SvelteKit 5, Tailwind CSS v4, bits-ui, inlang/paraglide (i18n), svelte-dnd-action. ### Models (`backend/innercontext/models/`) @@ -35,7 +55,7 @@ No test suite exists yet. | `routine.py` | `routines`, `routine_steps` | | `skincare.py` | `skin_condition_snapshots` | -**`Product`** is the core model. JSON columns store `inci` (list), `actives` (list of `ActiveIngredient`), `recommended_for`, `targets`, `incompatible_with`, `synergizes_with`, `context_rules`, and `product_effect_profile`. The `to_llm_context()` method returns a token-optimised dict for MCP. +**`Product`** is the core model. JSON columns store `inci` (list), `actives` (list of `ActiveIngredient`), `recommended_for`, `targets`, `incompatible_with`, `synergizes_with`, `context_rules`, and `product_effect_profile`. The `to_llm_context()` method returns a token-optimised dict for LLM usage. **`ProductInventory`** tracks physical packages (opened status, expiry, remaining weight). One product → many inventory entries. @@ -43,7 +63,7 @@ No test suite exists yet. **`SkinConditionSnapshot`** is a weekly LLM-filled record (skin state, metrics 1–5, active concerns). -### Key conventions +### Key Conventions - All `table=True` models use `Column(DateTime(timezone=True), onupdate=utc_now)` for `updated_at` via raw SQLAlchemy column — do not use plain `Field(default_factory=...)` for auto-update. - List/complex fields stored as JSON use `sa_column=Column(JSON, nullable=...)` pattern (DB-agnostic; not JSONB). diff --git a/README.md b/README.md index 0e2b13d..75a6f0b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # innercontext -Personal health and skincare data hub. Collects structured data (products, routines, lab results, medications, skin snapshots) and exposes it via a REST API, MCP, and a web UI to an LLM agent. +Personal health and skincare data hub. Collects structured data (products, routines, lab results, medications, skin snapshots) and exposes it via a REST API and a web UI to an LLM agent. ## Repository layout ``` -backend/ Python backend — FastAPI REST API + MCP server + SQLModel models +backend/ Python backend — FastAPI REST API + SQLModel models frontend/ SvelteKit web UI (Svelte 5, TypeScript, Tailwind CSS v4) docs/ Deployment guides nginx/ nginx config for production @@ -60,14 +60,6 @@ UI available at `http://localhost:5173`. | `/skincare` | Weekly skin condition snapshots | | `/health-check` | Liveness probe | -## MCP server - -innercontext exposes 14 tools via [FastMCP](https://github.com/jlowin/fastmcp) at the StreamableHTTP endpoint `http://localhost:8000/mcp/mcp`. - -Tools include: `get_products`, `get_product`, `get_open_inventory`, `get_recent_routines`, `get_latest_skin_snapshot`, `get_skin_history`, `get_medications`, `get_expiring_inventory`, `get_grooming_schedule`, `get_recent_lab_results`, and more. - -Connect an MCP-compatible LLM agent by pointing it at `http:///mcp/mcp`. - ## Frontend routes | Route | Description | @@ -102,7 +94,6 @@ uv run pytest ## Stack - **Backend:** Python 3.12, FastAPI, Uvicorn, SQLModel 0.0.37 + SQLAlchemy, Pydantic v2, PostgreSQL (psycopg3) -- **MCP:** FastMCP 3.0.2 (StreamableHTTP transport) - **Frontend:** SvelteKit 2, Svelte 5 (Runes), TypeScript, Tailwind CSS v4, shadcn-svelte ## Deployment diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 76f7495..4253c54 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -1,15 +1,16 @@ import os from logging.config import fileConfig -from alembic import context from dotenv import load_dotenv from sqlalchemy import engine_from_config, pool from sqlmodel import SQLModel +from alembic import context + load_dotenv() # Import all models so their tables are registered in SQLModel.metadata -import innercontext.models # noqa: F401 +import innercontext.models # noqa: F401, E402 config = context.config diff --git a/backend/alembic/versions/a1b2c3d4e5f6_add_ai_call_logs.py b/backend/alembic/versions/a1b2c3d4e5f6_add_ai_call_logs.py new file mode 100644 index 0000000..37d6f73 --- /dev/null +++ b/backend/alembic/versions/a1b2c3d4e5f6_add_ai_call_logs.py @@ -0,0 +1,51 @@ +"""add_ai_call_logs + +Revision ID: a1b2c3d4e5f6 +Revises: c2d626a2b36c +Create Date: 2026-03-01 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + +from alembic import op + +revision: str = "a1b2c3d4e5f6" +down_revision: Union[str, None] = "c2d626a2b36c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "ai_call_logs", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("endpoint", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("model", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("system_prompt", sa.Text(), nullable=True), + sa.Column("user_input", sa.Text(), nullable=True), + sa.Column("response_text", sa.Text(), nullable=True), + sa.Column("prompt_tokens", sa.Integer(), nullable=True), + sa.Column("completion_tokens", sa.Integer(), nullable=True), + sa.Column("total_tokens", sa.Integer(), nullable=True), + sa.Column("duration_ms", sa.Integer(), nullable=True), + sa.Column("success", sa.Boolean(), nullable=False), + sa.Column("error_detail", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_ai_call_logs_endpoint"), "ai_call_logs", ["endpoint"], unique=False + ) + op.create_index( + op.f("ix_ai_call_logs_success"), "ai_call_logs", ["success"], unique=False + ) + + +def downgrade() -> None: + op.drop_index(op.f("ix_ai_call_logs_success"), table_name="ai_call_logs") + op.drop_index(op.f("ix_ai_call_logs_endpoint"), table_name="ai_call_logs") + op.drop_table("ai_call_logs") diff --git a/backend/alembic/versions/b2c3d4e5f6a1_add_finish_reason_to_ai_call_logs.py b/backend/alembic/versions/b2c3d4e5f6a1_add_finish_reason_to_ai_call_logs.py new file mode 100644 index 0000000..d6c0de6 --- /dev/null +++ b/backend/alembic/versions/b2c3d4e5f6a1_add_finish_reason_to_ai_call_logs.py @@ -0,0 +1,29 @@ +"""add_finish_reason_to_ai_call_logs + +Revision ID: b2c3d4e5f6a1 +Revises: a1b2c3d4e5f6 +Create Date: 2026-03-01 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +revision: str = "b2c3d4e5f6a1" +down_revision: Union[str, None] = "a1b2c3d4e5f6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "ai_call_logs", + sa.Column("finish_reason", sa.Text(), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("ai_call_logs", "finish_reason") diff --git a/backend/alembic/versions/c2d626a2b36c_initial_schema.py b/backend/alembic/versions/c2d626a2b36c_initial_schema.py index 093aca0..7407207 100644 --- a/backend/alembic/versions/c2d626a2b36c_initial_schema.py +++ b/backend/alembic/versions/c2d626a2b36c_initial_schema.py @@ -1,19 +1,20 @@ """initial_schema Revision ID: c2d626a2b36c -Revises: +Revises: Create Date: 2026-02-28 20:13:55.499494 """ + from typing import Sequence, Union -from alembic import op import sqlalchemy as sa import sqlmodel.sql.sqltypes +from alembic import op # revision identifiers, used by Alembic. -revision: str = 'c2d626a2b36c' +revision: str = "c2d626a2b36c" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -22,217 +23,456 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.create_table('grooming_schedule', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('day_of_week', sa.Integer(), nullable=False), - sa.Column('action', sa.Enum('SHAVING_RAZOR', 'SHAVING_ONEBLADE', 'DERMAROLLING', name='groomingaction'), nullable=False), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "grooming_schedule", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("day_of_week", sa.Integer(), nullable=False), + sa.Column( + "action", + sa.Enum( + "SHAVING_RAZOR", + "SHAVING_ONEBLADE", + "DERMAROLLING", + name="groomingaction", + ), + nullable=False, + ), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_grooming_schedule_day_of_week'), 'grooming_schedule', ['day_of_week'], unique=False) - op.create_table('lab_results', - sa.Column('record_id', sa.Uuid(), nullable=False), - sa.Column('collected_at', sa.DateTime(), nullable=False), - sa.Column('test_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('test_name_original', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('test_name_loinc', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('value_num', sa.Float(), nullable=True), - sa.Column('value_text', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('value_bool', sa.Boolean(), nullable=True), - sa.Column('unit_original', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('unit_ucum', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('ref_low', sa.Float(), nullable=True), - sa.Column('ref_high', sa.Float(), nullable=True), - sa.Column('ref_text', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('flag', sa.Enum('NORMAL', 'ABNORMAL', 'POSITIVE', 'NEGATIVE', 'LOW', 'HIGH', name='resultflag'), nullable=True), - sa.Column('lab', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('source_file', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('record_id') + op.create_index( + op.f("ix_grooming_schedule_day_of_week"), + "grooming_schedule", + ["day_of_week"], + unique=False, ) - op.create_index(op.f('ix_lab_results_collected_at'), 'lab_results', ['collected_at'], unique=False) - op.create_index(op.f('ix_lab_results_flag'), 'lab_results', ['flag'], unique=False) - op.create_index(op.f('ix_lab_results_lab'), 'lab_results', ['lab'], unique=False) - op.create_index(op.f('ix_lab_results_test_code'), 'lab_results', ['test_code'], unique=False) - op.create_table('medication_entries', - sa.Column('record_id', sa.Uuid(), nullable=False), - sa.Column('kind', sa.Enum('PRESCRIPTION', 'OTC', 'SUPPLEMENT', 'HERBAL', 'OTHER', name='medicationkind'), nullable=False), - sa.Column('product_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('active_substance', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('formulation', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('route', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('source_file', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('record_id') + op.create_table( + "lab_results", + sa.Column("record_id", sa.Uuid(), nullable=False), + sa.Column("collected_at", sa.DateTime(), nullable=False), + sa.Column("test_code", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "test_name_original", sqlmodel.sql.sqltypes.AutoString(), nullable=True + ), + sa.Column("test_name_loinc", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("value_num", sa.Float(), nullable=True), + sa.Column("value_text", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("value_bool", sa.Boolean(), nullable=True), + sa.Column("unit_original", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("unit_ucum", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("ref_low", sa.Float(), nullable=True), + sa.Column("ref_high", sa.Float(), nullable=True), + sa.Column("ref_text", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column( + "flag", + sa.Enum( + "NORMAL", + "ABNORMAL", + "POSITIVE", + "NEGATIVE", + "LOW", + "HIGH", + name="resultflag", + ), + nullable=True, + ), + sa.Column("lab", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("source_file", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("record_id"), ) - op.create_index(op.f('ix_medication_entries_active_substance'), 'medication_entries', ['active_substance'], unique=False) - op.create_index(op.f('ix_medication_entries_kind'), 'medication_entries', ['kind'], unique=False) - op.create_index(op.f('ix_medication_entries_product_name'), 'medication_entries', ['product_name'], unique=False) - op.create_table('products', - sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('brand', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('line_name', sqlmodel.sql.sqltypes.AutoString(length=128), nullable=True), - sa.Column('sku', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True), - sa.Column('url', sqlmodel.sql.sqltypes.AutoString(length=512), nullable=True), - sa.Column('barcode', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True), - sa.Column('category', sa.Enum('CLEANSER', 'TONER', 'ESSENCE', 'SERUM', 'MOISTURIZER', 'SPF', 'MASK', 'EXFOLIANT', 'HAIR_TREATMENT', 'TOOL', 'SPOT_TREATMENT', 'OIL', name='productcategory'), nullable=False), - sa.Column('recommended_time', sa.Enum('AM', 'PM', 'BOTH', name='daytime'), nullable=False), - sa.Column('texture', sa.Enum('WATERY', 'GEL', 'EMULSION', 'CREAM', 'OIL', 'BALM', 'FOAM', 'FLUID', name='texturetype'), nullable=True), - sa.Column('absorption_speed', sa.Enum('VERY_FAST', 'FAST', 'MODERATE', 'SLOW', 'VERY_SLOW', name='absorptionspeed'), nullable=True), - sa.Column('leave_on', sa.Boolean(), nullable=False), - sa.Column('size_ml', sa.Float(), nullable=True), - sa.Column('full_weight_g', sa.Float(), nullable=True), - sa.Column('empty_weight_g', sa.Float(), nullable=True), - sa.Column('pao_months', sa.Integer(), nullable=True), - sa.Column('price_tier', sa.Enum('BUDGET', 'MID', 'PREMIUM', 'LUXURY', name='pricetier'), nullable=True), - sa.Column('inci', sa.JSON(), nullable=False), - sa.Column('actives', sa.JSON(), nullable=True), - sa.Column('recommended_for', sa.JSON(), nullable=False), - sa.Column('targets', sa.JSON(), nullable=False), - sa.Column('contraindications', sa.JSON(), nullable=False), - sa.Column('usage_notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('fragrance_free', sa.Boolean(), nullable=True), - sa.Column('essential_oils_free', sa.Boolean(), nullable=True), - sa.Column('alcohol_denat_free', sa.Boolean(), nullable=True), - sa.Column('pregnancy_safe', sa.Boolean(), nullable=True), - sa.Column('product_effect_profile', sa.JSON(), nullable=False), - sa.Column('ph_min', sa.Float(), nullable=True), - sa.Column('ph_max', sa.Float(), nullable=True), - sa.Column('incompatible_with', sa.JSON(), nullable=True), - sa.Column('synergizes_with', sa.JSON(), nullable=True), - sa.Column('context_rules', sa.JSON(), nullable=True), - sa.Column('min_interval_hours', sa.Integer(), nullable=True), - sa.Column('max_frequency_per_week', sa.Integer(), nullable=True), - sa.Column('is_medication', sa.Boolean(), nullable=False), - sa.Column('is_tool', sa.Boolean(), nullable=False), - sa.Column('needle_length_mm', sa.Float(), nullable=True), - sa.Column('personal_tolerance_notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('personal_repurchase_intent', sa.Boolean(), nullable=True), - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_lab_results_collected_at"), + "lab_results", + ["collected_at"], + unique=False, ) - op.create_index(op.f('ix_products_price_tier'), 'products', ['price_tier'], unique=False) - op.create_table('routines', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('routine_date', sa.Date(), nullable=False), - sa.Column('part_of_day', sa.Enum('AM', 'PM', name='partofday'), nullable=False), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('routine_date', 'part_of_day', name='uq_routine_date_part_of_day') + op.create_index(op.f("ix_lab_results_flag"), "lab_results", ["flag"], unique=False) + op.create_index(op.f("ix_lab_results_lab"), "lab_results", ["lab"], unique=False) + op.create_index( + op.f("ix_lab_results_test_code"), "lab_results", ["test_code"], unique=False ) - op.create_index(op.f('ix_routines_part_of_day'), 'routines', ['part_of_day'], unique=False) - op.create_index(op.f('ix_routines_routine_date'), 'routines', ['routine_date'], unique=False) - op.create_table('skin_condition_snapshots', - sa.Column('snapshot_date', sa.Date(), nullable=False), - sa.Column('overall_state', sa.Enum('EXCELLENT', 'GOOD', 'FAIR', 'POOR', name='overallskinstate'), nullable=True), - sa.Column('skin_type', sa.Enum('DRY', 'OILY', 'COMBINATION', 'SENSITIVE', 'NORMAL', 'ACNE_PRONE', name='skintype'), nullable=True), - sa.Column('texture', sa.Enum('SMOOTH', 'ROUGH', 'FLAKY', 'BUMPY', name='skintexture'), nullable=True), - sa.Column('hydration_level', sa.Integer(), nullable=True), - sa.Column('sebum_tzone', sa.Integer(), nullable=True), - sa.Column('sebum_cheeks', sa.Integer(), nullable=True), - sa.Column('sensitivity_level', sa.Integer(), nullable=True), - sa.Column('barrier_state', sa.Enum('INTACT', 'MILDLY_COMPROMISED', 'COMPROMISED', name='barrierstate'), nullable=True), - sa.Column('active_concerns', sa.JSON(), nullable=False), - sa.Column('risks', sa.JSON(), nullable=False), - sa.Column('priorities', sa.JSON(), nullable=False), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('snapshot_date', name='uq_skin_snapshot_date') + op.create_table( + "medication_entries", + sa.Column("record_id", sa.Uuid(), nullable=False), + sa.Column( + "kind", + sa.Enum( + "PRESCRIPTION", + "OTC", + "SUPPLEMENT", + "HERBAL", + "OTHER", + name="medicationkind", + ), + nullable=False, + ), + sa.Column("product_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "active_substance", sqlmodel.sql.sqltypes.AutoString(), nullable=True + ), + sa.Column("formulation", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("route", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("source_file", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("record_id"), ) - op.create_index(op.f('ix_skin_condition_snapshots_snapshot_date'), 'skin_condition_snapshots', ['snapshot_date'], unique=False) - op.create_table('medication_usages', - sa.Column('record_id', sa.Uuid(), nullable=False), - sa.Column('medication_record_id', sa.Uuid(), nullable=False), - sa.Column('dose_value', sa.Float(), nullable=True), - sa.Column('dose_unit', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('frequency', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('schedule_text', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('as_needed', sa.Boolean(), nullable=False), - sa.Column('valid_from', sa.DateTime(), nullable=False), - sa.Column('valid_to', sa.DateTime(), nullable=True), - sa.Column('source_file', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['medication_record_id'], ['medication_entries.record_id'], ), - sa.PrimaryKeyConstraint('record_id') + op.create_index( + op.f("ix_medication_entries_active_substance"), + "medication_entries", + ["active_substance"], + unique=False, ) - op.create_index(op.f('ix_medication_usages_as_needed'), 'medication_usages', ['as_needed'], unique=False) - op.create_index(op.f('ix_medication_usages_medication_record_id'), 'medication_usages', ['medication_record_id'], unique=False) - op.create_index(op.f('ix_medication_usages_valid_from'), 'medication_usages', ['valid_from'], unique=False) - op.create_index(op.f('ix_medication_usages_valid_to'), 'medication_usages', ['valid_to'], unique=False) - op.create_table('product_inventory', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('product_id', sa.Uuid(), nullable=False), - sa.Column('is_opened', sa.Boolean(), nullable=False), - sa.Column('opened_at', sa.Date(), nullable=True), - sa.Column('finished_at', sa.Date(), nullable=True), - sa.Column('expiry_date', sa.Date(), nullable=True), - sa.Column('current_weight_g', sa.Float(), nullable=True), - sa.Column('last_weighed_at', sa.Date(), nullable=True), - sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_medication_entries_kind"), "medication_entries", ["kind"], unique=False ) - op.create_index(op.f('ix_product_inventory_product_id'), 'product_inventory', ['product_id'], unique=False) - op.create_table('routine_steps', - sa.Column('id', sa.Uuid(), nullable=False), - sa.Column('routine_id', sa.Uuid(), nullable=False), - sa.Column('product_id', sa.Uuid(), nullable=True), - sa.Column('order_index', sa.Integer(), nullable=False), - sa.Column('action_type', sa.Enum('SHAVING_RAZOR', 'SHAVING_ONEBLADE', 'DERMAROLLING', name='groomingaction'), nullable=True), - sa.Column('action_notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('dose', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column('region', sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), - sa.ForeignKeyConstraint(['routine_id'], ['routines.id'], ), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_medication_entries_product_name"), + "medication_entries", + ["product_name"], + unique=False, + ) + op.create_table( + "products", + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("brand", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column( + "line_name", sqlmodel.sql.sqltypes.AutoString(length=128), nullable=True + ), + sa.Column("sku", sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True), + sa.Column("url", sqlmodel.sql.sqltypes.AutoString(length=512), nullable=True), + sa.Column( + "barcode", sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True + ), + sa.Column( + "category", + sa.Enum( + "CLEANSER", + "TONER", + "ESSENCE", + "SERUM", + "MOISTURIZER", + "SPF", + "MASK", + "EXFOLIANT", + "HAIR_TREATMENT", + "TOOL", + "SPOT_TREATMENT", + "OIL", + name="productcategory", + ), + nullable=False, + ), + sa.Column( + "recommended_time", + sa.Enum("AM", "PM", "BOTH", name="daytime"), + nullable=False, + ), + sa.Column( + "texture", + sa.Enum( + "WATERY", + "GEL", + "EMULSION", + "CREAM", + "OIL", + "BALM", + "FOAM", + "FLUID", + name="texturetype", + ), + nullable=True, + ), + sa.Column( + "absorption_speed", + sa.Enum( + "VERY_FAST", + "FAST", + "MODERATE", + "SLOW", + "VERY_SLOW", + name="absorptionspeed", + ), + nullable=True, + ), + sa.Column("leave_on", sa.Boolean(), nullable=False), + sa.Column("size_ml", sa.Float(), nullable=True), + sa.Column("full_weight_g", sa.Float(), nullable=True), + sa.Column("empty_weight_g", sa.Float(), nullable=True), + sa.Column("pao_months", sa.Integer(), nullable=True), + sa.Column( + "price_tier", + sa.Enum("BUDGET", "MID", "PREMIUM", "LUXURY", name="pricetier"), + nullable=True, + ), + sa.Column("inci", sa.JSON(), nullable=False), + sa.Column("actives", sa.JSON(), nullable=True), + sa.Column("recommended_for", sa.JSON(), nullable=False), + sa.Column("targets", sa.JSON(), nullable=False), + sa.Column("contraindications", sa.JSON(), nullable=False), + sa.Column("usage_notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("fragrance_free", sa.Boolean(), nullable=True), + sa.Column("essential_oils_free", sa.Boolean(), nullable=True), + sa.Column("alcohol_denat_free", sa.Boolean(), nullable=True), + sa.Column("pregnancy_safe", sa.Boolean(), nullable=True), + sa.Column("product_effect_profile", sa.JSON(), nullable=False), + sa.Column("ph_min", sa.Float(), nullable=True), + sa.Column("ph_max", sa.Float(), nullable=True), + sa.Column("incompatible_with", sa.JSON(), nullable=True), + sa.Column("synergizes_with", sa.JSON(), nullable=True), + sa.Column("context_rules", sa.JSON(), nullable=True), + sa.Column("min_interval_hours", sa.Integer(), nullable=True), + sa.Column("max_frequency_per_week", sa.Integer(), nullable=True), + sa.Column("is_medication", sa.Boolean(), nullable=False), + sa.Column("is_tool", sa.Boolean(), nullable=False), + sa.Column("needle_length_mm", sa.Float(), nullable=True), + sa.Column( + "personal_tolerance_notes", + sqlmodel.sql.sqltypes.AutoString(), + nullable=True, + ), + sa.Column("personal_repurchase_intent", sa.Boolean(), nullable=True), + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_products_price_tier"), "products", ["price_tier"], unique=False + ) + op.create_table( + "routines", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("routine_date", sa.Date(), nullable=False), + sa.Column("part_of_day", sa.Enum("AM", "PM", name="partofday"), nullable=False), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "routine_date", "part_of_day", name="uq_routine_date_part_of_day" + ), + ) + op.create_index( + op.f("ix_routines_part_of_day"), "routines", ["part_of_day"], unique=False + ) + op.create_index( + op.f("ix_routines_routine_date"), "routines", ["routine_date"], unique=False + ) + op.create_table( + "skin_condition_snapshots", + sa.Column("snapshot_date", sa.Date(), nullable=False), + sa.Column( + "overall_state", + sa.Enum("EXCELLENT", "GOOD", "FAIR", "POOR", name="overallskinstate"), + nullable=True, + ), + sa.Column( + "skin_type", + sa.Enum( + "DRY", + "OILY", + "COMBINATION", + "SENSITIVE", + "NORMAL", + "ACNE_PRONE", + name="skintype", + ), + nullable=True, + ), + sa.Column( + "texture", + sa.Enum("SMOOTH", "ROUGH", "FLAKY", "BUMPY", name="skintexture"), + nullable=True, + ), + sa.Column("hydration_level", sa.Integer(), nullable=True), + sa.Column("sebum_tzone", sa.Integer(), nullable=True), + sa.Column("sebum_cheeks", sa.Integer(), nullable=True), + sa.Column("sensitivity_level", sa.Integer(), nullable=True), + sa.Column( + "barrier_state", + sa.Enum("INTACT", "MILDLY_COMPROMISED", "COMPROMISED", name="barrierstate"), + nullable=True, + ), + sa.Column("active_concerns", sa.JSON(), nullable=False), + sa.Column("risks", sa.JSON(), nullable=False), + sa.Column("priorities", sa.JSON(), nullable=False), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("snapshot_date", name="uq_skin_snapshot_date"), + ) + op.create_index( + op.f("ix_skin_condition_snapshots_snapshot_date"), + "skin_condition_snapshots", + ["snapshot_date"], + unique=False, + ) + op.create_table( + "medication_usages", + sa.Column("record_id", sa.Uuid(), nullable=False), + sa.Column("medication_record_id", sa.Uuid(), nullable=False), + sa.Column("dose_value", sa.Float(), nullable=True), + sa.Column("dose_unit", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("frequency", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("schedule_text", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("as_needed", sa.Boolean(), nullable=False), + sa.Column("valid_from", sa.DateTime(), nullable=False), + sa.Column("valid_to", sa.DateTime(), nullable=True), + sa.Column("source_file", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["medication_record_id"], + ["medication_entries.record_id"], + ), + sa.PrimaryKeyConstraint("record_id"), + ) + op.create_index( + op.f("ix_medication_usages_as_needed"), + "medication_usages", + ["as_needed"], + unique=False, + ) + op.create_index( + op.f("ix_medication_usages_medication_record_id"), + "medication_usages", + ["medication_record_id"], + unique=False, + ) + op.create_index( + op.f("ix_medication_usages_valid_from"), + "medication_usages", + ["valid_from"], + unique=False, + ) + op.create_index( + op.f("ix_medication_usages_valid_to"), + "medication_usages", + ["valid_to"], + unique=False, + ) + op.create_table( + "product_inventory", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("product_id", sa.Uuid(), nullable=False), + sa.Column("is_opened", sa.Boolean(), nullable=False), + sa.Column("opened_at", sa.Date(), nullable=True), + sa.Column("finished_at", sa.Date(), nullable=True), + sa.Column("expiry_date", sa.Date(), nullable=True), + sa.Column("current_weight_g", sa.Float(), nullable=True), + sa.Column("last_weighed_at", sa.Date(), nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["product_id"], + ["products.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_product_inventory_product_id"), + "product_inventory", + ["product_id"], + unique=False, + ) + op.create_table( + "routine_steps", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("routine_id", sa.Uuid(), nullable=False), + sa.Column("product_id", sa.Uuid(), nullable=True), + sa.Column("order_index", sa.Integer(), nullable=False), + sa.Column( + "action_type", + sa.Enum( + "SHAVING_RAZOR", + "SHAVING_ONEBLADE", + "DERMAROLLING", + name="groomingaction", + ), + nullable=True, + ), + sa.Column("action_notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("dose", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("region", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint( + ["product_id"], + ["products.id"], + ), + sa.ForeignKeyConstraint( + ["routine_id"], + ["routines.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_routine_steps_product_id"), + "routine_steps", + ["product_id"], + unique=False, + ) + op.create_index( + op.f("ix_routine_steps_routine_id"), + "routine_steps", + ["routine_id"], + unique=False, ) - op.create_index(op.f('ix_routine_steps_product_id'), 'routine_steps', ['product_id'], unique=False) - op.create_index(op.f('ix_routine_steps_routine_id'), 'routine_steps', ['routine_id'], unique=False) # ### end Alembic commands ### def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_routine_steps_routine_id'), table_name='routine_steps') - op.drop_index(op.f('ix_routine_steps_product_id'), table_name='routine_steps') - op.drop_table('routine_steps') - op.drop_index(op.f('ix_product_inventory_product_id'), table_name='product_inventory') - op.drop_table('product_inventory') - op.drop_index(op.f('ix_medication_usages_valid_to'), table_name='medication_usages') - op.drop_index(op.f('ix_medication_usages_valid_from'), table_name='medication_usages') - op.drop_index(op.f('ix_medication_usages_medication_record_id'), table_name='medication_usages') - op.drop_index(op.f('ix_medication_usages_as_needed'), table_name='medication_usages') - op.drop_table('medication_usages') - op.drop_index(op.f('ix_skin_condition_snapshots_snapshot_date'), table_name='skin_condition_snapshots') - op.drop_table('skin_condition_snapshots') - op.drop_index(op.f('ix_routines_routine_date'), table_name='routines') - op.drop_index(op.f('ix_routines_part_of_day'), table_name='routines') - op.drop_table('routines') - op.drop_index(op.f('ix_products_price_tier'), table_name='products') - op.drop_table('products') - op.drop_index(op.f('ix_medication_entries_product_name'), table_name='medication_entries') - op.drop_index(op.f('ix_medication_entries_kind'), table_name='medication_entries') - op.drop_index(op.f('ix_medication_entries_active_substance'), table_name='medication_entries') - op.drop_table('medication_entries') - op.drop_index(op.f('ix_lab_results_test_code'), table_name='lab_results') - op.drop_index(op.f('ix_lab_results_lab'), table_name='lab_results') - op.drop_index(op.f('ix_lab_results_flag'), table_name='lab_results') - op.drop_index(op.f('ix_lab_results_collected_at'), table_name='lab_results') - op.drop_table('lab_results') - op.drop_index(op.f('ix_grooming_schedule_day_of_week'), table_name='grooming_schedule') - op.drop_table('grooming_schedule') + op.drop_index(op.f("ix_routine_steps_routine_id"), table_name="routine_steps") + op.drop_index(op.f("ix_routine_steps_product_id"), table_name="routine_steps") + op.drop_table("routine_steps") + op.drop_index( + op.f("ix_product_inventory_product_id"), table_name="product_inventory" + ) + op.drop_table("product_inventory") + op.drop_index(op.f("ix_medication_usages_valid_to"), table_name="medication_usages") + op.drop_index( + op.f("ix_medication_usages_valid_from"), table_name="medication_usages" + ) + op.drop_index( + op.f("ix_medication_usages_medication_record_id"), + table_name="medication_usages", + ) + op.drop_index( + op.f("ix_medication_usages_as_needed"), table_name="medication_usages" + ) + op.drop_table("medication_usages") + op.drop_index( + op.f("ix_skin_condition_snapshots_snapshot_date"), + table_name="skin_condition_snapshots", + ) + op.drop_table("skin_condition_snapshots") + op.drop_index(op.f("ix_routines_routine_date"), table_name="routines") + op.drop_index(op.f("ix_routines_part_of_day"), table_name="routines") + op.drop_table("routines") + op.drop_index(op.f("ix_products_price_tier"), table_name="products") + op.drop_table("products") + op.drop_index( + op.f("ix_medication_entries_product_name"), table_name="medication_entries" + ) + op.drop_index(op.f("ix_medication_entries_kind"), table_name="medication_entries") + op.drop_index( + op.f("ix_medication_entries_active_substance"), table_name="medication_entries" + ) + op.drop_table("medication_entries") + op.drop_index(op.f("ix_lab_results_test_code"), table_name="lab_results") + op.drop_index(op.f("ix_lab_results_lab"), table_name="lab_results") + op.drop_index(op.f("ix_lab_results_flag"), table_name="lab_results") + op.drop_index(op.f("ix_lab_results_collected_at"), table_name="lab_results") + op.drop_table("lab_results") + op.drop_index( + op.f("ix_grooming_schedule_day_of_week"), table_name="grooming_schedule" + ) + op.drop_table("grooming_schedule") # ### end Alembic commands ### diff --git a/backend/alembic/versions/d3e4f5a6b7c8_add_tool_trace_to_ai_call_logs.py b/backend/alembic/versions/d3e4f5a6b7c8_add_tool_trace_to_ai_call_logs.py new file mode 100644 index 0000000..a6def60 --- /dev/null +++ b/backend/alembic/versions/d3e4f5a6b7c8_add_tool_trace_to_ai_call_logs.py @@ -0,0 +1,29 @@ +"""add_tool_trace_to_ai_call_logs + +Revision ID: d3e4f5a6b7c8 +Revises: b2c3d4e5f6a1 +Create Date: 2026-03-04 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +revision: str = "d3e4f5a6b7c8" +down_revision: Union[str, None] = "b2c3d4e5f6a1" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "ai_call_logs", + sa.Column("tool_trace", sa.JSON(), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("ai_call_logs", "tool_trace") diff --git a/backend/innercontext/api/ai_logs.py b/backend/innercontext/api/ai_logs.py new file mode 100644 index 0000000..040d47e --- /dev/null +++ b/backend/innercontext/api/ai_logs.py @@ -0,0 +1,83 @@ +import json +from typing import Any, Optional +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import Session, SQLModel, col, select + +from db import get_session +from innercontext.models.ai_log import AICallLog + +router = APIRouter() + + +def _normalize_tool_trace(value: object) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, dict): + return {str(k): v for k, v in value.items()} + if isinstance(value, str): + try: + parsed = json.loads(value) + except json.JSONDecodeError: + return None + if isinstance(parsed, dict): + return {str(k): v for k, v in parsed.items()} + return None + return None + + +class AICallLogPublic(SQLModel): + """List-friendly view: omits large text fields.""" + + id: UUID + created_at: object + endpoint: str + model: str + prompt_tokens: Optional[int] = None + completion_tokens: Optional[int] = None + total_tokens: Optional[int] = None + duration_ms: Optional[int] = None + tool_trace: Optional[dict[str, Any]] = None + success: bool + error_detail: Optional[str] = None + + +@router.get("", response_model=list[AICallLogPublic]) +def list_ai_logs( + endpoint: Optional[str] = None, + success: Optional[bool] = None, + limit: int = 50, + session: Session = Depends(get_session), +): + stmt = select(AICallLog).order_by(col(AICallLog.created_at).desc()).limit(limit) + if endpoint is not None: + stmt = stmt.where(AICallLog.endpoint == endpoint) + if success is not None: + stmt = stmt.where(AICallLog.success == success) + logs = session.exec(stmt).all() + return [ + AICallLogPublic( + id=log.id, + created_at=log.created_at, + endpoint=log.endpoint, + model=log.model, + prompt_tokens=log.prompt_tokens, + completion_tokens=log.completion_tokens, + total_tokens=log.total_tokens, + duration_ms=log.duration_ms, + tool_trace=_normalize_tool_trace(getattr(log, "tool_trace", None)), + success=log.success, + error_detail=log.error_detail, + ) + for log in logs + ] + + +@router.get("/{log_id}", response_model=AICallLog) +def get_ai_log(log_id: UUID, session: Session = Depends(get_session)): + log = session.get(AICallLog, log_id) + if log is None: + raise HTTPException(status_code=404, detail="Log not found") + log.tool_trace = _normalize_tool_trace(getattr(log, "tool_trace", None)) + return log diff --git a/backend/innercontext/api/products.py b/backend/innercontext/api/products.py index 945ab53..743979b 100644 --- a/backend/innercontext/api/products.py +++ b/backend/innercontext/api/products.py @@ -5,12 +5,18 @@ from uuid import UUID, uuid4 from fastapi import APIRouter, Depends, HTTPException, Query from google.genai import types as genai_types +from pydantic import BaseModel as PydanticBase from pydantic import ValidationError -from sqlmodel import Session, SQLModel, select +from sqlmodel import Session, SQLModel, col, select from db import get_session from innercontext.api.utils import get_or_404 -from innercontext.llm import get_gemini_client +from innercontext.llm import ( + call_gemini, + call_gemini_with_function_tools, + get_creative_config, + get_extraction_config, +) from innercontext.models import ( Product, ProductBase, @@ -19,6 +25,7 @@ from innercontext.models import ( ProductPublic, ProductWithInventory, SkinConcern, + SkinConditionSnapshot, ) from innercontext.models.enums import ( AbsorptionSpeed, @@ -143,6 +150,19 @@ class ProductParseResponse(SQLModel): needle_length_mm: Optional[float] = None +class AIActiveIngredient(ActiveIngredient): + # Gemini API rejects int-enum values in response_schema; override with plain int. + strength_level: Optional[int] = None # type: ignore[assignment] + irritation_potential: Optional[int] = None # type: ignore[assignment] + + +class ProductParseLLMResponse(ProductParseResponse): + # Gemini response schema currently requires enum values to be strings. + # Strength fields are numeric in our domain (1-3), so keep them as ints here + # and convert via ProductParseResponse validation afterward. + actives: Optional[list[AIActiveIngredient]] = None # type: ignore[assignment] + + class InventoryCreate(SQLModel): is_opened: bool = False opened_at: Optional[date] = None @@ -163,6 +183,41 @@ class InventoryUpdate(SQLModel): notes: Optional[str] = None +# --------------------------------------------------------------------------- +# Shopping suggestion schemas +# --------------------------------------------------------------------------- + + +class ProductSuggestion(PydanticBase): + category: str + product_type: str + key_ingredients: list[str] + target_concerns: list[str] + why_needed: str + recommended_time: str + frequency: str + + +class ShoppingSuggestionResponse(PydanticBase): + suggestions: list[ProductSuggestion] + reasoning: str + + +class _ProductSuggestionOut(PydanticBase): + category: str + product_type: str + key_ingredients: list[str] + target_concerns: list[str] + why_needed: str + recommended_time: str + frequency: str + + +class _ShoppingSuggestionsOut(PydanticBase): + suggestions: list[_ProductSuggestionOut] + reasoning: str + + # --------------------------------------------------------------------------- # Product routes # --------------------------------------------------------------------------- @@ -205,7 +260,9 @@ def list_products( product_ids = [p.id for p in products] inventory_rows = ( session.exec( - select(ProductInventory).where(ProductInventory.product_id.in_(product_ids)) + select(ProductInventory).where( + col(ProductInventory.product_id).in_(product_ids) + ) ).all() if product_ids else [] @@ -367,17 +424,15 @@ OUTPUT SCHEMA (all fields optional — omit what you cannot determine): @router.post("/parse-text", response_model=ProductParseResponse) def parse_product_text(data: ProductParseRequest) -> ProductParseResponse: - client, model = get_gemini_client() - response = client.models.generate_content( - model=model, + response = call_gemini( + endpoint="products/parse-text", contents=f"Extract product data from this text:\n\n{data.text}", - config=genai_types.GenerateContentConfig( + config=get_extraction_config( system_instruction=_product_parse_system_prompt(), - response_mime_type="application/json", - response_schema=ProductParseResponse, + response_schema=ProductParseLLMResponse, max_output_tokens=16384, - temperature=0.0, ), + user_input=data.text, ) raw = response.text if not raw: @@ -387,7 +442,8 @@ def parse_product_text(data: ProductParseRequest) -> ProductParseResponse: except json.JSONDecodeError as e: raise HTTPException(status_code=502, detail=f"LLM returned invalid JSON: {e}") try: - return ProductParseResponse.model_validate(parsed) + llm_parsed = ProductParseLLMResponse.model_validate(parsed) + return ProductParseResponse.model_validate(llm_parsed.model_dump()) except ValidationError as e: raise HTTPException(status_code=422, detail=e.errors()) @@ -453,3 +509,425 @@ def create_product_inventory( session.commit() session.refresh(entry) return entry + + +# --------------------------------------------------------------------------- +# Shopping suggestion +# --------------------------------------------------------------------------- + + +def _ev(v: object) -> str: + if v is None: + return "" + value = getattr(v, "value", None) + if isinstance(value, str): + return value + return str(v) + + +def _build_shopping_context(session: Session) -> str: + snapshot = session.exec( + select(SkinConditionSnapshot).order_by( + col(SkinConditionSnapshot.snapshot_date).desc() + ) + ).first() + + skin_lines = ["STAN SKÓRY:"] + if snapshot: + skin_lines.append(f" Data: {snapshot.snapshot_date}") + skin_lines.append(f" Ogólny stan: {_ev(snapshot.overall_state)}") + skin_lines.append(f" Typ skóry: {_ev(snapshot.skin_type)}") + skin_lines.append(f" Nawilżenie: {snapshot.hydration_level}/5") + skin_lines.append(f" Wrażliwość: {snapshot.sensitivity_level}/5") + skin_lines.append(f" Bariera: {_ev(snapshot.barrier_state)}") + concerns = ", ".join(_ev(c) for c in (snapshot.active_concerns or [])) + skin_lines.append(f" Aktywne problemy: {concerns or 'brak'}") + if snapshot.priorities: + skin_lines.append(f" Priorytety: {', '.join(snapshot.priorities)}") + else: + skin_lines.append(" (brak danych)") + + products = _get_shopping_products(session) + + product_ids = [p.id for p in products] + inventory_rows = ( + session.exec( + select(ProductInventory).where( + col(ProductInventory.product_id).in_(product_ids) + ) + ).all() + if product_ids + else [] + ) + inv_by_product: dict = {} + for inv in inventory_rows: + inv_by_product.setdefault(inv.product_id, []).append(inv) + + products_lines = ["POSIADANE PRODUKTY:"] + products_lines.append( + " Legenda: [✓] = produkt dostępny (w magazynie), [✗] = brak w magazynie" + ) + for p in products: + active_inv = [i for i in inv_by_product.get(p.id, []) if i.finished_at is None] + has_stock = len(active_inv) > 0 # any unfinished inventory = in stock + stock = "✓" if has_stock else "✗" + + actives = _extract_active_names(p) + actives_str = f", actives: {actives}" if actives else "" + + ep = p.product_effect_profile + if isinstance(ep, dict): + effects = {k.replace("_strength", ""): v for k, v in ep.items() if v >= 3} + else: + effects = { + k.replace("_strength", ""): v + for k, v in ep.model_dump().items() + if v >= 3 + } + effects_str = f", effects: {effects}" if effects else "" + + targets = [_ev(t) for t in (p.targets or [])] + + products_lines.append( + f" [{stock}] id={p.id} {p.name} ({p.brand or ''}) - {_ev(p.category)}, " + f"targets: {targets}{actives_str}{effects_str}" + ) + + return "\n".join(skin_lines) + "\n\n" + "\n".join(products_lines) + + +def _get_shopping_products(session: Session) -> list[Product]: + stmt = select(Product).where(col(Product.is_tool).is_(False)) + products = session.exec(stmt).all() + return [p for p in products if not p.is_medication] + + +def _extract_active_names(product: Product) -> list[str]: + names: list[str] = [] + for active in product.actives or []: + if isinstance(active, dict): + name = str(active.get("name") or "").strip() + else: + name = str(getattr(active, "name", "") or "").strip() + if not name: + continue + if name in names: + continue + names.append(name) + if len(names) >= 12: + break + return names + + +def _extract_requested_product_ids( + args: dict[str, object], max_ids: int = 8 +) -> list[str]: + raw_ids = args.get("product_ids") + if not isinstance(raw_ids, list): + return [] + + requested_ids: list[str] = [] + seen: set[str] = set() + for raw_id in raw_ids: + if not isinstance(raw_id, str): + continue + if raw_id in seen: + continue + seen.add(raw_id) + requested_ids.append(raw_id) + if len(requested_ids) >= max_ids: + break + return requested_ids + + +def _build_product_details_tool_handler(products: list[Product], mapper): + available_by_id = {str(p.id): p for p in products} + + def _handler(args: dict[str, object]) -> dict[str, object]: + requested_ids = _extract_requested_product_ids(args) + products_payload = [] + for pid in requested_ids: + product = available_by_id.get(pid) + if product is None: + continue + products_payload.append(mapper(product, pid)) + return {"products": products_payload} + + return _handler + + +def _build_inci_tool_handler(products: list[Product]): + def _mapper(product: Product, pid: str) -> dict[str, object]: + inci = product.inci or [] + compact_inci = [str(i)[:120] for i in inci[:128]] + return {"id": pid, "name": product.name, "inci": compact_inci} + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +def _build_actives_tool_handler(products: list[Product]): + def _mapper(product: Product, pid: str) -> dict[str, object]: + payload = [] + for active in product.actives or []: + if isinstance(active, dict): + name = str(active.get("name") or "").strip() + if not name: + continue + item = {"name": name} + percent = active.get("percent") + if percent is not None: + item["percent"] = percent + functions = active.get("functions") + if isinstance(functions, list): + item["functions"] = [str(f) for f in functions[:4]] + strength_level = active.get("strength_level") + if strength_level is not None: + item["strength_level"] = str(strength_level) + payload.append(item) + continue + + name = str(getattr(active, "name", "") or "").strip() + if not name: + continue + item = {"name": name} + percent = getattr(active, "percent", None) + if percent is not None: + item["percent"] = percent + functions = getattr(active, "functions", None) + if isinstance(functions, list): + item["functions"] = [_ev(f) for f in functions[:4]] + strength_level = getattr(active, "strength_level", None) + if strength_level is not None: + item["strength_level"] = _ev(strength_level) + payload.append(item) + return {"id": pid, "name": product.name, "actives": payload[:24]} + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +def _build_usage_notes_tool_handler(products: list[Product]): + def _mapper(product: Product, pid: str) -> dict[str, object]: + notes = " ".join(str(product.usage_notes or "").split()) + if len(notes) > 500: + notes = notes[:497] + "..." + return {"id": pid, "name": product.name, "usage_notes": notes} + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +def _build_safety_rules_tool_handler(products: list[Product]): + def _mapper(product: Product, pid: str) -> dict[str, object]: + ctx = product.to_llm_context() + return { + "id": pid, + "name": product.name, + "incompatible_with": (ctx.get("incompatible_with") or [])[:24], + "contraindications": (ctx.get("contraindications") or [])[:24], + "context_rules": ctx.get("context_rules") or {}, + "safety": ctx.get("safety") or {}, + "min_interval_hours": ctx.get("min_interval_hours"), + "max_frequency_per_week": ctx.get("max_frequency_per_week"), + } + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +_INCI_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_inci", + description=( + "Return exact INCI ingredient lists for selected product UUIDs from " + "POSIADANE PRODUKTY." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from POSIADANE PRODUKTY.", + ) + }, + required=["product_ids"], + ), +) + +_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_safety_rules", + description=( + "Return safety and compatibility rules for selected product UUIDs, " + "including incompatible_with, contraindications, context_rules and safety flags." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from POSIADANE PRODUKTY.", + ) + }, + required=["product_ids"], + ), +) + +_ACTIVES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_actives", + description=( + "Return detailed active ingredients for selected product UUIDs, " + "including concentration and functions when available." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from POSIADANE PRODUKTY.", + ) + }, + required=["product_ids"], + ), +) + +_USAGE_NOTES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_usage_notes", + description=( + "Return compact usage notes for selected product UUIDs " + "(timing, application method and cautions)." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from POSIADANE PRODUKTY.", + ) + }, + required=["product_ids"], + ), +) + + +_SHOPPING_SYSTEM_PROMPT = """Jesteś asystentem zakupowym w dziedzinie pielęgnacji skóry. +Twoim zadaniem jest przeanalizować stan skóry użytkownika oraz produkty, które już posiada, +a następnie zasugerować TYPY produktów (bez marek), które mogłyby uzupełnić ich rutynę. + +LEGENDA: +- [✓] = produkt dostępny w magazynie (nawet jeśli jest zapieczętowany) +- [✗] = produkt niedostępny (brak w magazynie, wszystkie opakowania zużyte) + +ZASADY: +0. Sugeruj tylko wtedy, gdy jest realna potrzeba - nie zwracaj stałej liczby produktów +1. Sugeruj TYLKO typy produktów, NIGDY konkretne marki (np. "Salicylic Acid 2% Masque", nie "La Roche-Posay") +2. Produkty oznaczone [✗] to te, których NIE MA w magazynie - możesz je zasugerować +3. Produkty oznaczone [✓] są już dostępne - nie sugeruj ich ponownie +4. Bierz pod uwagę aktywne problemy skóry (acne, hyperpigmentacja, aging, etc.) +5. Sugeruj realistyczną częstotliwość użycia (dzienna, 2-3x tygodniowo, etc.) +6. Zachowaj kolejność warstw: cleanse → toner → serum → moisturizer → SPF +7. Jeśli użytkownik ma uszkodzoną barierę, unikaj silnych eksfoliantów i retinoidów +8. Zwracaj uwagę na ewentualne konflikty polecanych składników z tymi, które użytkownik już posiada (np. nie polecaj peptydów miedziowych jeśli użytkownik nadużywa kwasów) +9. Odpowiadaj w języku polskim + +Format odpowiedzi - zwróć wyłącznie JSON zgodny z podanym schematem.""" + + +@router.post("/suggest", response_model=ShoppingSuggestionResponse) +def suggest_shopping(session: Session = Depends(get_session)): + context = _build_shopping_context(session) + shopping_products = _get_shopping_products(session) + + prompt = ( + f"Na podstawie poniższych danych przeanalizuj, jakie TYPY produktów " + f"mogłyby uzupełnić rutynę pielęgnacyjną użytkownika.\n\n" + f"{context}\n\n" + "NARZEDZIA:\n" + "- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives, get_product_usage_notes.\n" + "- Wywoluj narzedzia tylko, gdy potrzebujesz detali do oceny konfliktow skladnikow lub ryzyka podraznien.\n" + "- Grupuj UUID: staraj sie pobierac dane dla wielu produktow jednym wywolaniem.\n" + f"Zwróć wyłącznie JSON zgodny ze schematem." + ) + + config = get_creative_config( + system_instruction=_SHOPPING_SYSTEM_PROMPT, + response_schema=_ShoppingSuggestionsOut, + max_output_tokens=4096, + ).model_copy( + update={ + "tools": [ + genai_types.Tool( + function_declarations=[ + _INCI_FUNCTION_DECLARATION, + _SAFETY_RULES_FUNCTION_DECLARATION, + _ACTIVES_FUNCTION_DECLARATION, + _USAGE_NOTES_FUNCTION_DECLARATION, + ] + ) + ], + "tool_config": genai_types.ToolConfig( + function_calling_config=genai_types.FunctionCallingConfig( + mode=genai_types.FunctionCallingConfigMode.AUTO, + ) + ), + } + ) + + function_handlers = { + "get_product_inci": _build_inci_tool_handler(shopping_products), + "get_product_safety_rules": _build_safety_rules_tool_handler(shopping_products), + "get_product_actives": _build_actives_tool_handler(shopping_products), + "get_product_usage_notes": _build_usage_notes_tool_handler(shopping_products), + } + + try: + response = call_gemini_with_function_tools( + endpoint="products/suggest", + contents=prompt, + config=config, + function_handlers=function_handlers, + user_input=prompt, + max_tool_roundtrips=3, + ) + except HTTPException as exc: + if ( + exc.status_code != 502 + or str(exc.detail) != "Gemini requested too many function calls" + ): + raise + + conservative_prompt = ( + f"{prompt}\n\n" + "TRYB AWARYJNY (KONSERWATYWNY):\n" + "- Osiagnieto limit wywolan narzedzi.\n" + "- Nie wywoluj narzedzi ponownie.\n" + "- Zasugeruj tylko najbardziej bezpieczne i realistyczne typy produktow do uzupelnienia brakow," + " unikaj agresywnych aktywnych przy niepelnych danych.\n" + ) + response = call_gemini( + endpoint="products/suggest", + contents=conservative_prompt, + config=get_creative_config( + system_instruction=_SHOPPING_SYSTEM_PROMPT, + response_schema=_ShoppingSuggestionsOut, + max_output_tokens=4096, + ), + user_input=conservative_prompt, + tool_trace={ + "mode": "fallback_conservative", + "reason": "max_tool_roundtrips_exceeded", + }, + ) + + raw = response.text + if not raw: + raise HTTPException(status_code=502, detail="LLM returned an empty response") + + try: + parsed = json.loads(raw) + except json.JSONDecodeError as e: + raise HTTPException(status_code=502, detail=f"LLM returned invalid JSON: {e}") + + return ShoppingSuggestionResponse( + suggestions=[ProductSuggestion(**s) for s in parsed.get("suggestions", [])], + reasoning=parsed.get("reasoning", ""), + ) diff --git a/backend/innercontext/api/routines.py b/backend/innercontext/api/routines.py index 5cc129f..503e0c9 100644 --- a/backend/innercontext/api/routines.py +++ b/backend/innercontext/api/routines.py @@ -6,12 +6,23 @@ from uuid import UUID, uuid4 from fastapi import APIRouter, Depends, HTTPException from google.genai import types as genai_types from pydantic import BaseModel as PydanticBase -from sqlmodel import Session, SQLModel, col, select +from sqlmodel import Field, Session, SQLModel, col, select from db import get_session from innercontext.api.utils import get_or_404 -from innercontext.llm import get_gemini_client -from innercontext.models import GroomingSchedule, Product, Routine, RoutineStep, SkinConditionSnapshot +from innercontext.llm import ( + call_gemini, + call_gemini_with_function_tools, + get_creative_config, +) +from innercontext.models import ( + GroomingSchedule, + Product, + ProductInventory, + Routine, + RoutineStep, + SkinConditionSnapshot, +) from innercontext.models.enums import GroomingAction, PartOfDay router = APIRouter() @@ -70,23 +81,36 @@ class SuggestedStep(SQLModel): action_notes: Optional[str] = None dose: Optional[str] = None region: Optional[str] = None + why_this_step: Optional[str] = None + optional: Optional[bool] = None class SuggestRoutineRequest(SQLModel): routine_date: date part_of_day: PartOfDay notes: Optional[str] = None + include_minoxidil_beard: bool = False + leaving_home: Optional[bool] = None + + +class RoutineSuggestionSummary(SQLModel): + primary_goal: str = "" + constraints_applied: list[str] = Field(default_factory=list) + confidence: float = 0.0 class RoutineSuggestion(SQLModel): steps: list[SuggestedStep] reasoning: str + summary: Optional[RoutineSuggestionSummary] = None class SuggestBatchRequest(SQLModel): from_date: date to_date: date notes: Optional[str] = None + include_minoxidil_beard: bool = False + minimize_products: Optional[bool] = None class DayPlan(SQLModel): @@ -106,7 +130,17 @@ class BatchSuggestion(SQLModel): # --------------------------------------------------------------------------- -class _StepOut(PydanticBase): +class _SingleStepOut(PydanticBase): + product_id: Optional[str] = None + action_type: Optional[GroomingAction] = None + dose: Optional[str] = None + region: Optional[str] = None + action_notes: Optional[str] = None + why_this_step: Optional[str] = None + optional: Optional[bool] = None + + +class _BatchStepOut(PydanticBase): product_id: Optional[str] = None action_type: Optional[GroomingAction] = None dose: Optional[str] = None @@ -114,15 +148,22 @@ class _StepOut(PydanticBase): action_notes: Optional[str] = None +class _SummaryOut(PydanticBase): + primary_goal: str + constraints_applied: list[str] + confidence: float + + class _SuggestionOut(PydanticBase): - steps: list[_StepOut] + steps: list[_SingleStepOut] reasoning: str + summary: _SummaryOut class _DayPlanOut(PydanticBase): date: str - am_steps: list[_StepOut] - pm_steps: list[_StepOut] + am_steps: list[_BatchStepOut] + pm_steps: list[_BatchStepOut] reasoning: str @@ -135,43 +176,96 @@ class _BatchOut(PydanticBase): # Prompt helpers # --------------------------------------------------------------------------- -_DAY_NAMES = ["poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota", "niedziela"] +_DAY_NAMES = [ + "poniedziałek", + "wtorek", + "środa", + "czwartek", + "piątek", + "sobota", + "niedziela", +] + + +def _contains_minoxidil_text(value: Optional[str]) -> bool: + if not value: + return False + text = value.lower() + return "minoxidil" in text or "minoksydyl" in text + + +def _is_minoxidil_product(product: Product) -> bool: + if _contains_minoxidil_text(product.name): + return True + if _contains_minoxidil_text(product.brand): + return True + if _contains_minoxidil_text(product.line_name): + return True + if _contains_minoxidil_text(product.usage_notes): + return True + if any(_contains_minoxidil_text(i) for i in (product.inci or [])): + return True + + actives = product.actives or [] + for a in actives: + if isinstance(a, dict): + if _contains_minoxidil_text(str(a.get("name", ""))): + return True + continue + if _contains_minoxidil_text(a.name): + return True + return False def _ev(v: object) -> str: - return v.value if v is not None and hasattr(v, "value") else str(v) if v is not None else "" + if v is None: + return "" + value = getattr(v, "value", None) + if isinstance(value, str): + return value + return str(v) def _build_skin_context(session: Session) -> str: snapshot = session.exec( - select(SkinConditionSnapshot).order_by(col(SkinConditionSnapshot.snapshot_date).desc()) + select(SkinConditionSnapshot).order_by( + col(SkinConditionSnapshot.snapshot_date).desc() + ) ).first() if snapshot is None: - return "STAN SKÓRY: brak danych\n" + return "SKIN CONDITION: no data\n" ev = _ev return ( - f"STAN SKÓRY (snapshot z {snapshot.snapshot_date}):\n" - f" Ogólny stan: {ev(snapshot.overall_state)}\n" - f" Nawilżenie: {snapshot.hydration_level}/5\n" - f" Bariera: {ev(snapshot.barrier_state)}\n" - f" Aktywne problemy: {', '.join(ev(c) for c in (snapshot.active_concerns or []))}\n" - f" Priorytety: {', '.join(snapshot.priorities or [])}\n" - f" Uwagi: {snapshot.notes or 'brak'}\n" + f"SKIN CONDITION (snapshot from {snapshot.snapshot_date}):\n" + f" Overall state: {ev(snapshot.overall_state)}\n" + f" Hydration: {snapshot.hydration_level}/5\n" + f" Barrier: {ev(snapshot.barrier_state)}\n" + f" Active concerns: {', '.join(ev(c) for c in (snapshot.active_concerns or []))}\n" + f" Priorities: {', '.join(snapshot.priorities or [])}\n" + f" Notes: {snapshot.notes or 'none'}\n" ) -def _build_grooming_context(session: Session, weekdays: Optional[list[int]] = None) -> str: - entries = session.exec(select(GroomingSchedule).order_by(GroomingSchedule.day_of_week)).all() +def _build_grooming_context( + session: Session, weekdays: Optional[list[int]] = None +) -> str: + entries = session.exec( + select(GroomingSchedule).order_by(col(GroomingSchedule.day_of_week)) + ).all() if not entries: - return "HARMONOGRAM PIELĘGNACJI: brak\n" - lines = ["HARMONOGRAM PIELĘGNACJI:"] + return "GROOMING SCHEDULE: none\n" + lines = ["GROOMING SCHEDULE:"] for e in entries: if weekdays is not None and e.day_of_week not in weekdays: continue - day_name = _DAY_NAMES[e.day_of_week] if 0 <= e.day_of_week <= 6 else str(e.day_of_week) - lines.append(f" {day_name}: {_ev(e.action)}" + (f" ({e.notes})" if e.notes else "")) + day_name = ( + _DAY_NAMES[e.day_of_week] if 0 <= e.day_of_week <= 6 else str(e.day_of_week) + ) + lines.append( + f" {day_name}: {_ev(e.action)}" + (f" ({e.notes})" if e.notes else "") + ) if len(lines) == 1: - lines.append(" (brak wpisów dla podanych dni)") + lines.append(" (no entries for specified days)") return "\n".join(lines) + "\n" @@ -183,66 +277,462 @@ def _build_recent_history(session: Session) -> str: .order_by(col(Routine.routine_date).desc()) ).all() if not routines: - return "OSTATNIE RUTYNY (7 dni): brak\n" - lines = ["OSTATNIE RUTYNY (7 dni):"] + return "RECENT ROUTINES: none\n" + lines = ["RECENT ROUTINES:"] for r in routines: steps = session.exec( select(RoutineStep) .where(RoutineStep.routine_id == r.id) - .order_by(RoutineStep.order_index) + .order_by(col(RoutineStep.order_index)) ).all() step_names = [] for s in steps: if s.product_id: p = session.get(Product, s.product_id) - step_names.append(p.name if p else str(s.product_id)) + if p: + short_id = str(p.id)[:8] + step_names.append(f"{_ev(p.category)} [{short_id}]") + else: + step_names.append(f"unknown [{str(s.product_id)[:8]}]") elif s.action_type: - step_names.append(_ev(s.action_type)) - lines.append(f" {r.routine_date} {_ev(r.part_of_day).upper()}: {', '.join(step_names)}") + step_names.append(f"action: {_ev(s.action_type)}") + lines.append( + f" {r.routine_date} {_ev(r.part_of_day).upper()}: {', '.join(step_names)}" + ) return "\n".join(lines) + "\n" -def _build_products_context(session: Session, time_filter: Optional[str] = None) -> str: - stmt = select(Product).where(Product.is_medication == False).where(Product.is_tool == False) # noqa: E712 - products = session.exec(stmt).all() - lines = ["DOSTĘPNE PRODUKTY:"] +def _build_products_context( + session: Session, + time_filter: Optional[str] = None, + reference_date: Optional[date] = None, +) -> str: + products = _get_available_products(session, time_filter=time_filter) + product_ids = [p.id for p in products] + inventory_rows = ( + session.exec( + select(ProductInventory).where( + col(ProductInventory.product_id).in_(product_ids) + ) + ).all() + if product_ids + else [] + ) + inv_by_product: dict[UUID, list[ProductInventory]] = {} + for inv in inventory_rows: + inv_by_product.setdefault(inv.product_id, []).append(inv) + + recent_usage_counts: dict[UUID, int] = {} + if reference_date is not None: + cutoff = reference_date - timedelta(days=7) + recent_usage = session.exec( + select(RoutineStep.product_id) + .join(Routine) + .where(col(Routine.routine_date) > cutoff) + .where(col(Routine.routine_date) <= reference_date) + ).all() + for pid in recent_usage: + if pid: + recent_usage_counts[pid] = recent_usage_counts.get(pid, 0) + 1 + + lines = ["AVAILABLE PRODUCTS:"] for p in products: - if time_filter and _ev(p.recommended_time) not in (time_filter, "both"): - continue + p.inventory = inv_by_product.get(p.id, []) ctx = p.to_llm_context() entry = ( - f" - id={ctx['id']} name=\"{ctx['name']}\" brand=\"{ctx['brand']}\"" + f' - id={ctx["id"]} name="{ctx["name"]}" brand="{ctx["brand"]}"' f" category={ctx.get('category', '')} recommended_time={ctx.get('recommended_time', '')}" + f" leave_on={ctx.get('leave_on', '')}" f" targets={ctx.get('targets', [])}" ) - profile = ctx.get("product_effect_profile", {}) + active_names = _extract_active_names(p) + if active_names: + entry += f" actives={active_names}" + + active_inventory = [inv for inv in p.inventory if inv.finished_at is None] + open_inventory = [inv for inv in active_inventory if inv.is_opened] + sealed_inventory = [inv for inv in active_inventory if not inv.is_opened] + entry += ( + " inventory_status={" + f"active:{len(active_inventory)},opened:{len(open_inventory)},sealed:{len(sealed_inventory)}" + "}" + ) + if open_inventory: + expiry_dates = sorted( + inv.expiry_date.isoformat() for inv in open_inventory if inv.expiry_date + ) + if expiry_dates: + entry += f" nearest_open_expiry={expiry_dates[0]}" + if p.pao_months is not None: + pao_deadlines = sorted( + (inv.opened_at + timedelta(days=30 * p.pao_months)).isoformat() + for inv in open_inventory + if inv.opened_at + ) + if pao_deadlines: + entry += f" nearest_open_pao_deadline={pao_deadlines[0]}" + if p.pao_months is not None: + entry += f" pao_months={p.pao_months}" + profile = ctx.get("effect_profile", {}) if profile: notable = {k: v for k, v in profile.items() if v and v > 0} if notable: entry += f" effects={notable}" if ctx.get("incompatible_with"): entry += f" incompatible_with={ctx['incompatible_with']}" + if ctx.get("contraindications"): + entry += f" contraindications={ctx['contraindications']}" if ctx.get("context_rules"): entry += f" context_rules={ctx['context_rules']}" + safety = ctx.get("safety") or {} + if isinstance(safety, dict): + not_safe = {k: v for k, v in safety.items() if v is False} + if not_safe: + entry += f" safety_alerts={not_safe}" if ctx.get("min_interval_hours"): entry += f" min_interval_hours={ctx['min_interval_hours']}" if ctx.get("max_frequency_per_week"): entry += f" max_frequency_per_week={ctx['max_frequency_per_week']}" + usage_count = recent_usage_counts.get(p.id, 0) + entry += f" used_in_last_7_days={usage_count}" lines.append(entry) return "\n".join(lines) + "\n" -_RULES = """\ -ZASADY: - - Kolejność warstw: cleanser → toner → essence → serum → moisturizer → [SPF dla AM] - - Respektuj incompatible_with (scope: same_step / same_day / same_period) - - Respektuj context_rules (safe_after_shaving, safe_after_acids itp.) - - Respektuj min_interval_hours i max_frequency_per_week - - 4–7 kroków na rutynę - - product_id musi być UUID produktu z listy lub null dla czynności pielęgnacyjnych - - action_type: tylko shaving_razor | shaving_oneblade | dermarolling (lub null) - - Nie używaj retinoidów i kwasów w tej samej rutynie - - W AM zawsze uwzględnij SPF jeśli dostępny +def _get_available_products( + session: Session, + time_filter: Optional[str] = None, +) -> list[Product]: + stmt = select(Product).where(col(Product.is_tool).is_(False)) + products = session.exec(stmt).all() + result: list[Product] = [] + for p in products: + if p.is_medication and not _is_minoxidil_product(p): + continue + if time_filter and _ev(p.recommended_time) not in (time_filter, "both"): + continue + result.append(p) + return result + + +def _build_inci_tool_handler( + products: list[Product], +): + def _mapper(product: Product, pid: str) -> dict[str, object]: + inci = product.inci or [] + compact_inci = [str(i)[:120] for i in inci[:128]] + return { + "id": pid, + "name": product.name, + "inci": compact_inci, + } + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +def _build_actives_tool_handler( + products: list[Product], +): + def _mapper(product: Product, pid: str) -> dict[str, object]: + actives_payload = [] + for a in product.actives or []: + if isinstance(a, dict): + active_name = str(a.get("name") or "").strip() + if not active_name: + continue + item = {"name": active_name} + percent = a.get("percent") + if percent is not None: + item["percent"] = percent + functions = a.get("functions") + if isinstance(functions, list): + item["functions"] = [str(f) for f in functions[:4]] + strength_level = a.get("strength_level") + if strength_level is not None: + item["strength_level"] = str(strength_level) + actives_payload.append(item) + continue + + active_name = str(getattr(a, "name", "") or "").strip() + if not active_name: + continue + item = {"name": active_name} + percent = getattr(a, "percent", None) + if percent is not None: + item["percent"] = percent + functions = getattr(a, "functions", None) + if isinstance(functions, list): + item["functions"] = [_ev(f) for f in functions[:4]] + strength_level = getattr(a, "strength_level", None) + if strength_level is not None: + item["strength_level"] = _ev(strength_level) + actives_payload.append(item) + + return { + "id": pid, + "name": product.name, + "actives": actives_payload[:24], + } + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +def _build_usage_notes_tool_handler( + products: list[Product], +): + def _mapper(product: Product, pid: str) -> dict[str, object]: + notes = " ".join(str(product.usage_notes or "").split()) + if len(notes) > 500: + notes = notes[:497] + "..." + return { + "id": pid, + "name": product.name, + "usage_notes": notes, + } + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +def _build_safety_rules_tool_handler( + products: list[Product], +): + def _mapper(product: Product, pid: str) -> dict[str, object]: + ctx = product.to_llm_context() + return { + "id": pid, + "name": product.name, + "incompatible_with": (ctx.get("incompatible_with") or [])[:24], + "contraindications": (ctx.get("contraindications") or [])[:24], + "context_rules": ctx.get("context_rules") or {}, + "safety": ctx.get("safety") or {}, + "min_interval_hours": ctx.get("min_interval_hours"), + "max_frequency_per_week": ctx.get("max_frequency_per_week"), + } + + return _build_product_details_tool_handler(products, mapper=_mapper) + + +def _build_product_details_tool_handler( + products: list[Product], + mapper, +): + available_by_id = {str(p.id): p for p in products} + + def _handler(args: dict[str, object]) -> dict[str, object]: + requested_ids = _extract_requested_product_ids(args) + products_payload = [] + for pid in requested_ids: + product = available_by_id.get(pid) + if product is None: + continue + products_payload.append(mapper(product, pid)) + return {"products": products_payload} + + return _handler + + +def _extract_requested_product_ids( + args: dict[str, object], max_ids: int = 8 +) -> list[str]: + raw_ids = args.get("product_ids") + if not isinstance(raw_ids, list): + return [] + + requested_ids: list[str] = [] + seen: set[str] = set() + for raw_id in raw_ids: + if not isinstance(raw_id, str): + continue + if raw_id in seen: + continue + seen.add(raw_id) + requested_ids.append(raw_id) + if len(requested_ids) >= max_ids: + break + return requested_ids + + +def _extract_active_names(product: Product) -> list[str]: + names: list[str] = [] + for a in product.actives or []: + if isinstance(a, dict): + name = str(a.get("name") or "").strip() + else: + name = str(getattr(a, "name", "") or "").strip() + if not name: + continue + if name in names: + continue + names.append(name) + if len(names) >= 12: + break + return names + + +_INCI_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_inci", + description=( + "Return exact INCI ingredient lists for products identified by UUID from " + "the AVAILABLE PRODUCTS list." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from AVAILABLE PRODUCTS.", + ) + }, + required=["product_ids"], + ), +) + +_ACTIVES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_actives", + description=( + "Return detailed active ingredients (name, strength, concentration, functions) " + "for selected product UUIDs." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from AVAILABLE PRODUCTS.", + ) + }, + required=["product_ids"], + ), +) + +_USAGE_NOTES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_usage_notes", + description=( + "Return compact usage notes for selected product UUIDs (application method, " + "timing, and cautions)." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from AVAILABLE PRODUCTS.", + ) + }, + required=["product_ids"], + ), +) + +_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration( + name="get_product_safety_rules", + description=( + "Return safety and compatibility rules for selected product UUIDs: " + "incompatible_with, contraindications, context_rules and safety flags." + ), + parameters=genai_types.Schema( + type=genai_types.Type.OBJECT, + properties={ + "product_ids": genai_types.Schema( + type=genai_types.Type.ARRAY, + items=genai_types.Schema(type=genai_types.Type.STRING), + description="Product UUIDs from AVAILABLE PRODUCTS.", + ) + }, + required=["product_ids"], + ), +) + + +def _build_objectives_context(include_minoxidil_beard: bool) -> str: + if include_minoxidil_beard: + return ( + "USER OBJECTIVES:\n" + " - Priority: improve beard and mustache density\n" + " - If a product with minoxidil is available, include it adhering strictly to safety rules\n" + ) + return "" + + +def _build_day_context(leaving_home: Optional[bool]) -> str: + if leaving_home is None: + return "" + val = "yes" if leaving_home else "no" + return f"DAY CONTEXT:\n Leaving home: {val}\n" + + +_ROUTINES_SYSTEM_PROMPT = """\ +Jesteś ekspertem planowania pielęgnacji. + +CEL: +Twórz realistyczne, bezpieczne i krótkie rutyny o wysokiej zgodności z danymi wejściowymi. + +PRIORYTETY DECYZYJNE (od najwyższego): +1) Bezpieczeństwo (brak realnego ryzyka klinicznego) +2) Cel terapeutyczny użytkownika +3) Reguły częstotliwości i odstępów +4) Zarządzanie inwentarzem +5) Prostota + +> Cel terapeutyczny oznacza maksymalizację realnego efektu klinicznego, +> nie tylko zgodność z deklarowanymi targetami produktu. + +WYMAGANIA ODPOWIEDZI: +- Zwracaj wyłącznie poprawny JSON (bez markdown, bez komentarzy, bez preambuły). +- Trzymaj się dokładnie przekazanego schematu odpowiedzi. +- Nie używaj żadnych pól spoza schematu. +- Nie twórz produktów spoza listy wejściowej. +- Jeśli nie da się bezpiecznie dodać kroku, pomiń go zamiast zgadywać. + +ZASADY PLANOWANIA: +- Kolejność warstw: cleanser -> toner -> essence -> serum -> moisturizer -> [SPF dla AM]. +- Respektuj: incompatible_with (same_step / same_day / same_period), context_rules, + min_interval_hours, max_frequency_per_week, usage_notes. +- Zarządzanie inwentarzem: + - Preferuj produkty już otwarte (miękka preferencja). +- Unikaj funkcjonalnej redundancji (np. wielokrotne źródła panthenolu, ceramidów lub niacynamidu w tej samej rutynie), + chyba że istnieje wyraźne uzasadnienie terapeutyczne. +- Maksymalnie 2 serum w rutynie. + Jeśli 2: jedno jako główny aktywny bodziec, drugie wyłącznie wspierające. +- Dla mildly_compromised nie eliminuj automatycznie umiarkowanych aktywnych; + decyzję opieraj na effect_profile (irritation_risk, barrier_disruption_risk) i regułach bezpieczeństwa. +- Nie zwiększaj intensywności terapii (retinoid/kwasy) dzień po dniu, + jeśli brak wyraźnej poprawy stanu skóry lub brak wskazań klinicznych. +- Nie łącz retinoidów i kwasów w tej samej rutynie ani tego samego dnia (dla planu wielodniowego). +- W AM zawsze uwzględnij SPF, jeśli kompatybilny produkt SPF istnieje na liście. + Wybór filtra (na podstawie KONTEKST DNIA): + - "Wyjście z domu: tak" → najwyższy współczynnik dostępny w nazwie (SPF50+, SPF50, SPF30); + - "Wyjście z domu: nie" → SPF30 wystarczy; wyższy dopuszczalny jeśli brak SPF30; + - brak KONTEKST DNIA → wybierz najwyższy dostępny. +- Dla minoksydylu (jeśli celem jest zarost i produkt jest dostępny): ustaw adekwatny region + broda/wąsy i nie naruszaj ograniczeń bezpieczeństwa. +- Preferuj 4-7 kroków na pojedynczą rutynę; unikaj zbędnych duplikatów aktywnych. +- Jeśli krok to produkt: podaj poprawny UUID z listy. +- Jeśli krok to czynność pielęgnacyjna: product_id = null. Dozwolone akcje są ściśle określone w schemacie (action_type). +- Nie zwracaj "pustych" kroków: każdy krok musi mieć product_id albo action_type. +- Pole region uzupełniaj tylko gdy ma znaczenie kliniczne/praktyczne (np. broda, wąsy, okolica oczu, szyja). + Dla standardowych kroków pielęgnacji całej twarzy pozostaw region puste. + +JAK ROZWIĄZYWAĆ KONFLIKTY: +- Bezpieczeństwo > wszystko. +- Jeśli MODE=travel: logistyka podróży > różnorodność terapeutyczna. +- W MODE=travel odejdź od minimalizacji produktów tylko gdy wymaga tego bezpieczeństwo + lub bez dodatkowego produktu nie da się osiągnąć głównego celu terapeutycznego. +- Jeśli MODE=standard i bezpieczeństwo jest zachowane, preferuj różnorodność terapeutyczną. +- Przy niepełnych danych wybierz wariant konserwatywny. +""" + + +_ROUTINES_SINGLE_EXTRA = """\ +DODATKOWE WYMAGANIA DLA TRYBU JEDNEJ RUTYNY: +- Każdy krok powinien mieć zwięzłe why_this_step (maks. jedno zdanie). +- Pole optional ustawiaj na true tylko dla kroków niekrytycznych. +- Uzupełnij summary: + - primary_goal: główny cel tej rutyny, + - constraints_applied: lista kluczowych ograniczeń zastosowanych przy planowaniu, + - confidence: liczba 0-1. """ @@ -256,7 +746,7 @@ ZASADY: # --------------------------------------------------------------------------- -@router.get("", response_model=list[Routine]) +@router.get("") def list_routines( from_date: Optional[date] = None, to_date: Optional[date] = None, @@ -270,7 +760,25 @@ def list_routines( stmt = stmt.where(Routine.routine_date <= to_date) if part_of_day is not None: stmt = stmt.where(Routine.part_of_day == part_of_day) - return session.exec(stmt).all() + routines = session.exec(stmt).all() + + routine_ids = [r.id for r in routines] + steps_by_routine: dict = {} + if routine_ids: + all_steps = session.exec( + select(RoutineStep).where(col(RoutineStep.routine_id).in_(routine_ids)) + ).all() + for step in all_steps: + steps_by_routine.setdefault(step.routine_id, []).append(step) + + result = [] + for r in routines: + data = r.model_dump(mode="json") + data["steps"] = [ + s.model_dump(mode="json") for s in steps_by_routine.get(r.id, []) + ] + result.append(data) + return result @router.post("", response_model=Routine, status_code=201) @@ -292,37 +800,112 @@ def suggest_routine( data: SuggestRoutineRequest, session: Session = Depends(get_session), ): - client, model = get_gemini_client() - weekday = data.routine_date.weekday() skin_ctx = _build_skin_context(session) grooming_ctx = _build_grooming_context(session, weekdays=[weekday]) history_ctx = _build_recent_history(session) - products_ctx = _build_products_context(session, time_filter=data.part_of_day.value) + day_ctx = _build_day_context(data.leaving_home) + products_ctx = _build_products_context( + session, time_filter=data.part_of_day.value, reference_date=data.routine_date + ) + available_products = _get_available_products( + session, + time_filter=data.part_of_day.value, + ) + objectives_ctx = _build_objectives_context(data.include_minoxidil_beard) - notes_line = f"\nKONTEKST OD UŻYTKOWNIKA: {data.notes}\n" if data.notes else "" + mode_line = "MODE: standard" + notes_line = f"USER CONTEXT: {data.notes}\n" if data.notes else "" day_name = _DAY_NAMES[weekday] prompt = ( f"Zaproponuj rutynę pielęgnacyjną {data.part_of_day.value.upper()} " f"na {data.routine_date} ({day_name}).\n\n" - f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{products_ctx}\n{_RULES}{notes_line}" - "\nZwróć JSON zgodny ze schematem." + f"{mode_line}\n" + "INPUT DATA:\n" + f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{day_ctx}\n{products_ctx}\n{objectives_ctx}" + "\nNARZEDZIA:\n" + "- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives, get_product_usage_notes.\n" + "- Wywoluj narzedzia tylko, gdy potrzebujesz detali do decyzji klinicznej/bezpieczenstwa.\n" + "- Staraj sie grupowac zapytania: podawaj wszystkie potrzebne UUID w jednym wywolaniu narzedzia.\n" + "- Nie zgaduj detali skladu i zasad bezpieczenstwa; jesli potrzebujesz szczegolow, wywolaj odpowiednie narzedzie.\n" + f"{notes_line}" + f"{_ROUTINES_SINGLE_EXTRA}\n" + "Zwróć JSON zgodny ze schematem." ) + config = get_creative_config( + system_instruction=_ROUTINES_SYSTEM_PROMPT, + response_schema=_SuggestionOut, + max_output_tokens=4096, + ).model_copy( + update={ + "tools": [ + genai_types.Tool( + function_declarations=[ + _INCI_FUNCTION_DECLARATION, + _SAFETY_RULES_FUNCTION_DECLARATION, + _ACTIVES_FUNCTION_DECLARATION, + _USAGE_NOTES_FUNCTION_DECLARATION, + ], + ) + ], + "tool_config": genai_types.ToolConfig( + function_calling_config=genai_types.FunctionCallingConfig( + mode=genai_types.FunctionCallingConfigMode.AUTO, + ) + ), + } + ) + + function_handlers = { + "get_product_inci": _build_inci_tool_handler(available_products), + "get_product_safety_rules": _build_safety_rules_tool_handler( + available_products + ), + "get_product_actives": _build_actives_tool_handler(available_products), + "get_product_usage_notes": _build_usage_notes_tool_handler(available_products), + } + try: - response = client.models.generate_content( - model=model, + response = call_gemini_with_function_tools( + endpoint="routines/suggest", contents=prompt, - config=genai_types.GenerateContentConfig( - response_mime_type="application/json", + config=config, + function_handlers=function_handlers, + user_input=prompt, + max_tool_roundtrips=3, + ) + except HTTPException as exc: + if ( + exc.status_code != 502 + or str(exc.detail) != "Gemini requested too many function calls" + ): + raise + + conservative_prompt = ( + f"{prompt}\n\n" + "TRYB AWARYJNY (KONSERWATYWNY):\n" + "- Osiagnieto limit wywolan narzedzi.\n" + "- Nie wywoluj narzedzi ponownie.\n" + "- Zaproponuj maksymalnie konserwatywna, bezpieczna rutyne na podstawie dostepnych juz danych," + " preferujac lagodne produkty wspierajace bariere i fotoprotekcje.\n" + "- Gdy masz watpliwosci, pomijaj ryzykowne aktywne kroki.\n" + ) + response = call_gemini( + endpoint="routines/suggest", + contents=conservative_prompt, + config=get_creative_config( + system_instruction=_ROUTINES_SYSTEM_PROMPT, response_schema=_SuggestionOut, max_output_tokens=4096, - temperature=0.4, ), + user_input=conservative_prompt, + tool_trace={ + "mode": "fallback_conservative", + "reason": "max_tool_roundtrips_exceeded", + }, ) - except Exception as e: - raise HTTPException(status_code=502, detail=f"Gemini API error: {e}") raw = response.text if not raw: @@ -340,10 +923,34 @@ def suggest_routine( action_notes=s.get("action_notes"), dose=s.get("dose"), region=s.get("region"), + why_this_step=s.get("why_this_step"), + optional=s.get("optional"), ) for s in parsed.get("steps", []) ] - return RoutineSuggestion(steps=steps, reasoning=parsed.get("reasoning", "")) + + summary_raw = parsed.get("summary") or {} + confidence_raw = summary_raw.get("confidence", 0) + try: + confidence = float(confidence_raw) + except (TypeError, ValueError): + confidence = 0.0 + confidence = max(0.0, min(1.0, confidence)) + constraints_applied = summary_raw.get("constraints_applied") or [] + if not isinstance(constraints_applied, list): + constraints_applied = [] + + summary = RoutineSuggestionSummary( + primary_goal=str(summary_raw.get("primary_goal") or ""), + constraints_applied=[str(x) for x in constraints_applied], + confidence=confidence, + ) + + return RoutineSuggestion( + steps=steps, + reasoning=parsed.get("reasoning", ""), + summary=summary, + ) @router.post("/suggest-batch", response_model=BatchSuggestion) @@ -353,17 +960,20 @@ def suggest_batch( ): delta = (data.to_date - data.from_date).days + 1 if delta > 14: - raise HTTPException(status_code=400, detail="Date range must not exceed 14 days.") + raise HTTPException( + status_code=400, detail="Date range must not exceed 14 days." + ) if data.from_date > data.to_date: raise HTTPException(status_code=400, detail="from_date must be <= to_date.") - client, model = get_gemini_client() - - weekdays = list({(data.from_date + timedelta(days=i)).weekday() for i in range(delta)}) + weekdays = list( + {(data.from_date + timedelta(days=i)).weekday() for i in range(delta)} + ) skin_ctx = _build_skin_context(session) grooming_ctx = _build_grooming_context(session, weekdays=weekdays) history_ctx = _build_recent_history(session) - products_ctx = _build_products_context(session) + products_ctx = _build_products_context(session, reference_date=data.from_date) + objectives_ctx = _build_objectives_context(data.include_minoxidil_beard) date_range_lines = [] for i in range(delta): @@ -371,33 +981,35 @@ def suggest_batch( date_range_lines.append(f" {d} ({_DAY_NAMES[d.weekday()]})") dates_str = "\n".join(date_range_lines) - notes_line = f"\nKONTEKST OD UŻYTKOWNIKA: {data.notes}\n" if data.notes else "" + notes_line = f"USER CONTEXT: {data.notes}\n" if data.notes else "" + mode_line = "MODE: travel" if data.minimize_products else "MODE: standard" + minimize_line = ( + "\nCONSTRAINTS (TRAVEL MODE):\n" + "- To tryb podróżny: minimalizuj liczbę unikalnych produktów w całym planie (wszystkie dni, AM+PM).\n" + "- Preferuj reużycie tych samych produktów między dniami, jeśli bezpieczeństwo i główny cel terapeutyczny są zachowane.\n" + "- Dodaj nowy produkt tylko gdy to konieczne dla bezpieczeństwa albo realizacji głównego celu terapeutycznego.\n" + if data.minimize_products + else "" + ) prompt = ( - f"Zaproponuj plan pielęgnacji AM + PM dla każdego dnia z zakresu:\n{dates_str}\n\n" - f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{products_ctx}\n{_RULES}{notes_line}" - "\nDodatkowe zasady dla planu wielodniowego:\n" - " - Retinol/retinoidy: przestrzegaj max_frequency_per_week i min_interval_hours między użyciami\n" - " - Nie stosuj kwasów i retinoidów tego samego dnia\n" - " - Uwzględnij safe_after_shaving dla dni golenia\n" - " - Zmienność aktywnych składników przez dni dla lepszej tolerancji\n" - " - Pole date w każdym dniu MUSI być w formacie YYYY-MM-DD\n" + f"Zaproponuj plan pielęgnacji AM + PM dla każdego dnia z zakresu:\n{dates_str}\n\n{mode_line}\n" + "INPUT DATA:\n" + f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{products_ctx}\n{objectives_ctx}" + f"{notes_line}{minimize_line}" "\nZwróć JSON zgodny ze schematem." ) - try: - response = client.models.generate_content( - model=model, - contents=prompt, - config=genai_types.GenerateContentConfig( - response_mime_type="application/json", - response_schema=_BatchOut, - max_output_tokens=8192, - temperature=0.4, - ), - ) - except Exception as e: - raise HTTPException(status_code=502, detail=f"Gemini API error: {e}") + response = call_gemini( + endpoint="routines/suggest-batch", + contents=prompt, + config=get_creative_config( + system_instruction=_ROUTINES_SYSTEM_PROMPT, + response_schema=_BatchOut, + max_output_tokens=8192, + ), + user_input=prompt, + ) raw = response.text if not raw: @@ -418,6 +1030,8 @@ def suggest_batch( action_notes=s.get("action_notes"), dose=s.get("dose"), region=s.get("region"), + why_this_step=s.get("why_this_step"), + optional=s.get("optional"), ) ) return result @@ -437,7 +1051,9 @@ def suggest_batch( ) ) - return BatchSuggestion(days=days, overall_reasoning=parsed.get("overall_reasoning", "")) + return BatchSuggestion( + days=days, overall_reasoning=parsed.get("overall_reasoning", "") + ) # Grooming-schedule GET must appear before /{routine_id} to avoid being shadowed diff --git a/backend/innercontext/api/skincare.py b/backend/innercontext/api/skincare.py index 57b60b1..8998e50 100644 --- a/backend/innercontext/api/skincare.py +++ b/backend/innercontext/api/skincare.py @@ -11,7 +11,7 @@ from sqlmodel import Session, SQLModel, select from db import get_session from innercontext.api.utils import get_or_404 -from innercontext.llm import get_gemini_client +from innercontext.llm import call_gemini, get_extraction_config from innercontext.models import ( SkinConditionSnapshot, SkinConditionSnapshotBase, @@ -140,37 +140,44 @@ async def analyze_skin_photos( if not (1 <= len(photos) <= 3): raise HTTPException(status_code=422, detail="Send between 1 and 3 photos.") - client, model = get_gemini_client() - - allowed = {"image/jpeg", "image/png", "image/webp"} + allowed = { + "image/heic", + "image/heif", + "image/jpeg", + "image/png", + "image/webp", + } parts: list[genai_types.Part] = [] for photo in photos: if photo.content_type not in allowed: - raise HTTPException(status_code=422, detail=f"Unsupported type: {photo.content_type}") + raise HTTPException( + status_code=422, detail=f"Unsupported type: {photo.content_type}" + ) data = await photo.read() if len(data) > MAX_IMAGE_BYTES: - raise HTTPException(status_code=413, detail=f"{photo.filename} exceeds 5 MB.") - parts.append(genai_types.Part.from_bytes(data=data, mime_type=photo.content_type)) + raise HTTPException( + status_code=413, detail=f"{photo.filename} exceeds 5 MB." + ) + parts.append( + genai_types.Part.from_bytes(data=data, mime_type=photo.content_type) + ) parts.append( genai_types.Part.from_text( text="Analyze the skin condition visible in the above photo(s) and return the JSON assessment." ) ) - try: - response = client.models.generate_content( - model=model, - contents=parts, - config=genai_types.GenerateContentConfig( - system_instruction=_skin_photo_system_prompt(), - response_mime_type="application/json", - response_schema=_SkinAnalysisOut, - max_output_tokens=2048, - temperature=0.0, - ), - ) - except Exception as e: - raise HTTPException(status_code=502, detail=f"Gemini API error: {e}") + image_summary = f"{len(photos)} image(s): {', '.join((p.content_type or 'unknown') for p in photos)}" + response = call_gemini( + endpoint="skincare/analyze-photos", + contents=parts, + config=get_extraction_config( + system_instruction=_skin_photo_system_prompt(), + response_schema=_SkinAnalysisOut, + max_output_tokens=2048, + ), + user_input=image_summary, + ) try: parsed = json.loads(response.text) diff --git a/backend/innercontext/llm.py b/backend/innercontext/llm.py index 428b744..13f0b10 100644 --- a/backend/innercontext/llm.py +++ b/backend/innercontext/llm.py @@ -1,12 +1,53 @@ """Shared helpers for Gemini API access.""" import os +import time +from collections.abc import Callable +from contextlib import suppress +from typing import Any from fastapi import HTTPException from google import genai +from google.genai import types as genai_types + +_DEFAULT_MODEL = "gemini-3-flash-preview" -_DEFAULT_MODEL = "gemini-flash-latest" +def get_extraction_config( + system_instruction: str, + response_schema: Any, + max_output_tokens: int = 8192, +) -> genai_types.GenerateContentConfig: + """Config for strict data extraction (deterministic, minimal thinking).""" + return genai_types.GenerateContentConfig( + system_instruction=system_instruction, + response_mime_type="application/json", + response_schema=response_schema, + max_output_tokens=max_output_tokens, + temperature=0.0, + thinking_config=genai_types.ThinkingConfig( + thinking_level=genai_types.ThinkingLevel.MINIMAL + ), + ) + + +def get_creative_config( + system_instruction: str, + response_schema: Any, + max_output_tokens: int = 4096, +) -> genai_types.GenerateContentConfig: + """Config for creative tasks like recommendations (balanced creativity).""" + return genai_types.GenerateContentConfig( + system_instruction=system_instruction, + response_mime_type="application/json", + response_schema=response_schema, + max_output_tokens=max_output_tokens, + temperature=0.4, + top_p=0.8, + thinking_config=genai_types.ThinkingConfig( + thinking_level=genai_types.ThinkingLevel.LOW + ), + ) def get_gemini_client() -> tuple[genai.Client, str]: @@ -19,3 +60,196 @@ def get_gemini_client() -> tuple[genai.Client, str]: raise HTTPException(status_code=503, detail="GEMINI_API_KEY not configured") model = os.environ.get("GEMINI_MODEL", _DEFAULT_MODEL) return genai.Client(api_key=api_key), model + + +def call_gemini( + *, + endpoint: str, + contents, + config: genai_types.GenerateContentConfig, + user_input: str | None = None, + tool_trace: dict[str, Any] | None = None, +): + """Call Gemini, log full request + response to DB, return response unchanged.""" + from sqlmodel import Session + + from db import engine # deferred to avoid circular import at module load + from innercontext.models.ai_log import AICallLog + + client, model = get_gemini_client() + + sys_prompt = None + if config.system_instruction: + raw = config.system_instruction + sys_prompt = raw if isinstance(raw, str) else str(raw) + if user_input is None: + with suppress(Exception): + user_input = str(contents) + + start = time.monotonic() + success, error_detail, response, finish_reason = True, None, None, None + try: + response = client.models.generate_content( + model=model, contents=contents, config=config + ) + candidates = getattr(response, "candidates", None) + if candidates: + first_candidate = candidates[0] + reason = getattr(first_candidate, "finish_reason", None) + reason_name = getattr(reason, "name", None) + if isinstance(reason_name, str): + finish_reason = reason_name + if finish_reason and finish_reason != "STOP": + success = False + error_detail = f"finish_reason: {finish_reason}" + raise HTTPException( + status_code=502, + detail=f"Gemini stopped early (finish_reason={finish_reason})", + ) + except HTTPException: + raise + except Exception as exc: + success = False + error_detail = str(exc) + raise HTTPException(status_code=502, detail=f"Gemini API error: {exc}") from exc + finally: + duration_ms = int((time.monotonic() - start) * 1000) + with suppress(Exception): + log = AICallLog( + endpoint=endpoint, + model=model, + system_prompt=sys_prompt, + user_input=user_input, + response_text=response.text if response else None, + tool_trace=tool_trace, + prompt_tokens=( + response.usage_metadata.prompt_token_count + if response and response.usage_metadata + else None + ), + completion_tokens=( + response.usage_metadata.candidates_token_count + if response and response.usage_metadata + else None + ), + total_tokens=( + response.usage_metadata.total_token_count + if response and response.usage_metadata + else None + ), + duration_ms=duration_ms, + finish_reason=finish_reason, + success=success, + error_detail=error_detail, + ) + with Session(engine) as s: + s.add(log) + s.commit() + return response + + +def call_gemini_with_function_tools( + *, + endpoint: str, + contents, + config: genai_types.GenerateContentConfig, + function_handlers: dict[str, Callable[[dict[str, Any]], dict[str, Any]]], + user_input: str | None = None, + max_tool_roundtrips: int = 2, +): + """Call Gemini with function-calling loop until final response text is produced.""" + if max_tool_roundtrips < 0: + raise ValueError("max_tool_roundtrips must be >= 0") + + history = list(contents) if isinstance(contents, list) else [contents] + rounds = 0 + trace_events: list[dict[str, Any]] = [] + + while True: + response = call_gemini( + endpoint=endpoint, + contents=history, + config=config, + user_input=user_input, + tool_trace={ + "mode": "function_tools", + "round": rounds, + "events": trace_events, + }, + ) + function_calls = list(getattr(response, "function_calls", None) or []) + if not function_calls: + return response + + if rounds >= max_tool_roundtrips: + raise HTTPException( + status_code=502, + detail="Gemini requested too many function calls", + ) + + candidate_content = None + candidates = getattr(response, "candidates", None) or [] + if candidates: + candidate_content = getattr(candidates[0], "content", None) + if candidate_content is not None: + history.append(candidate_content) + else: + history.append( + genai_types.ModelContent( + parts=[genai_types.Part(function_call=fc) for fc in function_calls] + ) + ) + + response_parts: list[genai_types.Part] = [] + for fc in function_calls: + name = getattr(fc, "name", None) + if not isinstance(name, str) or not name: + raise HTTPException( + status_code=502, + detail="Gemini requested a function without a valid name", + ) + + handler = function_handlers.get(name) + if handler is None: + raise HTTPException( + status_code=502, + detail=f"Gemini requested unknown function: {name}", + ) + + args = getattr(fc, "args", None) or {} + if not isinstance(args, dict): + raise HTTPException( + status_code=502, + detail=f"Gemini returned invalid arguments for function: {name}", + ) + + tool_response = handler(args) + if not isinstance(tool_response, dict): + raise HTTPException( + status_code=502, + detail=f"Function handler must return an object for: {name}", + ) + + trace_event: dict[str, Any] = { + "round": rounds + 1, + "function": name, + } + product_ids = args.get("product_ids") + if isinstance(product_ids, list): + clean_ids = [x for x in product_ids if isinstance(x, str)] + trace_event["requested_ids_count"] = len(clean_ids) + trace_event["requested_ids"] = clean_ids[:8] + products = tool_response.get("products") + if isinstance(products, list): + trace_event["returned_products_count"] = len(products) + trace_events.append(trace_event) + + response_parts.append( + genai_types.Part.from_function_response( + name=name, + response=tool_response, + ) + ) + + history.append(genai_types.UserContent(parts=response_parts)) + rounds += 1 diff --git a/backend/innercontext/mcp_server.py b/backend/innercontext/mcp_server.py deleted file mode 100644 index 618f267..0000000 --- a/backend/innercontext/mcp_server.py +++ /dev/null @@ -1,438 +0,0 @@ -from __future__ import annotations - -from datetime import date, timedelta -from typing import Optional -from uuid import UUID - -from fastmcp import FastMCP -from sqlmodel import Session, col, select - -from db import engine -from innercontext.models import ( - GroomingSchedule, - LabResult, - MedicationEntry, - MedicationUsage, - Product, - ProductInventory, - Routine, - RoutineStep, - SkinConditionSnapshot, -) - -mcp = FastMCP("innercontext") - - -# ── Products ────────────────────────────────────────────────────────────────── - - -@mcp.tool() -def get_products( - category: Optional[str] = None, - is_medication: bool = False, - is_tool: bool = False, -) -> list[dict]: - """List products. By default returns skincare products (excludes medications and tools). - Pass is_medication=True or is_tool=True to retrieve those categories instead.""" - with Session(engine) as session: - stmt = select(Product) - if category is not None: - stmt = stmt.where(Product.category == category) - stmt = stmt.where(Product.is_medication == is_medication) - stmt = stmt.where(Product.is_tool == is_tool) - products = session.exec(stmt).all() - return [p.to_llm_context() for p in products] - - -@mcp.tool() -def get_product(product_id: str) -> dict: - """Get full context for a single product (UUID) including all inventory entries.""" - with Session(engine) as session: - product = session.get(Product, UUID(product_id)) - if product is None: - return {"error": f"Product {product_id} not found"} - ctx = product.to_llm_context() - entries = session.exec( - select(ProductInventory).where(ProductInventory.product_id == product.id) - ).all() - ctx["inventory"] = [ - { - "id": str(inv.id), - "is_opened": inv.is_opened, - "opened_at": inv.opened_at.isoformat() if inv.opened_at else None, - "finished_at": inv.finished_at.isoformat() if inv.finished_at else None, - "expiry_date": inv.expiry_date.isoformat() if inv.expiry_date else None, - "current_weight_g": inv.current_weight_g, - } - for inv in entries - ] - return ctx - - -# ── Inventory ───────────────────────────────────────────────────────────────── - - -@mcp.tool() -def get_open_inventory() -> list[dict]: - """Return all currently open packages (is_opened=True, finished_at=None) - with product name, opening date, weight, and expiry date.""" - with Session(engine) as session: - stmt = ( - select(ProductInventory, Product) - .join(Product, ProductInventory.product_id == Product.id) - .where(ProductInventory.is_opened == True) # noqa: E712 - .where(ProductInventory.finished_at == None) # noqa: E711 - ) - rows = session.exec(stmt).all() - return [ - { - "inventory_id": str(inv.id), - "product_id": str(product.id), - "product_name": product.name, - "brand": product.brand, - "opened_at": inv.opened_at.isoformat() if inv.opened_at else None, - "current_weight_g": inv.current_weight_g, - "expiry_date": inv.expiry_date.isoformat() if inv.expiry_date else None, - } - for inv, product in rows - ] - - -# ── Routines ────────────────────────────────────────────────────────────────── - - -@mcp.tool() -def get_recent_routines(days: int = 14) -> list[dict]: - """Get skincare routines from the last N days, newest first. - Each routine includes its ordered steps with product name or action.""" - with Session(engine) as session: - cutoff = date.today() - timedelta(days=days) - routines = session.exec( - select(Routine) - .where(Routine.routine_date >= cutoff) - .order_by(col(Routine.routine_date).desc()) - ).all() - - result = [] - for routine in routines: - steps = session.exec( - select(RoutineStep) - .where(RoutineStep.routine_id == routine.id) - .order_by(RoutineStep.order_index) - ).all() - - steps_data = [] - for step in steps: - step_dict: dict = {"order": step.order_index} - if step.product_id: - product = session.get(Product, step.product_id) - if product: - step_dict["product"] = product.name - step_dict["product_id"] = str(product.id) - if step.action_type: - step_dict["action"] = ( - step.action_type.value - if hasattr(step.action_type, "value") - else str(step.action_type) - ) - if step.action_notes: - step_dict["notes"] = step.action_notes - if step.dose: - step_dict["dose"] = step.dose - if step.region: - step_dict["region"] = step.region - steps_data.append(step_dict) - - result.append( - { - "id": str(routine.id), - "date": routine.routine_date.isoformat(), - "part_of_day": ( - routine.part_of_day.value - if hasattr(routine.part_of_day, "value") - else str(routine.part_of_day) - ), - "notes": routine.notes, - "steps": steps_data, - } - ) - - return result - - -# ── Skin snapshots ──────────────────────────────────────────────────────────── - - -def _snapshot_to_dict(s: SkinConditionSnapshot, *, full: bool) -> dict: - def ev(v: object) -> object: - return v.value if v is not None and hasattr(v, "value") else v - - d: dict = { - "id": str(s.id), - "date": s.snapshot_date.isoformat(), - "overall_state": ev(s.overall_state), - "hydration_level": s.hydration_level, - "sensitivity_level": s.sensitivity_level, - "barrier_state": ev(s.barrier_state), - "active_concerns": [ev(c) for c in (s.active_concerns or [])], - } - if full: - d.update( - { - "skin_type": ev(s.skin_type), - "texture": ev(s.texture), - "sebum_tzone": s.sebum_tzone, - "sebum_cheeks": s.sebum_cheeks, - "risks": s.risks or [], - "priorities": s.priorities or [], - "notes": s.notes, - } - ) - return d - - -@mcp.tool() -def get_latest_skin_snapshot() -> dict | None: - """Get the most recent skin condition snapshot with all metrics.""" - with Session(engine) as session: - snapshot = session.exec( - select(SkinConditionSnapshot).order_by( - col(SkinConditionSnapshot.snapshot_date).desc() - ) - ).first() - if snapshot is None: - return None - return _snapshot_to_dict(snapshot, full=True) - - -@mcp.tool() -def get_skin_history(weeks: int = 8) -> list[dict]: - """Get skin condition snapshots from the last N weeks with key metrics.""" - with Session(engine) as session: - cutoff = date.today() - timedelta(weeks=weeks) - snapshots = session.exec( - select(SkinConditionSnapshot) - .where(SkinConditionSnapshot.snapshot_date >= cutoff) - .order_by(col(SkinConditionSnapshot.snapshot_date).desc()) - ).all() - return [_snapshot_to_dict(s, full=False) for s in snapshots] - - -@mcp.tool() -def get_skin_snapshot_dates() -> list[str]: - """List all dates (YYYY-MM-DD) for which skin snapshots exist, newest first.""" - with Session(engine) as session: - snapshots = session.exec( - select(SkinConditionSnapshot).order_by( - col(SkinConditionSnapshot.snapshot_date).desc() - ) - ).all() - return [s.snapshot_date.isoformat() for s in snapshots] - - -@mcp.tool() -def get_skin_snapshot(snapshot_date: str) -> dict | None: - """Get the full skin condition snapshot for a specific date (YYYY-MM-DD).""" - with Session(engine) as session: - target = date.fromisoformat(snapshot_date) - snapshot = session.exec( - select(SkinConditionSnapshot).where( - SkinConditionSnapshot.snapshot_date == target - ) - ).first() - if snapshot is None: - return None - return _snapshot_to_dict(snapshot, full=True) - - -# ── Health / medications ─────────────────────────────────────────────────────── - - -@mcp.tool() -def get_medications() -> list[dict]: - """Get all medication entries with their currently active usage records - (valid_to IS NULL or >= today).""" - with Session(engine) as session: - medications = session.exec(select(MedicationEntry)).all() - today = date.today() - result = [] - for med in medications: - usages = session.exec( - select(MedicationUsage) - .where(MedicationUsage.medication_record_id == med.record_id) - .where( - (MedicationUsage.valid_to == None) # noqa: E711 - | (col(MedicationUsage.valid_to) >= today) - ) - ).all() - result.append( - { - "id": str(med.record_id), - "product_name": med.product_name, - "kind": ( - med.kind.value if hasattr(med.kind, "value") else str(med.kind) - ), - "active_substance": med.active_substance, - "formulation": med.formulation, - "route": med.route, - "notes": med.notes, - "active_usages": [ - { - "id": str(u.record_id), - "dose": ( - f"{u.dose_value} {u.dose_unit}" - if u.dose_value is not None and u.dose_unit - else None - ), - "frequency": u.frequency, - "schedule_text": u.schedule_text, - "as_needed": u.as_needed, - "valid_from": u.valid_from.isoformat() - if u.valid_from - else None, - "valid_to": u.valid_to.isoformat() if u.valid_to else None, - } - for u in usages - ], - } - ) - return result - - -# ── Expiring inventory ──────────────────────────────────────────────────────── - - -@mcp.tool() -def get_expiring_inventory(days: int = 30) -> list[dict]: - """List open packages whose expiry date falls within the next N days. - Sorted by days remaining (soonest first).""" - with Session(engine) as session: - cutoff = date.today() + timedelta(days=days) - stmt = ( - select(ProductInventory, Product) - .join(Product, ProductInventory.product_id == Product.id) - .where(ProductInventory.is_opened == True) # noqa: E712 - .where(ProductInventory.finished_at == None) # noqa: E711 - .where(ProductInventory.expiry_date != None) # noqa: E711 - .where(col(ProductInventory.expiry_date) <= cutoff) - ) - rows = session.exec(stmt).all() - today = date.today() - result = [ - { - "product_name": product.name, - "brand": product.brand, - "expiry_date": inv.expiry_date.isoformat() if inv.expiry_date else None, - "days_remaining": (inv.expiry_date - today).days - if inv.expiry_date - else None, - "current_weight_g": inv.current_weight_g, - } - for inv, product in rows - ] - return sorted(result, key=lambda x: x["days_remaining"] or 0) - - -# ── Grooming schedule ───────────────────────────────────────────────────────── - - -@mcp.tool() -def get_grooming_schedule() -> list[dict]: - """Get the full grooming schedule sorted by day of week (0=Monday, 6=Sunday).""" - with Session(engine) as session: - entries = session.exec( - select(GroomingSchedule).order_by(GroomingSchedule.day_of_week) - ).all() - return [ - { - "id": str(e.id), - "day_of_week": e.day_of_week, - "action": ( - e.action.value if hasattr(e.action, "value") else str(e.action) - ), - "notes": e.notes, - } - for e in entries - ] - - -# ── Lab results ─────────────────────────────────────────────────────────────── - - -def _lab_result_to_dict(r: LabResult) -> dict: - return { - "id": str(r.record_id), - "collected_at": r.collected_at.isoformat(), - "test_code": r.test_code, - "test_name_loinc": r.test_name_loinc, - "test_name_original": r.test_name_original, - "value_num": r.value_num, - "value_text": r.value_text, - "value_bool": r.value_bool, - "unit": r.unit_ucum or r.unit_original, - "ref_low": r.ref_low, - "ref_high": r.ref_high, - "ref_text": r.ref_text, - "flag": r.flag.value if r.flag and hasattr(r.flag, "value") else r.flag, - "lab": r.lab, - "notes": r.notes, - } - - -@mcp.tool() -def get_recent_lab_results(limit: int = 30) -> list[dict]: - """Get the most recent lab results sorted by collection date descending.""" - with Session(engine) as session: - results = session.exec( - select(LabResult) - .order_by(col(LabResult.collected_at).desc()) - .limit(limit) - ).all() - return [_lab_result_to_dict(r) for r in results] - - -@mcp.tool() -def get_available_lab_tests() -> list[dict]: - """List all distinct lab tests ever performed, grouped by LOINC test_code. - Returns test_code, LOINC name, original lab names, result count, and last collection date.""" - with Session(engine) as session: - results = session.exec(select(LabResult)).all() - tests: dict[str, dict] = {} - for r in results: - code = r.test_code - if code not in tests: - tests[code] = { - "test_code": code, - "test_name_loinc": r.test_name_loinc, - "test_names_original": set(), - "count": 0, - "last_collected_at": r.collected_at, - } - tests[code]["count"] += 1 - if r.test_name_original: - tests[code]["test_names_original"].add(r.test_name_original) - if r.collected_at > tests[code]["last_collected_at"]: - tests[code]["last_collected_at"] = r.collected_at - - return [ - { - "test_code": v["test_code"], - "test_name_loinc": v["test_name_loinc"], - "test_names_original": sorted(v["test_names_original"]), - "count": v["count"], - "last_collected_at": v["last_collected_at"].isoformat(), - } - for v in sorted(tests.values(), key=lambda x: x["test_code"]) - ] - - -@mcp.tool() -def get_lab_results_for_test(test_code: str) -> list[dict]: - """Get the full chronological history of results for a specific LOINC test code.""" - with Session(engine) as session: - results = session.exec( - select(LabResult) - .where(LabResult.test_code == test_code) - .order_by(col(LabResult.collected_at).asc()) - ).all() - return [_lab_result_to_dict(r) for r in results] diff --git a/backend/innercontext/models/__init__.py b/backend/innercontext/models/__init__.py index 1ffe287..5fa50b0 100644 --- a/backend/innercontext/models/__init__.py +++ b/backend/innercontext/models/__init__.py @@ -1,3 +1,4 @@ +from .ai_log import AICallLog from .domain import Domain from .enums import ( AbsorptionSpeed, @@ -41,6 +42,8 @@ from .skincare import ( ) __all__ = [ + # ai logs + "AICallLog", # domain "Domain", # enums diff --git a/backend/innercontext/models/ai_log.py b/backend/innercontext/models/ai_log.py new file mode 100644 index 0000000..dbd2d5e --- /dev/null +++ b/backend/innercontext/models/ai_log.py @@ -0,0 +1,33 @@ +from datetime import datetime +from typing import Any, ClassVar +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel + +from .base import utc_now +from .domain import Domain + + +class AICallLog(SQLModel, table=True): + __tablename__ = "ai_call_logs" + __domains__: ClassVar[frozenset[Domain]] = frozenset() + + id: UUID = Field(default_factory=uuid4, primary_key=True) + created_at: datetime = Field(default_factory=utc_now, nullable=False) + endpoint: str = Field(index=True) + model: str + system_prompt: str | None = Field(default=None) + user_input: str | None = Field(default=None) + response_text: str | None = Field(default=None) + prompt_tokens: int | None = Field(default=None) + completion_tokens: int | None = Field(default=None) + total_tokens: int | None = Field(default=None) + duration_ms: int | None = Field(default=None) + finish_reason: str | None = Field(default=None) + tool_trace: dict[str, Any] | None = Field( + default=None, + sa_column=Column(JSON, nullable=True), + ) + success: bool = Field(default=True, index=True) + error_detail: str | None = Field(default=None) diff --git a/backend/main.py b/backend/main.py index 93c035a..d2a529d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,5 @@ from contextlib import asynccontextmanager +from typing import AsyncIterator from dotenv import load_dotenv @@ -6,30 +7,27 @@ load_dotenv() # load .env before db.py reads DATABASE_URL from fastapi import FastAPI # noqa: E402 from fastapi.middleware.cors import CORSMiddleware # noqa: E402 -from fastmcp.utilities.lifespan import combine_lifespans # noqa: E402 from db import create_db_and_tables # noqa: E402 from innercontext.api import ( # noqa: E402 + ai_logs, health, inventory, products, routines, skincare, ) -from innercontext.mcp_server import mcp # noqa: E402 - -mcp_app = mcp.http_app(path="/mcp") @asynccontextmanager -async def lifespan(app: FastAPI): +async def lifespan(app: FastAPI) -> AsyncIterator[None]: create_db_and_tables() yield app = FastAPI( title="innercontext API", - lifespan=combine_lifespans(lifespan, mcp_app.lifespan), + lifespan=lifespan, redirect_slashes=False, ) @@ -45,9 +43,7 @@ app.include_router(inventory.router, prefix="/inventory", tags=["inventory"]) app.include_router(health.router, prefix="/health", tags=["health"]) app.include_router(routines.router, prefix="/routines", tags=["routines"]) app.include_router(skincare.router, prefix="/skincare", tags=["skincare"]) - - -app.mount("/mcp", mcp_app) +app.include_router(ai_logs.router, prefix="/ai-logs", tags=["ai-logs"]) @app.get("/health-check") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d7c215c..49f1708 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -7,7 +7,6 @@ requires-python = ">=3.12" dependencies = [ "alembic>=1.14", "fastapi>=0.132.0", - "fastmcp>=2.0", "google-genai>=1.65.0", "psycopg>=3.3.3", "python-dotenv>=1.2.1", @@ -22,6 +21,7 @@ dev = [ "httpx>=0.28.1", "isort>=8.0.0", "pytest>=9.0.2", + "pytest-cov>=6.0.0", "ruff>=0.15.2", "ty>=0.0.18", ] @@ -29,7 +29,7 @@ dev = [ [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] -addopts = "-v --tb=short" +addopts = "-v --tb=short --cov=innercontext --cov-report=term-missing --cov-report=html" [tool.isort] profile = "black" diff --git a/backend/test_query.py b/backend/test_query.py new file mode 100644 index 0000000..46b5f20 --- /dev/null +++ b/backend/test_query.py @@ -0,0 +1,25 @@ +from datetime import date, timedelta + +from sqlmodel import select + +from db import get_session +from innercontext.models import Routine, RoutineStep + + +def run(): + session = next(get_session()) + ref_date = date.today() + cutoff = ref_date - timedelta(days=7) + + recent_usage = session.exec( + select(RoutineStep.product_id) + .join(Routine, Routine.id == RoutineStep.routine_id) + .where(Routine.routine_date >= cutoff) + .where(Routine.routine_date <= ref_date) + ).all() + + print("Found:", len(recent_usage)) + + +if __name__ == "__main__": + run() diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 2cc5b80..3c4f465 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,5 +1,4 @@ import os -from contextlib import asynccontextmanager # Must be set before importing db (which calls create_engine at module level) os.environ.setdefault("DATABASE_URL", "sqlite://") @@ -14,18 +13,6 @@ from db import get_session from main import app -@asynccontextmanager -async def _db_only_lifespan(a): - """Lifespan without the MCP server for test isolation. - - StreamableHTTPSessionManager.run() can only be called once per instance, - which conflicts with the per-test TestClient lifecycle. We replace the - combined (db + MCP) lifespan with one that only does DB setup. - """ - db_module.create_db_and_tables() - yield - - @pytest.fixture() def session(monkeypatch): """Per-test fresh SQLite in-memory database with full isolation.""" @@ -47,9 +34,6 @@ def session(monkeypatch): @pytest.fixture() def client(session, monkeypatch): """TestClient using the per-test session for every request.""" - # Replace combined (db+MCP) lifespan with DB-only to avoid the - # StreamableHTTPSessionManager single-run limitation. - monkeypatch.setattr(app.router, "lifespan_context", _db_only_lifespan) def _override(): yield session diff --git a/backend/tests/test_ai_logs.py b/backend/tests/test_ai_logs.py new file mode 100644 index 0000000..47a168e --- /dev/null +++ b/backend/tests/test_ai_logs.py @@ -0,0 +1,44 @@ +import uuid +from typing import Any, cast + +from innercontext.models.ai_log import AICallLog + + +def test_list_ai_logs_normalizes_tool_trace_string(client, session): + log = AICallLog( + id=uuid.uuid4(), + endpoint="routines/suggest", + model="gemini-3-flash-preview", + success=True, + ) + log.tool_trace = cast( + Any, + '{"mode":"function_tools","events":[{"function":"get_product_inci"}]}', + ) + session.add(log) + session.commit() + + response = client.get("/ai-logs") + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]["tool_trace"]["mode"] == "function_tools" + assert data[0]["tool_trace"]["events"][0]["function"] == "get_product_inci" + + +def test_get_ai_log_normalizes_tool_trace_string(client, session): + log = AICallLog( + id=uuid.uuid4(), + endpoint="routines/suggest", + model="gemini-3-flash-preview", + success=True, + ) + log.tool_trace = cast(Any, '{"mode":"function_tools","round":1}') + session.add(log) + session.commit() + + response = client.get(f"/ai-logs/{log.id}") + assert response.status_code == 200 + payload = response.json() + assert payload["tool_trace"]["mode"] == "function_tools" + assert payload["tool_trace"]["round"] == 1 diff --git a/backend/tests/test_products.py b/backend/tests/test_products.py index 1fae538..956cc75 100644 --- a/backend/tests/test_products.py +++ b/backend/tests/test_products.py @@ -199,3 +199,22 @@ def test_create_inventory(client, created_product): def test_create_inventory_product_not_found(client): r = client.post(f"/products/{uuid.uuid4()}/inventory", json={}) assert r.status_code == 404 + + +def test_parse_text_accepts_numeric_strength_levels(client, monkeypatch): + from innercontext.api import products as products_api + + class _FakeResponse: + text = ( + '{"name":"Test Serum","actives":[{"name":"Niacinamide","percent":10,' + '"functions":["niacinamide"],"strength_level":2,"irritation_potential":1}]}' + ) + + monkeypatch.setattr(products_api, "call_gemini", lambda **kwargs: _FakeResponse()) + + r = client.post("/products/parse-text", json={"text": "dummy input"}) + assert r.status_code == 200 + data = r.json() + assert data["name"] == "Test Serum" + assert data["actives"][0]["strength_level"] == 2 + assert data["actives"][0]["irritation_potential"] == 1 diff --git a/backend/tests/test_products_helpers.py b/backend/tests/test_products_helpers.py new file mode 100644 index 0000000..805f35f --- /dev/null +++ b/backend/tests/test_products_helpers.py @@ -0,0 +1,157 @@ +import uuid +from datetime import date +from unittest.mock import patch + +from sqlmodel import Session + +from innercontext.api.products import ( + _build_actives_tool_handler, + _build_inci_tool_handler, + _build_safety_rules_tool_handler, + _build_shopping_context, + _build_usage_notes_tool_handler, + _extract_requested_product_ids, +) +from innercontext.models import Product, ProductInventory, SkinConditionSnapshot + + +def test_build_shopping_context(session: Session): + # Empty context + ctx = _build_shopping_context(session) + assert "(brak danych)" in ctx + assert "POSIADANE PRODUKTY" in ctx + + # Add snapshot + snap = SkinConditionSnapshot( + id=uuid.uuid4(), + snapshot_date=date.today(), + overall_state="fair", + skin_type="combination", + hydration_level=3, + sensitivity_level=4, + barrier_state="mildly_compromised", + active_concerns=["redness"], + priorities=["soothing"], + ) + session.add(snap) + + # Add product + p = Product( + id=uuid.uuid4(), + name="Soothing Serum", + brand="BrandX", + category="serum", + recommended_time="both", + leave_on=True, + targets=["redness"], + product_effect_profile={"soothing_strength": 4, "hydration_immediate": 1}, + actives=[{"name": "Centella"}], + ) + session.add(p) + session.commit() + + # Add inventory + inv = ProductInventory(id=uuid.uuid4(), product_id=p.id, is_opened=True) + session.add(inv) + session.commit() + + ctx = _build_shopping_context(session) + assert "Typ skóry: combination" in ctx + assert "Nawilżenie: 3/5" in ctx + assert "Wrażliwość: 4/5" in ctx + assert "Aktywne problemy: redness" in ctx + assert "Priorytety: soothing" in ctx + + # Check product + assert "[✓] id=" in ctx + assert "Soothing Serum" in ctx + assert f"id={p.id}" in ctx + assert "BrandX" in ctx + assert "targets: ['redness']" in ctx + assert "actives: ['Centella']" in ctx + assert "effects: {'soothing': 4}" in ctx + + +def test_suggest_shopping(client, session): + with patch( + "innercontext.api.products.call_gemini_with_function_tools" + ) as mock_gemini: + mock_response = type( + "Response", + (), + { + "text": '{"suggestions": [{"category": "cleanser", "product_type": "cleanser", "priority": "high", "key_ingredients": [], "target_concerns": [], "why_needed": "reason", "recommended_time": "am", "frequency": "daily"}], "reasoning": "Test shopping"}' + }, + ) + mock_gemini.return_value = mock_response + + r = client.post("/products/suggest") + assert r.status_code == 200 + data = r.json() + assert len(data["suggestions"]) == 1 + assert data["suggestions"][0]["product_type"] == "cleanser" + assert data["reasoning"] == "Test shopping" + kwargs = mock_gemini.call_args.kwargs + assert "function_handlers" in kwargs + assert "get_product_inci" in kwargs["function_handlers"] + assert "get_product_safety_rules" in kwargs["function_handlers"] + assert "get_product_actives" in kwargs["function_handlers"] + assert "get_product_usage_notes" in kwargs["function_handlers"] + + +def test_shopping_context_medication_skip(session: Session): + p = Product( + id=uuid.uuid4(), + name="Epiduo", + brand="Galderma", + category="serum", + recommended_time="pm", + leave_on=True, + is_medication=True, + product_effect_profile={}, + ) + session.add(p) + session.commit() + + ctx = _build_shopping_context(session) + assert "Epiduo" not in ctx + + +def test_extract_requested_product_ids_dedupes_and_limits(): + ids = _extract_requested_product_ids( + {"product_ids": ["a", "b", "a", 1, "c", "d"]}, + max_ids=3, + ) + assert ids == ["a", "b", "c"] + + +def test_shopping_tool_handlers_return_payloads(session: Session): + product = Product( + id=uuid.uuid4(), + name="Test Product", + brand="Brand", + category="serum", + recommended_time="both", + leave_on=True, + usage_notes="Use AM and PM on clean skin.", + inci=["Water", "Niacinamide"], + actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}], + incompatible_with=[{"target": "Vitamin C", "scope": "same_step"}], + context_rules={"safe_after_shaving": True}, + product_effect_profile={}, + ) + + payload = {"product_ids": [str(product.id)]} + + inci_data = _build_inci_tool_handler([product])(payload) + assert inci_data["products"][0]["inci"] == ["Water", "Niacinamide"] + + actives_data = _build_actives_tool_handler([product])(payload) + assert actives_data["products"][0]["actives"][0]["name"] == "Niacinamide" + + notes_data = _build_usage_notes_tool_handler([product])(payload) + assert notes_data["products"][0]["usage_notes"] == "Use AM and PM on clean skin." + + safety_data = _build_safety_rules_tool_handler([product])(payload) + assert "incompatible_with" in safety_data["products"][0] + assert "context_rules" in safety_data["products"][0] diff --git a/backend/tests/test_routines.py b/backend/tests/test_routines.py index b240079..8f04045 100644 --- a/backend/tests/test_routines.py +++ b/backend/tests/test_routines.py @@ -1,4 +1,5 @@ import uuid +from unittest.mock import patch # --------------------------------------------------------------------------- # Routines @@ -216,3 +217,82 @@ def test_delete_grooming_schedule(client): def test_delete_grooming_schedule_not_found(client): r = client.delete(f"/routines/grooming-schedule/{uuid.uuid4()}") assert r.status_code == 404 + + +def test_suggest_routine(client, session): + with patch( + "innercontext.api.routines.call_gemini_with_function_tools" + ) as mock_gemini: + # Mock the Gemini response + mock_response = type( + "Response", + (), + { + "text": '{"steps": [{"product_id": null, "action_type": "shaving_razor"}], "reasoning": "because"}' + }, + ) + mock_gemini.return_value = mock_response + + r = client.post( + "/routines/suggest", + json={ + "routine_date": "2026-03-03", + "part_of_day": "am", + "notes": "Testing", + "include_minoxidil_beard": True, + }, + ) + assert r.status_code == 200 + data = r.json() + assert len(data["steps"]) == 1 + assert data["steps"][0]["action_type"] == "shaving_razor" + assert data["reasoning"] == "because" + kwargs = mock_gemini.call_args.kwargs + assert "function_handlers" in kwargs + assert "get_product_inci" in kwargs["function_handlers"] + assert "get_product_safety_rules" in kwargs["function_handlers"] + assert "get_product_actives" in kwargs["function_handlers"] + assert "get_product_usage_notes" in kwargs["function_handlers"] + + +def test_suggest_batch(client, session): + with patch("innercontext.api.routines.call_gemini") as mock_gemini: + # Mock the Gemini response + mock_response = type( + "Response", + (), + { + "text": '{"days": [{"date": "2026-03-03", "am_steps": [], "pm_steps": [], "reasoning": "none"}], "overall_reasoning": "batch test"}' + }, + ) + mock_gemini.return_value = mock_response + + r = client.post( + "/routines/suggest-batch", + json={ + "from_date": "2026-03-03", + "to_date": "2026-03-04", + "minimize_products": True, + }, + ) + assert r.status_code == 200 + data = r.json() + assert len(data["days"]) == 1 + assert data["days"][0]["date"] == "2026-03-03" + assert data["overall_reasoning"] == "batch test" + + +def test_suggest_batch_invalid_date_range(client): + r = client.post( + "/routines/suggest-batch", + json={"from_date": "2026-03-04", "to_date": "2026-03-03"}, + ) + assert r.status_code == 400 + + +def test_suggest_batch_too_long(client): + r = client.post( + "/routines/suggest-batch", + json={"from_date": "2026-03-01", "to_date": "2026-03-20"}, + ) + assert r.status_code == 400 diff --git a/backend/tests/test_routines_helpers.py b/backend/tests/test_routines_helpers.py new file mode 100644 index 0000000..5d78872 --- /dev/null +++ b/backend/tests/test_routines_helpers.py @@ -0,0 +1,408 @@ +import uuid +from datetime import date, timedelta + +from sqlmodel import Session + +from innercontext.api.routines import ( + _build_actives_tool_handler, + _build_day_context, + _build_grooming_context, + _build_inci_tool_handler, + _build_objectives_context, + _build_products_context, + _build_recent_history, + _build_safety_rules_tool_handler, + _build_skin_context, + _build_usage_notes_tool_handler, + _contains_minoxidil_text, + _ev, + _extract_active_names, + _extract_requested_product_ids, + _get_available_products, + _is_minoxidil_product, +) +from innercontext.models import ( + GroomingSchedule, + Product, + ProductInventory, + Routine, + RoutineStep, + SkinConditionSnapshot, +) + + +def test_contains_minoxidil_text(): + assert _contains_minoxidil_text(None) is False + assert _contains_minoxidil_text("") is False + assert _contains_minoxidil_text("some random text") is False + assert _contains_minoxidil_text("contains MINOXIDIL here") is True + assert _contains_minoxidil_text("minoksydyl 5%") is True + + +def test_is_minoxidil_product(): + # Setup product + p = Product(id=uuid.uuid4(), name="Test", brand="Brand", is_medication=True) + assert _is_minoxidil_product(p) is False + + p.name = "Minoxidil 5%" + assert _is_minoxidil_product(p) is True + + p.name = "Test" + p.brand = "Brand with minoksydyl" + assert _is_minoxidil_product(p) is True + + p.brand = "Brand" + p.line_name = "Minoxidil Line" + assert _is_minoxidil_product(p) is True + + p.line_name = None + p.usage_notes = "Use minoxidil daily" + assert _is_minoxidil_product(p) is True + + p.usage_notes = None + p.inci = ["water", "minoxidil"] + assert _is_minoxidil_product(p) is True + + p.inci = None + p.actives = [{"name": "minoxidil", "strength": "5%"}] + assert _is_minoxidil_product(p) is True + + # As Pydantic model representation isn't exactly a dict in db sometimes, we just test dict + p.actives = [{"name": "Retinol", "strength": "1%"}] + assert _is_minoxidil_product(p) is False + + +def test_ev(): + class DummyEnum: + value = "dummy" + + assert _ev(None) == "" + assert _ev(DummyEnum()) == "dummy" + assert _ev("string") == "string" + + +def test_build_skin_context(session: Session): + # Empty + assert _build_skin_context(session) == "SKIN CONDITION: no data\n" + + # With data + snap = SkinConditionSnapshot( + id=uuid.uuid4(), + snapshot_date=date.today(), + overall_state="good", + hydration_level=4, + barrier_state="intact", + active_concerns=["acne", "dryness"], + priorities=["hydration"], + notes="Feeling good", + ) + session.add(snap) + session.commit() + + ctx = _build_skin_context(session) + assert "SKIN CONDITION (snapshot from" in ctx + assert "Overall state: good" in ctx + assert "Hydration: 4/5" in ctx + assert "Barrier: intact" in ctx + assert "Active concerns: acne, dryness" in ctx + assert "Priorities: hydration" in ctx + assert "Notes: Feeling good" in ctx + + +def test_build_grooming_context(session: Session): + assert _build_grooming_context(session) == "GROOMING SCHEDULE: none\n" + + sch = GroomingSchedule( + id=uuid.uuid4(), day_of_week=0, action="shaving_oneblade", notes="Morning" + ) + session.add(sch) + session.commit() + + ctx = _build_grooming_context(session) + assert "GROOMING SCHEDULE:" in ctx + assert "poniedziałek: shaving_oneblade (Morning)" in ctx + + # Test weekdays filter + ctx2 = _build_grooming_context(session, weekdays=[1]) # not monday + assert "(no entries for specified days)" in ctx2 + + +def test_build_recent_history(session: Session): + assert _build_recent_history(session) == "RECENT ROUTINES: none\n" + + r = Routine(id=uuid.uuid4(), routine_date=date.today(), part_of_day="am") + session.add(r) + p = Product( + id=uuid.uuid4(), + name="Cleanser", + category="cleanser", + brand="Test", + recommended_time="both", + leave_on=False, + product_effect_profile={}, + ) + session.add(p) + session.commit() + + s1 = RoutineStep(id=uuid.uuid4(), routine_id=r.id, order_index=1, product_id=p.id) + s2 = RoutineStep( + id=uuid.uuid4(), routine_id=r.id, order_index=2, action_type="shaving_razor" + ) + # Step with non-existent product + s3 = RoutineStep( + id=uuid.uuid4(), routine_id=r.id, order_index=3, product_id=uuid.uuid4() + ) + + session.add_all([s1, s2, s3]) + session.commit() + + ctx = _build_recent_history(session) + assert "RECENT ROUTINES:" in ctx + assert "AM:" in ctx + assert "cleanser [" in ctx + assert "action: shaving_razor" in ctx + assert "unknown [" in ctx + + +def test_build_products_context(session: Session): + p1 = Product( + id=uuid.uuid4(), + name="Regaine", + category="serum", + is_medication=True, + brand="J&J", + recommended_time="both", + leave_on=True, + product_effect_profile={}, + ) + p2 = Product( + id=uuid.uuid4(), + name="Sunscreen", + category="spf", + brand="Test", + leave_on=True, + recommended_time="am", + pao_months=6, + product_effect_profile={"hydration_immediate": 2, "exfoliation_strength": 0}, + incompatible_with=[{"target": "retinol", "scope": "same_routine"}], + context_rules={"safe_after_shaving": False}, + min_interval_hours=12, + max_frequency_per_week=7, + ) + session.add_all([p1, p2]) + session.commit() + + # Inventory + inv1 = ProductInventory( + id=uuid.uuid4(), + product_id=p2.id, + is_opened=True, + opened_at=date.today() - timedelta(days=10), + expiry_date=date.today() + timedelta(days=365), + ) + inv2 = ProductInventory(id=uuid.uuid4(), product_id=p2.id, is_opened=False) + session.add_all([inv1, inv2]) + session.commit() + + # Usage + r = Routine(id=uuid.uuid4(), routine_date=date.today(), part_of_day="am") + session.add(r) + session.commit() + s = RoutineStep(id=uuid.uuid4(), routine_id=r.id, order_index=1, product_id=p2.id) + session.add(s) + session.commit() + + ctx = _build_products_context( + session, time_filter="am", reference_date=date.today() + ) + # p1 is medication but not minoxidil (wait, Regaine name doesn't contain minoxidil!) -> skipped + assert "Regaine" not in ctx + + # Let's fix p1 to be minoxidil + p1.name = "Regaine Minoxidil" + session.add(p1) + session.commit() + + ctx = _build_products_context( + session, time_filter="am", reference_date=date.today() + ) + assert "Regaine Minoxidil" in ctx + assert "Sunscreen" in ctx + assert "inventory_status={active:2,opened:1,sealed:1}" in ctx + assert "nearest_open_expiry=" in ctx + assert "nearest_open_pao_deadline=" in ctx + assert "pao_months=6" in ctx + assert "effects={'hydration_immediate': 2}" in ctx + assert "incompatible_with=['avoid retinol (same_routine)']" in ctx + assert "context_rules={'safe_after_shaving': False}" in ctx + assert "min_interval_hours=12" in ctx + assert "max_frequency_per_week=7" in ctx + assert "used_in_last_7_days=1" in ctx + + +def test_build_objectives_context(): + assert _build_objectives_context(False) == "" + assert "improve beard" in _build_objectives_context(True) + + +def test_build_day_context(): + assert _build_day_context(None) == "" + assert "Leaving home: yes" in _build_day_context(True) + assert "Leaving home: no" in _build_day_context(False) + + +def test_get_available_products_respects_filters(session: Session): + regular_med = Product( + id=uuid.uuid4(), + name="Tretinoin", + category="serum", + is_medication=True, + brand="Test", + recommended_time="pm", + leave_on=True, + product_effect_profile={}, + ) + minoxidil_med = Product( + id=uuid.uuid4(), + name="Minoxidil 5%", + category="serum", + is_medication=True, + brand="Test", + recommended_time="both", + leave_on=True, + product_effect_profile={}, + ) + am_product = Product( + id=uuid.uuid4(), + name="AM SPF", + category="spf", + brand="Test", + recommended_time="am", + leave_on=True, + product_effect_profile={}, + ) + pm_product = Product( + id=uuid.uuid4(), + name="PM Cream", + category="moisturizer", + brand="Test", + recommended_time="pm", + leave_on=True, + product_effect_profile={}, + ) + session.add_all([regular_med, minoxidil_med, am_product, pm_product]) + session.commit() + + am_available = _get_available_products(session, time_filter="am") + am_names = {p.name for p in am_available} + assert "Tretinoin" not in am_names + assert "Minoxidil 5%" in am_names + assert "AM SPF" in am_names + assert "PM Cream" not in am_names + + +def test_build_inci_tool_handler_returns_only_available_ids(session: Session): + available = Product( + id=uuid.uuid4(), + name="Available", + category="serum", + brand="Test", + recommended_time="both", + leave_on=True, + inci=["Water", "Niacinamide"], + product_effect_profile={}, + ) + unavailable = Product( + id=uuid.uuid4(), + name="Unavailable", + category="serum", + brand="Test", + recommended_time="both", + leave_on=True, + inci=["Water", "Retinol"], + product_effect_profile={}, + ) + + handler = _build_inci_tool_handler([available]) + payload = handler( + { + "product_ids": [ + str(available.id), + str(unavailable.id), + str(available.id), + 123, + ] + } + ) + + assert "products" in payload + products = payload["products"] + assert len(products) == 1 + assert products[0]["id"] == str(available.id) + assert products[0]["name"] == "Available" + assert products[0]["inci"] == ["Water", "Niacinamide"] + + +def test_extract_requested_product_ids_dedupes_and_limits(): + ids = _extract_requested_product_ids( + { + "product_ids": [ + "id-1", + "id-2", + "id-1", + 3, + "id-3", + "id-4", + ] + }, + max_ids=3, + ) + assert ids == ["id-1", "id-2", "id-3"] + + +def test_extract_active_names_uses_compact_distinct_names(session: Session): + p = Product( + id=uuid.uuid4(), + name="Test", + category="serum", + brand="Test", + recommended_time="both", + leave_on=True, + actives=[ + {"name": "Niacinamide", "percent": 10}, + {"name": "Niacinamide", "percent": 5}, + {"name": "Zinc PCA", "percent": 1}, + ], + product_effect_profile={}, + ) + + names = _extract_active_names(p) + assert names == ["Niacinamide", "Zinc PCA"] + + +def test_additional_tool_handlers_return_product_payloads(session: Session): + p = Product( + id=uuid.uuid4(), + name="Detail Product", + category="serum", + brand="Test", + recommended_time="both", + leave_on=True, + usage_notes="Apply morning and evening.", + actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}], + incompatible_with=[{"target": "Retinol", "scope": "same_step"}], + context_rules={"safe_after_shaving": True}, + product_effect_profile={}, + ) + + ids_payload = {"product_ids": [str(p.id)]} + + actives_out = _build_actives_tool_handler([p])(ids_payload) + assert actives_out["products"][0]["actives"][0]["name"] == "Niacinamide" + + notes_out = _build_usage_notes_tool_handler([p])(ids_payload) + assert notes_out["products"][0]["usage_notes"] == "Apply morning and evening." + + safety_out = _build_safety_rules_tool_handler([p])(ids_payload) + assert "incompatible_with" in safety_out["products"][0] + assert "context_rules" in safety_out["products"][0] diff --git a/backend/uv.lock b/backend/uv.lock index a168b04..52286f9 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -2,18 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.12" -[[package]] -name = "aiofile" -version = "3.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "caio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, -] - [[package]] name = "alembic" version = "1.18.4" @@ -59,36 +47,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "authlib" -version = "1.6.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6b/6c/c88eac87468c607f88bc24df1f3b31445ee6fc9ba123b09e666adf687cd9/authlib-1.6.8.tar.gz", hash = "sha256:41ae180a17cf672bc784e4a518e5c82687f1fe1e98b0cafaeda80c8e4ab2d1cb", size = 165074, upload-time = "2026-02-14T04:02:17.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/73/f7084bf12755113cd535ae586782ff3a6e710bfbe6a0d13d1c2f81ffbbfa/authlib-1.6.8-py2.py3-none-any.whl", hash = "sha256:97286fd7a15e6cfefc32771c8ef9c54f0ed58028f1322de6a2a7c969c3817888", size = 244116, upload-time = "2026-02-14T04:02:15.579Z" }, -] - -[[package]] -name = "beartype" -version = "0.22.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, -] - [[package]] name = "black" version = "26.1.0" @@ -121,30 +79,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, ] -[[package]] -name = "cachetools" -version = "7.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" }, -] - -[[package]] -name = "caio" -version = "0.9.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, - { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, -] - [[package]] name = "certifi" version = "2026.2.25" @@ -289,6 +223,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + [[package]] name = "cryptography" version = "46.0.5" @@ -342,21 +360,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, ] -[[package]] -name = "cyclopts" -version = "4.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "docstring-parser" }, - { name = "rich" }, - { name = "rich-rst" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/5c/88a4068c660a096bbe87efc5b7c190080c9e86919c36ec5f092cb08d852f/cyclopts-4.6.0.tar.gz", hash = "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5", size = 162724, upload-time = "2026-02-23T15:44:49.286Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/eb/1e8337755a70dc7d7ff10a73dc8f20e9352c9ad6c2256ed863ac95cd3539/cyclopts-4.6.0-py3-none-any.whl", hash = "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", size = 200518, upload-time = "2026-02-23T15:44:47.854Z" }, -] - [[package]] name = "distro" version = "1.9.0" @@ -366,58 +369,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - -[[package]] -name = "email-validator" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "fastapi" version = "0.132.0" @@ -434,37 +385,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/de/6171c3363bbc5e01686e200e0880647c9270daa476d91030435cf14d32f5/fastapi-0.132.0-py3-none-any.whl", hash = "sha256:3c487d5afce196fa8ea509ae1531e96ccd5cdd2fd6eae78b73e2c20fba706689", size = 104652, upload-time = "2026-02-23T17:56:20.836Z" }, ] -[[package]] -name = "fastmcp" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "authlib" }, - { name = "cyclopts" }, - { name = "exceptiongroup" }, - { name = "httpx" }, - { name = "jsonref" }, - { name = "jsonschema-path" }, - { name = "mcp" }, - { name = "openapi-pydantic" }, - { name = "opentelemetry-api" }, - { name = "packaging" }, - { name = "platformdirs" }, - { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, - { name = "pydantic", extra = ["email"] }, - { name = "pyperclip" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "uvicorn" }, - { name = "watchfiles" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/6b/1a7ec89727797fb07ec0928e9070fa2f45e7b35718e1fe01633a34c35e45/fastmcp-3.0.2.tar.gz", hash = "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", size = 17239351, upload-time = "2026-02-22T16:32:28.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/5a/f410a9015cfde71adf646dab4ef2feae49f92f34f6050fcfb265eb126b30/fastmcp-3.0.2-py3-none-any.whl", hash = "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028", size = 606268, upload-time = "2026-02-22T16:32:30.992Z" }, -] - [[package]] name = "google-auth" version = "2.48.0" @@ -610,15 +530,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - [[package]] name = "idna" version = "3.11" @@ -628,18 +539,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -656,7 +555,6 @@ source = { virtual = "." } dependencies = [ { name = "alembic" }, { name = "fastapi" }, - { name = "fastmcp" }, { name = "google-genai" }, { name = "psycopg" }, { name = "python-dotenv" }, @@ -671,6 +569,7 @@ dev = [ { name = "httpx" }, { name = "isort" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "ty" }, ] @@ -679,7 +578,6 @@ dev = [ requires-dist = [ { name = "alembic", specifier = ">=1.14" }, { name = "fastapi", specifier = ">=0.132.0" }, - { name = "fastmcp", specifier = ">=2.0" }, { name = "google-genai", specifier = ">=1.65.0" }, { name = "psycopg", specifier = ">=3.3.3" }, { name = "python-dotenv", specifier = ">=1.2.1" }, @@ -694,6 +592,7 @@ dev = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "isort", specifier = ">=8.0.0" }, { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.15.2" }, { name = "ty", specifier = ">=0.0.18" }, ] @@ -707,115 +606,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/ea/cf3aad99dd12c026e2d6835d559efb6fc50ccfd5b46d42d5fec2608b116a/isort-8.0.0-py3-none-any.whl", hash = "sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9", size = 89715, upload-time = "2026-02-19T16:31:57.745Z" }, ] -[[package]] -name = "jaraco-classes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, -] - -[[package]] -name = "jaraco-context" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, -] - -[[package]] -name = "jaraco-functools" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, -] - -[[package]] -name = "jeepney" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, -] - -[[package]] -name = "jsonref" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-path" -version = "0.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pathable" }, - { name = "pyyaml" }, - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4e/b4/41315eea8301a5353bca3578792767135b8edbc081b20618a3f0b4d78307/jsonschema_path-0.4.4.tar.gz", hash = "sha256:4c55842890fc384262a59fb63a25c86cc0e2b059e929c18b851c1d19ef612026", size = 14923, upload-time = "2026-02-28T11:58:26.289Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/36/cb2cd6543776d02875de600f12fcd81611daf359544c9ad2abb12d3122a5/jsonschema_path-0.4.4-py3-none-any.whl", hash = "sha256:669bb69cb92cd4c54acf38ee2ff7c3d9ab6b69991698f7a2f17d2bb0e5c9c394", size = 19226, upload-time = "2026-02-28T11:58:25.143Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "keyring" -version = "25.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, -] - [[package]] name = "mako" version = "1.3.10" @@ -828,18 +618,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - [[package]] name = "markupsafe" version = "3.0.3" @@ -903,49 +681,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "mcp" -version = "1.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "more-itertools" -version = "10.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, -] - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -955,31 +690,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "openapi-pydantic" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, -] - [[package]] name = "packaging" version = "26.0" @@ -989,15 +699,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] -[[package]] -name = "pathable" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, -] - [[package]] name = "pathspec" version = "1.0.4" @@ -1038,31 +739,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, ] -[[package]] -name = "py-key-value-aio" -version = "0.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beartype" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, -] - -[package.optional-dependencies] -filetree = [ - { name = "aiofile" }, - { name = "anyio" }, -] -keyring = [ - { name = "keyring" }, -] -memory = [ - { name = "cachetools" }, -] - [[package]] name = "pyasn1" version = "0.6.2" @@ -1108,11 +784,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] -[package.optional-dependencies] -email = [ - { name = "email-validator" }, -] - [[package]] name = "pydantic-core" version = "2.41.5" @@ -1184,20 +855,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -1207,29 +864,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyjwt" -version = "2.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pyperclip" -version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, -] - [[package]] name = "pytest" version = "9.0.2" @@ -1246,6 +880,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -1293,31 +941,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1364,20 +987,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - [[package]] name = "requests" version = "2.32.5" @@ -1393,113 +1002,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "rich" -version = "14.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, -] - -[[package]] -name = "rich-rst" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, -] - [[package]] name = "rsa" version = "4.9.1" @@ -1537,19 +1039,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, ] -[[package]] -name = "secretstorage" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -1614,19 +1103,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/e1/7c8d18e737433f3b5bbe27b56a9072a9fcb36342b48f1bef34b6da1d61f2/sqlmodel-0.0.37-py3-none-any.whl", hash = "sha256:2137a4045ef3fd66a917a7717ada959a1ceb3630d95e1f6aaab39dd2c0aef278", size = 27224, upload-time = "2026-02-21T16:39:47.781Z" }, ] -[[package]] -name = "sse-starlette" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, -] - [[package]] name = "starlette" version = "0.52.1" @@ -1882,12 +1358,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..d60cc43 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Usage: ./deploy.sh [frontend|backend|all] +# default: all +# +# SSH config (~/.ssh/config) — recommended: +# Host innercontext +# HostName +# User innercontext +# +# The innercontext user needs passwordless sudo for systemctl only: +# /etc/sudoers.d/innercontext-deploy: +# innercontext ALL=(root) NOPASSWD: /usr/bin/systemctl restart innercontext, /usr/bin/systemctl restart innercontext-node, /usr/bin/systemctl is-active innercontext, /usr/bin/systemctl is-active innercontext-node +set -euo pipefail + +SERVER="${DEPLOY_SERVER:-innercontext}" # ssh host alias or user@host +REMOTE="/opt/innercontext" +SCOPE="${1:-all}" + +# ── Frontend ─────────────────────────────────────────────────────────────── +deploy_frontend() { + echo "==> [frontend] Building locally..." + (cd frontend && pnpm run build) + + echo "==> [frontend] Uploading build/ and package files..." + rsync -az --delete frontend/build/ "$SERVER:$REMOTE/frontend/build/" + rsync -az frontend/package.json frontend/pnpm-lock.yaml "$SERVER:$REMOTE/frontend/" + + echo "==> [frontend] Installing production dependencies on server..." + ssh "$SERVER" "cd $REMOTE/frontend && pnpm install --prod --frozen-lockfile --ignore-scripts" + + echo "==> [frontend] Restarting service..." + ssh "$SERVER" "sudo systemctl restart innercontext-node && echo OK" +} + +# ── Backend ──────────────────────────────────────────────────────────────── +deploy_backend() { + echo "==> [backend] Uploading source..." + rsync -az --delete \ + --exclude='.venv/' \ + --exclude='__pycache__/' \ + --exclude='*.pyc' \ + --exclude='.env' \ + backend/ "$SERVER:$REMOTE/backend/" + + echo "==> [backend] Syncing dependencies..." + ssh "$SERVER" "cd $REMOTE/backend && uv sync --frozen --no-dev --no-editable" + + echo "==> [backend] Restarting service (alembic runs on start)..." + ssh "$SERVER" "sudo systemctl restart innercontext && echo OK" +} + +# ── Dispatch ─────────────────────────────────────────────────────────────── +case "$SCOPE" in + frontend) deploy_frontend ;; + backend) deploy_backend ;; + all) deploy_frontend; deploy_backend ;; + *) + echo "Usage: $0 [frontend|backend|all]" + exit 1 + ;; +esac + +echo "==> Done." diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 36084bd..5f88f0d 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -7,13 +7,16 @@ Reverse proxy (existing) innercontext LXC (new, Debian 13) ┌──────────────────────┐ ┌────────────────────────────────────┐ │ reverse proxy │────────────▶│ nginx :80 │ │ innercontext.lan → * │ │ /api/* → uvicorn :8000/* │ -└──────────────────────┘ │ /mcp/* → uvicorn :8000/mcp/* │ - │ /* → SvelteKit Node :3000 │ +└──────────────────────┘ │ /* → SvelteKit Node :3000 │ └────────────────────────────────────┘ │ │ - FastAPI + MCP SvelteKit Node + FastAPI SvelteKit Node ``` +> **Frontend is never built on the server.** The `vite build` + `adapter-node` +> esbuild step is CPU/RAM-intensive and will hang on a small LXC. Build locally, +> deploy the `build/` artifact via `deploy.sh`. + ## 1. Prerequisites - Proxmox VE host with an existing PostgreSQL LXC and a reverse proxy @@ -53,7 +56,7 @@ 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 libpq5 rsync ``` ### Python 3.12+ + uv @@ -67,6 +70,11 @@ Installing to `/usr/local/bin` makes `uv` available system-wide (required for `s ### Node.js 24 LTS + pnpm +The server needs Node.js to **run** the pre-built frontend bundle, and pnpm to +**install production runtime dependencies** (`clsx`, `bits-ui`, etc. — +`adapter-node` bundles the SvelteKit framework but leaves these external). +The frontend is never **built** on the server. + ```bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash . "$HOME/.nvm/nvm.sh" @@ -75,16 +83,14 @@ nvm install 24 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: ```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): +Install pnpm as a standalone binary — self-contained, no wrapper scripts, +works system-wide: ```bash curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linux-x64" \ @@ -194,11 +200,10 @@ systemctl status innercontext --- -## 7. Frontend build and setup +## 7. Frontend setup -```bash -cd /opt/innercontext/frontend -``` +The frontend is **built locally and uploaded** via `deploy.sh` — never built on the server. +This section only covers the one-time server-side configuration. ### Create `.env.production` @@ -211,25 +216,24 @@ chmod 600 /opt/innercontext/frontend/.env.production chown innercontext:innercontext /opt/innercontext/frontend/.env.production ``` -### Install dependencies and build +### Grant `innercontext` passwordless sudo for service restarts ```bash -sudo -u innercontext bash -c ' - cd /opt/innercontext/frontend - pnpm install - PUBLIC_API_BASE=http://innercontext.lan/api pnpm build -' +cat > /etc/sudoers.d/innercontext-deploy << 'EOF' +innercontext ALL=(root) NOPASSWD: \ + /usr/bin/systemctl restart innercontext, \ + /usr/bin/systemctl restart innercontext-node +EOF +chmod 440 /etc/sudoers.d/innercontext-deploy ``` -The production build lands in `/opt/innercontext/frontend/build/`. - ### Install systemd service ```bash cp /opt/innercontext/systemd/innercontext-node.service /etc/systemd/system/ systemctl daemon-reload -systemctl enable --now innercontext-node -systemctl status innercontext-node +systemctl enable innercontext-node +# Do NOT start yet — build/ is empty until the first deploy.sh run ``` --- @@ -276,44 +280,64 @@ Reload your reverse proxy after applying the change. --- -## 10. Verification +## 10. First deploy from local machine + +All subsequent deploys (including the first one) use `deploy.sh` from your local machine. + +### SSH config + +Add to `~/.ssh/config` on your local machine: + +``` +Host innercontext + HostName + User innercontext +``` + +Make sure your SSH public key is in `/home/innercontext/.ssh/authorized_keys` on the server. + +### Run the first deploy + +```bash +# From the repo root on your local machine: +./deploy.sh +``` + +This will: +1. Build the frontend locally (`pnpm run build`) +2. Upload `frontend/build/` to the server via rsync +3. Restart `innercontext-node` +4. Upload `backend/` source to the server +5. Run `uv sync --frozen` on the server +6. Restart `innercontext` (runs alembic migrations on start) + +--- + +## 11. Verification ```bash # From any machine on the LAN: curl http://innercontext.lan/api/health-check # {"status":"ok"} curl http://innercontext.lan/api/products # [] curl http://innercontext.lan/ # SvelteKit HTML shell -curl -N http://innercontext.lan/mcp/mcp # MCP StreamableHTTP endpoint ``` The web UI should be accessible at `http://innercontext.lan`. --- -## 11. Updating the application +## 12. Updating the application ```bash -cd /opt/innercontext -git pull - -# Sync backend dependencies if pyproject.toml changed: -cd backend && sudo -u innercontext uv sync && cd .. - -# Apply any new DB migrations (runs automatically via ExecStartPre, but safe to run manually first): -sudo -u innercontext bash -c 'cd /opt/innercontext/backend && uv run alembic upgrade head' - -# Rebuild frontend: -cd frontend && sudo -u innercontext bash -c ' - pnpm install - PUBLIC_API_BASE=http://innercontext.lan/api pnpm build -' - -systemctl restart innercontext innercontext-node +# From the repo root on your local machine: +./deploy.sh # full deploy (frontend + backend) +./deploy.sh frontend # frontend only +./deploy.sh backend # backend only ``` --- -## 12. Troubleshooting +## 13. Troubleshooting ### 502 Bad Gateway on `/api/*` @@ -328,17 +352,7 @@ journalctl -u innercontext -n 50 ```bash systemctl status innercontext-node journalctl -u innercontext-node -n 50 -# Verify /opt/innercontext/frontend/build/index.js exists (pnpm build ran successfully) -``` - -### MCP endpoint not responding - -```bash -# MCP uses SSE — disable buffering is already in nginx config -# Verify the backend started successfully: -curl http://127.0.0.1:8000/health-check -# Check FastAPI logs: -journalctl -u innercontext -n 50 +# Verify /opt/innercontext/frontend/build/index.js exists (deploy.sh ran successfully) ``` ### Database connection refused diff --git a/frontend/.mcp.json b/frontend/.mcp.json new file mode 100644 index 0000000..35c1203 --- /dev/null +++ b/frontend/.mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "svelte": { + "type": "stdio", + "command": "npx", + "env": {}, + "args": ["-y", "@sveltejs/mcp"] + } + } +} diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..82eddec --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,8 @@ +node_modules +.svelte-kit +paraglide +build +dist +.env +.env.* +!.env.example diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..a927295 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,9 @@ +{ + "plugins": ["svelte"], + "overrides": [ + { + "files": ["*.svelte"], + "parser": "svelte-eslint-parser" + } + ] +} diff --git a/frontend/README.md b/frontend/README.md index c48d921..c0ec761 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -24,8 +24,8 @@ The backend must be running at `http://localhost:8000`. See `../backend/` for se ## Environment variables -| Variable | Description | Default | -|---|---|---| +| Variable | Description | Default | +| ----------------- | ------------------------------- | ----------------------- | | `PUBLIC_API_BASE` | Base URL of the FastAPI backend | `http://localhost:8000` | Set `PUBLIC_API_BASE` at **build time** for production: @@ -51,24 +51,24 @@ Or use the provided systemd service: `../systemd/innercontext-node.service`. ## Routes -| Route | Description | -|---|---| -| `/` | Dashboard | -| `/products` | Product list | -| `/products/new` | Add product | -| `/products/[id]` | Product detail / edit | -| `/routines` | Routine list | -| `/routines/new` | Create routine | -| `/routines/[id]` | Routine detail | -| `/health/medications` | Medications | -| `/health/lab-results` | Lab results | -| `/skin` | Skin condition snapshots | +| Route | Description | +| --------------------- | ------------------------ | +| `/` | Dashboard | +| `/products` | Product list | +| `/products/new` | Add product | +| `/products/[id]` | Product detail / edit | +| `/routines` | Routine list | +| `/routines/new` | Create routine | +| `/routines/[id]` | Routine detail | +| `/health/medications` | Medications | +| `/health/lab-results` | Lab results | +| `/skin` | Skin condition snapshots | ## Key files -| File | Purpose | -|---|---| -| `src/lib/api.ts` | API client (typed fetch wrappers) | -| `src/lib/types.ts` | Shared TypeScript types | -| `src/app.css` | Tailwind v4 theme + global styles | -| `svelte.config.js` | SvelteKit config (adapter-node) | +| File | Purpose | +| ------------------ | --------------------------------- | +| `src/lib/api.ts` | API client (typed fetch wrappers) | +| `src/lib/types.ts` | Shared TypeScript types | +| `src/app.css` | Tailwind v4 theme + global styles | +| `svelte.config.js` | SvelteKit config (adapter-node) | diff --git a/frontend/components.json b/frontend/components.json index 76eb346..77ccd6a 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -1,17 +1,17 @@ { - "$schema": "https://shadcn-svelte.com/schema.json", - "style": "default", - "tailwind": { - "config": "", - "css": "src/app.css", - "baseColor": "zinc" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils", - "ui": "$lib/components/ui", - "hooks": "$lib/hooks", - "lib": "$lib" - }, - "registry": "https://shadcn-svelte.com/registry" + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "default", + "tailwind": { + "config": "", + "css": "src/app.css", + "baseColor": "zinc" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "registry": "https://shadcn-svelte.com/registry" } diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..fd016cd --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,47 @@ +import js from "@eslint/js"; +import svelte from "eslint-plugin-svelte"; +import ts from "typescript-eslint"; +import globals from "globals"; + +export default [ + { + ignores: [ + ".svelte-kit", + "node_modules", + "build", + "dist", + "**/paraglide/**", + "**/lib/paraglide/**", + ], + }, + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs["flat/recommended"], + { + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.browser, + }, + }, + rules: { + "svelte/no-at-html-tags": "off", + "svelte/require-each-key": "off", + // TODO: Set ignoreGoto to false when https://github.com/sveltejs/eslint-plugin-svelte/issues/1327 is fixed + // The rule doesn't detect resolve() when used with string concatenation for query params + "svelte/no-navigation-without-resolve": [ + "error", + { ignoreLinks: true, ignoreGoto: true }, + ], + }, + }, + { + files: ["**/*.svelte"], + languageOptions: { + parserOptions: { + parser: ts.parser, + }, + }, + }, +]; diff --git a/frontend/messages/en.json b/frontend/messages/en.json index a57eba2..9948336 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,437 +1,522 @@ { - "nav_dashboard": "Dashboard", - "nav_products": "Products", - "nav_routines": "Routines", - "nav_grooming": "Grooming", - "nav_medications": "Medications", - "nav_labResults": "Lab Results", - "nav_skin": "Skin", - "nav_appName": "innercontext", - "nav_appSubtitle": "personal health & skincare", + "nav_dashboard": "Dashboard", + "nav_products": "Products", + "nav_routines": "Routines", + "nav_grooming": "Grooming", + "nav_medications": "Medications", + "nav_labResults": "Lab Results", + "nav_skin": "Skin", + "nav_appName": "innercontext", + "nav_appSubtitle": "personal health & skincare", - "common_save": "Save", - "common_cancel": "Cancel", - "common_add": "Add", - "common_edit": "Edit", - "common_delete": "Delete", - "common_saved": "Saved.", - "common_select": "Select", - "common_unknown": "Unknown", - "common_yes": "Yes", - "common_no": "No", - "common_unknown_value": "Unknown", - "common_optional_notes": "optional", - "common_steps": "steps", + "common_save": "Save", + "common_cancel": "Cancel", + "common_add": "Add", + "common_edit": "Edit", + "common_delete": "Delete", + "common_saved": "Saved.", + "common_select": "Select", + "common_unknown": "Unknown", + "common_yes": "Yes", + "common_no": "No", + "common_unknown_value": "Unknown", + "common_optional_notes": "optional", + "common_steps": "steps", - "dashboard_title": "Dashboard", - "dashboard_subtitle": "Your recent health & skincare overview", - "dashboard_latestSnapshot": "Latest Skin Snapshot", - "dashboard_recentRoutines": "Recent Routines", - "dashboard_noSnapshots": "No skin snapshots yet.", - "dashboard_noRoutines": "No routines in the past 2 weeks.", + "dashboard_title": "Dashboard", + "dashboard_subtitle": "Your recent health & skincare overview", + "dashboard_latestSnapshot": "Latest Skin Snapshot", + "dashboard_recentRoutines": "Recent Routines", + "dashboard_noSnapshots": "No skin snapshots yet.", + "dashboard_noRoutines": "No routines in the past 2 weeks.", - "products_title": "Products", - "products_count": "{count} products", - "products_addNew": "+ Add product", - "products_noProducts": "No products found.", - "products_filterAll": "All", - "products_filterOwned": "Owned", - "products_filterUnowned": "Not owned", - "products_colName": "Name", - "products_colBrand": "Brand", - "products_colTargets": "Targets", - "products_colTime": "Time", - "products_newTitle": "New Product", - "products_backToList": "← Products", - "products_createProduct": "Create product", - "products_saveChanges": "Save changes", - "products_deleteProduct": "Delete product", - "products_confirmDelete": "Delete this product?", - "products_noInventory": "No inventory packages.", + "products_title": "Products", + "products_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} product", + "countPlural=*": "{count} products" + } + } + ], + "products_addNew": "+ Add product", + "products_suggest": "Suggest", + "products_suggestTitle": "Shopping suggestions", + "products_suggestSubtitle": "What to buy?", + "products_suggestDescription": "Based on your skin condition and products you own, I'll suggest product types that could complement your routine.", + "products_suggestGenerating": "Analyzing...", + "products_suggestBtn": "Generate suggestions", + "products_suggestResults": "Suggestions", + "products_suggestTime": "Time", + "products_suggestFrequency": "Frequency", + "products_suggestRegenerate": "Regenerate", + "products_suggestNoResults": "No suggestions.", + "products_noProducts": "No products found.", + "products_filterAll": "All", + "products_filterOwned": "Owned", + "products_filterUnowned": "Not owned", + "products_colName": "Name", + "products_colBrand": "Brand", + "products_colTargets": "Targets", + "products_colTime": "Time", + "products_newTitle": "New Product", + "products_backToList": "← Products", + "products_createProduct": "Create product", + "products_saveChanges": "Save changes", + "products_deleteProduct": "Delete product", + "products_confirmDelete": "Delete this product?", + "products_noInventory": "No inventory packages.", - "inventory_title": "Inventory packages ({count})", - "inventory_addPackage": "+ Add package", - "inventory_packageAdded": "Package added.", - "inventory_packageUpdated": "Package updated.", - "inventory_packageDeleted": "Package deleted.", - "inventory_alreadyOpened": "Already opened", - "inventory_openedDate": "Opened date", - "inventory_finishedDate": "Finished date", - "inventory_expiryDate": "Expiry date", - "inventory_currentWeight": "Current weight (g)", - "inventory_lastWeighed": "Last weighed", - "inventory_notes": "Notes", - "inventory_badgeOpen": "Open", - "inventory_badgeSealed": "Sealed", - "inventory_badgeFinished": "Finished", - "inventory_exp": "Exp:", - "inventory_opened": "Opened:", - "inventory_finished": "Finished:", - "inventory_remaining": "g remaining", - "inventory_weighed": "Weighed:", - "inventory_confirmDelete": "Delete this package?", + "inventory_title": "Inventory packages ({count})", + "inventory_addPackage": "+ Add package", + "inventory_packageAdded": "Package added.", + "inventory_packageUpdated": "Package updated.", + "inventory_packageDeleted": "Package deleted.", + "inventory_alreadyOpened": "Already opened", + "inventory_openedDate": "Opened date", + "inventory_finishedDate": "Finished date", + "inventory_expiryDate": "Expiry date", + "inventory_currentWeight": "Current weight (g)", + "inventory_lastWeighed": "Last weighed", + "inventory_notes": "Notes", + "inventory_badgeOpen": "Open", + "inventory_badgeSealed": "Sealed", + "inventory_badgeFinished": "Finished", + "inventory_exp": "Exp:", + "inventory_opened": "Opened:", + "inventory_finished": "Finished:", + "inventory_remaining": "g remaining", + "inventory_weighed": "Weighed:", + "inventory_confirmDelete": "Delete this package?", - "routines_title": "Routines", - "routines_count": "{count} routines (last 30 days)", - "routines_suggestAI": "Suggest AI routine", - "routines_addNew": "+ New routine", - "routines_noRoutines": "No routines found.", - "routines_newTitle": "New Routine", - "routines_backToList": "← Routines", - "routines_detailsTitle": "Routine details", - "routines_date": "Date *", - "routines_amOrPm": "AM or PM *", - "routines_notes": "Notes", - "routines_notesPlaceholder": "Optional notes", - "routines_createRoutine": "Create routine", - "routines_deleteRoutine": "Delete routine", - "routines_confirmDelete": "Delete this routine?", - "routines_steps": "Steps ({count})", - "routines_addStep": "+ Add step", - "routines_addStepTitle": "Add step", - "routines_product": "Product", - "routines_selectProduct": "Select product", - "routines_dose": "Dose", - "routines_dosePlaceholder": "e.g. 2 pumps", - "routines_region": "Region", - "routines_regionPlaceholder": "e.g. face", - "routines_addStepBtn": "Add step", - "routines_unknownStep": "Unknown step", - "routines_noSteps": "No steps yet.", + "routines_title": "Routines", + "routines_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} routine (last 30 days)", + "countPlural=*": "{count} routines (last 30 days)" + } + } + ], + "routines_suggestAI": "Suggest AI routine", + "routines_addNew": "+ New routine", + "routines_noRoutines": "No routines found.", + "routines_newTitle": "New Routine", + "routines_backToList": "← Routines", + "routines_detailsTitle": "Routine details", + "routines_date": "Date *", + "routines_amOrPm": "AM or PM *", + "routines_notes": "Notes", + "routines_notesPlaceholder": "Optional notes", + "routines_createRoutine": "Create routine", + "routines_deleteRoutine": "Delete routine", + "routines_confirmDelete": "Delete this routine?", + "routines_steps": "Steps ({count})", + "routines_addStep": "+ Add step", + "routines_addStepTitle": "Add step", + "routines_product": "Product", + "routines_selectProduct": "Select product", + "routines_dose": "Dose", + "routines_dosePlaceholder": "e.g. 2 pumps", + "routines_region": "Region", + "routines_regionPlaceholder": "e.g. face", + "routines_addStepBtn": "Add step", + "routines_unknownStep": "Unknown step", + "routines_noSteps": "No steps yet.", - "grooming_title": "Grooming Schedule", - "grooming_backToRoutines": "← Routines", - "grooming_addEntry": "+ Add entry", - "grooming_entryAdded": "Entry added.", - "grooming_entryUpdated": "Entry updated.", - "grooming_entryDeleted": "Entry deleted.", - "grooming_dayOfWeek": "Day of week", - "grooming_action": "Action", - "grooming_notesOptional": "Notes (optional)", - "grooming_notesPlaceholder": "e.g. every 2 weeks", - "grooming_noEntries": "No entries yet. Click \"+ Add entry\" to get started.", - "grooming_confirmDelete": "Delete this entry?", - "grooming_actionShavingRazor": "Razor shaving", - "grooming_actionShavingOneblade": "OneBlade shaving", - "grooming_actionDermarolling": "Dermarolling", - "grooming_dayMonday": "Monday", - "grooming_dayTuesday": "Tuesday", - "grooming_dayWednesday": "Wednesday", - "grooming_dayThursday": "Thursday", - "grooming_dayFriday": "Friday", - "grooming_daySaturday": "Saturday", - "grooming_daySunday": "Sunday", + "grooming_title": "Grooming Schedule", + "grooming_backToRoutines": "← Routines", + "grooming_addEntry": "+ Add entry", + "grooming_entryAdded": "Entry added.", + "grooming_entryUpdated": "Entry updated.", + "grooming_entryDeleted": "Entry deleted.", + "grooming_dayOfWeek": "Day of week", + "grooming_action": "Action", + "grooming_notesOptional": "Notes (optional)", + "grooming_notesPlaceholder": "e.g. every 2 weeks", + "grooming_noEntries": "No entries yet. Click \"+ Add entry\" to get started.", + "grooming_confirmDelete": "Delete this entry?", + "grooming_actionShavingRazor": "Razor shaving", + "grooming_actionShavingOneblade": "OneBlade shaving", + "grooming_actionDermarolling": "Dermarolling", + "grooming_dayMonday": "Monday", + "grooming_dayTuesday": "Tuesday", + "grooming_dayWednesday": "Wednesday", + "grooming_dayThursday": "Thursday", + "grooming_dayFriday": "Friday", + "grooming_daySaturday": "Saturday", + "grooming_daySunday": "Sunday", - "suggest_title": "AI Routine Suggestion", - "suggest_backToRoutines": "← Routines", - "suggest_singleTab": "Single routine", - "suggest_batchTab": "Batch / Vacation", - "suggest_singleParams": "Parameters", - "suggest_date": "Date", - "suggest_timeOfDay": "Time of day", - "suggest_contextLabel": "Additional context for AI", - "suggest_contextOptional": "(optional)", - "suggest_contextPlaceholder": "e.g. party night, focusing on hydration...", - "suggest_generateBtn": "Generate suggestion", - "suggest_generating": "Generating…", - "suggest_proposalTitle": "Suggestion", - "suggest_saveRoutine": "Save routine", - "suggest_saving": "Saving…", - "suggest_regenerate": "Regenerate", - "suggest_batchRange": "Date range", - "suggest_fromDate": "From", - "suggest_toDate": "To (max 14 days)", - "suggest_batchContextLabel": "Context / trip purpose", - "suggest_batchContextPlaceholder": "e.g. sunny trip to Italy, active mountain vacation...", - "suggest_generatePlan": "Generate plan", - "suggest_generatingPlan": "Generating plan…", - "suggest_planTitle": "Plan ({count} days)", - "suggest_saveAllRoutines": "Save all routines", - "suggest_amSteps": "steps", - "suggest_pmSteps": "steps", - "suggest_noAmSteps": "No AM steps.", - "suggest_noPmSteps": "No PM steps.", - "suggest_errorDefault": "Error generating suggestion.", - "suggest_errorBatch": "Error generating plan.", - "suggest_errorSave": "Error saving.", - "suggest_amMorning": "AM (morning)", - "suggest_pmEvening": "PM (evening)", + "suggest_title": "AI Routine Suggestion", + "suggest_backToRoutines": "← Routines", + "suggest_singleTab": "Single routine", + "suggest_batchTab": "Batch / Vacation", + "suggest_singleParams": "Parameters", + "suggest_date": "Date", + "suggest_timeOfDay": "Time of day", + "suggest_contextLabel": "Additional context for AI", + "suggest_contextOptional": "(optional)", + "suggest_contextPlaceholder": "e.g. party night, focusing on hydration...", + "suggest_leavingHomeLabel": "Going outside today", + "suggest_leavingHomeHint": "Affects SPF selection — checked: SPF50+, unchecked: SPF30.", + "suggest_minoxidilToggleLabel": "Prioritize beard/mustache density (minoxidil)", + "suggest_minoxidilToggleHint": "When enabled, AI will explicitly consider minoxidil for beard/mustache areas if available.", + "suggest_generateBtn": "Generate suggestion", + "suggest_generating": "Generating…", + "suggest_proposalTitle": "Suggestion", + "suggest_saveRoutine": "Save routine", + "suggest_saving": "Saving…", + "suggest_regenerate": "Regenerate", + "suggest_batchRange": "Date range", + "suggest_fromDate": "From", + "suggest_toDate": "To (max 14 days)", + "suggest_batchContextLabel": "Context / trip purpose", + "suggest_batchContextPlaceholder": "e.g. sunny trip to Italy, active mountain vacation...", + "suggest_generatePlan": "Generate plan", + "suggest_generatingPlan": "Generating plan…", + "suggest_planTitle": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "Plan ({count} day)", + "countPlural=*": "Plan ({count} days)" + } + } + ], + "suggest_saveAllRoutines": "Save all routines", + "suggest_amSteps": "steps", + "suggest_pmSteps": "steps", + "suggest_noAmSteps": "No AM steps.", + "suggest_noPmSteps": "No PM steps.", + "suggest_errorDefault": "Error generating suggestion.", + "suggest_errorBatch": "Error generating plan.", + "suggest_errorSave": "Error saving.", + "suggest_amMorning": "AM (morning)", + "suggest_pmEvening": "PM (evening)", + "suggest_summaryPrimaryGoal": "Primary goal", + "suggest_summaryConfidence": "Confidence", + "suggest_summaryConstraints": "Constraints", + "suggest_stepOptionalBadge": "optional", - "medications_title": "Medications", - "medications_count": "{count} entries", - "medications_addNew": "+ Add medication", - "medications_newTitle": "New medication", - "medications_kind": "Kind", - "medications_productName": "Product name *", - "medications_productNamePlaceholder": "e.g. Vitamin D3", - "medications_activeSubstance": "Active substance", - "medications_activeSubstancePlaceholder": "e.g. cholecalciferol", - "medications_notes": "Notes", - "medications_added": "Medication added.", - "medications_usages": "{count} usages", - "medications_noMedications": "No medications recorded.", - "medications_kindPrescription": "Prescription", - "medications_kindOtc": "OTC", - "medications_kindSupplement": "Supplement", - "medications_kindHerbal": "Herbal", - "medications_kindOther": "Other", + "medications_title": "Medications", + "medications_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} entry", + "countPlural=*": "{count} entries" + } + } + ], + "medications_addNew": "+ Add medication", + "medications_newTitle": "New medication", + "medications_kind": "Kind", + "medications_productName": "Product name *", + "medications_productNamePlaceholder": "e.g. Vitamin D3", + "medications_activeSubstance": "Active substance", + "medications_activeSubstancePlaceholder": "e.g. cholecalciferol", + "medications_notes": "Notes", + "medications_added": "Medication added.", + "medications_usages": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} usage", + "countPlural=*": "{count} usages" + } + } + ], + "medications_noMedications": "No medications recorded.", + "medications_kindPrescription": "Prescription", + "medications_kindOtc": "OTC", + "medications_kindSupplement": "Supplement", + "medications_kindHerbal": "Herbal", + "medications_kindOther": "Other", - "labResults_title": "Lab Results", - "labResults_count": "{count} results", - "labResults_addNew": "+ Add result", - "labResults_newTitle": "New lab result", - "labResults_flagFilter": "Flag:", - "labResults_flagAll": "All", - "labResults_flagNone": "None", - "labResults_date": "Date *", - "labResults_loincCode": "LOINC code *", - "labResults_testName": "Test name", - "labResults_testNamePlaceholder": "e.g. Hemoglobin", - "labResults_lab": "Lab", - "labResults_labPlaceholder": "e.g. LabCorp", - "labResults_value": "Value", - "labResults_unit": "Unit", - "labResults_unitPlaceholder": "e.g. g/dL", - "labResults_flag": "Flag", - "labResults_added": "Result added.", - "labResults_colDate": "Date", - "labResults_colTest": "Test", - "labResults_colLoinc": "LOINC", - "labResults_colValue": "Value", - "labResults_colFlag": "Flag", - "labResults_colLab": "Lab", - "labResults_noResults": "No lab results found.", + "labResults_title": "Lab Results", + "labResults_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} result", + "countPlural=*": "{count} results" + } + } + ], + "labResults_addNew": "+ Add result", + "labResults_newTitle": "New lab result", + "labResults_flagFilter": "Flag:", + "labResults_flagAll": "All", + "labResults_flagNone": "None", + "labResults_date": "Date *", + "labResults_loincCode": "LOINC code *", + "labResults_testName": "Test name", + "labResults_testNamePlaceholder": "e.g. Hemoglobin", + "labResults_lab": "Lab", + "labResults_labPlaceholder": "e.g. LabCorp", + "labResults_value": "Value", + "labResults_unit": "Unit", + "labResults_unitPlaceholder": "e.g. g/dL", + "labResults_flag": "Flag", + "labResults_added": "Result added.", + "labResults_colDate": "Date", + "labResults_colTest": "Test", + "labResults_colLoinc": "LOINC", + "labResults_colValue": "Value", + "labResults_colFlag": "Flag", + "labResults_colLab": "Lab", + "labResults_noResults": "No lab results found.", - "skin_title": "Skin Snapshots", - "skin_count": "{count} snapshots", - "skin_addNew": "+ Add snapshot", - "skin_aiAnalysisTitle": "AI analysis from photos", - "skin_aiUploadText": "Upload 1–3 photos of your skin. AI will pre-fill the form fields below.", - "skin_analyzePhotos": "Analyze photos", - "skin_analyzing": "Analyzing…", - "skin_newSnapshotTitle": "New skin snapshot", - "skin_date": "Date *", - "skin_overallState": "Overall state", - "skin_texture": "Texture", - "skin_skinType": "Skin type", - "skin_barrierState": "Barrier state", - "skin_hydration": "Hydration (1–5)", - "skin_sensitivity": "Sensitivity (1–5)", - "skin_sebumTzone": "Sebum T-zone (1–5)", - "skin_sebumCheeks": "Sebum cheeks (1–5)", - "skin_activeConcerns": "Active concerns (comma-separated)", - "skin_activeConcernsPlaceholder": "acne, redness, dehydration", - "skin_notes": "Notes", - "skin_addSnapshot": "Add snapshot", - "skin_snapshotAdded": "Snapshot added.", - "skin_snapshotUpdated": "Snapshot updated.", - "skin_snapshotDeleted": "Snapshot deleted.", - "skin_noSnapshots": "No skin snapshots yet.", - "skin_hydrationLabel": "Hydration", - "skin_sensitivityLabel": "Sensitivity", - "skin_barrierLabel": "Barrier", - "skin_stateExcellent": "excellent", - "skin_stateGood": "good", - "skin_stateFair": "fair", - "skin_statePoor": "poor", - "skin_textureSmooth": "smooth", - "skin_textureRough": "rough", - "skin_textureFlaky": "flaky", - "skin_textureBumpy": "bumpy", - "skin_barrierIntact": "intact", - "skin_barrierMildly": "mildly compromised", - "skin_barrierCompromised": "compromised", - "skin_typeDry": "dry", - "skin_typeOily": "oily", - "skin_typeCombination": "combination", - "skin_typeSensitive": "sensitive", - "skin_typeNormal": "normal", - "skin_typeAcneProne": "acne prone", + "skin_title": "Skin Snapshots", + "skin_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} snapshot", + "countPlural=*": "{count} snapshots" + } + } + ], + "skin_addNew": "+ Add snapshot", + "skin_aiAnalysisTitle": "AI analysis from photos", + "skin_aiUploadText": "Upload 1–3 photos of your skin. AI will pre-fill the form fields below.", + "skin_analyzePhotos": "Analyze photos", + "skin_analyzing": "Analyzing…", + "skin_newSnapshotTitle": "New skin snapshot", + "skin_date": "Date *", + "skin_overallState": "Overall state", + "skin_texture": "Texture", + "skin_skinType": "Skin type", + "skin_barrierState": "Barrier state", + "skin_hydration": "Hydration (1–5)", + "skin_sensitivity": "Sensitivity (1–5)", + "skin_sebumTzone": "Sebum T-zone (1–5)", + "skin_sebumCheeks": "Sebum cheeks (1–5)", + "skin_activeConcerns": "Active concerns (comma-separated)", + "skin_activeConcernsPlaceholder": "acne, redness, dehydration", + "skin_priorities": "Priorities (comma-separated)", + "skin_prioritiesPlaceholder": "strengthen barrier, reduce redness", + "skin_prioritiesLabel": "Priorities", + "skin_notes": "Notes", + "skin_addSnapshot": "Add snapshot", + "skin_snapshotAdded": "Snapshot added.", + "skin_snapshotUpdated": "Snapshot updated.", + "skin_snapshotDeleted": "Snapshot deleted.", + "skin_noSnapshots": "No skin snapshots yet.", + "skin_hydrationLabel": "Hydration", + "skin_sensitivityLabel": "Sensitivity", + "skin_barrierLabel": "Barrier", + "skin_stateExcellent": "excellent", + "skin_stateGood": "good", + "skin_stateFair": "fair", + "skin_statePoor": "poor", + "skin_textureSmooth": "smooth", + "skin_textureRough": "rough", + "skin_textureFlaky": "flaky", + "skin_textureBumpy": "bumpy", + "skin_barrierIntact": "intact", + "skin_barrierMildly": "mildly compromised", + "skin_barrierCompromised": "compromised", + "skin_typeDry": "dry", + "skin_typeOily": "oily", + "skin_typeCombination": "combination", + "skin_typeSensitive": "sensitive", + "skin_typeNormal": "normal", + "skin_typeAcneProne": "acne prone", - "productForm_aiPrefill": "AI pre-fill", - "productForm_aiPrefillText": "Paste product description from a website, ingredient list, or other text. AI will fill in available fields — you can review and correct before saving.", - "productForm_pasteText": "Paste product description, INCI ingredients here...", - "productForm_parseWithAI": "Fill fields (AI)", - "productForm_parsing": "Processing…", - "productForm_basicInfo": "Basic info", - "productForm_name": "Name *", - "productForm_namePlaceholder": "e.g. Hydro Boost Water Gel", - "productForm_brand": "Brand *", - "productForm_brandPlaceholder": "e.g. Neutrogena", - "productForm_lineName": "Line / series", - "productForm_lineNamePlaceholder": "e.g. Hydro Boost", - "productForm_url": "URL", - "productForm_sku": "SKU", - "productForm_skuPlaceholder": "e.g. NTR-HB-50", - "productForm_barcode": "Barcode / EAN", - "productForm_barcodePlaceholder": "e.g. 3614273258975", - "productForm_classification": "Classification", - "productForm_category": "Category *", - "productForm_selectCategory": "Select category", - "productForm_time": "Time *", - "productForm_timeOptions": "AM / PM / Both", - "productForm_timeBoth": "Both", - "productForm_leaveOn": "Leave-on *", - "productForm_leaveOnYes": "Yes (leave-on)", - "productForm_leaveOnNo": "No (rinse-off)", - "productForm_texture": "Texture", - "productForm_selectTexture": "Select texture", - "productForm_absorptionSpeed": "Absorption speed", - "productForm_selectSpeed": "Select speed", - "productForm_skinProfile": "Skin profile", - "productForm_recommendedFor": "Recommended for skin types", - "productForm_targetConcerns": "Target concerns", - "productForm_contraindications": "Contraindications (one per line)", - "productForm_contraindicationsPlaceholder": "e.g. active rosacea flares", - "productForm_ingredients": "Ingredients", - "productForm_inciList": "INCI list (one ingredient per line)", - "productForm_activeIngredients": "Active ingredients", - "productForm_addActive": "+ Add active", - "productForm_noActives": "No actives added yet.", - "productForm_activeName": "Name", - "productForm_activePercent": "%", - "productForm_activeStrength": "Strength", - "productForm_activeIrritation": "Irritation", - "productForm_activeFunctions": "Functions", - "productForm_effectProfile": "Effect profile (0–5)", - "productForm_interactions": "Interactions", - "productForm_synergizesWith": "Synergizes with (one per line)", - "productForm_incompatibleWith": "Incompatible with", - "productForm_addIncompatibility": "+ Add incompatibility", - "productForm_noIncompatibilities": "No incompatibilities added.", - "productForm_incompTarget": "Target ingredient", - "productForm_incompScope": "Scope", - "productForm_incompReason": "Reason (optional)", - "productForm_incompReasonPlaceholder": "e.g. reduces efficacy", - "productForm_incompScopeSelect": "Select…", - "productForm_contextRules": "Context rules", - "productForm_ctxAfterShaving": "Safe after shaving", - "productForm_ctxAfterAcids": "Safe after acids", - "productForm_ctxAfterRetinoids": "Safe after retinoids", - "productForm_ctxCompromisedBarrier": "Safe with compromised barrier", - "productForm_ctxLowUvOnly": "Low UV only (evening/covered)", - "productForm_productDetails": "Product details", - "productForm_priceTier": "Price tier", - "productForm_selectTier": "Select tier", - "productForm_sizeMl": "Size (ml)", - "productForm_fullWeightG": "Full weight (g)", - "productForm_emptyWeightG": "Empty weight (g)", - "productForm_paoMonths": "PAO (months)", - "productForm_phMin": "pH min", - "productForm_phMax": "pH max", - "productForm_usageNotes": "Usage notes", - "productForm_usageNotesPlaceholder": "e.g. Apply to damp skin, avoid eye area", - "productForm_safetyFlags": "Safety flags", - "productForm_fragranceFree": "Fragrance-free", - "productForm_essentialOilsFree": "Essential oils-free", - "productForm_alcoholDenatFree": "Alcohol denat-free", - "productForm_pregnancySafe": "Pregnancy safe", - "productForm_usageConstraints": "Usage constraints", - "productForm_minIntervalHours": "Min interval (hours)", - "productForm_maxFrequencyPerWeek": "Max uses per week", - "productForm_isMedication": "Is medication", - "productForm_isTool": "Is tool (e.g. dermaroller)", - "productForm_needleLengthMm": "Needle length (mm, tools only)", - "productForm_personalNotes": "Personal notes", - "productForm_repurchaseIntent": "Repurchase intent", - "productForm_toleranceNotes": "Tolerance notes", - "productForm_toleranceNotesPlaceholder": "e.g. Causes mild stinging, fine after 2 weeks", + "productForm_aiPrefill": "AI pre-fill", + "productForm_aiPrefillText": "Paste product description from a website, ingredient list, or other text. AI will fill in available fields — you can review and correct before saving.", + "productForm_pasteText": "Paste product description, INCI ingredients here...", + "productForm_parseWithAI": "Fill fields (AI)", + "productForm_parsing": "Processing…", + "productForm_basicInfo": "Basic info", + "productForm_name": "Name *", + "productForm_namePlaceholder": "e.g. Hydro Boost Water Gel", + "productForm_brand": "Brand *", + "productForm_brandPlaceholder": "e.g. Neutrogena", + "productForm_lineName": "Line / series", + "productForm_lineNamePlaceholder": "e.g. Hydro Boost", + "productForm_url": "URL", + "productForm_sku": "SKU", + "productForm_skuPlaceholder": "e.g. NTR-HB-50", + "productForm_barcode": "Barcode / EAN", + "productForm_barcodePlaceholder": "e.g. 3614273258975", + "productForm_classification": "Classification", + "productForm_category": "Category *", + "productForm_selectCategory": "Select category", + "productForm_time": "Time *", + "productForm_timeOptions": "AM / PM / Both", + "productForm_timeBoth": "Both", + "productForm_leaveOn": "Leave-on *", + "productForm_leaveOnYes": "Yes (leave-on)", + "productForm_leaveOnNo": "No (rinse-off)", + "productForm_texture": "Texture", + "productForm_selectTexture": "Select texture", + "productForm_absorptionSpeed": "Absorption speed", + "productForm_selectSpeed": "Select speed", + "productForm_skinProfile": "Skin profile", + "productForm_recommendedFor": "Recommended for skin types", + "productForm_targetConcerns": "Target concerns", + "productForm_contraindications": "Contraindications (one per line)", + "productForm_contraindicationsPlaceholder": "e.g. active rosacea flares", + "productForm_ingredients": "Ingredients", + "productForm_inciList": "INCI list (one ingredient per line)", + "productForm_activeIngredients": "Active ingredients", + "productForm_addActive": "+ Add active", + "productForm_noActives": "No actives added yet.", + "productForm_activeName": "Name", + "productForm_activePercent": "%", + "productForm_activeStrength": "Strength", + "productForm_activeIrritation": "Irritation", + "productForm_activeFunctions": "Functions", + "productForm_effectProfile": "Effect profile (0–5)", + "productForm_interactions": "Interactions", + "productForm_synergizesWith": "Synergizes with (one per line)", + "productForm_incompatibleWith": "Incompatible with", + "productForm_addIncompatibility": "+ Add incompatibility", + "productForm_noIncompatibilities": "No incompatibilities added.", + "productForm_incompTarget": "Target ingredient", + "productForm_incompScope": "Scope", + "productForm_incompReason": "Reason (optional)", + "productForm_incompReasonPlaceholder": "e.g. reduces efficacy", + "productForm_incompScopeSelect": "Select…", + "productForm_contextRules": "Context rules", + "productForm_ctxAfterShaving": "Safe after shaving", + "productForm_ctxAfterAcids": "Safe after acids", + "productForm_ctxAfterRetinoids": "Safe after retinoids", + "productForm_ctxCompromisedBarrier": "Safe with compromised barrier", + "productForm_ctxLowUvOnly": "Low UV only (evening/covered)", + "productForm_productDetails": "Product details", + "productForm_priceTier": "Price tier", + "productForm_selectTier": "Select tier", + "productForm_sizeMl": "Size (ml)", + "productForm_fullWeightG": "Full weight (g)", + "productForm_emptyWeightG": "Empty weight (g)", + "productForm_paoMonths": "PAO (months)", + "productForm_phMin": "pH min", + "productForm_phMax": "pH max", + "productForm_usageNotes": "Usage notes", + "productForm_usageNotesPlaceholder": "e.g. Apply to damp skin, avoid eye area", + "productForm_safetyFlags": "Safety flags", + "productForm_fragranceFree": "Fragrance-free", + "productForm_essentialOilsFree": "Essential oils-free", + "productForm_alcoholDenatFree": "Alcohol denat-free", + "productForm_pregnancySafe": "Pregnancy safe", + "productForm_usageConstraints": "Usage constraints", + "productForm_minIntervalHours": "Min interval (hours)", + "productForm_maxFrequencyPerWeek": "Max uses per week", + "productForm_isMedication": "Is medication", + "productForm_isTool": "Is tool (e.g. dermaroller)", + "productForm_needleLengthMm": "Needle length (mm, tools only)", + "productForm_personalNotes": "Personal notes", + "productForm_repurchaseIntent": "Repurchase intent", + "productForm_toleranceNotes": "Tolerance notes", + "productForm_toleranceNotesPlaceholder": "e.g. Causes mild stinging, fine after 2 weeks", - "productForm_categoryCleanser": "Cleanser", - "productForm_categoryToner": "Toner", - "productForm_categoryEssence": "Essence", - "productForm_categorySerum": "Serum", - "productForm_categoryMoisturizer": "Moisturizer", - "productForm_categorySpf": "SPF", - "productForm_categoryMask": "Mask", - "productForm_categoryExfoliant": "Exfoliant", - "productForm_categoryHairTreatment": "Hair treatment", - "productForm_categoryTool": "Tool", - "productForm_categorySpotTreatment": "Spot treatment", - "productForm_categoryOil": "Oil", + "productForm_categoryCleanser": "Cleanser", + "productForm_categoryToner": "Toner", + "productForm_categoryEssence": "Essence", + "productForm_categorySerum": "Serum", + "productForm_categoryMoisturizer": "Moisturizer", + "productForm_categorySpf": "SPF", + "productForm_categoryMask": "Mask", + "productForm_categoryExfoliant": "Exfoliant", + "productForm_categoryHairTreatment": "Hair treatment", + "productForm_categoryTool": "Tool", + "productForm_categorySpotTreatment": "Spot treatment", + "productForm_categoryOil": "Oil", - "productForm_textureWatery": "Watery", - "productForm_textureGel": "Gel", - "productForm_textureEmulsion": "Emulsion", - "productForm_textureCream": "Cream", - "productForm_textureOil": "Oil", - "productForm_textureBalm": "Balm", - "productForm_textureFoam": "Foam", - "productForm_textureFluid": "Fluid", + "productForm_textureWatery": "Watery", + "productForm_textureGel": "Gel", + "productForm_textureEmulsion": "Emulsion", + "productForm_textureCream": "Cream", + "productForm_textureOil": "Oil", + "productForm_textureBalm": "Balm", + "productForm_textureFoam": "Foam", + "productForm_textureFluid": "Fluid", - "productForm_absorptionVeryFast": "Very fast", - "productForm_absorptionFast": "Fast", - "productForm_absorptionModerate": "Moderate", - "productForm_absorptionSlow": "Slow", - "productForm_absorptionVerySlow": "Very slow", + "productForm_absorptionVeryFast": "Very fast", + "productForm_absorptionFast": "Fast", + "productForm_absorptionModerate": "Moderate", + "productForm_absorptionSlow": "Slow", + "productForm_absorptionVerySlow": "Very slow", - "productForm_priceBudget": "Budget", - "productForm_priceMid": "Mid", - "productForm_pricePremium": "Premium", - "productForm_priceLuxury": "Luxury", + "productForm_priceBudget": "Budget", + "productForm_priceMid": "Mid", + "productForm_pricePremium": "Premium", + "productForm_priceLuxury": "Luxury", - "productForm_skinTypeDry": "dry", - "productForm_skinTypeOily": "oily", - "productForm_skinTypeCombination": "combination", - "productForm_skinTypeSensitive": "sensitive", - "productForm_skinTypeNormal": "normal", - "productForm_skinTypeAcneProne": "acne prone", + "productForm_skinTypeDry": "dry", + "productForm_skinTypeOily": "oily", + "productForm_skinTypeCombination": "combination", + "productForm_skinTypeSensitive": "sensitive", + "productForm_skinTypeNormal": "normal", + "productForm_skinTypeAcneProne": "acne prone", - "productForm_concernAcne": "acne", - "productForm_concernRosacea": "rosacea", - "productForm_concernHyperpigmentation": "hyperpigmentation", - "productForm_concernAging": "aging", - "productForm_concernDehydration": "dehydration", - "productForm_concernRedness": "redness", - "productForm_concernDamagedBarrier": "damaged barrier", - "productForm_concernPoreVisibility": "pore visibility", - "productForm_concernUnevenTexture": "uneven texture", - "productForm_concernHairGrowth": "hair growth", - "productForm_concernSebumExcess": "sebum excess", + "productForm_concernAcne": "acne", + "productForm_concernRosacea": "rosacea", + "productForm_concernHyperpigmentation": "hyperpigmentation", + "productForm_concernAging": "aging", + "productForm_concernDehydration": "dehydration", + "productForm_concernRedness": "redness", + "productForm_concernDamagedBarrier": "damaged barrier", + "productForm_concernPoreVisibility": "pore visibility", + "productForm_concernUnevenTexture": "uneven texture", + "productForm_concernHairGrowth": "hair growth", + "productForm_concernSebumExcess": "sebum excess", - "productForm_fnHumectant": "humectant", - "productForm_fnEmollient": "emollient", - "productForm_fnOcclusive": "occlusive", - "productForm_fnExfoliantAha": "AHA exfoliant", - "productForm_fnExfoliantBha": "BHA exfoliant", - "productForm_fnExfoliantPha": "PHA exfoliant", - "productForm_fnRetinoid": "retinoid", - "productForm_fnAntioxidant": "antioxidant", - "productForm_fnSoothing": "soothing", - "productForm_fnBarrierSupport": "barrier support", - "productForm_fnBrightening": "brightening", - "productForm_fnAntiAcne": "anti-acne", - "productForm_fnCeramide": "ceramide", - "productForm_fnNiacinamide": "niacinamide", - "productForm_fnSunscreen": "sunscreen", - "productForm_fnPeptide": "peptide", - "productForm_fnHairGrowth": "hair growth stimulant", - "productForm_fnPrebiotic": "prebiotic", - "productForm_fnVitaminC": "vitamin C", - "productForm_fnAntiAging": "anti-aging", + "productForm_fnHumectant": "humectant", + "productForm_fnEmollient": "emollient", + "productForm_fnOcclusive": "occlusive", + "productForm_fnExfoliantAha": "AHA exfoliant", + "productForm_fnExfoliantBha": "BHA exfoliant", + "productForm_fnExfoliantPha": "PHA exfoliant", + "productForm_fnRetinoid": "retinoid", + "productForm_fnAntioxidant": "antioxidant", + "productForm_fnSoothing": "soothing", + "productForm_fnBarrierSupport": "barrier support", + "productForm_fnBrightening": "brightening", + "productForm_fnAntiAcne": "anti-acne", + "productForm_fnCeramide": "ceramide", + "productForm_fnNiacinamide": "niacinamide", + "productForm_fnSunscreen": "sunscreen", + "productForm_fnPeptide": "peptide", + "productForm_fnHairGrowth": "hair growth stimulant", + "productForm_fnPrebiotic": "prebiotic", + "productForm_fnVitaminC": "vitamin C", + "productForm_fnAntiAging": "anti-aging", - "productForm_scopeSameStep": "same step", - "productForm_scopeSameDay": "same day", - "productForm_scopeSamePeriod": "same period", + "productForm_scopeSameStep": "same step", + "productForm_scopeSameDay": "same day", + "productForm_scopeSamePeriod": "same period", - "productForm_strengthLow": "1 Low", - "productForm_strengthMedium": "2 Medium", - "productForm_strengthHigh": "3 High", + "productForm_strengthLow": "1 Low", + "productForm_strengthMedium": "2 Medium", + "productForm_strengthHigh": "3 High", - "productForm_effectHydrationImmediate": "Hydration (immediate)", - "productForm_effectHydrationLongTerm": "Hydration (long term)", - "productForm_effectBarrierRepair": "Barrier repair", - "productForm_effectSoothing": "Soothing", - "productForm_effectExfoliation": "Exfoliation", - "productForm_effectRetinoid": "Retinoid activity", - "productForm_effectIrritation": "Irritation risk", - "productForm_effectComedogenic": "Comedogenic risk", - "productForm_effectBarrierDisruption": "Barrier disruption risk", - "productForm_effectDryness": "Dryness risk", - "productForm_effectBrightening": "Brightening", - "productForm_effectAntiAcne": "Anti-acne", - "productForm_effectAntiAging": "Anti-aging", + "productForm_effectHydrationImmediate": "Hydration (immediate)", + "productForm_effectHydrationLongTerm": "Hydration (long term)", + "productForm_effectBarrierRepair": "Barrier repair", + "productForm_effectSoothing": "Soothing", + "productForm_effectExfoliation": "Exfoliation", + "productForm_effectRetinoid": "Retinoid activity", + "productForm_effectIrritation": "Irritation risk", + "productForm_effectComedogenic": "Comedogenic risk", + "productForm_effectBarrierDisruption": "Barrier disruption risk", + "productForm_effectDryness": "Dryness risk", + "productForm_effectBrightening": "Brightening", + "productForm_effectAntiAcne": "Anti-acne", + "productForm_effectAntiAging": "Anti-aging", - "lang_pl": "PL", - "lang_en": "EN" + "lang_pl": "PL", + "lang_en": "EN" } diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index ba11dfb..42f2199 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -1,437 +1,536 @@ { - "nav_dashboard": "Dashboard", - "nav_products": "Produkty", - "nav_routines": "Rutyny", - "nav_grooming": "Pielęgnacja", - "nav_medications": "Leki", - "nav_labResults": "Wyniki badań", - "nav_skin": "Skóra", - "nav_appName": "innercontext", - "nav_appSubtitle": "zdrowie & pielęgnacja", + "nav_dashboard": "Dashboard", + "nav_products": "Produkty", + "nav_routines": "Rutyny", + "nav_grooming": "Pielęgnacja", + "nav_medications": "Leki", + "nav_labResults": "Wyniki badań", + "nav_skin": "Skóra", + "nav_appName": "innercontext", + "nav_appSubtitle": "zdrowie & pielęgnacja", - "common_save": "Zapisz", - "common_cancel": "Anuluj", - "common_add": "Dodaj", - "common_edit": "Edytuj", - "common_delete": "Usuń", - "common_saved": "Zapisano.", - "common_select": "Wybierz", - "common_unknown": "Nieznane", - "common_yes": "Tak", - "common_no": "Nie", - "common_unknown_value": "Nieznane", - "common_optional_notes": "opcjonalnie", - "common_steps": "kroków", + "common_save": "Zapisz", + "common_cancel": "Anuluj", + "common_add": "Dodaj", + "common_edit": "Edytuj", + "common_delete": "Usuń", + "common_saved": "Zapisano.", + "common_select": "Wybierz", + "common_unknown": "Nieznane", + "common_yes": "Tak", + "common_no": "Nie", + "common_unknown_value": "Nieznane", + "common_optional_notes": "opcjonalnie", + "common_steps": "kroków", - "dashboard_title": "Dashboard", - "dashboard_subtitle": "Przegląd zdrowia i pielęgnacji", - "dashboard_latestSnapshot": "Ostatni stan skóry", - "dashboard_recentRoutines": "Ostatnie rutyny", - "dashboard_noSnapshots": "Brak wpisów o stanie skóry.", - "dashboard_noRoutines": "Brak rutyno w ciągu ostatnich 2 tygodni.", + "dashboard_title": "Dashboard", + "dashboard_subtitle": "Przegląd zdrowia i pielęgnacji", + "dashboard_latestSnapshot": "Ostatni stan skóry", + "dashboard_recentRoutines": "Ostatnie rutyny", + "dashboard_noSnapshots": "Brak wpisów o stanie skóry.", + "dashboard_noRoutines": "Brak rutyn w ciągu ostatnich 2 tygodni.", - "products_title": "Produkty", - "products_count": "{count} produktów", - "products_addNew": "+ Dodaj produkt", - "products_noProducts": "Nie znaleziono produktów.", - "products_filterAll": "Wszystkie", - "products_filterOwned": "Posiadane", - "products_filterUnowned": "Nieposiadane", - "products_colName": "Nazwa", - "products_colBrand": "Marka", - "products_colTargets": "Cele", - "products_colTime": "Pora", - "products_newTitle": "Nowy produkt", - "products_backToList": "← Produkty", - "products_createProduct": "Utwórz produkt", - "products_saveChanges": "Zapisz zmiany", - "products_deleteProduct": "Usuń produkt", - "products_confirmDelete": "Usunąć ten produkt?", - "products_noInventory": "Brak opakowań w magazynie.", + "products_title": "Produkty", + "products_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} produkt", + "countPlural=few": "{count} produkty", + "countPlural=many": "{count} produktów", + "countPlural=*": "{count} produktów" + } + } + ], + "products_addNew": "+ Dodaj produkt", + "products_suggest": "Sugeruj", + "products_suggestTitle": "Sugestie zakupowe", + "products_suggestSubtitle": "Co warto kupić?", + "products_suggestDescription": "Na podstawie Twojego stanu skóry i posiadanych produktów zasugeruję typy produktów, które mogłyby uzupełnić Twoją rutynę.", + "products_suggestGenerating": "Analizuję...", + "products_suggestBtn": "Generuj sugestie", + "products_suggestResults": "Propozycje", + "products_suggestTime": "Pora", + "products_suggestFrequency": "Częstotliwość", + "products_suggestRegenerate": "Wygeneruj ponownie", + "products_suggestNoResults": "Brak propozycji.", + "products_noProducts": "Nie znaleziono produktów.", + "products_filterAll": "Wszystkie", + "products_filterOwned": "Posiadane", + "products_filterUnowned": "Nieposiadane", + "products_colName": "Nazwa", + "products_colBrand": "Marka", + "products_colTargets": "Cele", + "products_colTime": "Pora", + "products_newTitle": "Nowy produkt", + "products_backToList": "← Produkty", + "products_createProduct": "Utwórz produkt", + "products_saveChanges": "Zapisz zmiany", + "products_deleteProduct": "Usuń produkt", + "products_confirmDelete": "Usunąć ten produkt?", + "products_noInventory": "Brak opakowań w magazynie.", - "inventory_title": "Opakowania ({count})", - "inventory_addPackage": "+ Dodaj opakowanie", - "inventory_packageAdded": "Opakowanie dodane.", - "inventory_packageUpdated": "Opakowanie zaktualizowane.", - "inventory_packageDeleted": "Opakowanie usunięte.", - "inventory_alreadyOpened": "Już otwarte", - "inventory_openedDate": "Data otwarcia", - "inventory_finishedDate": "Data skończenia", - "inventory_expiryDate": "Data ważności", - "inventory_currentWeight": "Aktualna waga (g)", - "inventory_lastWeighed": "Ostatnie ważenie", - "inventory_notes": "Notatki", - "inventory_badgeOpen": "Otwarte", - "inventory_badgeSealed": "Zamknięte", - "inventory_badgeFinished": "Skończone", - "inventory_exp": "Wazność:", - "inventory_opened": "Otwarto:", - "inventory_finished": "Skończono:", - "inventory_remaining": "g pozostało", - "inventory_weighed": "Ważono:", - "inventory_confirmDelete": "Usunąć to opakowanie?", + "inventory_title": "Opakowania ({count})", + "inventory_addPackage": "+ Dodaj opakowanie", + "inventory_packageAdded": "Opakowanie dodane.", + "inventory_packageUpdated": "Opakowanie zaktualizowane.", + "inventory_packageDeleted": "Opakowanie usunięte.", + "inventory_alreadyOpened": "Już otwarte", + "inventory_openedDate": "Data otwarcia", + "inventory_finishedDate": "Data skończenia", + "inventory_expiryDate": "Data ważności", + "inventory_currentWeight": "Aktualna waga (g)", + "inventory_lastWeighed": "Ostatnie ważenie", + "inventory_notes": "Notatki", + "inventory_badgeOpen": "Otwarte", + "inventory_badgeSealed": "Zamknięte", + "inventory_badgeFinished": "Skończone", + "inventory_exp": "Wazność:", + "inventory_opened": "Otwarto:", + "inventory_finished": "Skończono:", + "inventory_remaining": "g pozostało", + "inventory_weighed": "Ważono:", + "inventory_confirmDelete": "Usunąć to opakowanie?", - "routines_title": "Rutyny", - "routines_count": "{count} rutyno (ostatnie 30 dni)", - "routines_suggestAI": "Zaproponuj rutynę AI", - "routines_addNew": "+ Nowa rutyna", - "routines_noRoutines": "Nie znaleziono rutyno.", - "routines_newTitle": "Nowa rutyna", - "routines_backToList": "← Rutyny", - "routines_detailsTitle": "Szczegóły rutyny", - "routines_date": "Data *", - "routines_amOrPm": "AM lub PM *", - "routines_notes": "Notatki", - "routines_notesPlaceholder": "Opcjonalne notatki", - "routines_createRoutine": "Utwórz rutynę", - "routines_deleteRoutine": "Usuń rutynę", - "routines_confirmDelete": "Usunąć tę rutynę?", - "routines_steps": "Kroki ({count})", - "routines_addStep": "+ Dodaj krok", - "routines_addStepTitle": "Dodaj krok", - "routines_product": "Produkt", - "routines_selectProduct": "Wybierz produkt", - "routines_dose": "Dawka", - "routines_dosePlaceholder": "np. 2 pompki", - "routines_region": "Okolica", - "routines_regionPlaceholder": "np. twarz", - "routines_addStepBtn": "Dodaj krok", - "routines_unknownStep": "Nieznany krok", - "routines_noSteps": "Brak kroków.", + "routines_title": "Rutyny", + "routines_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} rutyna (ostatnie 30 dni)", + "countPlural=few": "{count} rutyny (ostatnie 30 dni)", + "countPlural=many": "{count} rutyn (ostatnie 30 dni)", + "countPlural=*": "{count} rutyn (ostatnie 30 dni)" + } + } + ], + "routines_suggestAI": "Zaproponuj rutynę AI", + "routines_addNew": "+ Nowa rutyna", + "routines_noRoutines": "Nie znaleziono rutyn.", + "routines_newTitle": "Nowa rutyna", + "routines_backToList": "← Rutyny", + "routines_detailsTitle": "Szczegóły rutyny", + "routines_date": "Data *", + "routines_amOrPm": "AM lub PM *", + "routines_notes": "Notatki", + "routines_notesPlaceholder": "Opcjonalne notatki", + "routines_createRoutine": "Utwórz rutynę", + "routines_deleteRoutine": "Usuń rutynę", + "routines_confirmDelete": "Usunąć tę rutynę?", + "routines_steps": "Kroki ({count})", + "routines_addStep": "+ Dodaj krok", + "routines_addStepTitle": "Dodaj krok", + "routines_product": "Produkt", + "routines_selectProduct": "Wybierz produkt", + "routines_dose": "Dawka", + "routines_dosePlaceholder": "np. 2 pompki", + "routines_region": "Okolica", + "routines_regionPlaceholder": "np. twarz", + "routines_addStepBtn": "Dodaj krok", + "routines_unknownStep": "Nieznany krok", + "routines_noSteps": "Brak kroków.", - "grooming_title": "Harmonogram pielęgnacji", - "grooming_backToRoutines": "← Rutyny", - "grooming_addEntry": "+ Dodaj wpis", - "grooming_entryAdded": "Wpis dodany.", - "grooming_entryUpdated": "Wpis zaktualizowany.", - "grooming_entryDeleted": "Wpis usunięty.", - "grooming_dayOfWeek": "Dzień tygodnia", - "grooming_action": "Czynność", - "grooming_notesOptional": "Notatki (opcjonalnie)", - "grooming_notesPlaceholder": "np. co 2 tygodnie", - "grooming_noEntries": "Brak wpisów. Kliknij \"+ Dodaj wpis\", aby zacząć.", - "grooming_confirmDelete": "Usunąć ten wpis?", - "grooming_actionShavingRazor": "Golenie maszynką", - "grooming_actionShavingOneblade": "Golenie OneBlade", - "grooming_actionDermarolling": "Dermarolling", - "grooming_dayMonday": "Poniedziałek", - "grooming_dayTuesday": "Wtorek", - "grooming_dayWednesday": "Środa", - "grooming_dayThursday": "Czwartek", - "grooming_dayFriday": "Piątek", - "grooming_daySaturday": "Sobota", - "grooming_daySunday": "Niedziela", + "grooming_title": "Harmonogram pielęgnacji", + "grooming_backToRoutines": "← Rutyny", + "grooming_addEntry": "+ Dodaj wpis", + "grooming_entryAdded": "Wpis dodany.", + "grooming_entryUpdated": "Wpis zaktualizowany.", + "grooming_entryDeleted": "Wpis usunięty.", + "grooming_dayOfWeek": "Dzień tygodnia", + "grooming_action": "Czynność", + "grooming_notesOptional": "Notatki (opcjonalnie)", + "grooming_notesPlaceholder": "np. co 2 tygodnie", + "grooming_noEntries": "Brak wpisów. Kliknij \"+ Dodaj wpis\", aby zacząć.", + "grooming_confirmDelete": "Usunąć ten wpis?", + "grooming_actionShavingRazor": "Golenie maszynką", + "grooming_actionShavingOneblade": "Golenie OneBlade", + "grooming_actionDermarolling": "Dermarolling", + "grooming_dayMonday": "Poniedziałek", + "grooming_dayTuesday": "Wtorek", + "grooming_dayWednesday": "Środa", + "grooming_dayThursday": "Czwartek", + "grooming_dayFriday": "Piątek", + "grooming_daySaturday": "Sobota", + "grooming_daySunday": "Niedziela", - "suggest_title": "Propozycja rutyny AI", - "suggest_backToRoutines": "← Rutyny", - "suggest_singleTab": "Jedna rutyna", - "suggest_batchTab": "Batch / Urlop", - "suggest_singleParams": "Parametry", - "suggest_date": "Data", - "suggest_timeOfDay": "Pora dnia", - "suggest_contextLabel": "Dodatkowy kontekst dla AI", - "suggest_contextOptional": "(opcjonalny)", - "suggest_contextPlaceholder": "np. wieczór imprezowy, skupiam się na nawilżeniu...", - "suggest_generateBtn": "Generuj propozycję", - "suggest_generating": "Generuję…", - "suggest_proposalTitle": "Propozycja", - "suggest_saveRoutine": "Zapisz rutynę", - "suggest_saving": "Zapisuję…", - "suggest_regenerate": "Wygeneruj ponownie", - "suggest_batchRange": "Zakres dat", - "suggest_fromDate": "Od", - "suggest_toDate": "Do (max 14 dni)", - "suggest_batchContextLabel": "Kontekst / cel wyjazdu", - "suggest_batchContextPlaceholder": "np. słoneczna podróż do Włoch, aktywny urlop górski...", - "suggest_generatePlan": "Generuj plan", - "suggest_generatingPlan": "Generuję plan…", - "suggest_planTitle": "Plan ({count} dni)", - "suggest_saveAllRoutines": "Zapisz wszystkie rutyny", - "suggest_amSteps": "kroków", - "suggest_pmSteps": "kroków", - "suggest_noAmSteps": "Brak kroków AM.", - "suggest_noPmSteps": "Brak kroków PM.", - "suggest_errorDefault": "Błąd podczas generowania.", - "suggest_errorBatch": "Błąd podczas generowania planu.", - "suggest_errorSave": "Błąd podczas zapisywania.", - "suggest_amMorning": "AM (rano)", - "suggest_pmEvening": "PM (wieczór)", + "suggest_title": "Propozycja rutyny AI", + "suggest_backToRoutines": "← Rutyny", + "suggest_singleTab": "Jedna rutyna", + "suggest_batchTab": "Batch / Urlop", + "suggest_singleParams": "Parametry", + "suggest_date": "Data", + "suggest_timeOfDay": "Pora dnia", + "suggest_contextLabel": "Dodatkowy kontekst dla AI", + "suggest_contextOptional": "(opcjonalny)", + "suggest_contextPlaceholder": "np. wieczór imprezowy, skupiam się na nawilżeniu...", + "suggest_leavingHomeLabel": "Wychodzę dziś z domu", + "suggest_leavingHomeHint": "Wpływa na wybór SPF — zaznaczone: SPF50+, odznaczone: SPF30.", + "suggest_minoxidilToggleLabel": "Priorytet: gęstość brody/wąsów (minoksydyl)", + "suggest_minoxidilToggleHint": "Po włączeniu AI jawnie uwzględni minoksydyl dla obszaru brody/wąsów, jeśli jest dostępny.", + "suggest_generateBtn": "Generuj propozycję", + "suggest_generating": "Generuję…", + "suggest_proposalTitle": "Propozycja", + "suggest_saveRoutine": "Zapisz rutynę", + "suggest_saving": "Zapisuję…", + "suggest_regenerate": "Wygeneruj ponownie", + "suggest_batchRange": "Zakres dat", + "suggest_fromDate": "Od", + "suggest_toDate": "Do (max 14 dni)", + "suggest_batchContextLabel": "Kontekst / cel wyjazdu", + "suggest_batchContextPlaceholder": "np. słoneczna podróż do Włoch, aktywny urlop górski...", + "suggest_generatePlan": "Generuj plan", + "suggest_generatingPlan": "Generuję plan…", + "suggest_planTitle": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "Plan ({count} dzień)", + "countPlural=few": "Plan ({count} dni)", + "countPlural=many": "Plan ({count} dni)", + "countPlural=*": "Plan ({count} dni)" + } + } + ], + "suggest_saveAllRoutines": "Zapisz wszystkie rutyny", + "suggest_amSteps": "kroków", + "suggest_pmSteps": "kroków", + "suggest_noAmSteps": "Brak kroków AM.", + "suggest_noPmSteps": "Brak kroków PM.", + "suggest_errorDefault": "Błąd podczas generowania.", + "suggest_errorBatch": "Błąd podczas generowania planu.", + "suggest_errorSave": "Błąd podczas zapisywania.", + "suggest_amMorning": "AM (rano)", + "suggest_pmEvening": "PM (wieczór)", + "suggest_summaryPrimaryGoal": "Główny cel", + "suggest_summaryConfidence": "Pewność", + "suggest_summaryConstraints": "Ograniczenia", + "suggest_stepOptionalBadge": "opcjonalny", - "medications_title": "Leki", - "medications_count": "{count} wpisów", - "medications_addNew": "+ Dodaj lek", - "medications_newTitle": "Nowy lek", - "medications_kind": "Rodzaj", - "medications_productName": "Nazwa produktu *", - "medications_productNamePlaceholder": "np. Witamina D3", - "medications_activeSubstance": "Substancja czynna", - "medications_activeSubstancePlaceholder": "np. cholekalcyferol", - "medications_notes": "Notatki", - "medications_added": "Lek dodany.", - "medications_usages": "{count} użyć", - "medications_noMedications": "Brak leków.", - "medications_kindPrescription": "Na receptę", - "medications_kindOtc": "OTC (bez recepty)", - "medications_kindSupplement": "Suplement", - "medications_kindHerbal": "Zioła", - "medications_kindOther": "Inne", + "medications_title": "Leki", + "medications_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} wpis", + "countPlural=few": "{count} wpisy", + "countPlural=many": "{count} wpisów", + "countPlural=*": "{count} wpisów" + } + } + ], + "medications_addNew": "+ Dodaj lek", + "medications_newTitle": "Nowy lek", + "medications_kind": "Rodzaj", + "medications_productName": "Nazwa produktu *", + "medications_productNamePlaceholder": "np. Witamina D3", + "medications_activeSubstance": "Substancja czynna", + "medications_activeSubstancePlaceholder": "np. cholekalcyferol", + "medications_notes": "Notatki", + "medications_added": "Lek dodany.", + "medications_usages": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} użycie", + "countPlural=few": "{count} użycia", + "countPlural=many": "{count} użyć", + "countPlural=*": "{count} użyć" + } + } + ], + "medications_noMedications": "Brak leków.", + "medications_kindPrescription": "Na receptę", + "medications_kindOtc": "OTC (bez recepty)", + "medications_kindSupplement": "Suplement", + "medications_kindHerbal": "Zioła", + "medications_kindOther": "Inne", - "labResults_title": "Wyniki badań", - "labResults_count": "{count} wyników", - "labResults_addNew": "+ Dodaj wynik", - "labResults_newTitle": "Nowy wynik badania", - "labResults_flagFilter": "Flaga:", - "labResults_flagAll": "Wszystkie", - "labResults_flagNone": "Brak", - "labResults_date": "Data *", - "labResults_loincCode": "Kod LOINC *", - "labResults_testName": "Nazwa badania", - "labResults_testNamePlaceholder": "np. Hemoglobina", - "labResults_lab": "Laboratorium", - "labResults_labPlaceholder": "np. LabCorp", - "labResults_value": "Wartość", - "labResults_unit": "Jednostka", - "labResults_unitPlaceholder": "np. g/dL", - "labResults_flag": "Flaga", - "labResults_added": "Wynik dodany.", - "labResults_colDate": "Data", - "labResults_colTest": "Badanie", - "labResults_colLoinc": "LOINC", - "labResults_colValue": "Wartość", - "labResults_colFlag": "Flaga", - "labResults_colLab": "Lab", - "labResults_noResults": "Nie znaleziono wyników badań.", + "labResults_title": "Wyniki badań", + "labResults_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} wynik", + "countPlural=few": "{count} wyniki", + "countPlural=many": "{count} wyników", + "countPlural=*": "{count} wyników" + } + } + ], + "labResults_addNew": "+ Dodaj wynik", + "labResults_newTitle": "Nowy wynik badania", + "labResults_flagFilter": "Flaga:", + "labResults_flagAll": "Wszystkie", + "labResults_flagNone": "Brak", + "labResults_date": "Data *", + "labResults_loincCode": "Kod LOINC *", + "labResults_testName": "Nazwa badania", + "labResults_testNamePlaceholder": "np. Hemoglobina", + "labResults_lab": "Laboratorium", + "labResults_labPlaceholder": "np. LabCorp", + "labResults_value": "Wartość", + "labResults_unit": "Jednostka", + "labResults_unitPlaceholder": "np. g/dL", + "labResults_flag": "Flaga", + "labResults_added": "Wynik dodany.", + "labResults_colDate": "Data", + "labResults_colTest": "Badanie", + "labResults_colLoinc": "LOINC", + "labResults_colValue": "Wartość", + "labResults_colFlag": "Flaga", + "labResults_colLab": "Lab", + "labResults_noResults": "Nie znaleziono wyników badań.", - "skin_title": "Stan skóry", - "skin_count": "{count} wpisów", - "skin_addNew": "+ Dodaj wpis", - "skin_aiAnalysisTitle": "Analiza AI ze zdjęć", - "skin_aiUploadText": "Prześlij 1–3 zdjęcia skóry. AI wypełni pola formularza poniżej.", - "skin_analyzePhotos": "Analizuj zdjęcia", - "skin_analyzing": "Analizuję…", - "skin_newSnapshotTitle": "Nowy wpis", - "skin_date": "Data *", - "skin_overallState": "Ogólny stan", - "skin_texture": "Tekstura", - "skin_skinType": "Typ skóry", - "skin_barrierState": "Stan bariery", - "skin_hydration": "Nawilżenie (1–5)", - "skin_sensitivity": "Wrażliwość (1–5)", - "skin_sebumTzone": "Sebum T-zone (1–5)", - "skin_sebumCheeks": "Sebum policzki (1–5)", - "skin_activeConcerns": "Aktywne problemy (przecinek)", - "skin_activeConcernsPlaceholder": "trądzik, zaczerwienienie, odwodnienie", - "skin_notes": "Notatki", - "skin_addSnapshot": "Dodaj wpis", - "skin_snapshotAdded": "Wpis dodany.", - "skin_snapshotUpdated": "Wpis zaktualizowany.", - "skin_snapshotDeleted": "Wpis usunięty.", - "skin_noSnapshots": "Brak wpisów o stanie skóry.", - "skin_hydrationLabel": "Nawilżenie", - "skin_sensitivityLabel": "Wrażliwość", - "skin_barrierLabel": "Bariera", - "skin_stateExcellent": "doskonały", - "skin_stateGood": "dobry", - "skin_stateFair": "przeciętny", - "skin_statePoor": "zły", - "skin_textureSmooth": "gładka", - "skin_textureRough": "szorstka", - "skin_textureFlaky": "łuszcząca się", - "skin_textureBumpy": "nierówna", - "skin_barrierIntact": "nienaruszona", - "skin_barrierMildly": "lekko naruszona", - "skin_barrierCompromised": "naruszona", - "skin_typeDry": "sucha", - "skin_typeOily": "tłusta", - "skin_typeCombination": "mieszana", - "skin_typeSensitive": "wrażliwa", - "skin_typeNormal": "normalna", - "skin_typeAcneProne": "trądzikowa", + "skin_title": "Stan skóry", + "skin_count": [ + { + "declarations": ["input count", "local countPlural = count: plural"], + "selectors": ["countPlural"], + "match": { + "countPlural=one": "{count} wpis", + "countPlural=few": "{count} wpisy", + "countPlural=many": "{count} wpisów", + "countPlural=*": "{count} wpisów" + } + } + ], + "skin_addNew": "+ Dodaj wpis", + "skin_aiAnalysisTitle": "Analiza AI ze zdjęć", + "skin_aiUploadText": "Prześlij 1–3 zdjęcia skóry. AI wypełni pola formularza poniżej.", + "skin_analyzePhotos": "Analizuj zdjęcia", + "skin_analyzing": "Analizuję…", + "skin_newSnapshotTitle": "Nowy wpis", + "skin_date": "Data *", + "skin_overallState": "Ogólny stan", + "skin_texture": "Tekstura", + "skin_skinType": "Typ skóry", + "skin_barrierState": "Stan bariery", + "skin_hydration": "Nawilżenie (1–5)", + "skin_sensitivity": "Wrażliwość (1–5)", + "skin_sebumTzone": "Sebum T-zone (1–5)", + "skin_sebumCheeks": "Sebum policzki (1–5)", + "skin_activeConcerns": "Aktywne problemy (przecinek)", + "skin_activeConcernsPlaceholder": "trądzik, zaczerwienienie, odwodnienie", + "skin_priorities": "Priorytety (przecinek)", + "skin_prioritiesPlaceholder": "wzmocnić barierę, redukować zaczerwienienie", + "skin_prioritiesLabel": "Priorytety", + "skin_notes": "Notatki", + "skin_addSnapshot": "Dodaj wpis", + "skin_snapshotAdded": "Wpis dodany.", + "skin_snapshotUpdated": "Wpis zaktualizowany.", + "skin_snapshotDeleted": "Wpis usunięty.", + "skin_noSnapshots": "Brak wpisów o stanie skóry.", + "skin_hydrationLabel": "Nawilżenie", + "skin_sensitivityLabel": "Wrażliwość", + "skin_barrierLabel": "Bariera", + "skin_stateExcellent": "doskonały", + "skin_stateGood": "dobry", + "skin_stateFair": "przeciętny", + "skin_statePoor": "zły", + "skin_textureSmooth": "gładka", + "skin_textureRough": "szorstka", + "skin_textureFlaky": "łuszcząca się", + "skin_textureBumpy": "nierówna", + "skin_barrierIntact": "nienaruszona", + "skin_barrierMildly": "lekko naruszona", + "skin_barrierCompromised": "naruszona", + "skin_typeDry": "sucha", + "skin_typeOily": "tłusta", + "skin_typeCombination": "mieszana", + "skin_typeSensitive": "wrażliwa", + "skin_typeNormal": "normalna", + "skin_typeAcneProne": "trądzikowa", - "productForm_aiPrefill": "Uzupełnienie AI", - "productForm_aiPrefillText": "Wklej opis produktu ze strony, listę składników lub inny tekst. AI uzupełni dostępne pola — możesz je przejrzeć i poprawić przed zapisem.", - "productForm_pasteText": "Wklej tutaj opis produktu, składniki INCI...", - "productForm_parseWithAI": "Uzupełnij pola (AI)", - "productForm_parsing": "Przetwarzam…", - "productForm_basicInfo": "Informacje podstawowe", - "productForm_name": "Nazwa *", - "productForm_namePlaceholder": "np. Hydro Boost Water Gel", - "productForm_brand": "Marka *", - "productForm_brandPlaceholder": "np. Neutrogena", - "productForm_lineName": "Linia / seria", - "productForm_lineNamePlaceholder": "np. Hydro Boost", - "productForm_url": "URL", - "productForm_sku": "SKU", - "productForm_skuPlaceholder": "np. NTR-HB-50", - "productForm_barcode": "Kod kreskowy / EAN", - "productForm_barcodePlaceholder": "np. 3614273258975", - "productForm_classification": "Klasyfikacja", - "productForm_category": "Kategoria *", - "productForm_selectCategory": "Wybierz kategorię", - "productForm_time": "Pora *", - "productForm_timeOptions": "AM / PM / Oba", - "productForm_timeBoth": "Oba", - "productForm_leaveOn": "Leave-on *", - "productForm_leaveOnYes": "Tak (leave-on)", - "productForm_leaveOnNo": "Nie (rinse-off)", - "productForm_texture": "Tekstura", - "productForm_selectTexture": "Wybierz teksturę", - "productForm_absorptionSpeed": "Szybkość wchłaniania", - "productForm_selectSpeed": "Wybierz szybkość", - "productForm_skinProfile": "Profil skóry", - "productForm_recommendedFor": "Polecane dla typów skóry", - "productForm_targetConcerns": "Problemy docelowe", - "productForm_contraindications": "Przeciwwskazania (jedno na linię)", - "productForm_contraindicationsPlaceholder": "np. aktywna rosacea", - "productForm_ingredients": "Składniki", - "productForm_inciList": "Lista INCI (jeden składnik na linię)", - "productForm_activeIngredients": "Składniki aktywne", - "productForm_addActive": "+ Dodaj aktywny", - "productForm_noActives": "Brak składników aktywnych.", - "productForm_activeName": "Nazwa", - "productForm_activePercent": "%", - "productForm_activeStrength": "Siła", - "productForm_activeIrritation": "Podrażnienie", - "productForm_activeFunctions": "Funkcje", - "productForm_effectProfile": "Profil działania (0–5)", - "productForm_interactions": "Interakcje", - "productForm_synergizesWith": "Synergizuje z (jedno na linię)", - "productForm_incompatibleWith": "Niekompatybilny z", - "productForm_addIncompatibility": "+ Dodaj niekompatybilność", - "productForm_noIncompatibilities": "Brak niekompatybilności.", - "productForm_incompTarget": "Składnik docelowy", - "productForm_incompScope": "Zakres", - "productForm_incompReason": "Powód (opcjonalny)", - "productForm_incompReasonPlaceholder": "np. zmniejsza skuteczność", - "productForm_incompScopeSelect": "Wybierz…", - "productForm_contextRules": "Reguły kontekstu", - "productForm_ctxAfterShaving": "Bezpieczny po goleniu", - "productForm_ctxAfterAcids": "Bezpieczny po kwasach", - "productForm_ctxAfterRetinoids": "Bezpieczny po retinoidach", - "productForm_ctxCompromisedBarrier": "Bezpieczny przy naruszonej barierze", - "productForm_ctxLowUvOnly": "Tylko przy niskim UV (wieczór/zakrycie)", - "productForm_productDetails": "Szczegóły produktu", - "productForm_priceTier": "Przedział cenowy", - "productForm_selectTier": "Wybierz przedział", - "productForm_sizeMl": "Rozmiar (ml)", - "productForm_fullWeightG": "Waga pełna (g)", - "productForm_emptyWeightG": "Waga pustego (g)", - "productForm_paoMonths": "PAO (miesiące)", - "productForm_phMin": "pH min", - "productForm_phMax": "pH max", - "productForm_usageNotes": "Notatki o stosowaniu", - "productForm_usageNotesPlaceholder": "np. Nakładaj na wilgotną skórę, unikaj okolic oczu", - "productForm_safetyFlags": "Flagi bezpieczeństwa", - "productForm_fragranceFree": "Bez zapachów", - "productForm_essentialOilsFree": "Bez olejków eterycznych", - "productForm_alcoholDenatFree": "Bez alkoholu denat.", - "productForm_pregnancySafe": "Bezpieczny w ciąży", - "productForm_usageConstraints": "Ograniczenia stosowania", - "productForm_minIntervalHours": "Min. przerwa (godziny)", - "productForm_maxFrequencyPerWeek": "Max użyć na tydzień", - "productForm_isMedication": "To lek", - "productForm_isTool": "To narzędzie (np. dermaroller)", - "productForm_needleLengthMm": "Długość igły (mm, tylko narzędzia)", - "productForm_personalNotes": "Notatki osobiste", - "productForm_repurchaseIntent": "Zamiar ponownego zakupu", - "productForm_toleranceNotes": "Notatki o tolerancji", - "productForm_toleranceNotesPlaceholder": "np. Lekkie pieczenie, ustępuje po 2 tygodniach", + "productForm_aiPrefill": "Uzupełnienie AI", + "productForm_aiPrefillText": "Wklej opis produktu ze strony, listę składników lub inny tekst. AI uzupełni dostępne pola — możesz je przejrzeć i poprawić przed zapisem.", + "productForm_pasteText": "Wklej tutaj opis produktu, składniki INCI...", + "productForm_parseWithAI": "Uzupełnij pola (AI)", + "productForm_parsing": "Przetwarzam…", + "productForm_basicInfo": "Informacje podstawowe", + "productForm_name": "Nazwa *", + "productForm_namePlaceholder": "np. Hydro Boost Water Gel", + "productForm_brand": "Marka *", + "productForm_brandPlaceholder": "np. Neutrogena", + "productForm_lineName": "Linia / seria", + "productForm_lineNamePlaceholder": "np. Hydro Boost", + "productForm_url": "URL", + "productForm_sku": "SKU", + "productForm_skuPlaceholder": "np. NTR-HB-50", + "productForm_barcode": "Kod kreskowy / EAN", + "productForm_barcodePlaceholder": "np. 3614273258975", + "productForm_classification": "Klasyfikacja", + "productForm_category": "Kategoria *", + "productForm_selectCategory": "Wybierz kategorię", + "productForm_time": "Pora *", + "productForm_timeOptions": "AM / PM / Oba", + "productForm_timeBoth": "Oba", + "productForm_leaveOn": "Leave-on *", + "productForm_leaveOnYes": "Tak (leave-on)", + "productForm_leaveOnNo": "Nie (rinse-off)", + "productForm_texture": "Tekstura", + "productForm_selectTexture": "Wybierz teksturę", + "productForm_absorptionSpeed": "Szybkość wchłaniania", + "productForm_selectSpeed": "Wybierz szybkość", + "productForm_skinProfile": "Profil skóry", + "productForm_recommendedFor": "Polecane dla typów skóry", + "productForm_targetConcerns": "Problemy docelowe", + "productForm_contraindications": "Przeciwwskazania (jedno na linię)", + "productForm_contraindicationsPlaceholder": "np. aktywna rosacea", + "productForm_ingredients": "Składniki", + "productForm_inciList": "Lista INCI (jeden składnik na linię)", + "productForm_activeIngredients": "Składniki aktywne", + "productForm_addActive": "+ Dodaj aktywny", + "productForm_noActives": "Brak składników aktywnych.", + "productForm_activeName": "Nazwa", + "productForm_activePercent": "%", + "productForm_activeStrength": "Siła", + "productForm_activeIrritation": "Podrażnienie", + "productForm_activeFunctions": "Funkcje", + "productForm_effectProfile": "Profil działania (0–5)", + "productForm_interactions": "Interakcje", + "productForm_synergizesWith": "Synergizuje z (jedno na linię)", + "productForm_incompatibleWith": "Niekompatybilny z", + "productForm_addIncompatibility": "+ Dodaj niekompatybilność", + "productForm_noIncompatibilities": "Brak niekompatybilności.", + "productForm_incompTarget": "Składnik docelowy", + "productForm_incompScope": "Zakres", + "productForm_incompReason": "Powód (opcjonalny)", + "productForm_incompReasonPlaceholder": "np. zmniejsza skuteczność", + "productForm_incompScopeSelect": "Wybierz…", + "productForm_contextRules": "Reguły kontekstu", + "productForm_ctxAfterShaving": "Bezpieczny po goleniu", + "productForm_ctxAfterAcids": "Bezpieczny po kwasach", + "productForm_ctxAfterRetinoids": "Bezpieczny po retinoidach", + "productForm_ctxCompromisedBarrier": "Bezpieczny przy naruszonej barierze", + "productForm_ctxLowUvOnly": "Tylko przy niskim UV (wieczór/zakrycie)", + "productForm_productDetails": "Szczegóły produktu", + "productForm_priceTier": "Przedział cenowy", + "productForm_selectTier": "Wybierz przedział", + "productForm_sizeMl": "Rozmiar (ml)", + "productForm_fullWeightG": "Waga pełna (g)", + "productForm_emptyWeightG": "Waga pustego (g)", + "productForm_paoMonths": "PAO (miesiące)", + "productForm_phMin": "pH min", + "productForm_phMax": "pH max", + "productForm_usageNotes": "Notatki o stosowaniu", + "productForm_usageNotesPlaceholder": "np. Nakładaj na wilgotną skórę, unikaj okolic oczu", + "productForm_safetyFlags": "Flagi bezpieczeństwa", + "productForm_fragranceFree": "Bez zapachów", + "productForm_essentialOilsFree": "Bez olejków eterycznych", + "productForm_alcoholDenatFree": "Bez alkoholu denat.", + "productForm_pregnancySafe": "Bezpieczny w ciąży", + "productForm_usageConstraints": "Ograniczenia stosowania", + "productForm_minIntervalHours": "Min. przerwa (godziny)", + "productForm_maxFrequencyPerWeek": "Max użyć na tydzień", + "productForm_isMedication": "To lek", + "productForm_isTool": "To narzędzie (np. dermaroller)", + "productForm_needleLengthMm": "Długość igły (mm, tylko narzędzia)", + "productForm_personalNotes": "Notatki osobiste", + "productForm_repurchaseIntent": "Zamiar ponownego zakupu", + "productForm_toleranceNotes": "Notatki o tolerancji", + "productForm_toleranceNotesPlaceholder": "np. Lekkie pieczenie, ustępuje po 2 tygodniach", - "productForm_categoryCleanser": "Żel/pianka do mycia", - "productForm_categoryToner": "Tonik", - "productForm_categoryEssence": "Esencja", - "productForm_categorySerum": "Serum", - "productForm_categoryMoisturizer": "Krem", - "productForm_categorySpf": "SPF", - "productForm_categoryMask": "Maska", - "productForm_categoryExfoliant": "Peeling", - "productForm_categoryHairTreatment": "Pielęgnacja włosów", - "productForm_categoryTool": "Narzędzie", - "productForm_categorySpotTreatment": "Punkt leczenia", - "productForm_categoryOil": "Olejek", + "productForm_categoryCleanser": "Żel/pianka do mycia", + "productForm_categoryToner": "Tonik", + "productForm_categoryEssence": "Esencja", + "productForm_categorySerum": "Serum", + "productForm_categoryMoisturizer": "Krem", + "productForm_categorySpf": "SPF", + "productForm_categoryMask": "Maska", + "productForm_categoryExfoliant": "Peeling", + "productForm_categoryHairTreatment": "Pielęgnacja włosów", + "productForm_categoryTool": "Narzędzie", + "productForm_categorySpotTreatment": "Punkt leczenia", + "productForm_categoryOil": "Olejek", - "productForm_textureWatery": "Wodnista", - "productForm_textureGel": "Żel", - "productForm_textureEmulsion": "Emulsja", - "productForm_textureCream": "Krem", - "productForm_textureOil": "Olejek", - "productForm_textureBalm": "Balsam", - "productForm_textureFoam": "Pianka", - "productForm_textureFluid": "Fluid", + "productForm_textureWatery": "Wodnista", + "productForm_textureGel": "Żel", + "productForm_textureEmulsion": "Emulsja", + "productForm_textureCream": "Krem", + "productForm_textureOil": "Olejek", + "productForm_textureBalm": "Balsam", + "productForm_textureFoam": "Pianka", + "productForm_textureFluid": "Fluid", - "productForm_absorptionVeryFast": "Bardzo szybkie", - "productForm_absorptionFast": "Szybkie", - "productForm_absorptionModerate": "Umiarkowane", - "productForm_absorptionSlow": "Wolne", - "productForm_absorptionVerySlow": "Bardzo wolne", + "productForm_absorptionVeryFast": "Bardzo szybkie", + "productForm_absorptionFast": "Szybkie", + "productForm_absorptionModerate": "Umiarkowane", + "productForm_absorptionSlow": "Wolne", + "productForm_absorptionVerySlow": "Bardzo wolne", - "productForm_priceBudget": "Budżetowy", - "productForm_priceMid": "Średni", - "productForm_pricePremium": "Premium", - "productForm_priceLuxury": "Luksusowy", + "productForm_priceBudget": "Budżetowy", + "productForm_priceMid": "Średni", + "productForm_pricePremium": "Premium", + "productForm_priceLuxury": "Luksusowy", - "productForm_skinTypeDry": "sucha", - "productForm_skinTypeOily": "tłusta", - "productForm_skinTypeCombination": "mieszana", - "productForm_skinTypeSensitive": "wrażliwa", - "productForm_skinTypeNormal": "normalna", - "productForm_skinTypeAcneProne": "trądzikowa", + "productForm_skinTypeDry": "sucha", + "productForm_skinTypeOily": "tłusta", + "productForm_skinTypeCombination": "mieszana", + "productForm_skinTypeSensitive": "wrażliwa", + "productForm_skinTypeNormal": "normalna", + "productForm_skinTypeAcneProne": "trądzikowa", - "productForm_concernAcne": "trądzik", - "productForm_concernRosacea": "rosacea", - "productForm_concernHyperpigmentation": "przebarwienia", - "productForm_concernAging": "starzenie", - "productForm_concernDehydration": "odwodnienie", - "productForm_concernRedness": "zaczerwienienie", - "productForm_concernDamagedBarrier": "naruszona bariera", - "productForm_concernPoreVisibility": "widoczność porów", - "productForm_concernUnevenTexture": "nierówna tekstura", - "productForm_concernHairGrowth": "wzrost włosów", - "productForm_concernSebumExcess": "nadmiar sebum", + "productForm_concernAcne": "trądzik", + "productForm_concernRosacea": "rosacea", + "productForm_concernHyperpigmentation": "przebarwienia", + "productForm_concernAging": "starzenie", + "productForm_concernDehydration": "odwodnienie", + "productForm_concernRedness": "zaczerwienienie", + "productForm_concernDamagedBarrier": "naruszona bariera", + "productForm_concernPoreVisibility": "widoczność porów", + "productForm_concernUnevenTexture": "nierówna tekstura", + "productForm_concernHairGrowth": "wzrost włosów", + "productForm_concernSebumExcess": "nadmiar sebum", - "productForm_fnHumectant": "humektant", - "productForm_fnEmollient": "emolient", - "productForm_fnOcclusive": "okluzja", - "productForm_fnExfoliantAha": "peeling AHA", - "productForm_fnExfoliantBha": "peeling BHA", - "productForm_fnExfoliantPha": "peeling PHA", - "productForm_fnRetinoid": "retinoid", - "productForm_fnAntioxidant": "antyoksydant", - "productForm_fnSoothing": "łagodzący", - "productForm_fnBarrierSupport": "wsparcie bariery", - "productForm_fnBrightening": "rozjaśniający", - "productForm_fnAntiAcne": "przeciwtrądzikowy", - "productForm_fnCeramide": "ceramid", - "productForm_fnNiacinamide": "niacynamid", - "productForm_fnSunscreen": "filtr UV", - "productForm_fnPeptide": "peptyd", - "productForm_fnHairGrowth": "stymulator wzrostu włosów", - "productForm_fnPrebiotic": "prebiotyk", - "productForm_fnVitaminC": "witamina C", - "productForm_fnAntiAging": "przeciwstarzeniowy", + "productForm_fnHumectant": "humektant", + "productForm_fnEmollient": "emolient", + "productForm_fnOcclusive": "okluzja", + "productForm_fnExfoliantAha": "peeling AHA", + "productForm_fnExfoliantBha": "peeling BHA", + "productForm_fnExfoliantPha": "peeling PHA", + "productForm_fnRetinoid": "retinoid", + "productForm_fnAntioxidant": "antyoksydant", + "productForm_fnSoothing": "łagodzący", + "productForm_fnBarrierSupport": "wsparcie bariery", + "productForm_fnBrightening": "rozjaśniający", + "productForm_fnAntiAcne": "przeciwtrądzikowy", + "productForm_fnCeramide": "ceramid", + "productForm_fnNiacinamide": "niacynamid", + "productForm_fnSunscreen": "filtr UV", + "productForm_fnPeptide": "peptyd", + "productForm_fnHairGrowth": "stymulator wzrostu włosów", + "productForm_fnPrebiotic": "prebiotyk", + "productForm_fnVitaminC": "witamina C", + "productForm_fnAntiAging": "przeciwstarzeniowy", - "productForm_scopeSameStep": "ten sam krok", - "productForm_scopeSameDay": "ten sam dzień", - "productForm_scopeSamePeriod": "ten sam okres", + "productForm_scopeSameStep": "ten sam krok", + "productForm_scopeSameDay": "ten sam dzień", + "productForm_scopeSamePeriod": "ten sam okres", - "productForm_strengthLow": "1 Niskie", - "productForm_strengthMedium": "2 Średnie", - "productForm_strengthHigh": "3 Wysokie", + "productForm_strengthLow": "1 Niskie", + "productForm_strengthMedium": "2 Średnie", + "productForm_strengthHigh": "3 Wysokie", - "productForm_effectHydrationImmediate": "Nawilżenie (natychmiastowe)", - "productForm_effectHydrationLongTerm": "Nawilżenie (długoterminowe)", - "productForm_effectBarrierRepair": "Naprawa bariery", - "productForm_effectSoothing": "Łagodzenie", - "productForm_effectExfoliation": "Złuszczanie", - "productForm_effectRetinoid": "Aktywność retinoidu", - "productForm_effectIrritation": "Ryzyko podrażnienia", - "productForm_effectComedogenic": "Ryzyko komedogenności", - "productForm_effectBarrierDisruption": "Ryzyko naruszenia bariery", - "productForm_effectDryness": "Ryzyko przesuszenia", - "productForm_effectBrightening": "Rozjaśnienie", - "productForm_effectAntiAcne": "Działanie przeciwtrądzikowe", - "productForm_effectAntiAging": "Działanie przeciwstarzeniowe", + "productForm_effectHydrationImmediate": "Nawilżenie (natychmiastowe)", + "productForm_effectHydrationLongTerm": "Nawilżenie (długoterminowe)", + "productForm_effectBarrierRepair": "Naprawa bariery", + "productForm_effectSoothing": "Łagodzenie", + "productForm_effectExfoliation": "Złuszczanie", + "productForm_effectRetinoid": "Aktywność retinoidu", + "productForm_effectIrritation": "Ryzyko podrażnienia", + "productForm_effectComedogenic": "Ryzyko komedogenności", + "productForm_effectBarrierDisruption": "Ryzyko naruszenia bariery", + "productForm_effectDryness": "Ryzyko przesuszenia", + "productForm_effectBrightening": "Rozjaśnienie", + "productForm_effectAntiAcne": "Działanie przeciwtrądzikowe", + "productForm_effectAntiAging": "Działanie przeciwstarzeniowe", - "lang_pl": "PL", - "lang_en": "EN" + "lang_pl": "PL", + "lang_en": "EN" } diff --git a/frontend/package.json b/frontend/package.json index 0d75051..b3169c9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,36 +1,47 @@ { - "name": "frontend", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" - }, - "devDependencies": { - "@internationalized/date": "^3.11.0", - "@lucide/svelte": "^0.561.0", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.50.2", - "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@tailwindcss/vite": "^4.2.1", - "svelte": "^5.51.0", - "svelte-check": "^4.3.6", - "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.2.1", - "typescript": "^5.9.3", - "vite": "^7.3.1" - }, - "dependencies": { - "@inlang/paraglide-js": "^2.13.0", - "bits-ui": "^2.16.2", - "clsx": "^2.1.1", - "lucide-svelte": "^0.575.0", - "mode-watcher": "^1.1.0", - "tailwind-merge": "^3.5.0" - } + "name": "frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@internationalized/date": "^3.11.0", + "@lucide/svelte": "^0.561.0", + "@sveltejs/adapter-node": "^5.0.0", + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/vite": "^4.2.1", + "eslint": "^10.0.2", + "eslint-plugin-svelte": "^3.15.0", + "globals": "^17.4.0", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.5.0", + "svelte": "^5.51.0", + "svelte-check": "^4.3.6", + "svelte-eslint-parser": "^1.5.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.2.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^7.3.1" + }, + "dependencies": { + "@inlang/paraglide-js": "^2.13.0", + "bits-ui": "^2.16.2", + "clsx": "^2.1.1", + "lucide-svelte": "^0.575.0", + "mode-watcher": "^1.1.0", + "svelte-dnd-action": "^0.9.69", + "tailwind-merge": "^3.5.0" + } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index f5aa872..5bfa6f0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,14 +1,13 @@ -lockfileVersion: '9.0' +lockfileVersion: "9.0" settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: - .: dependencies: - '@inlang/paraglide-js': + "@inlang/paraglide-js": specifier: ^2.13.0 version: 2.13.0 bits-ui: @@ -23,34 +22,58 @@ importers: mode-watcher: specifier: ^1.1.0 version: 1.1.0(svelte@5.53.5) + svelte-dnd-action: + specifier: ^0.9.69 + version: 0.9.69(svelte@5.53.5) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 devDependencies: - '@internationalized/date': + "@eslint/js": + specifier: ^10.0.1 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) + "@internationalized/date": specifier: ^3.11.0 version: 3.11.0 - '@lucide/svelte': + "@lucide/svelte": specifier: ^0.561.0 version: 0.561.0(svelte@5.53.5) - '@sveltejs/adapter-node': + "@sveltejs/adapter-node": specifier: ^5.0.0 version: 5.5.4(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))) - '@sveltejs/kit': + "@sveltejs/kit": specifier: ^2.50.2 version: 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) - '@sveltejs/vite-plugin-svelte': + "@sveltejs/vite-plugin-svelte": specifier: ^6.2.4 version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) - '@tailwindcss/vite': + "@tailwindcss/vite": specifier: ^4.2.1 version: 4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) + eslint: + specifier: ^10.0.2 + version: 10.0.2(jiti@2.6.1) + eslint-plugin-svelte: + specifier: ^3.15.0 + version: 3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.5) + globals: + specifier: ^17.4.0 + version: 17.4.0 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + prettier-plugin-svelte: + specifier: ^3.5.0 + version: 3.5.0(prettier@3.8.1)(svelte@5.53.5) svelte: specifier: ^5.51.0 version: 5.53.5 svelte-check: specifier: ^4.3.6 version: 4.4.4(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3) + svelte-eslint-parser: + specifier: ^1.5.1 + version: 1.5.1(svelte@5.53.5) tailwind-variants: specifier: ^3.2.2 version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1) @@ -60,1005 +83,2324 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + typescript-eslint: + specifier: ^8.56.1 + version: 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.3.1 version: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1) packages: - - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} + "@esbuild/aix-ppc64@0.27.3": + resolution: + { + integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} + "@esbuild/android-arm64@0.27.3": + resolution: + { + integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} + "@esbuild/android-arm@0.27.3": + resolution: + { + integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==, + } + engines: { node: ">=18" } cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} + "@esbuild/android-x64@0.27.3": + resolution: + { + integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} + "@esbuild/darwin-arm64@0.27.3": + resolution: + { + integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} + "@esbuild/darwin-x64@0.27.3": + resolution: + { + integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==, + } + engines: { node: ">=18" } cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} + "@esbuild/freebsd-arm64@0.27.3": + resolution: + { + integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==, + } + engines: { node: ">=18" } cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} + "@esbuild/freebsd-x64@0.27.3": + resolution: + { + integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==, + } + engines: { node: ">=18" } cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} + "@esbuild/linux-arm64@0.27.3": + resolution: + { + integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} + "@esbuild/linux-arm@0.27.3": + resolution: + { + integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==, + } + engines: { node: ">=18" } cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} + "@esbuild/linux-ia32@0.27.3": + resolution: + { + integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==, + } + engines: { node: ">=18" } cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} + "@esbuild/linux-loong64@0.27.3": + resolution: + { + integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==, + } + engines: { node: ">=18" } cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} + "@esbuild/linux-mips64el@0.27.3": + resolution: + { + integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==, + } + engines: { node: ">=18" } cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} + "@esbuild/linux-ppc64@0.27.3": + resolution: + { + integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} + "@esbuild/linux-riscv64@0.27.3": + resolution: + { + integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==, + } + engines: { node: ">=18" } cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} + "@esbuild/linux-s390x@0.27.3": + resolution: + { + integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==, + } + engines: { node: ">=18" } cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} + "@esbuild/linux-x64@0.27.3": + resolution: + { + integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==, + } + engines: { node: ">=18" } cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} + "@esbuild/netbsd-arm64@0.27.3": + resolution: + { + integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==, + } + engines: { node: ">=18" } cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} + "@esbuild/netbsd-x64@0.27.3": + resolution: + { + integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==, + } + engines: { node: ">=18" } cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} + "@esbuild/openbsd-arm64@0.27.3": + resolution: + { + integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} + "@esbuild/openbsd-x64@0.27.3": + resolution: + { + integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} + "@esbuild/openharmony-arm64@0.27.3": + resolution: + { + integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} + "@esbuild/sunos-x64@0.27.3": + resolution: + { + integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==, + } + engines: { node: ">=18" } cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} + "@esbuild/win32-arm64@0.27.3": + resolution: + { + integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==, + } + engines: { node: ">=18" } cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} + "@esbuild/win32-ia32@0.27.3": + resolution: + { + integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==, + } + engines: { node: ">=18" } cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} + "@esbuild/win32-x64@0.27.3": + resolution: + { + integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==, + } + engines: { node: ">=18" } cpu: [x64] os: [win32] - '@floating-ui/core@1.7.4': - resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + "@eslint-community/eslint-utils@4.9.1": + resolution: + { + integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@floating-ui/dom@1.7.5': - resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + "@eslint-community/regexpp@4.12.2": + resolution: + { + integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + "@eslint/config-array@0.23.2": + resolution: + { + integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } - '@inlang/paraglide-js@2.13.0': - resolution: {integrity: sha512-m7JQiTeLC3tY3DusUCc4iRWlsKoMuDLhw4iGhkY0yI96ki7PK42DLsi1kMk8ubSVenKOwgrs7eqQZN1Htvkhew==} + "@eslint/config-helpers@0.5.2": + resolution: + { + integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + "@eslint/core@1.1.0": + resolution: + { + integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + "@eslint/js@10.0.1": + resolution: + { + integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + "@eslint/object-schema@3.0.2": + resolution: + { + integrity: sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + "@eslint/plugin-kit@0.6.0": + resolution: + { + integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + "@floating-ui/core@1.7.4": + resolution: + { + integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==, + } + + "@floating-ui/dom@1.7.5": + resolution: + { + integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==, + } + + "@floating-ui/utils@0.2.10": + resolution: + { + integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==, + } + + "@humanfs/core@0.19.1": + resolution: + { + integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, + } + engines: { node: ">=18.18.0" } + + "@humanfs/node@0.16.7": + resolution: + { + integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==, + } + engines: { node: ">=18.18.0" } + + "@humanwhocodes/module-importer@1.0.1": + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: ">=12.22" } + + "@humanwhocodes/retry@0.4.3": + resolution: + { + integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, + } + engines: { node: ">=18.18" } + + "@inlang/paraglide-js@2.13.0": + resolution: + { + integrity: sha512-m7JQiTeLC3tY3DusUCc4iRWlsKoMuDLhw4iGhkY0yI96ki7PK42DLsi1kMk8ubSVenKOwgrs7eqQZN1Htvkhew==, + } hasBin: true - '@inlang/recommend-sherlock@0.2.1': - resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==} + "@inlang/recommend-sherlock@0.2.1": + resolution: + { + integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==, + } - '@inlang/sdk@2.7.0': - resolution: {integrity: sha512-yJNBD0o8i29TTJqWX5uDRHxnalDGcsUDctxepzFXsUfkzqGWfiFBxODdxvReqvM2CuKAAOo/kib/F1UcgdYFNQ==} - engines: {node: '>=18.0.0'} + "@inlang/sdk@2.7.0": + resolution: + { + integrity: sha512-yJNBD0o8i29TTJqWX5uDRHxnalDGcsUDctxepzFXsUfkzqGWfiFBxODdxvReqvM2CuKAAOo/kib/F1UcgdYFNQ==, + } + engines: { node: ">=18.0.0" } - '@internationalized/date@3.11.0': - resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} + "@internationalized/date@3.11.0": + resolution: + { + integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==, + } - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + "@jridgewell/gen-mapping@0.3.13": + resolution: + { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + } - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + "@jridgewell/remapping@2.3.5": + resolution: + { + integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, + } - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + "@jridgewell/trace-mapping@0.3.31": + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + } - '@lix-js/sdk@0.4.7': - resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==} - engines: {node: '>=18'} + "@lix-js/sdk@0.4.7": + resolution: + { + integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==, + } + engines: { node: ">=18" } - '@lix-js/server-protocol-schema@0.1.1': - resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} + "@lix-js/server-protocol-schema@0.1.1": + resolution: + { + integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==, + } - '@lucide/svelte@0.561.0': - resolution: {integrity: sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==} + "@lucide/svelte@0.561.0": + resolution: + { + integrity: sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==, + } peerDependencies: svelte: ^5 - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + "@polka/url@1.0.0-next.29": + resolution: + { + integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, + } - '@rollup/plugin-commonjs@29.0.0': - resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} + "@rollup/plugin-commonjs@29.0.0": + resolution: + { + integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==, + } + engines: { node: ">=16.0.0 || 14 >= 14.17" } peerDependencies: rollup: ^2.68.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} + "@rollup/plugin-json@6.1.0": + resolution: + { + integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/plugin-node-resolve@16.0.3': - resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} - engines: {node: '>=14.0.0'} + "@rollup/plugin-node-resolve@16.0.3": + resolution: + { + integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^2.78.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} + "@rollup/pluginutils@5.3.0": + resolution: + { + integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==, + } + engines: { node: ">=14.0.0" } peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + "@rollup/rollup-android-arm-eabi@4.59.0": + resolution: + { + integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==, + } cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + "@rollup/rollup-android-arm64@4.59.0": + resolution: + { + integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==, + } cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + "@rollup/rollup-darwin-arm64@4.59.0": + resolution: + { + integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==, + } cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + "@rollup/rollup-darwin-x64@4.59.0": + resolution: + { + integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==, + } cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + "@rollup/rollup-freebsd-arm64@4.59.0": + resolution: + { + integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==, + } cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + "@rollup/rollup-freebsd-x64@4.59.0": + resolution: + { + integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==, + } cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + "@rollup/rollup-linux-arm-gnueabihf@4.59.0": + resolution: + { + integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==, + } cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + "@rollup/rollup-linux-arm-musleabihf@4.59.0": + resolution: + { + integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==, + } cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + "@rollup/rollup-linux-arm64-gnu@4.59.0": + resolution: + { + integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==, + } cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + "@rollup/rollup-linux-arm64-musl@4.59.0": + resolution: + { + integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==, + } cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + "@rollup/rollup-linux-loong64-gnu@4.59.0": + resolution: + { + integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==, + } cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + "@rollup/rollup-linux-loong64-musl@4.59.0": + resolution: + { + integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==, + } cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + "@rollup/rollup-linux-ppc64-gnu@4.59.0": + resolution: + { + integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==, + } cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + "@rollup/rollup-linux-ppc64-musl@4.59.0": + resolution: + { + integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==, + } cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + "@rollup/rollup-linux-riscv64-gnu@4.59.0": + resolution: + { + integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==, + } cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + "@rollup/rollup-linux-riscv64-musl@4.59.0": + resolution: + { + integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==, + } cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + "@rollup/rollup-linux-s390x-gnu@4.59.0": + resolution: + { + integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==, + } cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + "@rollup/rollup-linux-x64-gnu@4.59.0": + resolution: + { + integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==, + } cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + "@rollup/rollup-linux-x64-musl@4.59.0": + resolution: + { + integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==, + } cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + "@rollup/rollup-openbsd-x64@4.59.0": + resolution: + { + integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==, + } cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + "@rollup/rollup-openharmony-arm64@4.59.0": + resolution: + { + integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==, + } cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + "@rollup/rollup-win32-arm64-msvc@4.59.0": + resolution: + { + integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==, + } cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + "@rollup/rollup-win32-ia32-msvc@4.59.0": + resolution: + { + integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==, + } cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + "@rollup/rollup-win32-x64-gnu@4.59.0": + resolution: + { + integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==, + } cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + "@rollup/rollup-win32-x64-msvc@4.59.0": + resolution: + { + integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==, + } cpu: [x64] os: [win32] - '@sinclair/typebox@0.31.28': - resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} + "@sinclair/typebox@0.31.28": + resolution: + { + integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==, + } - '@sqlite.org/sqlite-wasm@3.48.0-build4': - resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==} + "@sqlite.org/sqlite-wasm@3.48.0-build4": + resolution: + { + integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==, + } hasBin: true - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + "@standard-schema/spec@1.1.0": + resolution: + { + integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, + } - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + "@sveltejs/acorn-typescript@1.0.9": + resolution: + { + integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==, + } peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-node@5.5.4': - resolution: {integrity: sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==} + "@sveltejs/adapter-node@5.5.4": + resolution: + { + integrity: sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==, + } peerDependencies: - '@sveltejs/kit': ^2.4.0 + "@sveltejs/kit": ^2.4.0 - '@sveltejs/kit@2.53.2': - resolution: {integrity: sha512-M+MqAvFve12T1HWws/2npP/s3hFtyjw3GB/OXW/8a1jZBk48qnvPJrtgE+VOMc3RnjUMxc4mv/vQ73nvj2uNMg==} - engines: {node: '>=18.13'} + "@sveltejs/kit@2.53.2": + resolution: + { + integrity: sha512-M+MqAvFve12T1HWws/2npP/s3hFtyjw3GB/OXW/8a1jZBk48qnvPJrtgE+VOMc3RnjUMxc4mv/vQ73nvj2uNMg==, + } + engines: { node: ">=18.13" } hasBin: true peerDependencies: - '@opentelemetry/api': ^1.0.0 - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + "@opentelemetry/api": ^1.0.0 + "@sveltejs/vite-plugin-svelte": ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 typescript: ^5.3.3 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 peerDependenciesMeta: - '@opentelemetry/api': + "@opentelemetry/api": optional: true typescript: optional: true - '@sveltejs/vite-plugin-svelte-inspector@5.0.2': - resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} - engines: {node: ^20.19 || ^22.12 || >=24} + "@sveltejs/vite-plugin-svelte-inspector@5.0.2": + resolution: + { + integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==, + } + engines: { node: ^20.19 || ^22.12 || >=24 } peerDependencies: - '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + "@sveltejs/vite-plugin-svelte": ^6.0.0-next.0 svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@sveltejs/vite-plugin-svelte@6.2.4': - resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} - engines: {node: ^20.19 || ^22.12 || >=24} + "@sveltejs/vite-plugin-svelte@6.2.4": + resolution: + { + integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==, + } + engines: { node: ^20.19 || ^22.12 || >=24 } peerDependencies: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@swc/helpers@0.5.19': - resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==} + "@swc/helpers@0.5.19": + resolution: + { + integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==, + } - '@tailwindcss/node@4.2.1': - resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + "@tailwindcss/node@4.2.1": + resolution: + { + integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==, + } - '@tailwindcss/oxide-android-arm64@4.2.1': - resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-android-arm64@4.2.1": + resolution: + { + integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==, + } + engines: { node: ">= 20" } cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.2.1': - resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-darwin-arm64@4.2.1": + resolution: + { + integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==, + } + engines: { node: ">= 20" } cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.2.1': - resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-darwin-x64@4.2.1": + resolution: + { + integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==, + } + engines: { node: ">= 20" } cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.2.1': - resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-freebsd-x64@4.2.1": + resolution: + { + integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==, + } + engines: { node: ">= 20" } cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': - resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1": + resolution: + { + integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==, + } + engines: { node: ">= 20" } cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': - resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-linux-arm64-gnu@4.2.1": + resolution: + { + integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==, + } + engines: { node: ">= 20" } cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.2.1': - resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-linux-arm64-musl@4.2.1": + resolution: + { + integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==, + } + engines: { node: ">= 20" } cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.2.1': - resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-linux-x64-gnu@4.2.1": + resolution: + { + integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==, + } + engines: { node: ">= 20" } cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.2.1': - resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-linux-x64-musl@4.2.1": + resolution: + { + integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==, + } + engines: { node: ">= 20" } cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.2.1': - resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} - engines: {node: '>=14.0.0'} + "@tailwindcss/oxide-wasm32-wasi@4.2.1": + resolution: + { + integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==, + } + engines: { node: ">=14.0.0" } cpu: [wasm32] bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' + - "@napi-rs/wasm-runtime" + - "@emnapi/core" + - "@emnapi/runtime" + - "@tybys/wasm-util" + - "@emnapi/wasi-threads" - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': - resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-win32-arm64-msvc@4.2.1": + resolution: + { + integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==, + } + engines: { node: ">= 20" } cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.2.1': - resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} - engines: {node: '>= 20'} + "@tailwindcss/oxide-win32-x64-msvc@4.2.1": + resolution: + { + integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==, + } + engines: { node: ">= 20" } cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.2.1': - resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} - engines: {node: '>= 20'} + "@tailwindcss/oxide@4.2.1": + resolution: + { + integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==, + } + engines: { node: ">= 20" } - '@tailwindcss/vite@4.2.1': - resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + "@tailwindcss/vite@4.2.1": + resolution: + { + integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==, + } peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + "@types/cookie@0.6.0": + resolution: + { + integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==, + } - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + "@types/esrecurse@4.3.1": + resolution: + { + integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==, + } - '@types/node@25.3.3': - resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} + "@types/estree@1.0.8": + resolution: + { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, + } - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + "@types/json-schema@7.0.15": + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + "@types/node@25.3.3": + resolution: + { + integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==, + } + + "@types/resolve@1.20.2": + resolution: + { + integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==, + } + + "@types/trusted-types@2.0.7": + resolution: + { + integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, + } + + "@typescript-eslint/eslint-plugin@8.56.1": + resolution: + { + integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + "@typescript-eslint/parser": ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/parser@8.56.1": + resolution: + { + integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/project-service@8.56.1": + resolution: + { + integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/scope-manager@8.56.1": + resolution: + { + integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/tsconfig-utils@8.56.1": + resolution: + { + integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/type-utils@8.56.1": + resolution: + { + integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/types@8.56.1": + resolution: + { + integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + "@typescript-eslint/typescript-estree@8.56.1": + resolution: + { + integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/utils@8.56.1": + resolution: + { + integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + + "@typescript-eslint/visitor-keys@8.56.1": + resolution: + { + integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + acorn-jsx@5.3.2: + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==, + } + engines: { node: ">=0.4.0" } hasBin: true + ajv@6.14.0: + resolution: + { + integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==, + } + aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==, + } + engines: { node: ">= 0.4" } array-timsort@1.0.3: - resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + resolution: + { + integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==, + } axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==, + } + engines: { node: ">= 0.4" } + + balanced-match@4.0.4: + resolution: + { + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, + } + engines: { node: 18 || 20 || >=22 } bits-ui@2.16.2: - resolution: {integrity: sha512-bgEpRRF7Ck9nRP1pbuKVxpaSMrz+8Pm0y+dmuvlkrSe+uUwIQECef29y6eslFHM6pCAubUh7STrsTLUUp8fzFQ==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-bgEpRRF7Ck9nRP1pbuKVxpaSMrz+8Pm0y+dmuvlkrSe+uUwIQECef29y6eslFHM6pCAubUh7STrsTLUUp8fzFQ==, + } + engines: { node: ">=20" } peerDependencies: - '@internationalized/date': ^3.8.1 + "@internationalized/date": ^3.8.1 svelte: ^5.33.0 + brace-expansion@5.0.4: + resolution: + { + integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==, + } + engines: { node: 18 || 20 || >=22 } + chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} + resolution: + { + integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==, + } + engines: { node: ">= 14.16.0" } clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, + } + engines: { node: ">=6" } commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==, + } + engines: { node: ">=16" } comment-json@4.5.1: - resolution: {integrity: sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==, + } + engines: { node: ">= 6" } commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + resolution: + { + integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==, + } consola@3.4.0: - resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} - engines: {node: ^14.18.0 || >=16.10.0} + resolution: + { + integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==, + } + engines: { node: ^14.18.0 || >=16.10.0 } cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, + } + engines: { node: ">= 0.6" } core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + resolution: + { + integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, + } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } + + cssesc@3.0.0: + resolution: + { + integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, + } + engines: { node: ">=4" } + hasBin: true + + debug@4.4.3: + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true dedent@1.5.1: - resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + resolution: + { + integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==, + } peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: babel-plugin-macros: optional: true + deep-is@0.1.4: + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } + deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==, + } + engines: { node: ">=0.10.0" } dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, + } + engines: { node: ">=6" } detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, + } + engines: { node: ">=8" } devalue@5.6.3: - resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + resolution: + { + integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==, + } enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==, + } + engines: { node: ">=10.13.0" } esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==, + } + engines: { node: ">=18" } hasBin: true + escape-string-regexp@4.0.0: + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: ">=10" } + + eslint-plugin-svelte@3.15.0: + resolution: + { + integrity: sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: + { + integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-scope@9.1.1: + resolution: + { + integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + eslint-visitor-keys@3.4.3: + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + + eslint-visitor-keys@4.2.1: + resolution: + { + integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@5.0.1: + resolution: + { + integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + + eslint@10.0.2: + resolution: + { + integrity: sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + hasBin: true + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + resolution: + { + integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==, + } + + espree@10.4.0: + resolution: + { + integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + espree@11.1.1: + resolution: + { + integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, + } + engines: { node: ">=4" } hasBin: true + esquery@1.7.0: + resolution: + { + integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, + } + engines: { node: ">=0.10" } + esrap@2.2.3: - resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + resolution: + { + integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==, + } + + esrecurse@4.3.0: + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: ">=4.0" } + + estraverse@5.3.0: + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: ">=4.0" } estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + resolution: + { + integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, + } + + esutils@2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: ">=0.10.0" } + + fast-deep-equal@3.1.3: + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } + + fast-json-stable-stringify@2.1.0: + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } + + fast-levenshtein@2.0.6: + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: + { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, + } + engines: { node: ">=16.0.0" } + + find-up@5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: ">=10" } + + flat-cache@4.0.1: + resolution: + { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, + } + engines: { node: ">=16" } + + flatted@3.3.4: + resolution: + { + integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==, + } + fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } + + glob-parent@6.0.2: + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: ">=10.13.0" } + + globals@16.5.0: + resolution: + { + integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==, + } + engines: { node: ">=18" } + + globals@17.4.0: + resolution: + { + integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==, + } + engines: { node: ">=18" } graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, + } hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, + } + engines: { node: ">= 0.4" } human-id@4.1.3: - resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + resolution: + { + integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==, + } hasBin: true + ignore@5.3.2: + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: ">= 4" } + + ignore@7.0.5: + resolution: + { + integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==, + } + engines: { node: ">= 4" } + + imurmurhash@0.1.4: + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: ">=0.8.19" } + inline-style-parser@0.2.7: - resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + resolution: + { + integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==, + } is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==, + } + engines: { node: ">= 0.4" } + + is-extglob@2.1.1: + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } + + is-glob@4.0.3: + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + resolution: + { + integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==, + } is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + resolution: + { + integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==, + } is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + resolution: + { + integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==, + } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + resolution: + { + integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==, + } hasBin: true js-sha256@0.11.1: - resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} + resolution: + { + integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==, + } + + json-buffer@3.0.1: + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } + + json-schema-traverse@0.4.1: + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } + + json-stable-stringify-without-jsonify@1.0.1: + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } hasBin: true + keyv@4.5.4: + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } + kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, + } + engines: { node: ">=6" } + + known-css-properties@0.37.0: + resolution: + { + integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==, + } kysely@0.27.6: - resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==, + } + engines: { node: ">=14.0.0" } + + levn@0.4.1: + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: ">= 0.8.0" } lightningcss-android-arm64@1.31.1: - resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [android] lightningcss-darwin-arm64@1.31.1: - resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.31.1: - resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.31.1: - resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [freebsd] lightningcss-linux-arm-gnueabihf@1.31.1: - resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==, + } + engines: { node: ">= 12.0.0" } cpu: [arm] os: [linux] lightningcss-linux-arm64-gnu@1.31.1: - resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: - resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] libc: [musl] lightningcss-linux-x64-gnu@1.31.1: - resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] libc: [glibc] lightningcss-linux-x64-musl@1.31.1: - resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: - resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==, + } + engines: { node: ">= 12.0.0" } cpu: [arm64] os: [win32] lightningcss-win32-x64-msvc@1.31.1: - resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==, + } + engines: { node: ">= 12.0.0" } cpu: [x64] os: [win32] lightningcss@1.31.1: - resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} - engines: {node: '>= 12.0.0'} + resolution: + { + integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==, + } + engines: { node: ">= 12.0.0" } + + lilconfig@2.1.0: + resolution: + { + integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==, + } + engines: { node: ">=10" } locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + resolution: + { + integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==, + } + + locate-path@6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: ">=10" } lucide-svelte@0.575.0: - resolution: {integrity: sha512-Tu15tJfbmRNPaU61yeNFf3jfRHs8ABA+NwTt7TWmwVbhlSA3H7sW65tX6RttcP7HGV4aHUlYhXixZOlntoFBdw==} + resolution: + { + integrity: sha512-Tu15tJfbmRNPaU61yeNFf3jfRHs8ABA+NwTt7TWmwVbhlSA3H7sW65tX6RttcP7HGV4aHUlYhXixZOlntoFBdw==, + } peerDependencies: svelte: ^3 || ^4 || ^5.0.0-next.42 lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + resolution: + { + integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, + } hasBin: true magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + resolution: + { + integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, + } + + minimatch@10.2.4: + resolution: + { + integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==, + } + engines: { node: 18 || 20 || >=22 } mode-watcher@1.1.0: - resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==} + resolution: + { + integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==, + } peerDependencies: svelte: ^5.27.0 mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, + } + engines: { node: ">=4" } mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==, + } + engines: { node: ">=10" } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true + natural-compare@1.4.0: + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } + obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + resolution: + { + integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==, + } + + optionator@0.9.4: + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: ">= 0.8.0" } + + p-limit@3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: ">=10" } + + p-locate@5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: ">=10" } + + path-exists@4.0.0: + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + resolution: + { + integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, + } picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==, + } + engines: { node: ">=12" } + + postcss-load-config@3.1.4: + resolution: + { + integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==, + } + engines: { node: ">= 10" } + peerDependencies: + postcss: ">=8.0.9" + ts-node: ">=9.0.0" + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@7.0.1: + resolution: + { + integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==, + } + engines: { node: ">=18.0" } + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: + { + integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==, + } + engines: { node: ">=12.0" } + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: + { + integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==, + } + engines: { node: ">=4" } postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, + } + engines: { node: ^10 || ^12 || >=14 } + + prelude-ls@1.2.1: + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: ">= 0.8.0" } + + prettier-plugin-svelte@3.5.0: + resolution: + { + integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==, + } + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier@3.8.1: + resolution: + { + integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==, + } + engines: { node: ">=14" } + hasBin: true + + punycode@2.3.1: + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: ">=6" } readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + resolution: + { + integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==, + } + engines: { node: ">= 14.18.0" } resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==, + } + engines: { node: ">= 0.4" } hasBin: true rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==, + } + engines: { node: ">=18.0.0", npm: ">=8.0.0" } hasBin: true runed@0.23.4: - resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} + resolution: + { + integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==, + } peerDependencies: svelte: ^5.7.0 runed@0.25.0: - resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==} + resolution: + { + integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==, + } peerDependencies: svelte: ^5.7.0 runed@0.35.1: - resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + resolution: + { + integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==, + } peerDependencies: - '@sveltejs/kit': ^2.21.0 + "@sveltejs/kit": ^2.21.0 svelte: ^5.7.0 peerDependenciesMeta: - '@sveltejs/kit': + "@sveltejs/kit": optional: true sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==, + } + engines: { node: ">=6" } + + semver@7.7.4: + resolution: + { + integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, + } + engines: { node: ">=10" } + hasBin: true set-cookie-parser@3.0.1: - resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + resolution: + { + integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==, + } + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==, + } + engines: { node: ">=18" } source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: ">=0.10.0" } sqlite-wasm-kysely@0.3.0: - resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==} + resolution: + { + integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==, + } peerDependencies: - kysely: '*' + kysely: "*" style-to-object@1.0.14: - resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + resolution: + { + integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==, + } supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, + } + engines: { node: ">= 0.4" } svelte-check@4.4.4: - resolution: {integrity: sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ==} - engines: {node: '>= 18.0.0'} + resolution: + { + integrity: sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ==, + } + engines: { node: ">= 18.0.0" } hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' + typescript: ">=5.0.0" + + svelte-dnd-action@0.9.69: + resolution: + { + integrity: sha512-NAmSOH7htJoYraTQvr+q5whlIuVoq88vEuHr4NcFgscDRUxfWPPxgie2OoxepBCQCikrXZV4pqV86aun60wVyw==, + } + peerDependencies: + svelte: ">=3.23.0 || ^5.0.0-next.0" + + svelte-eslint-parser@1.5.1: + resolution: + { + integrity: sha512-UbY7DYoDg+x4AKLUcX5xWuEWylgmm8ZD2Z89YT/AK6Wm/ckeMTnOMwr6AVC99znXbRC26xzWEPhSgmB62E07Gg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.2 } + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true svelte-toolbelt@0.10.6: - resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} - engines: {node: '>=18', pnpm: '>=8.7.0'} + resolution: + { + integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==, + } + engines: { node: ">=18", pnpm: ">=8.7.0" } peerDependencies: svelte: ^5.30.2 svelte-toolbelt@0.7.1: - resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} - engines: {node: '>=18', pnpm: '>=8.7.0'} + resolution: + { + integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==, + } + engines: { node: ">=18", pnpm: ">=8.7.0" } peerDependencies: svelte: ^5.0.0 svelte@5.53.5: - resolution: {integrity: sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==, + } + engines: { node: ">=18" } tabbable@6.4.0: - resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + resolution: + { + integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==, + } tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + resolution: + { + integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==, + } tailwind-variants@3.2.2: - resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} - engines: {node: '>=16.x', pnpm: '>=7.x'} + resolution: + { + integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==, + } + engines: { node: ">=16.x", pnpm: ">=7.x" } peerDependencies: - tailwind-merge: '>=3.0.0' - tailwindcss: '*' + tailwind-merge: ">=3.0.0" + tailwindcss: "*" peerDependenciesMeta: tailwind-merge: optional: true tailwindcss@4.2.1: - resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + resolution: + { + integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==, + } tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==, + } + engines: { node: ">=6" } tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, + } + engines: { node: ">=12.0.0" } totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, + } + engines: { node: ">=6" } + + ts-api-utils@2.4.0: + resolution: + { + integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==, + } + engines: { node: ">=18.12" } + peerDependencies: + typescript: ">=4.8.4" tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + + type-check@0.4.0: + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: ">= 0.8.0" } + + typescript-eslint@8.56.1: + resolution: + { + integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } hasBin: true undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + resolution: + { + integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==, + } unplugin@2.3.11: - resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} - engines: {node: '>=18.12.0'} + resolution: + { + integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==, + } + engines: { node: ">=18.12.0" } + + uri-js@4.4.1: + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } urlpattern-polyfill@10.1.0: - resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + resolution: + { + integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==, + } + + util-deprecate@1.0.2: + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + resolution: + { + integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==, + } hasBin: true uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + resolution: + { + integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==, + } hasBin: true vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" less: ^4.0.0 lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 - stylus: '>=0.54.8' + stylus: ">=0.54.8" sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: - '@types/node': + "@types/node": optional: true jiti: optional: true @@ -1082,7 +2424,10 @@ packages: optional: true vitefu@1.1.2: - resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + resolution: + { + integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==, + } peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 peerDependenciesMeta: @@ -1090,106 +2435,185 @@ packages: optional: true webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + resolution: + { + integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==, + } + + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } + hasBin: true + + word-wrap@1.2.5: + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, + } + engines: { node: ">=0.10.0" } + + yaml@1.10.2: + resolution: + { + integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==, + } + engines: { node: ">= 6" } + + yocto-queue@0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: ">=10" } zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + resolution: + { + integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==, + } snapshots: - - '@esbuild/aix-ppc64@0.27.3': + "@esbuild/aix-ppc64@0.27.3": optional: true - '@esbuild/android-arm64@0.27.3': + "@esbuild/android-arm64@0.27.3": optional: true - '@esbuild/android-arm@0.27.3': + "@esbuild/android-arm@0.27.3": optional: true - '@esbuild/android-x64@0.27.3': + "@esbuild/android-x64@0.27.3": optional: true - '@esbuild/darwin-arm64@0.27.3': + "@esbuild/darwin-arm64@0.27.3": optional: true - '@esbuild/darwin-x64@0.27.3': + "@esbuild/darwin-x64@0.27.3": optional: true - '@esbuild/freebsd-arm64@0.27.3': + "@esbuild/freebsd-arm64@0.27.3": optional: true - '@esbuild/freebsd-x64@0.27.3': + "@esbuild/freebsd-x64@0.27.3": optional: true - '@esbuild/linux-arm64@0.27.3': + "@esbuild/linux-arm64@0.27.3": optional: true - '@esbuild/linux-arm@0.27.3': + "@esbuild/linux-arm@0.27.3": optional: true - '@esbuild/linux-ia32@0.27.3': + "@esbuild/linux-ia32@0.27.3": optional: true - '@esbuild/linux-loong64@0.27.3': + "@esbuild/linux-loong64@0.27.3": optional: true - '@esbuild/linux-mips64el@0.27.3': + "@esbuild/linux-mips64el@0.27.3": optional: true - '@esbuild/linux-ppc64@0.27.3': + "@esbuild/linux-ppc64@0.27.3": optional: true - '@esbuild/linux-riscv64@0.27.3': + "@esbuild/linux-riscv64@0.27.3": optional: true - '@esbuild/linux-s390x@0.27.3': + "@esbuild/linux-s390x@0.27.3": optional: true - '@esbuild/linux-x64@0.27.3': + "@esbuild/linux-x64@0.27.3": optional: true - '@esbuild/netbsd-arm64@0.27.3': + "@esbuild/netbsd-arm64@0.27.3": optional: true - '@esbuild/netbsd-x64@0.27.3': + "@esbuild/netbsd-x64@0.27.3": optional: true - '@esbuild/openbsd-arm64@0.27.3': + "@esbuild/openbsd-arm64@0.27.3": optional: true - '@esbuild/openbsd-x64@0.27.3': + "@esbuild/openbsd-x64@0.27.3": optional: true - '@esbuild/openharmony-arm64@0.27.3': + "@esbuild/openharmony-arm64@0.27.3": optional: true - '@esbuild/sunos-x64@0.27.3': + "@esbuild/sunos-x64@0.27.3": optional: true - '@esbuild/win32-arm64@0.27.3': + "@esbuild/win32-arm64@0.27.3": optional: true - '@esbuild/win32-ia32@0.27.3': + "@esbuild/win32-ia32@0.27.3": optional: true - '@esbuild/win32-x64@0.27.3': + "@esbuild/win32-x64@0.27.3": optional: true - '@floating-ui/core@1.7.4': + "@eslint-community/eslint-utils@4.9.1(eslint@10.0.2(jiti@2.6.1))": dependencies: - '@floating-ui/utils': 0.2.10 + eslint: 10.0.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 - '@floating-ui/dom@1.7.5': + "@eslint-community/regexpp@4.12.2": {} + + "@eslint/config-array@0.23.2": dependencies: - '@floating-ui/core': 1.7.4 - '@floating-ui/utils': 0.2.10 + "@eslint/object-schema": 3.0.2 + debug: 4.4.3 + minimatch: 10.2.4 + transitivePeerDependencies: + - supports-color - '@floating-ui/utils@0.2.10': {} - - '@inlang/paraglide-js@2.13.0': + "@eslint/config-helpers@0.5.2": dependencies: - '@inlang/recommend-sherlock': 0.2.1 - '@inlang/sdk': 2.7.0 + "@eslint/core": 1.1.0 + + "@eslint/core@1.1.0": + dependencies: + "@types/json-schema": 7.0.15 + + "@eslint/js@10.0.1(eslint@10.0.2(jiti@2.6.1))": + optionalDependencies: + eslint: 10.0.2(jiti@2.6.1) + + "@eslint/object-schema@3.0.2": {} + + "@eslint/plugin-kit@0.6.0": + dependencies: + "@eslint/core": 1.1.0 + levn: 0.4.1 + + "@floating-ui/core@1.7.4": + dependencies: + "@floating-ui/utils": 0.2.10 + + "@floating-ui/dom@1.7.5": + dependencies: + "@floating-ui/core": 1.7.4 + "@floating-ui/utils": 0.2.10 + + "@floating-ui/utils@0.2.10": {} + + "@humanfs/core@0.19.1": {} + + "@humanfs/node@0.16.7": + dependencies: + "@humanfs/core": 0.19.1 + "@humanwhocodes/retry": 0.4.3 + + "@humanwhocodes/module-importer@1.0.1": {} + + "@humanwhocodes/retry@0.4.3": {} + + "@inlang/paraglide-js@2.13.0": + dependencies: + "@inlang/recommend-sherlock": 0.2.1 + "@inlang/sdk": 2.7.0 commander: 11.1.0 consola: 3.4.0 json5: 2.2.3 @@ -1198,46 +2622,46 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@inlang/recommend-sherlock@0.2.1': + "@inlang/recommend-sherlock@0.2.1": dependencies: comment-json: 4.5.1 - '@inlang/sdk@2.7.0': + "@inlang/sdk@2.7.0": dependencies: - '@lix-js/sdk': 0.4.7 - '@sinclair/typebox': 0.31.28 + "@lix-js/sdk": 0.4.7 + "@sinclair/typebox": 0.31.28 kysely: 0.27.6 sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) uuid: 13.0.0 transitivePeerDependencies: - babel-plugin-macros - '@internationalized/date@3.11.0': + "@internationalized/date@3.11.0": dependencies: - '@swc/helpers': 0.5.19 + "@swc/helpers": 0.5.19 - '@jridgewell/gen-mapping@0.3.13': + "@jridgewell/gen-mapping@0.3.13": dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/remapping@2.3.5': + "@jridgewell/remapping@2.3.5": dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} + "@jridgewell/resolve-uri@3.1.2": {} - '@jridgewell/sourcemap-codec@1.5.5': {} + "@jridgewell/sourcemap-codec@1.5.5": {} - '@jridgewell/trace-mapping@0.3.31': + "@jridgewell/trace-mapping@0.3.31": dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 - '@lix-js/sdk@0.4.7': + "@lix-js/sdk@0.4.7": dependencies: - '@lix-js/server-protocol-schema': 0.1.1 + "@lix-js/server-protocol-schema": 0.1.1 dedent: 1.5.1 human-id: 4.1.3 js-sha256: 0.11.1 @@ -1247,17 +2671,17 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@lix-js/server-protocol-schema@0.1.1': {} + "@lix-js/server-protocol-schema@0.1.1": {} - '@lucide/svelte@0.561.0(svelte@5.53.5)': + "@lucide/svelte@0.561.0(svelte@5.53.5)": dependencies: svelte: 5.53.5 - '@polka/url@1.0.0-next.29': {} + "@polka/url@1.0.0-next.29": {} - '@rollup/plugin-commonjs@29.0.0(rollup@4.59.0)': + "@rollup/plugin-commonjs@29.0.0(rollup@4.59.0)": dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + "@rollup/pluginutils": 5.3.0(rollup@4.59.0) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.3) @@ -1267,129 +2691,129 @@ snapshots: optionalDependencies: rollup: 4.59.0 - '@rollup/plugin-json@6.1.0(rollup@4.59.0)': + "@rollup/plugin-json@6.1.0(rollup@4.59.0)": dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + "@rollup/pluginutils": 5.3.0(rollup@4.59.0) optionalDependencies: rollup: 4.59.0 - '@rollup/plugin-node-resolve@16.0.3(rollup@4.59.0)': + "@rollup/plugin-node-resolve@16.0.3(rollup@4.59.0)": dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@types/resolve': 1.20.2 + "@rollup/pluginutils": 5.3.0(rollup@4.59.0) + "@types/resolve": 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.11 optionalDependencies: rollup: 4.59.0 - '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + "@rollup/pluginutils@5.3.0(rollup@4.59.0)": dependencies: - '@types/estree': 1.0.8 + "@types/estree": 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: rollup: 4.59.0 - '@rollup/rollup-android-arm-eabi@4.59.0': + "@rollup/rollup-android-arm-eabi@4.59.0": optional: true - '@rollup/rollup-android-arm64@4.59.0': + "@rollup/rollup-android-arm64@4.59.0": optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + "@rollup/rollup-darwin-arm64@4.59.0": optional: true - '@rollup/rollup-darwin-x64@4.59.0': + "@rollup/rollup-darwin-x64@4.59.0": optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + "@rollup/rollup-freebsd-arm64@4.59.0": optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + "@rollup/rollup-freebsd-x64@4.59.0": optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + "@rollup/rollup-linux-arm-gnueabihf@4.59.0": optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + "@rollup/rollup-linux-arm-musleabihf@4.59.0": optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + "@rollup/rollup-linux-arm64-gnu@4.59.0": optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + "@rollup/rollup-linux-arm64-musl@4.59.0": optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + "@rollup/rollup-linux-loong64-gnu@4.59.0": optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + "@rollup/rollup-linux-loong64-musl@4.59.0": optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + "@rollup/rollup-linux-ppc64-gnu@4.59.0": optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + "@rollup/rollup-linux-ppc64-musl@4.59.0": optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + "@rollup/rollup-linux-riscv64-gnu@4.59.0": optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + "@rollup/rollup-linux-riscv64-musl@4.59.0": optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + "@rollup/rollup-linux-s390x-gnu@4.59.0": optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + "@rollup/rollup-linux-x64-gnu@4.59.0": optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + "@rollup/rollup-linux-x64-musl@4.59.0": optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + "@rollup/rollup-openbsd-x64@4.59.0": optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + "@rollup/rollup-openharmony-arm64@4.59.0": optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + "@rollup/rollup-win32-arm64-msvc@4.59.0": optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + "@rollup/rollup-win32-ia32-msvc@4.59.0": optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + "@rollup/rollup-win32-x64-gnu@4.59.0": optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + "@rollup/rollup-win32-x64-msvc@4.59.0": optional: true - '@sinclair/typebox@0.31.28': {} + "@sinclair/typebox@0.31.28": {} - '@sqlite.org/sqlite-wasm@3.48.0-build4': {} + "@sqlite.org/sqlite-wasm@3.48.0-build4": {} - '@standard-schema/spec@1.1.0': {} + "@standard-schema/spec@1.1.0": {} - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + "@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)": dependencies: acorn: 8.16.0 - '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))': + "@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))": dependencies: - '@rollup/plugin-commonjs': 29.0.0(rollup@4.59.0) - '@rollup/plugin-json': 6.1.0(rollup@4.59.0) - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0) - '@sveltejs/kit': 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) + "@rollup/plugin-commonjs": 29.0.0(rollup@4.59.0) + "@rollup/plugin-json": 6.1.0(rollup@4.59.0) + "@rollup/plugin-node-resolve": 16.0.3(rollup@4.59.0) + "@sveltejs/kit": 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) rollup: 4.59.0 - '@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))': + "@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))": dependencies: - '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) - '@types/cookie': 0.6.0 + "@standard-schema/spec": 1.1.0 + "@sveltejs/acorn-typescript": 1.0.9(acorn@8.16.0) + "@sveltejs/vite-plugin-svelte": 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) + "@types/cookie": 0.6.0 acorn: 8.16.0 cookie: 0.6.0 devalue: 5.6.3 @@ -1404,16 +2828,16 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))': + "@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))": dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) + "@sveltejs/vite-plugin-svelte": 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) obug: 2.1.1 svelte: 5.53.5 vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1) - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))': + "@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))": dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) + "@sveltejs/vite-plugin-svelte-inspector": 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 @@ -1421,13 +2845,13 @@ snapshots: vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1) vitefu: 1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) - '@swc/helpers@0.5.19': + "@swc/helpers@0.5.19": dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.2.1': + "@tailwindcss/node@4.2.1": dependencies: - '@jridgewell/remapping': 2.3.5 + "@jridgewell/remapping": 2.3.5 enhanced-resolve: 5.19.0 jiti: 2.6.1 lightningcss: 1.31.1 @@ -1435,97 +2859,209 @@ snapshots: source-map-js: 1.2.1 tailwindcss: 4.2.1 - '@tailwindcss/oxide-android-arm64@4.2.1': + "@tailwindcss/oxide-android-arm64@4.2.1": optional: true - '@tailwindcss/oxide-darwin-arm64@4.2.1': + "@tailwindcss/oxide-darwin-arm64@4.2.1": optional: true - '@tailwindcss/oxide-darwin-x64@4.2.1': + "@tailwindcss/oxide-darwin-x64@4.2.1": optional: true - '@tailwindcss/oxide-freebsd-x64@4.2.1': + "@tailwindcss/oxide-freebsd-x64@4.2.1": optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + "@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1": optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + "@tailwindcss/oxide-linux-arm64-gnu@4.2.1": optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + "@tailwindcss/oxide-linux-arm64-musl@4.2.1": optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + "@tailwindcss/oxide-linux-x64-gnu@4.2.1": optional: true - '@tailwindcss/oxide-linux-x64-musl@4.2.1': + "@tailwindcss/oxide-linux-x64-musl@4.2.1": optional: true - '@tailwindcss/oxide-wasm32-wasi@4.2.1': + "@tailwindcss/oxide-wasm32-wasi@4.2.1": optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + "@tailwindcss/oxide-win32-arm64-msvc@4.2.1": optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + "@tailwindcss/oxide-win32-x64-msvc@4.2.1": optional: true - '@tailwindcss/oxide@4.2.1': + "@tailwindcss/oxide@4.2.1": optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.1 - '@tailwindcss/oxide-darwin-arm64': 4.2.1 - '@tailwindcss/oxide-darwin-x64': 4.2.1 - '@tailwindcss/oxide-freebsd-x64': 4.2.1 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 - '@tailwindcss/oxide-linux-x64-musl': 4.2.1 - '@tailwindcss/oxide-wasm32-wasi': 4.2.1 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + "@tailwindcss/oxide-android-arm64": 4.2.1 + "@tailwindcss/oxide-darwin-arm64": 4.2.1 + "@tailwindcss/oxide-darwin-x64": 4.2.1 + "@tailwindcss/oxide-freebsd-x64": 4.2.1 + "@tailwindcss/oxide-linux-arm-gnueabihf": 4.2.1 + "@tailwindcss/oxide-linux-arm64-gnu": 4.2.1 + "@tailwindcss/oxide-linux-arm64-musl": 4.2.1 + "@tailwindcss/oxide-linux-x64-gnu": 4.2.1 + "@tailwindcss/oxide-linux-x64-musl": 4.2.1 + "@tailwindcss/oxide-wasm32-wasi": 4.2.1 + "@tailwindcss/oxide-win32-arm64-msvc": 4.2.1 + "@tailwindcss/oxide-win32-x64-msvc": 4.2.1 - '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))': + "@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))": dependencies: - '@tailwindcss/node': 4.2.1 - '@tailwindcss/oxide': 4.2.1 + "@tailwindcss/node": 4.2.1 + "@tailwindcss/oxide": 4.2.1 tailwindcss: 4.2.1 vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1) - '@types/cookie@0.6.0': {} + "@types/cookie@0.6.0": {} - '@types/estree@1.0.8': {} + "@types/esrecurse@4.3.1": {} - '@types/node@25.3.3': + "@types/estree@1.0.8": {} + + "@types/json-schema@7.0.15": {} + + "@types/node@25.3.3": dependencies: undici-types: 7.18.2 optional: true - '@types/resolve@1.20.2': {} + "@types/resolve@1.20.2": {} - '@types/trusted-types@2.0.7': {} + "@types/trusted-types@2.0.7": {} + + "@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)": + dependencies: + "@eslint-community/regexpp": 4.12.2 + "@typescript-eslint/parser": 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + "@typescript-eslint/scope-manager": 8.56.1 + "@typescript-eslint/type-utils": 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + "@typescript-eslint/utils": 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.56.1 + eslint: 10.0.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)": + dependencies: + "@typescript-eslint/scope-manager": 8.56.1 + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/typescript-estree": 8.56.1(typescript@5.9.3) + "@typescript-eslint/visitor-keys": 8.56.1 + debug: 4.4.3 + eslint: 10.0.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/project-service@8.56.1(typescript@5.9.3)": + dependencies: + "@typescript-eslint/tsconfig-utils": 8.56.1(typescript@5.9.3) + "@typescript-eslint/types": 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/scope-manager@8.56.1": + dependencies: + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/visitor-keys": 8.56.1 + + "@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)": + dependencies: + typescript: 5.9.3 + + "@typescript-eslint/type-utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)": + dependencies: + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/typescript-estree": 8.56.1(typescript@5.9.3) + "@typescript-eslint/utils": 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.0.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/types@8.56.1": {} + + "@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)": + dependencies: + "@typescript-eslint/project-service": 8.56.1(typescript@5.9.3) + "@typescript-eslint/tsconfig-utils": 8.56.1(typescript@5.9.3) + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/visitor-keys": 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/utils@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)": + dependencies: + "@eslint-community/eslint-utils": 4.9.1(eslint@10.0.2(jiti@2.6.1)) + "@typescript-eslint/scope-manager": 8.56.1 + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/typescript-estree": 8.56.1(typescript@5.9.3) + eslint: 10.0.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + "@typescript-eslint/visitor-keys@8.56.1": + dependencies: + "@typescript-eslint/types": 8.56.1 + eslint-visitor-keys: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 acorn@8.16.0: {} + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + aria-query@5.3.1: {} array-timsort@1.0.3: {} axobject-query@4.1.0: {} + balanced-match@4.0.4: {} + bits-ui@2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5): dependencies: - '@floating-ui/core': 1.7.4 - '@floating-ui/dom': 1.7.5 - '@internationalized/date': 3.11.0 + "@floating-ui/core": 1.7.4 + "@floating-ui/dom": 1.7.5 + "@internationalized/date": 3.11.0 esm-env: 1.2.2 runed: 0.35.1(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5) svelte: 5.53.5 svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5) tabbable: 6.4.0 transitivePeerDependencies: - - '@sveltejs/kit' + - "@sveltejs/kit" + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 chokidar@4.0.3: dependencies: @@ -1549,8 +3085,22 @@ snapshots: core-util-is@1.0.3: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + dedent@1.5.1: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} dequal@2.0.3: {} @@ -1566,52 +3116,181 @@ snapshots: esbuild@0.27.3: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + "@esbuild/aix-ppc64": 0.27.3 + "@esbuild/android-arm": 0.27.3 + "@esbuild/android-arm64": 0.27.3 + "@esbuild/android-x64": 0.27.3 + "@esbuild/darwin-arm64": 0.27.3 + "@esbuild/darwin-x64": 0.27.3 + "@esbuild/freebsd-arm64": 0.27.3 + "@esbuild/freebsd-x64": 0.27.3 + "@esbuild/linux-arm": 0.27.3 + "@esbuild/linux-arm64": 0.27.3 + "@esbuild/linux-ia32": 0.27.3 + "@esbuild/linux-loong64": 0.27.3 + "@esbuild/linux-mips64el": 0.27.3 + "@esbuild/linux-ppc64": 0.27.3 + "@esbuild/linux-riscv64": 0.27.3 + "@esbuild/linux-s390x": 0.27.3 + "@esbuild/linux-x64": 0.27.3 + "@esbuild/netbsd-arm64": 0.27.3 + "@esbuild/netbsd-x64": 0.27.3 + "@esbuild/openbsd-arm64": 0.27.3 + "@esbuild/openbsd-x64": 0.27.3 + "@esbuild/openharmony-arm64": 0.27.3 + "@esbuild/sunos-x64": 0.27.3 + "@esbuild/win32-arm64": 0.27.3 + "@esbuild/win32-ia32": 0.27.3 + "@esbuild/win32-x64": 0.27.3 + + escape-string-regexp@4.0.0: {} + + eslint-plugin-svelte@3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.5): + dependencies: + "@eslint-community/eslint-utils": 4.9.1(eslint@10.0.2(jiti@2.6.1)) + "@jridgewell/sourcemap-codec": 1.5.5 + eslint: 10.0.2(jiti@2.6.1) + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-safe-parser: 7.0.1(postcss@8.5.6) + semver: 7.7.4 + svelte-eslint-parser: 1.5.1(svelte@5.53.5) + optionalDependencies: + svelte: 5.53.5 + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.1: + dependencies: + "@types/esrecurse": 4.3.1 + "@types/estree": 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.0.2(jiti@2.6.1): + dependencies: + "@eslint-community/eslint-utils": 4.9.1(eslint@10.0.2(jiti@2.6.1)) + "@eslint-community/regexpp": 4.12.2 + "@eslint/config-array": 0.23.2 + "@eslint/config-helpers": 0.5.2 + "@eslint/core": 1.1.0 + "@eslint/plugin-kit": 0.6.0 + "@humanfs/node": 0.16.7 + "@humanwhocodes/module-importer": 1.0.1 + "@humanwhocodes/retry": 0.4.3 + "@types/estree": 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.1 + eslint-visitor-keys: 5.0.1 + espree: 11.1.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color esm-env@1.2.2: {} + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@11.1.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + esrap@2.2.3: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/sourcemap-codec": 1.5.5 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} estree-walker@2.0.2: {} + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.4 + keyv: 4.5.4 + + flatted@3.3.4: {} + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@16.5.0: {} + + globals@17.4.0: {} + graceful-fs@4.2.11: {} hasown@2.0.2: @@ -1620,32 +3299,63 @@ snapshots: human-id@4.1.3: {} + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + inline-style-parser@0.2.7: {} is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-module@1.0.0: {} is-reference@1.2.1: dependencies: - '@types/estree': 1.0.8 + "@types/estree": 1.0.8 is-reference@3.0.3: dependencies: - '@types/estree': 1.0.8 + "@types/estree": 1.0.8 + + isexe@2.0.0: {} jiti@2.6.1: {} js-sha256@0.11.1: {} + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@4.1.5: {} + known-css-properties@0.37.0: {} + kysely@0.27.6: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.31.1: optional: true @@ -1695,8 +3405,14 @@ snapshots: lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + lilconfig@2.1.0: {} + locate-character@3.0.0: {} + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lucide-svelte@0.575.0(svelte@5.53.5): dependencies: svelte: 5.53.5 @@ -1705,7 +3421,11 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/sourcemap-codec": 1.5.5 + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 mode-watcher@1.1.0(svelte@5.53.5): dependencies: @@ -1717,22 +3437,78 @@ snapshots: mrmime@2.0.1: {} + ms@2.1.3: {} + nanoid@3.3.11: {} + natural-compare@1.4.0: {} + obug@2.1.1: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + path-parse@1.0.7: {} picocolors@1.1.1: {} picomatch@4.0.3: {} + postcss-load-config@3.1.4(postcss@8.5.6): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.5.6 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.5): + dependencies: + prettier: 3.8.1 + svelte: 5.53.5 + + prettier@3.8.1: {} + + punycode@2.3.1: {} + readdirp@4.1.2: {} resolve@1.22.11: @@ -1743,33 +3519,33 @@ snapshots: rollup@4.59.0: dependencies: - '@types/estree': 1.0.8 + "@types/estree": 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + "@rollup/rollup-android-arm-eabi": 4.59.0 + "@rollup/rollup-android-arm64": 4.59.0 + "@rollup/rollup-darwin-arm64": 4.59.0 + "@rollup/rollup-darwin-x64": 4.59.0 + "@rollup/rollup-freebsd-arm64": 4.59.0 + "@rollup/rollup-freebsd-x64": 4.59.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.59.0 + "@rollup/rollup-linux-arm-musleabihf": 4.59.0 + "@rollup/rollup-linux-arm64-gnu": 4.59.0 + "@rollup/rollup-linux-arm64-musl": 4.59.0 + "@rollup/rollup-linux-loong64-gnu": 4.59.0 + "@rollup/rollup-linux-loong64-musl": 4.59.0 + "@rollup/rollup-linux-ppc64-gnu": 4.59.0 + "@rollup/rollup-linux-ppc64-musl": 4.59.0 + "@rollup/rollup-linux-riscv64-gnu": 4.59.0 + "@rollup/rollup-linux-riscv64-musl": 4.59.0 + "@rollup/rollup-linux-s390x-gnu": 4.59.0 + "@rollup/rollup-linux-x64-gnu": 4.59.0 + "@rollup/rollup-linux-x64-musl": 4.59.0 + "@rollup/rollup-openbsd-x64": 4.59.0 + "@rollup/rollup-openharmony-arm64": 4.59.0 + "@rollup/rollup-win32-arm64-msvc": 4.59.0 + "@rollup/rollup-win32-ia32-msvc": 4.59.0 + "@rollup/rollup-win32-x64-gnu": 4.59.0 + "@rollup/rollup-win32-x64-msvc": 4.59.0 fsevents: 2.3.3 runed@0.23.4(svelte@5.53.5): @@ -1789,17 +3565,25 @@ snapshots: lz-string: 1.5.0 svelte: 5.53.5 optionalDependencies: - '@sveltejs/kit': 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) + "@sveltejs/kit": 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)) sade@1.8.1: dependencies: mri: 1.2.0 + semver@7.7.4: {} + set-cookie-parser@3.0.1: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + sirv@3.0.2: dependencies: - '@polka/url': 1.0.0-next.29 + "@polka/url": 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 @@ -1807,7 +3591,7 @@ snapshots: sqlite-wasm-kysely@0.3.0(kysely@0.27.6): dependencies: - '@sqlite.org/sqlite-wasm': 3.48.0-build4 + "@sqlite.org/sqlite-wasm": 3.48.0-build4 kysely: 0.27.6 style-to-object@1.0.14: @@ -1818,7 +3602,7 @@ snapshots: svelte-check@4.4.4(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3): dependencies: - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/trace-mapping": 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 @@ -1828,6 +3612,22 @@ snapshots: transitivePeerDependencies: - picomatch + svelte-dnd-action@0.9.69(svelte@5.53.5): + dependencies: + svelte: 5.53.5 + + svelte-eslint-parser@1.5.1(svelte@5.53.5): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + optionalDependencies: + svelte: 5.53.5 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5): dependencies: clsx: 2.1.1 @@ -1835,7 +3635,7 @@ snapshots: style-to-object: 1.0.14 svelte: 5.53.5 transitivePeerDependencies: - - '@sveltejs/kit' + - "@sveltejs/kit" svelte-toolbelt@0.7.1(svelte@5.53.5): dependencies: @@ -1846,11 +3646,11 @@ snapshots: svelte@5.53.5: dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@types/estree': 1.0.8 - '@types/trusted-types': 2.0.7 + "@jridgewell/remapping": 2.3.5 + "@jridgewell/sourcemap-codec": 1.5.5 + "@sveltejs/acorn-typescript": 1.0.9(acorn@8.16.0) + "@types/estree": 1.0.8 + "@types/trusted-types": 2.0.7 acorn: 8.16.0 aria-query: 5.3.1 axobject-query: 4.1.0 @@ -1884,8 +3684,27 @@ snapshots: totalist@3.0.1: {} + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + tslib@2.8.1: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + "@typescript-eslint/eslint-plugin": 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + "@typescript-eslint/parser": 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + "@typescript-eslint/typescript-estree": 8.56.1(typescript@5.9.3) + "@typescript-eslint/utils": 8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} undici-types@7.18.2: @@ -1893,13 +3712,19 @@ snapshots: unplugin@2.3.11: dependencies: - '@jridgewell/remapping': 2.3.5 + "@jridgewell/remapping": 2.3.5 acorn: 8.16.0 picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + urlpattern-polyfill@10.1.0: {} + util-deprecate@1.0.2: {} + uuid@10.0.0: {} uuid@13.0.0: {} @@ -1913,7 +3738,7 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.3.3 + "@types/node": 25.3.3 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.31.1 @@ -1924,4 +3749,14 @@ snapshots: webpack-virtual-modules@0.6.2: {} + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yaml@1.10.2: {} + + yocto-queue@0.1.0: {} + zimmerframe@1.1.4: {} diff --git a/frontend/project.inlang/settings.json b/frontend/project.inlang/settings.json index 8342010..057bbee 100644 --- a/frontend/project.inlang/settings.json +++ b/frontend/project.inlang/settings.json @@ -1,12 +1,12 @@ { - "$schema": "https://inlang.com/schema/project-settings", - "baseLocale": "pl", - "locales": ["pl", "en"], - "modules": [ - "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", - "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" - ], - "plugin.inlang.messageFormat": { - "pathPattern": "./messages/{locale}.json" - } + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "pl", + "locales": ["pl", "en"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } } diff --git a/frontend/src/app.css b/frontend/src/app.css index 130b9cb..f7b8231 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -5,85 +5,85 @@ /* ── CSS variable definitions (light / dark) ─────────────────────────────── */ :root { - --background: hsl(0 0% 100%); - --foreground: hsl(240 10% 3.9%); - --card: hsl(0 0% 100%); - --card-foreground: hsl(240 10% 3.9%); - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(240 10% 3.9%); - --primary: hsl(240 5.9% 10%); - --primary-foreground: hsl(0 0% 98%); - --secondary: hsl(240 4.8% 95.9%); - --secondary-foreground: hsl(240 5.9% 10%); - --muted: hsl(240 4.8% 95.9%); - --muted-foreground: hsl(240 3.8% 46.1%); - --accent: hsl(240 4.8% 95.9%); - --accent-foreground: hsl(240 5.9% 10%); - --destructive: hsl(0 84.2% 60.2%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(240 5.9% 90%); - --input: hsl(240 5.9% 90%); - --ring: hsl(240 5.9% 10%); - --radius: 0.5rem; + --background: hsl(0 0% 100%); + --foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --primary: hsl(240 5.9% 10%); + --primary-foreground: hsl(0 0% 98%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --ring: hsl(240 5.9% 10%); + --radius: 0.5rem; } .dark { - --background: hsl(240 10% 3.9%); - --foreground: hsl(0 0% 98%); - --card: hsl(240 10% 3.9%); - --card-foreground: hsl(0 0% 98%); - --popover: hsl(240 10% 3.9%); - --popover-foreground: hsl(0 0% 98%); - --primary: hsl(0 0% 98%); - --primary-foreground: hsl(240 5.9% 10%); - --secondary: hsl(240 3.7% 15.9%); - --secondary-foreground: hsl(0 0% 98%); - --muted: hsl(240 3.7% 15.9%); - --muted-foreground: hsl(240 5% 64.9%); - --accent: hsl(240 3.7% 15.9%); - --accent-foreground: hsl(0 0% 98%); - --destructive: hsl(0 62.8% 30.6%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(240 3.7% 15.9%); - --input: hsl(240 3.7% 15.9%); - --ring: hsl(240 4.9% 83.9%); + --background: hsl(240 10% 3.9%); + --foreground: hsl(0 0% 98%); + --card: hsl(240 10% 3.9%); + --card-foreground: hsl(0 0% 98%); + --popover: hsl(240 10% 3.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(0 0% 98%); + --primary-foreground: hsl(240 5.9% 10%); + --secondary: hsl(240 3.7% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(240 3.7% 15.9%); + --muted-foreground: hsl(240 5% 64.9%); + --accent: hsl(240 3.7% 15.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(240 3.7% 15.9%); + --input: hsl(240 3.7% 15.9%); + --ring: hsl(240 4.9% 83.9%); } /* ── Map CSS vars → Tailwind v4 design tokens ────────────────────────────── */ @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } /* ── Base resets ─────────────────────────────────────────────────────────── */ * { - border-color: var(--border); + border-color: var(--border); } body { - background-color: var(--background); - color: var(--foreground); + background-color: var(--background); + color: var(--foreground); } diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index da08e6d..520c421 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -1,13 +1,13 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } } export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html index f273cc5..adf8bd8 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,11 +1,11 @@ - - - - %sveltekit.head% - - -
%sveltekit.body%
- + + + + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 4449456..cb5906d 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,6 +1,6 @@ -import { paraglideMiddleware } from '$lib/paraglide/server.js'; -import type { Handle } from '@sveltejs/kit'; +import { paraglideMiddleware } from "$lib/paraglide/server.js"; +import type { Handle } from "@sveltejs/kit"; export const handle: Handle = async ({ event, resolve }) => { - return paraglideMiddleware(event.request, () => resolve(event)); + return paraglideMiddleware(event.request, () => resolve(event)); }; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index dc0f712..8ca13ab 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,271 +1,349 @@ -import { PUBLIC_API_BASE } from '$env/static/public'; +import { browser } from "$app/environment"; +import { PUBLIC_API_BASE } from "$env/static/public"; import type { - ActiveIngredient, - BatchSuggestion, - GroomingSchedule, - LabResult, - MedicationEntry, - MedicationUsage, - PartOfDay, - Product, - ProductContext, - ProductEffectProfile, - ProductInteraction, - ProductInventory, - Routine, - RoutineSuggestion, - RoutineStep, - SkinConditionSnapshot -} from './types'; + ActiveIngredient, + BatchSuggestion, + GroomingSchedule, + LabResult, + MedicationEntry, + MedicationUsage, + PartOfDay, + Product, + ProductContext, + ProductEffectProfile, + ProductInteraction, + ProductInventory, + Routine, + RoutineSuggestion, + RoutineStep, + SkinConditionSnapshot, +} from "./types"; // ─── Core fetch helpers ────────────────────────────────────────────────────── async function request(path: string, init: RequestInit = {}): Promise { - const url = `${PUBLIC_API_BASE}${path}`; - const res = await fetch(url, { - headers: { 'Content-Type': 'application/json', ...init.headers }, - ...init - }); - if (!res.ok) { - const detail = await res.json().catch(() => ({ detail: res.statusText })); - throw new Error(detail?.detail ?? res.statusText); - } - if (res.status === 204) return undefined as T; - return res.json(); + // Server-side uses PUBLIC_API_BASE (e.g. http://localhost:8000). + // Browser-side uses /api so nginx proxies the request on the correct host. + const base = browser ? "/api" : PUBLIC_API_BASE; + const url = `${base}${path}`; + const res = await fetch(url, { + headers: { "Content-Type": "application/json", ...init.headers }, + ...init, + }); + if (!res.ok) { + const detail = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(detail?.detail ?? res.statusText); + } + if (res.status === 204) return undefined as T; + return res.json(); } export const api = { - get: (path: string) => request(path), - post: (path: string, body: unknown) => - request(path, { method: 'POST', body: JSON.stringify(body) }), - patch: (path: string, body: unknown) => - request(path, { method: 'PATCH', body: JSON.stringify(body) }), - del: (path: string) => request(path, { method: 'DELETE' }) + get: (path: string) => request(path), + post: (path: string, body: unknown) => + request(path, { method: "POST", body: JSON.stringify(body) }), + patch: (path: string, body: unknown) => + request(path, { method: "PATCH", body: JSON.stringify(body) }), + del: (path: string) => request(path, { method: "DELETE" }), }; // ─── Products ──────────────────────────────────────────────────────────────── export interface ProductListParams { - category?: string; - brand?: string; - targets?: string[]; - is_medication?: boolean; - is_tool?: boolean; + category?: string; + brand?: string; + targets?: string[]; + is_medication?: boolean; + is_tool?: boolean; } -export function getProducts(params: ProductListParams = {}): Promise { - const q = new URLSearchParams(); - if (params.category) q.set('category', params.category); - if (params.brand) q.set('brand', params.brand); - if (params.targets) params.targets.forEach((t) => q.append('targets', t)); - if (params.is_medication != null) q.set('is_medication', String(params.is_medication)); - if (params.is_tool != null) q.set('is_tool', String(params.is_tool)); - const qs = q.toString(); - return api.get(`/products${qs ? `?${qs}` : ''}`); +export function getProducts( + params: ProductListParams = {}, +): Promise { + const q = new URLSearchParams(); + if (params.category) q.set("category", params.category); + if (params.brand) q.set("brand", params.brand); + if (params.targets) params.targets.forEach((t) => q.append("targets", t)); + if (params.is_medication != null) + q.set("is_medication", String(params.is_medication)); + if (params.is_tool != null) q.set("is_tool", String(params.is_tool)); + const qs = q.toString(); + return api.get(`/products${qs ? `?${qs}` : ""}`); } -export const getProduct = (id: string): Promise => api.get(`/products/${id}`); -export const createProduct = (body: Record): Promise => - api.post('/products', body); -export const updateProduct = (id: string, body: Record): Promise => - api.patch(`/products/${id}`, body); -export const deleteProduct = (id: string): Promise => api.del(`/products/${id}`); +export const getProduct = (id: string): Promise => + api.get(`/products/${id}`); +export const createProduct = ( + body: Record, +): Promise => api.post("/products", body); +export const updateProduct = ( + id: string, + body: Record, +): Promise => api.patch(`/products/${id}`, body); +export const deleteProduct = (id: string): Promise => + api.del(`/products/${id}`); export const getInventory = (productId: string): Promise => - api.get(`/products/${productId}/inventory`); + api.get(`/products/${productId}/inventory`); export const createInventory = ( - productId: string, - body: Record -): Promise => api.post(`/products/${productId}/inventory`, body); -export const updateInventory = (id: string, body: Record): Promise => - api.patch(`/inventory/${id}`, body); -export const deleteInventory = (id: string): Promise => api.del(`/inventory/${id}`); + productId: string, + body: Record, +): Promise => + api.post(`/products/${productId}/inventory`, body); +export const updateInventory = ( + id: string, + body: Record, +): Promise => api.patch(`/inventory/${id}`, body); +export const deleteInventory = (id: string): Promise => + api.del(`/inventory/${id}`); export interface ProductParseResponse { - name?: string; brand?: string; line_name?: string; sku?: string; url?: string; barcode?: string; - category?: string; recommended_time?: string; texture?: string; absorption_speed?: string; - leave_on?: boolean; price_tier?: string; - size_ml?: number; full_weight_g?: number; empty_weight_g?: number; pao_months?: number; - inci?: string[]; actives?: ActiveIngredient[]; - recommended_for?: string[]; targets?: string[]; - contraindications?: string[]; usage_notes?: string; - fragrance_free?: boolean; essential_oils_free?: boolean; - alcohol_denat_free?: boolean; pregnancy_safe?: boolean; - product_effect_profile?: ProductEffectProfile; - ph_min?: number; ph_max?: number; - incompatible_with?: ProductInteraction[]; synergizes_with?: string[]; - context_rules?: ProductContext; - min_interval_hours?: number; max_frequency_per_week?: number; - is_medication?: boolean; is_tool?: boolean; needle_length_mm?: number; + name?: string; + brand?: string; + line_name?: string; + sku?: string; + url?: string; + barcode?: string; + category?: string; + recommended_time?: string; + texture?: string; + absorption_speed?: string; + leave_on?: boolean; + price_tier?: string; + size_ml?: number; + full_weight_g?: number; + empty_weight_g?: number; + pao_months?: number; + inci?: string[]; + actives?: ActiveIngredient[]; + recommended_for?: string[]; + targets?: string[]; + contraindications?: string[]; + usage_notes?: string; + fragrance_free?: boolean; + essential_oils_free?: boolean; + alcohol_denat_free?: boolean; + pregnancy_safe?: boolean; + product_effect_profile?: ProductEffectProfile; + ph_min?: number; + ph_max?: number; + incompatible_with?: ProductInteraction[]; + synergizes_with?: string[]; + context_rules?: ProductContext; + min_interval_hours?: number; + max_frequency_per_week?: number; + is_medication?: boolean; + is_tool?: boolean; + needle_length_mm?: number; } export const parseProductText = (text: string): Promise => - api.post('/products/parse-text', { text }); + api.post("/products/parse-text", { text }); // ─── Routines ──────────────────────────────────────────────────────────────── export interface RoutineListParams { - from_date?: string; - to_date?: string; - part_of_day?: string; + from_date?: string; + to_date?: string; + part_of_day?: string; } -export function getRoutines(params: RoutineListParams = {}): Promise { - const q = new URLSearchParams(); - if (params.from_date) q.set('from_date', params.from_date); - if (params.to_date) q.set('to_date', params.to_date); - if (params.part_of_day) q.set('part_of_day', params.part_of_day); - const qs = q.toString(); - return api.get(`/routines${qs ? `?${qs}` : ''}`); +export function getRoutines( + params: RoutineListParams = {}, +): Promise { + const q = new URLSearchParams(); + if (params.from_date) q.set("from_date", params.from_date); + if (params.to_date) q.set("to_date", params.to_date); + if (params.part_of_day) q.set("part_of_day", params.part_of_day); + const qs = q.toString(); + return api.get(`/routines${qs ? `?${qs}` : ""}`); } -export const getRoutine = (id: string): Promise => api.get(`/routines/${id}`); -export const createRoutine = (body: Record): Promise => - api.post('/routines', body); -export const updateRoutine = (id: string, body: Record): Promise => - api.patch(`/routines/${id}`, body); -export const deleteRoutine = (id: string): Promise => api.del(`/routines/${id}`); +export const getRoutine = (id: string): Promise => + api.get(`/routines/${id}`); +export const createRoutine = ( + body: Record, +): Promise => api.post("/routines", body); +export const updateRoutine = ( + id: string, + body: Record, +): Promise => api.patch(`/routines/${id}`, body); +export const deleteRoutine = (id: string): Promise => + api.del(`/routines/${id}`); -export const addRoutineStep = (routineId: string, body: Record): Promise => - api.post(`/routines/${routineId}/steps`, body); -export const updateRoutineStep = (stepId: string, body: Record): Promise => - api.patch(`/routines/steps/${stepId}`, body); +export const addRoutineStep = ( + routineId: string, + body: Record, +): Promise => api.post(`/routines/${routineId}/steps`, body); +export const updateRoutineStep = ( + stepId: string, + body: Record, +): Promise => api.patch(`/routines/steps/${stepId}`, body); export const deleteRoutineStep = (stepId: string): Promise => - api.del(`/routines/steps/${stepId}`); + api.del(`/routines/steps/${stepId}`); export const suggestRoutine = (body: { - routine_date: string; - part_of_day: PartOfDay; - notes?: string; -}): Promise => api.post('/routines/suggest', body); + routine_date: string; + part_of_day: PartOfDay; + notes?: string; + include_minoxidil_beard?: boolean; + leaving_home?: boolean; +}): Promise => api.post("/routines/suggest", body); export const suggestBatch = (body: { - from_date: string; - to_date: string; - notes?: string; -}): Promise => api.post('/routines/suggest-batch', body); + from_date: string; + to_date: string; + notes?: string; + include_minoxidil_beard?: boolean; + minimize_products?: boolean; +}): Promise => api.post("/routines/suggest-batch", body); export const getGroomingSchedule = (): Promise => - api.get('/routines/grooming-schedule'); -export const createGroomingScheduleEntry = (body: Record): Promise => - api.post('/routines/grooming-schedule', body); -export const updateGroomingScheduleEntry = (id: string, body: Record): Promise => - api.patch(`/routines/grooming-schedule/${id}`, body); + api.get("/routines/grooming-schedule"); +export const createGroomingScheduleEntry = ( + body: Record, +): Promise => api.post("/routines/grooming-schedule", body); +export const updateGroomingScheduleEntry = ( + id: string, + body: Record, +): Promise => + api.patch(`/routines/grooming-schedule/${id}`, body); export const deleteGroomingScheduleEntry = (id: string): Promise => - api.del(`/routines/grooming-schedule/${id}`); + api.del(`/routines/grooming-schedule/${id}`); // ─── Health – Medications ──────────────────────────────────────────────────── export interface MedicationListParams { - kind?: string; - product_name?: string; + kind?: string; + product_name?: string; } -export function getMedications(params: MedicationListParams = {}): Promise { - const q = new URLSearchParams(); - if (params.kind) q.set('kind', params.kind); - if (params.product_name) q.set('product_name', params.product_name); - const qs = q.toString(); - return api.get(`/health/medications${qs ? `?${qs}` : ''}`); +export function getMedications( + params: MedicationListParams = {}, +): Promise { + const q = new URLSearchParams(); + if (params.kind) q.set("kind", params.kind); + if (params.product_name) q.set("product_name", params.product_name); + const qs = q.toString(); + return api.get(`/health/medications${qs ? `?${qs}` : ""}`); } export const getMedication = (id: string): Promise => - api.get(`/health/medications/${id}`); -export const createMedication = (body: Record): Promise => - api.post('/health/medications', body); + api.get(`/health/medications/${id}`); +export const createMedication = ( + body: Record, +): Promise => api.post("/health/medications", body); export const updateMedication = ( - id: string, - body: Record + id: string, + body: Record, ): Promise => api.patch(`/health/medications/${id}`, body); export const deleteMedication = (id: string): Promise => - api.del(`/health/medications/${id}`); + api.del(`/health/medications/${id}`); -export const getMedicationUsages = (medicationId: string): Promise => - api.get(`/health/medications/${medicationId}/usages`); +export const getMedicationUsages = ( + medicationId: string, +): Promise => + api.get(`/health/medications/${medicationId}/usages`); export const createMedicationUsage = ( - medicationId: string, - body: Record -): Promise => api.post(`/health/medications/${medicationId}/usages`, body); + medicationId: string, + body: Record, +): Promise => + api.post(`/health/medications/${medicationId}/usages`, body); // ─── Health – Lab results ──────────────────────────────────────────────────── export interface LabResultListParams { - test_code?: string; - flag?: string; - lab?: string; - from_date?: string; - to_date?: string; + test_code?: string; + flag?: string; + lab?: string; + from_date?: string; + to_date?: string; } -export function getLabResults(params: LabResultListParams = {}): Promise { - const q = new URLSearchParams(); - if (params.test_code) q.set('test_code', params.test_code); - if (params.flag) q.set('flag', params.flag); - if (params.lab) q.set('lab', params.lab); - if (params.from_date) q.set('from_date', params.from_date); - if (params.to_date) q.set('to_date', params.to_date); - const qs = q.toString(); - return api.get(`/health/lab-results${qs ? `?${qs}` : ''}`); +export function getLabResults( + params: LabResultListParams = {}, +): Promise { + const q = new URLSearchParams(); + if (params.test_code) q.set("test_code", params.test_code); + if (params.flag) q.set("flag", params.flag); + if (params.lab) q.set("lab", params.lab); + if (params.from_date) q.set("from_date", params.from_date); + if (params.to_date) q.set("to_date", params.to_date); + const qs = q.toString(); + return api.get(`/health/lab-results${qs ? `?${qs}` : ""}`); } export const getLabResult = (id: string): Promise => - api.get(`/health/lab-results/${id}`); -export const createLabResult = (body: Record): Promise => - api.post('/health/lab-results', body); -export const updateLabResult = (id: string, body: Record): Promise => - api.patch(`/health/lab-results/${id}`, body); + api.get(`/health/lab-results/${id}`); +export const createLabResult = ( + body: Record, +): Promise => api.post("/health/lab-results", body); +export const updateLabResult = ( + id: string, + body: Record, +): Promise => api.patch(`/health/lab-results/${id}`, body); export const deleteLabResult = (id: string): Promise => - api.del(`/health/lab-results/${id}`); + api.del(`/health/lab-results/${id}`); // ─── Skin ──────────────────────────────────────────────────────────────────── export interface SnapshotListParams { - from_date?: string; - to_date?: string; - overall_state?: string; + from_date?: string; + to_date?: string; + overall_state?: string; } -export function getSkinSnapshots(params: SnapshotListParams = {}): Promise { - const q = new URLSearchParams(); - if (params.from_date) q.set('from_date', params.from_date); - if (params.to_date) q.set('to_date', params.to_date); - if (params.overall_state) q.set('overall_state', params.overall_state); - const qs = q.toString(); - return api.get(`/skincare${qs ? `?${qs}` : ''}`); +export function getSkinSnapshots( + params: SnapshotListParams = {}, +): Promise { + const q = new URLSearchParams(); + if (params.from_date) q.set("from_date", params.from_date); + if (params.to_date) q.set("to_date", params.to_date); + if (params.overall_state) q.set("overall_state", params.overall_state); + const qs = q.toString(); + return api.get(`/skincare${qs ? `?${qs}` : ""}`); } export const getSkinSnapshot = (id: string): Promise => - api.get(`/skincare/${id}`); -export const createSkinSnapshot = (body: Record): Promise => - api.post('/skincare', body); + api.get(`/skincare/${id}`); +export const createSkinSnapshot = ( + body: Record, +): Promise => api.post("/skincare", body); export const updateSkinSnapshot = ( - id: string, - body: Record + id: string, + body: Record, ): Promise => api.patch(`/skincare/${id}`, body); -export const deleteSkinSnapshot = (id: string): Promise => api.del(`/skincare/${id}`); +export const deleteSkinSnapshot = (id: string): Promise => + api.del(`/skincare/${id}`); export interface SkinPhotoAnalysisResponse { - overall_state?: string; - texture?: string; - skin_type?: string; - hydration_level?: number; - sebum_tzone?: number; - sebum_cheeks?: number; - sensitivity_level?: number; - barrier_state?: string; - active_concerns?: string[]; - risks?: string[]; - priorities?: string[]; - notes?: string; + overall_state?: string; + texture?: string; + skin_type?: string; + hydration_level?: number; + sebum_tzone?: number; + sebum_cheeks?: number; + sensitivity_level?: number; + barrier_state?: string; + active_concerns?: string[]; + risks?: string[]; + priorities?: string[]; + notes?: string; } -export async function analyzeSkinPhotos(files: File[]): Promise { - const body = new FormData(); - for (const file of files) body.append('photos', file); - const res = await fetch(`${PUBLIC_API_BASE}/skincare/analyze-photos`, { method: 'POST', body }); - if (!res.ok) { - const detail = await res.json().catch(() => ({ detail: res.statusText })); - throw new Error(detail?.detail ?? res.statusText); - } - return res.json(); +export async function analyzeSkinPhotos( + files: File[], +): Promise { + const body = new FormData(); + for (const file of files) body.append("photos", file); + const base = browser ? "/api" : PUBLIC_API_BASE; + const res = await fetch(`${base}/skincare/analyze-photos`, { + method: "POST", + body, + }); + if (!res.ok) { + const detail = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(detail?.detail ?? res.statusText); + } + return res.json(); } diff --git a/frontend/src/lib/components/ProductForm.svelte b/frontend/src/lib/components/ProductForm.svelte index 430b101..b0cc24f 100644 --- a/frontend/src/lib/components/ProductForm.svelte +++ b/frontend/src/lib/components/ProductForm.svelte @@ -451,7 +451,7 @@ {m["productForm_basicInfo"]()} -
+
@@ -461,7 +461,7 @@
-
+
@@ -471,7 +471,7 @@
-
+
@@ -488,7 +488,7 @@ {m["productForm_classification"]()} -
+
@@ -564,7 +564,7 @@
-
+
{#each skinTypes as st}