Merge branch 'worktree-check-security-audit'

Security and frontend-correctness hardening: analytics IDOR fix, scraper
log scrubbing, lowercase email normalisation (migration 009), stale-response
guards in the plan hooks, busy flags cleared unconditionally across all
useLatestRequest consumers, Calendar no longer blanks on mutation, plus
dependabot/CI config and removal of the stray frontend/out.js.

Conflicts came from main's later "day assertions" restructure, which made
usePlannedDays read-only and replaced the calendar's per-day add/update with
useDayAssertions.assertDay. Both intents kept:

- usePlannedDays: branch's fetch machinery (isRefreshing, in-flight dedup,
  unguarded busy clear); mutators dropped as on main.
- usePlanSync: branch's useLatestRequest guard also applied to main's new
  syncWithWish.
- useDayAssertions / useLatestBalance (new on main): busy flag now cleared
  unconditionally, as the branch did for every other consumer.
- useDayAssertions.assertDay gains the branch's `skipRefresh` option and a
  `refresh`, so Calendar's multi-day save keeps the batch shape (N writes,
  one trailing bumpPlanVersion + one assertions refresh) on main's
  assertDay path. Branch's M7 tests ported from the removed add/update API
  to assertDay.
- PlanSyncProvider: branch's isAuthenticated gate kept; eviction-preview
  state stays removed as on main.
- usePlannedDayExclusions.ts stays deleted (superseded by useDayAssertions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
This commit is contained in:
2026-09-21 07:40:09 +02:00
47 changed files with 1271 additions and 190 deletions
+4
View File
@@ -44,7 +44,9 @@ async def get_overview(
pd_repo: PersonalDataRepositoryDep,
workspace_repo: WorkspaceRepositoryDep,
vacation_repo: VacationDaysRepositoryDep,
scope: WorkspaceScopeDep,
) -> WorkspaceOverviewResponse:
await scope.check_child(child_id)
service = _make_service(
leave_repo, planned_repo, child_repo, pd_repo, workspace_repo, vacation_repo
)
@@ -65,7 +67,9 @@ async def get_projection(
pd_repo: PersonalDataRepositoryDep,
workspace_repo: WorkspaceRepositoryDep,
vacation_repo: VacationDaysRepositoryDep,
scope: WorkspaceScopeDep,
) -> WorkspaceOverviewResponse:
await scope.check_child(child_id)
service = _make_service(
leave_repo, planned_repo, child_repo, pd_repo, workspace_repo, vacation_repo
)
+11
View File
@@ -10,6 +10,7 @@ from app.dependencies import (
ChildRepositoryDep,
CurrentUserDep,
PersonalDataRepositoryDep,
WorkspaceRepositoryDep,
)
from app.rate_limit import limiter
from app.models.auth import LoginRequest, RegisterRequest, TokenResponse, UserMeResponse
@@ -105,6 +106,11 @@ class _UserContext(BaseModel):
workspace_id: str
parent_name: str
children: list[_ChildContext]
# Mirrors `WorkspaceResponse.take_pl_on_red_days` — included here so the
# frontend's cold-load `refreshContext()` can build its `workspace`
# state from this response alone, without a follow-up `GET
# /workspace/:id` (see AGENTS.md H12 step 1).
take_pl_on_red_days: bool
@router.get("/context", response_model=_UserContext)
@@ -112,12 +118,16 @@ async def get_context(
current_user: CurrentUserDep,
pd_repo: PersonalDataRepositoryDep,
child_repo: ChildRepositoryDep,
ws_repo: WorkspaceRepositoryDep,
) -> _UserContext:
"""Return workspace + children for the current user. Used by the dev harness."""
pd = await pd_repo.get_by_id(current_user.personal_data_uuid)
if pd is None:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, "No parent profile found")
children = await child_repo.list_by_workspace(pd.workspace_id)
workspace = await ws_repo.get_by_id(pd.workspace_id)
if workspace is None:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "No workspace found")
return _UserContext(
personal_data_uuid=str(pd.uuid),
workspace_id=str(pd.workspace_id),
@@ -125,6 +135,7 @@ async def get_context(
children=[
_ChildContext(id=str(c.id), name=c.name, birth_date=str(c.birth_date)) for c in children
],
take_pl_on_red_days=workspace.take_pl_on_red_days,
)