diff --git a/AGENTS.md b/CLAUDE.md similarity index 63% rename from AGENTS.md rename to CLAUDE.md index 6eb1aa6..6634b64 100644 --- a/AGENTS.md +++ b/CLAUDE.md @@ -1,21 +1,17 @@ -# AGENTS.md +# CLAUDE.md -This file provides guidance to AI coding agents when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Repository Structure +## Repository structure -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. +This is a monorepo. The backend lives in `backend/`; a frontend will be added in the future. ## Commands -Run the backend from the `backend/` directory: +Run all backend commands from the `backend/` directory: ```bash -# Backend +# Run scripts cd backend && uv run python main.py # Linting / formatting @@ -24,27 +20,11 @@ cd backend && uv run black . cd backend && uv run isort . ``` -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). +No test suite exists yet. ## Architecture -**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. +**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). ### Models (`backend/innercontext/models/`) @@ -55,7 +35,7 @@ No test suite exists yet (backend has some test files but they're not integrated | `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 LLM usage. +**`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. **`ProductInventory`** tracks physical packages (opened status, expiry, remaining weight). One product → many inventory entries. @@ -63,7 +43,7 @@ No test suite exists yet (backend has some test files but they're not integrated **`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 75a6f0b..0e2b13d 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 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, MCP, and a web UI to an LLM agent. ## Repository layout ``` -backend/ Python backend — FastAPI REST API + SQLModel models +backend/ Python backend — FastAPI REST API + MCP server + SQLModel models frontend/ SvelteKit web UI (Svelte 5, TypeScript, Tailwind CSS v4) docs/ Deployment guides nginx/ nginx config for production @@ -60,6 +60,14 @@ 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 | @@ -94,6 +102,7 @@ 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 4253c54..76f7495 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -1,16 +1,15 @@ 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, E402 +import innercontext.models # noqa: F401 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 deleted file mode 100644 index 37d6f73..0000000 --- a/backend/alembic/versions/a1b2c3d4e5f6_add_ai_call_logs.py +++ /dev/null @@ -1,51 +0,0 @@ -"""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 deleted file mode 100644 index d6c0de6..0000000 --- a/backend/alembic/versions/b2c3d4e5f6a1_add_finish_reason_to_ai_call_logs.py +++ /dev/null @@ -1,29 +0,0 @@ -"""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 7407207..093aca0 100644 --- a/backend/alembic/versions/c2d626a2b36c_initial_schema.py +++ b/backend/alembic/versions/c2d626a2b36c_initial_schema.py @@ -1,20 +1,19 @@ """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 @@ -23,456 +22,217 @@ 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_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_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_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_index( - op.f("ix_lab_results_collected_at"), - "lab_results", - ["collected_at"], - unique=False, + 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_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_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_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_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_medication_entries_active_substance"), - "medication_entries", - ["active_substance"], - unique=False, + 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_kind"), "medication_entries", ["kind"], 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_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_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) # ### 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 deleted file mode 100644 index a6def60..0000000 --- a/backend/alembic/versions/d3e4f5a6b7c8_add_tool_trace_to_ai_call_logs.py +++ /dev/null @@ -1,29 +0,0 @@ -"""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 deleted file mode 100644 index 040d47e..0000000 --- a/backend/innercontext/api/ai_logs.py +++ /dev/null @@ -1,83 +0,0 @@ -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 743979b..945ab53 100644 --- a/backend/innercontext/api/products.py +++ b/backend/innercontext/api/products.py @@ -5,18 +5,12 @@ 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, col, select +from sqlmodel import Session, SQLModel, select from db import get_session from innercontext.api.utils import get_or_404 -from innercontext.llm import ( - call_gemini, - call_gemini_with_function_tools, - get_creative_config, - get_extraction_config, -) +from innercontext.llm import get_gemini_client from innercontext.models import ( Product, ProductBase, @@ -25,7 +19,6 @@ from innercontext.models import ( ProductPublic, ProductWithInventory, SkinConcern, - SkinConditionSnapshot, ) from innercontext.models.enums import ( AbsorptionSpeed, @@ -150,19 +143,6 @@ 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 @@ -183,41 +163,6 @@ 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 # --------------------------------------------------------------------------- @@ -260,9 +205,7 @@ def list_products( product_ids = [p.id for p in products] inventory_rows = ( session.exec( - select(ProductInventory).where( - col(ProductInventory.product_id).in_(product_ids) - ) + select(ProductInventory).where(ProductInventory.product_id.in_(product_ids)) ).all() if product_ids else [] @@ -424,15 +367,17 @@ OUTPUT SCHEMA (all fields optional — omit what you cannot determine): @router.post("/parse-text", response_model=ProductParseResponse) def parse_product_text(data: ProductParseRequest) -> ProductParseResponse: - response = call_gemini( - endpoint="products/parse-text", + client, model = get_gemini_client() + response = client.models.generate_content( + model=model, contents=f"Extract product data from this text:\n\n{data.text}", - config=get_extraction_config( + config=genai_types.GenerateContentConfig( system_instruction=_product_parse_system_prompt(), - response_schema=ProductParseLLMResponse, + response_mime_type="application/json", + response_schema=ProductParseResponse, max_output_tokens=16384, + temperature=0.0, ), - user_input=data.text, ) raw = response.text if not raw: @@ -442,8 +387,7 @@ 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: - llm_parsed = ProductParseLLMResponse.model_validate(parsed) - return ProductParseResponse.model_validate(llm_parsed.model_dump()) + return ProductParseResponse.model_validate(parsed) except ValidationError as e: raise HTTPException(status_code=422, detail=e.errors()) @@ -509,425 +453,3 @@ 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 503e0c9..5cc129f 100644 --- a/backend/innercontext/api/routines.py +++ b/backend/innercontext/api/routines.py @@ -6,23 +6,12 @@ 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 Field, Session, SQLModel, col, 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 ( - call_gemini, - call_gemini_with_function_tools, - get_creative_config, -) -from innercontext.models import ( - GroomingSchedule, - Product, - ProductInventory, - Routine, - RoutineStep, - SkinConditionSnapshot, -) +from innercontext.llm import get_gemini_client +from innercontext.models import GroomingSchedule, Product, Routine, RoutineStep, SkinConditionSnapshot from innercontext.models.enums import GroomingAction, PartOfDay router = APIRouter() @@ -81,36 +70,23 @@ 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): @@ -130,40 +106,23 @@ class BatchSuggestion(SQLModel): # --------------------------------------------------------------------------- -class _SingleStepOut(PydanticBase): +class _StepOut(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 - region: Optional[str] = None - action_notes: Optional[str] = None - - -class _SummaryOut(PydanticBase): - primary_goal: str - constraints_applied: list[str] - confidence: float class _SuggestionOut(PydanticBase): - steps: list[_SingleStepOut] + steps: list[_StepOut] reasoning: str - summary: _SummaryOut class _DayPlanOut(PydanticBase): date: str - am_steps: list[_BatchStepOut] - pm_steps: list[_BatchStepOut] + am_steps: list[_StepOut] + pm_steps: list[_StepOut] reasoning: str @@ -176,96 +135,43 @@ class _BatchOut(PydanticBase): # Prompt helpers # --------------------------------------------------------------------------- -_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 +_DAY_NAMES = ["poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota", "niedziela"] def _ev(v: object) -> str: - if v is None: - return "" - value = getattr(v, "value", None) - if isinstance(value, str): - return value - return str(v) + return v.value if v is not None and hasattr(v, "value") else str(v) if v is not None else "" 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 "SKIN CONDITION: no data\n" + return "STAN SKÓRY: brak danych\n" ev = _ev return ( - 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" + 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" ) -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() +def _build_grooming_context(session: Session, weekdays: Optional[list[int]] = None) -> str: + entries = session.exec(select(GroomingSchedule).order_by(GroomingSchedule.day_of_week)).all() if not entries: - return "GROOMING SCHEDULE: none\n" - lines = ["GROOMING SCHEDULE:"] + return "HARMONOGRAM PIELĘGNACJI: brak\n" + lines = ["HARMONOGRAM PIELĘGNACJI:"] 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(" (no entries for specified days)") + lines.append(" (brak wpisów dla podanych dni)") return "\n".join(lines) + "\n" @@ -277,462 +183,66 @@ def _build_recent_history(session: Session) -> str: .order_by(col(Routine.routine_date).desc()) ).all() if not routines: - return "RECENT ROUTINES: none\n" - lines = ["RECENT ROUTINES:"] + return "OSTATNIE RUTYNY (7 dni): brak\n" + lines = ["OSTATNIE RUTYNY (7 dni):"] for r in routines: steps = session.exec( select(RoutineStep) .where(RoutineStep.routine_id == r.id) - .order_by(col(RoutineStep.order_index)) + .order_by(RoutineStep.order_index) ).all() step_names = [] for s in steps: if s.product_id: p = session.get(Product, 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]}]") + step_names.append(p.name if p else str(s.product_id)) elif s.action_type: - step_names.append(f"action: {_ev(s.action_type)}") - lines.append( - f" {r.routine_date} {_ev(r.part_of_day).upper()}: {', '.join(step_names)}" - ) + step_names.append(_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, - 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:"] +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:"] for p in products: - p.inventory = inv_by_product.get(p.id, []) + if time_filter and _ev(p.recommended_time) not in (time_filter, "both"): + continue 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', [])}" ) - 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", {}) + profile = ctx.get("product_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" -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. +_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 """ @@ -746,7 +256,7 @@ DODATKOWE WYMAGANIA DLA TRYBU JEDNEJ RUTYNY: # --------------------------------------------------------------------------- -@router.get("") +@router.get("", response_model=list[Routine]) def list_routines( from_date: Optional[date] = None, to_date: Optional[date] = None, @@ -760,25 +270,7 @@ 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) - 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 + return session.exec(stmt).all() @router.post("", response_model=Routine, status_code=201) @@ -800,112 +292,37 @@ 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) - 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) + products_ctx = _build_products_context(session, time_filter=data.part_of_day.value) - mode_line = "MODE: standard" - notes_line = f"USER CONTEXT: {data.notes}\n" if data.notes else "" + notes_line = f"\nKONTEKST OD UŻYTKOWNIKA: {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"{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." + f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{products_ctx}\n{_RULES}{notes_line}" + "\nZwróć 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 = call_gemini_with_function_tools( - endpoint="routines/suggest", + response = client.models.generate_content( + model=model, 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" - "- 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, + config=genai_types.GenerateContentConfig( + response_mime_type="application/json", 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: @@ -923,34 +340,10 @@ 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", []) ] - - 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, - ) + return RoutineSuggestion(steps=steps, reasoning=parsed.get("reasoning", "")) @router.post("/suggest-batch", response_model=BatchSuggestion) @@ -960,20 +353,17 @@ 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.") - weekdays = list( - {(data.from_date + timedelta(days=i)).weekday() for i in range(delta)} - ) + client, model = get_gemini_client() + + 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, reference_date=data.from_date) - objectives_ctx = _build_objectives_context(data.include_minoxidil_beard) + products_ctx = _build_products_context(session) date_range_lines = [] for i in range(delta): @@ -981,35 +371,33 @@ def suggest_batch( date_range_lines.append(f" {d} ({_DAY_NAMES[d.weekday()]})") dates_str = "\n".join(date_range_lines) - 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 "" - ) + notes_line = f"\nKONTEKST OD UŻYTKOWNIKA: {data.notes}\n" if data.notes else "" prompt = ( - 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}" + 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" "\nZwróć JSON zgodny ze schematem." ) - 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, - ) + 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}") raw = response.text if not raw: @@ -1030,8 +418,6 @@ 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 @@ -1051,9 +437,7 @@ 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 8998e50..57b60b1 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 call_gemini, get_extraction_config +from innercontext.llm import get_gemini_client from innercontext.models import ( SkinConditionSnapshot, SkinConditionSnapshotBase, @@ -140,44 +140,37 @@ async def analyze_skin_photos( if not (1 <= len(photos) <= 3): raise HTTPException(status_code=422, detail="Send between 1 and 3 photos.") - allowed = { - "image/heic", - "image/heif", - "image/jpeg", - "image/png", - "image/webp", - } + client, model = get_gemini_client() + + allowed = {"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." ) ) - 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: + 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}") try: parsed = json.loads(response.text) diff --git a/backend/innercontext/llm.py b/backend/innercontext/llm.py index 13f0b10..428b744 100644 --- a/backend/innercontext/llm.py +++ b/backend/innercontext/llm.py @@ -1,53 +1,12 @@ """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" -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 - ), - ) +_DEFAULT_MODEL = "gemini-flash-latest" def get_gemini_client() -> tuple[genai.Client, str]: @@ -60,196 +19,3 @@ 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 new file mode 100644 index 0000000..618f267 --- /dev/null +++ b/backend/innercontext/mcp_server.py @@ -0,0 +1,438 @@ +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 5fa50b0..1ffe287 100644 --- a/backend/innercontext/models/__init__.py +++ b/backend/innercontext/models/__init__.py @@ -1,4 +1,3 @@ -from .ai_log import AICallLog from .domain import Domain from .enums import ( AbsorptionSpeed, @@ -42,8 +41,6 @@ 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 deleted file mode 100644 index dbd2d5e..0000000 --- a/backend/innercontext/models/ai_log.py +++ /dev/null @@ -1,33 +0,0 @@ -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 d2a529d..93c035a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,4 @@ from contextlib import asynccontextmanager -from typing import AsyncIterator from dotenv import load_dotenv @@ -7,27 +6,30 @@ 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) -> AsyncIterator[None]: +async def lifespan(app: FastAPI): create_db_and_tables() yield app = FastAPI( title="innercontext API", - lifespan=lifespan, + lifespan=combine_lifespans(lifespan, mcp_app.lifespan), redirect_slashes=False, ) @@ -43,7 +45,9 @@ 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.include_router(ai_logs.router, prefix="/ai-logs", tags=["ai-logs"]) + + +app.mount("/mcp", mcp_app) @app.get("/health-check") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 49f1708..d7c215c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -7,6 +7,7 @@ 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", @@ -21,7 +22,6 @@ 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 --cov=innercontext --cov-report=term-missing --cov-report=html" +addopts = "-v --tb=short" [tool.isort] profile = "black" diff --git a/backend/test_query.py b/backend/test_query.py deleted file mode 100644 index 46b5f20..0000000 --- a/backend/test_query.py +++ /dev/null @@ -1,25 +0,0 @@ -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 3c4f465..2cc5b80 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,4 +1,5 @@ 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://") @@ -13,6 +14,18 @@ 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.""" @@ -34,6 +47,9 @@ 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 deleted file mode 100644 index 47a168e..0000000 --- a/backend/tests/test_ai_logs.py +++ /dev/null @@ -1,44 +0,0 @@ -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 956cc75..1fae538 100644 --- a/backend/tests/test_products.py +++ b/backend/tests/test_products.py @@ -199,22 +199,3 @@ 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 deleted file mode 100644 index 805f35f..0000000 --- a/backend/tests/test_products_helpers.py +++ /dev/null @@ -1,157 +0,0 @@ -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 8f04045..b240079 100644 --- a/backend/tests/test_routines.py +++ b/backend/tests/test_routines.py @@ -1,5 +1,4 @@ import uuid -from unittest.mock import patch # --------------------------------------------------------------------------- # Routines @@ -217,82 +216,3 @@ 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 deleted file mode 100644 index 5d78872..0000000 --- a/backend/tests/test_routines_helpers.py +++ /dev/null @@ -1,408 +0,0 @@ -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 52286f9..a168b04 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -2,6 +2,18 @@ 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" @@ -47,6 +59,36 @@ 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" @@ -79,6 +121,30 @@ 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" @@ -223,90 +289,6 @@ 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" @@ -360,6 +342,21 @@ 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" @@ -369,6 +366,58 @@ 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" @@ -385,6 +434,37 @@ 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" @@ -530,6 +610,15 @@ 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" @@ -539,6 +628,18 @@ 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" @@ -555,6 +656,7 @@ source = { virtual = "." } dependencies = [ { name = "alembic" }, { name = "fastapi" }, + { name = "fastmcp" }, { name = "google-genai" }, { name = "psycopg" }, { name = "python-dotenv" }, @@ -569,7 +671,6 @@ dev = [ { name = "httpx" }, { name = "isort" }, { name = "pytest" }, - { name = "pytest-cov" }, { name = "ruff" }, { name = "ty" }, ] @@ -578,6 +679,7 @@ 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" }, @@ -592,7 +694,6 @@ 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" }, ] @@ -606,6 +707,115 @@ 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" @@ -618,6 +828,18 @@ 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" @@ -681,6 +903,49 @@ 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" @@ -690,6 +955,31 @@ 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" @@ -699,6 +989,15 @@ 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" @@ -739,6 +1038,31 @@ 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" @@ -784,6 +1108,11 @@ 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" @@ -855,6 +1184,20 @@ 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" @@ -864,6 +1207,29 @@ 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" @@ -880,20 +1246,6 @@ 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" @@ -941,6 +1293,31 @@ 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" @@ -987,6 +1364,20 @@ 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" @@ -1002,6 +1393,113 @@ 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" @@ -1039,6 +1537,19 @@ 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" @@ -1103,6 +1614,19 @@ 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" @@ -1358,3 +1882,12 @@ 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 deleted file mode 100755 index d60cc43..0000000 --- a/deploy.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/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 5f88f0d..36084bd 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -7,16 +7,13 @@ Reverse proxy (existing) innercontext LXC (new, Debian 13) ┌──────────────────────┐ ┌────────────────────────────────────┐ │ reverse proxy │────────────▶│ nginx :80 │ │ innercontext.lan → * │ │ /api/* → uvicorn :8000/* │ -└──────────────────────┘ │ /* → SvelteKit Node :3000 │ +└──────────────────────┘ │ /mcp/* → uvicorn :8000/mcp/* │ + │ /* → SvelteKit Node :3000 │ └────────────────────────────────────┘ │ │ - FastAPI SvelteKit Node + FastAPI + MCP 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 @@ -56,7 +53,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 rsync +apt install -y git nginx curl ca-certificates gnupg lsb-release libpq5 ``` ### Python 3.12+ + uv @@ -70,11 +67,6 @@ 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" @@ -83,14 +75,16 @@ 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 — self-contained, no wrapper scripts, -works system-wide: +Install pnpm as a standalone binary from GitHub releases — self-contained, +no wrapper scripts, works system-wide. Do **not** use `corepack enable pnpm` +(the shim requires its nvm directory structure and breaks when copied/linked): ```bash curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linux-x64" \ @@ -200,10 +194,11 @@ systemctl status innercontext --- -## 7. Frontend setup +## 7. Frontend build and setup -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. +```bash +cd /opt/innercontext/frontend +``` ### Create `.env.production` @@ -216,24 +211,25 @@ chmod 600 /opt/innercontext/frontend/.env.production chown innercontext:innercontext /opt/innercontext/frontend/.env.production ``` -### Grant `innercontext` passwordless sudo for service restarts +### Install dependencies and build ```bash -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 +sudo -u innercontext bash -c ' + cd /opt/innercontext/frontend + pnpm install + PUBLIC_API_BASE=http://innercontext.lan/api pnpm build +' ``` +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 innercontext-node -# Do NOT start yet — build/ is empty until the first deploy.sh run +systemctl enable --now innercontext-node +systemctl status innercontext-node ``` --- @@ -280,64 +276,44 @@ Reload your reverse proxy after applying the change. --- -## 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 +## 10. 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`. --- -## 12. Updating the application +## 11. Updating the application ```bash -# From the repo root on your local machine: -./deploy.sh # full deploy (frontend + backend) -./deploy.sh frontend # frontend only -./deploy.sh backend # backend only +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 ``` --- -## 13. Troubleshooting +## 12. Troubleshooting ### 502 Bad Gateway on `/api/*` @@ -352,7 +328,17 @@ journalctl -u innercontext -n 50 ```bash systemctl status innercontext-node journalctl -u innercontext-node -n 50 -# Verify /opt/innercontext/frontend/build/index.js exists (deploy.sh ran successfully) +# 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 ``` ### Database connection refused diff --git a/frontend/.mcp.json b/frontend/.mcp.json deleted file mode 100644 index 35c1203..0000000 --- a/frontend/.mcp.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mcpServers": { - "svelte": { - "type": "stdio", - "command": "npx", - "env": {}, - "args": ["-y", "@sveltejs/mcp"] - } - } -} diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index 82eddec..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -.svelte-kit -paraglide -build -dist -.env -.env.* -!.env.example diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index a927295..0000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plugins": ["svelte"], - "overrides": [ - { - "files": ["*.svelte"], - "parser": "svelte-eslint-parser" - } - ] -} diff --git a/frontend/README.md b/frontend/README.md index c0ec761..c48d921 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 77ccd6a..76eb346 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 deleted file mode 100644 index fd016cd..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,47 +0,0 @@ -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 9948336..a57eba2 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,522 +1,437 @@ { - "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": [ - { - "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.", + "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.", - "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": [ - { - "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.", + "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.", - "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_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", + "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)", - "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", + "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", - "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.", + "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.", - "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", + "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", - "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 42f2199..ba11dfb 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -1,536 +1,437 @@ { - "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 rutyn 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 rutyno w ciągu ostatnich 2 tygodni.", - "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.", + "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.", - "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": [ - { - "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.", + "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.", - "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_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", + "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)", - "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", + "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", - "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ń.", + "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ń.", - "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", + "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", - "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 b3169c9..0d75051 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,47 +1,36 @@ { - "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" - } + "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" + } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5bfa6f0..f5aa872 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,13 +1,14 @@ -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: @@ -22,58 +23,34 @@ 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: - "@eslint/js": - specifier: ^10.0.1 - version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) - "@internationalized/date": + '@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) @@ -83,2324 +60,1005 @@ 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] - "@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/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} - "@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/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} - "@eslint/config-array@0.23.2": - resolution: - { - integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==, - } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - "@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==, - } + '@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/esrecurse@4.3.1": - resolution: - { - integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==, - } + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - "@types/estree@1.0.8": - resolution: - { - integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, - } + '@types/node@25.3.3': + resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} - "@types/json-schema@7.0.15": - resolution: - { - integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, - } + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - "@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 + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 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" } - - balanced-match@4.0.4: - resolution: - { - integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, - } - engines: { node: 18 || 20 || >=22 } + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} 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==, - } - - 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 + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} 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==, - } - - 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 } + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 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==, - } - - 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" } + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} estree-walker@2.0.2: - 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==, - } + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 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==, - } - - 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" } + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 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" } - - 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" } + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} 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==, - } - - isexe@2.0.0: - resolution: - { - integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, - } + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} 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==, - } - - 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==, - } + resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} 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" } - - known-css-properties@0.37.0: - resolution: - { - integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==, - } + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} kysely@0.27.6: - 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" } + resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} + engines: {node: '>=14.0.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" } - - lilconfig@2.1.0: - resolution: - { - integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} locate-character@3.0.0: - resolution: - { - integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==, - } - - locate-path@6.0.0: - resolution: - { - integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 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==, - } - - minimatch@10.2.4: - resolution: - { - integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==, - } - engines: { node: 18 || 20 || >=22 } + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 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" } - - ms@2.1.3: - resolution: - { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, - } + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} 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==, - } - - 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" } + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 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" } - - 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" } + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} postcss@8.5.6: - 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" } + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} 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" } - - semver@7.7.4: - resolution: - { - integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, - } - engines: { node: ">=10" } - hasBin: true + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} set-cookie-parser@3.0.1: - 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" } + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} 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" - - 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 + typescript: '>=5.0.0' 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" } - - ts-api-utils@2.4.0: - resolution: - { - integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==, - } - engines: { node: ">=18.12" } - peerDependencies: - typescript: ">=4.8.4" + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} tslib@2.8.1: - 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" + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 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" } - - uri-js@4.4.1: - resolution: - { - integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, - } + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} urlpattern-polyfill@10.1.0: - resolution: - { - integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==, - } - - util-deprecate@1.0.2: - resolution: - { - integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, - } + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} 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 @@ -2424,10 +1082,7 @@ 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: @@ -2435,185 +1090,106 @@ packages: optional: true webpack-virtual-modules@0.6.2: - 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" } + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} 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 - "@eslint-community/eslint-utils@4.9.1(eslint@10.0.2(jiti@2.6.1))": + '@floating-ui/core@1.7.4': dependencies: - eslint: 10.0.2(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 + '@floating-ui/utils': 0.2.10 - "@eslint-community/regexpp@4.12.2": {} - - "@eslint/config-array@0.23.2": + '@floating-ui/dom@1.7.5': dependencies: - "@eslint/object-schema": 3.0.2 - debug: 4.4.3 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 - "@eslint/config-helpers@0.5.2": + '@floating-ui/utils@0.2.10': {} + + '@inlang/paraglide-js@2.13.0': dependencies: - "@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 + '@inlang/recommend-sherlock': 0.2.1 + '@inlang/sdk': 2.7.0 commander: 11.1.0 consola: 3.4.0 json5: 2.2.3 @@ -2622,46 +1198,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 @@ -2671,17 +1247,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) @@ -2691,129 +1267,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 @@ -2828,16 +1404,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 @@ -2845,13 +1421,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 @@ -2859,209 +1435,97 @@ 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/esrecurse@4.3.1": {} + '@types/estree@1.0.8': {} - "@types/estree@1.0.8": {} - - "@types/json-schema@7.0.15": {} - - "@types/node@25.3.3": + '@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": {} - - "@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 + '@types/trusted-types@2.0.7': {} 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" - - brace-expansion@5.0.4: - dependencies: - balanced-match: 4.0.4 + - '@sveltejs/kit' chokidar@4.0.3: dependencies: @@ -3085,22 +1549,8 @@ 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: {} @@ -3116,181 +1566,52 @@ 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 - - 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 + '@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 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 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} + '@jridgewell/sourcemap-codec': 1.5.5 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: @@ -3299,63 +1620,32 @@ 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 - - isexe@2.0.0: {} + '@types/estree': 1.0.8 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 @@ -3405,14 +1695,8 @@ 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 @@ -3421,11 +1705,7 @@ snapshots: magic-string@0.30.21: dependencies: - "@jridgewell/sourcemap-codec": 1.5.5 - - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.4 + '@jridgewell/sourcemap-codec': 1.5.5 mode-watcher@1.1.0(svelte@5.53.5): dependencies: @@ -3437,78 +1717,22 @@ 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: @@ -3519,33 +1743,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): @@ -3565,25 +1789,17 @@ 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 @@ -3591,7 +1807,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: @@ -3602,7 +1818,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 @@ -3612,22 +1828,6 @@ 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 @@ -3635,7 +1835,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: @@ -3646,11 +1846,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 @@ -3684,27 +1884,8 @@ 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: @@ -3712,19 +1893,13 @@ 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: {} @@ -3738,7 +1913,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 @@ -3749,14 +1924,4 @@ 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 057bbee..8342010 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 f7b8231..130b9cb 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 520c421..da08e6d 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 adf8bd8..f273cc5 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 cb5906d..4449456 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 8ca13ab..dc0f712 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,349 +1,271 @@ -import { browser } from "$app/environment"; -import { PUBLIC_API_BASE } from "$env/static/public"; +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 { - // 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(); + 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(); } 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; - include_minoxidil_beard?: boolean; - leaving_home?: boolean; -}): Promise => api.post("/routines/suggest", body); + routine_date: string; + part_of_day: PartOfDay; + notes?: string; +}): Promise => api.post('/routines/suggest', body); export const suggestBatch = (body: { - from_date: string; - to_date: string; - notes?: string; - include_minoxidil_beard?: boolean; - minimize_products?: boolean; -}): Promise => api.post("/routines/suggest-batch", body); + from_date: string; + to_date: string; + notes?: string; +}): 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 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(); +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(); } diff --git a/frontend/src/lib/components/ProductForm.svelte b/frontend/src/lib/components/ProductForm.svelte index b0cc24f..430b101 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}