Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions stackunderflow/routes/cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from typing import Annotated, Any

from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query

import stackunderflow.deps as deps
from stackunderflow.infra.currency import active_currency_payload
Expand Down Expand Up @@ -153,7 +153,7 @@ async def get_cost_data(log_path: str | None = None, timezone_offset: int = 0):
@router.get("/api/cost-data/by-provider")
async def get_cost_by_provider(
period: str = "month",
provider: list[str] | None = None,
provider: Annotated[list[str] | None, Query()] = None,
):
"""Return total cost / message count / session count grouped by provider.

Expand Down
11 changes: 6 additions & 5 deletions stackunderflow/routes/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import threading
import time
from pathlib import Path
from typing import Annotated

from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import JSONResponse

import stackunderflow.deps as deps
Expand Down Expand Up @@ -113,8 +114,8 @@ async def get_stats(timezone_offset: int = 0):
@router.get("/api/dashboard-data")
async def get_dashboard_data(
timezone_offset: int = 0,
provider: list[str] | None = None,
model: list[str] | None = None,
provider: Annotated[list[str] | None, Query()] = None,
model: Annotated[list[str] | None, Query()] = None,
):
"""Get optimized data for initial dashboard load.

Expand Down Expand Up @@ -278,8 +279,8 @@ def _apply_currency_to_stats(stats: dict) -> dict:
async def get_messages(
limit: int | None = None,
timezone_offset: int = 0,
provider: list[str] | None = None,
model: list[str] | None = None,
provider: Annotated[list[str] | None, Query()] = None,
model: Annotated[list[str] | None, Query()] = None,
):
"""Get messages for the current project.

Expand Down
8 changes: 5 additions & 3 deletions stackunderflow/routes/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

from __future__ import annotations

from fastapi import APIRouter, HTTPException
from typing import Annotated

from fastapi import APIRouter, HTTPException, Query

import stackunderflow.deps as deps
from stackunderflow.reports.optimize import find_patterns, find_waste
Expand All @@ -30,8 +32,8 @@
@router.get("/api/optimize")
async def get_optimize_report(
period: str = "30days",
project: list[str] | None = None,
exclude: list[str] | None = None,
project: Annotated[list[str] | None, Query()] = None,
exclude: Annotated[list[str] | None, Query()] = None,
):
"""Run waste + structural-pattern detection over *period*.

Expand Down
5 changes: 3 additions & 2 deletions stackunderflow/routes/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import os
from collections import defaultdict
from pathlib import Path
from typing import Annotated

from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import JSONResponse

import stackunderflow.deps as deps
Expand Down Expand Up @@ -155,7 +156,7 @@ async def get_projects(
sort_by: str = "last_modified",
limit: int | None = None,
offset: int = 0,
provider: list[str] | None = None,
provider: Annotated[list[str] | None, Query()] = None,
):
"""
Get all available Claude projects with metadata.
Expand Down
5 changes: 3 additions & 2 deletions stackunderflow/routes/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import json
from datetime import datetime
from pathlib import Path
from typing import Annotated

from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import JSONResponse

import stackunderflow.deps as deps
Expand Down Expand Up @@ -38,7 +39,7 @@ def _duration_minutes(first: str | None, last: str | None) -> float | None:
@router.get("/api/jsonl-files")
async def get_jsonl_files(
project: str | None = None,
provider: list[str] | None = None,
provider: Annotated[list[str] | None, Query()] = None,
):
"""Get list of JSONL files for a project with metadata.

Expand Down
36 changes: 28 additions & 8 deletions stackunderflow/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,40 @@ async def _lifespan(_app: FastAPI):
if failed:
logger.warning(f"Failed services: {', '.join(failed)}")

# Initialise the session store and run one ingest pass.
# Initialise the session store schema synchronously (cheap), then
# run the ingest in a background thread so HTTP starts serving
# immediately. Without this, the lifespan blocks the bind for the
# full duration of the reindex (~90s on 7 small projects, 30+min
# on a cold 188-project store) — and the "live at..." line that
# already printed from the CLI wrapper is misleading because the
# HTTP server hasn't actually started yet.
import threading
from stackunderflow.adapters import registered
from stackunderflow.ingest import run_ingest
from stackunderflow.store import db, schema

try:
store_conn = db.connect(deps.store_path)
schema.apply(store_conn)
counts = run_ingest(store_conn, registered())
logger.info("Ingest complete: %s", counts)
store_conn.close()
_maybe_clean_cold_cache()
_schema_conn = db.connect(deps.store_path)
schema.apply(_schema_conn)
_schema_conn.close()
except Exception as e:
logger.error("Ingest failed at startup: %s", e)
logger.error("Schema apply failed at startup: %s", e)

def _background_ingest() -> None:
try:
conn = db.connect(deps.store_path)
counts = run_ingest(conn, registered())
logger.info("Ingest complete: %s", counts)
conn.close()
_maybe_clean_cold_cache()
except Exception as exc: # noqa: BLE001 — top of background thread
logger.error("Background ingest failed: %s", exc)

threading.Thread(
target=_background_ingest,
name="stackunderflow-ingest",
daemon=True,
).start()

yield

Expand Down
Loading