4 Commits

Author SHA1 Message Date
f85b90c95e fix(frontend): satisfy phase 2 lint checks
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 7m18s
CI / backend (pull_request) Successful in 10m23s
CI / e2e (pull_request) Successful in 9m31s
Co-Authored-By: Codex <noreply@openai.com>
2026-05-07 12:02:49 -04:00
5e6541ab92 fix(ci): set up node in gitea workflow
Some checks failed
Mirror to GitHub / mirror (push) Successful in 7s
CI / frontend (pull_request) Failing after 2m48s
CI / backend (pull_request) Successful in 15m5s
CI / e2e (pull_request) Successful in 8m47s
Co-Authored-By: Codex <noreply@openai.com>
2026-05-07 11:45:58 -04:00
4a37a47887 chore(env): standardize backend python on 3.12
Some checks failed
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Failing after 1m8s
CI / e2e (pull_request) Successful in 12m9s
CI / backend (pull_request) Successful in 15m24s
Co-Authored-By: Codex <noreply@openai.com>
2026-05-07 11:31:28 -04:00
f31b873459 wip(handoff): record native python status
Co-Authored-By: Codex <noreply@openai.com>
2026-05-07 11:14:59 -04:00
19 changed files with 136 additions and 106 deletions

View File

@@ -1,6 +1,6 @@
# CURRENT_TASK.md # CURRENT_TASK.md
**Active task:** Self-serve signup Phase 2 — code complete on `feat/self-serve-signup-phase-2` (HEAD `c75ce0c`). PR not yet opened. Next: review/merge, then Phase O manual ops (Stripe live setup, internal validation, flag flip). See `.ai/HANDOFF.md` for the resume point. **Active task:** Self-serve signup Phase 2 — PR #162 is open on `feat/self-serve-signup-phase-2`. Current focus is resolving its failing Gitea checks. Phase O manual ops (Stripe live setup, internal validation, flag flip) remain pending after review/merge. See `.ai/HANDOFF.md` for the resume point.
## Recently shipped ## Recently shipped

View File

@@ -13,6 +13,16 @@
--- ---
## 2026-05-07 — Standardize backend Python on 3.12
**Context:** Runtime facts had drifted from docs. The backend Dockerfiles and running dev container were already on Python 3.12, GitHub CI had just been updated to 3.12, but project docs still said Python 3.11 and Gitea CI relied on the runner's ambient Python.
**Decision:** Treat Python 3.12 as the backend standard. Pin local pyenv via `.python-version` to 3.12.13, matching the current `python:3.12-slim` container patch level. Add explicit Python 3.12 setup to Gitea CI and keep GitHub CI on Python 3.12.
**Rejected:** Moving Docker/runtime back to Python 3.11. The application was already building and running on 3.12, so reverting the runtime would add churn without a product or dependency reason.
**Consequences:** Native backend work should use `backend/venv` created from Python 3.12.13. Future docs/CI/runtime changes should preserve Python 3.12 unless a deliberate upgrade decision is recorded.
## 2026-04-30 — Add `applied_pending` non-terminal status to suggested fixes ## 2026-04-30 — Add `applied_pending` non-terminal status to suggested fixes
**Context:** The verifying banner forces a synchronous verdict — worked / didn't / partial — but a lot of real MSP fixes are async. Engineer ran the script but is waiting on the client to power-cycle, AD replication, an O365 license sync. With only the existing outcomes, the engineer either leaves the banner stale (eroding the verifying signal) or guesses wrong (corrupting outcome data). User flagged the gap directly. Today's `NudgeBanner` "Still checking" button just silences the nudge — it doesn't tell the system anything. **Context:** The verifying banner forces a synchronous verdict — worked / didn't / partial — but a lot of real MSP fixes are async. Engineer ran the script but is waiting on the client to power-cycle, AD replication, an O365 license sync. With only the existing outcomes, the engineer either leaves the banner stale (eroding the verifying signal) or guesses wrong (corrupting outcome data). User flagged the gap directly. Today's `NudgeBanner` "Still checking" button just silences the nudge — it doesn't tell the system anything.

View File

@@ -2,71 +2,56 @@
# HANDOFF.md # HANDOFF.md
**Last updated:** 2026-05-07 (Phase 2 code complete + four post-implementation fixes; cutover ops still pending) **Last updated:** 2026-05-07 (PR #162 CI investigation/fixes)
**Active task:** None mid-flight. Branch `feat/self-serve-signup-phase-2` (HEAD `502c0a4`) is ready to PR. **Active task:** PR #162 (`feat/self-serve-signup-phase-2`) is open in Gitea. Current session is resolving its failing checks.
## Where this session ended ## Where this session ended
Tasks 2744 of Phase 2 implemented across 18 commits on `feat/self-serve-signup-phase-2`, branched off `main` (`f918b76`). Each task went through implementer + spec review + code-quality review per `superpowers:subagent-driven-development`. Cross-cutting review caught one redirect bug (fixed in-flight). After initial handoff, external code review surfaced four more real bugs — all fixed (commits `5e0c9d2`, `3630dd5`, `06200fa`, `502c0a4`): PR #162 originally failed quickly in Gitea CI. Public Gitea status metadata was available, but job logs redirected to login and no `GITEA_TOKEN` was present. The branch was pushed over SSH.
1. **OAuth refresh tokens never stored** (Phase 1 latent): `_store_refresh_token` extracted to module-public `store_refresh_token`; both Google + Microsoft callbacks now persist the JTI before returning. Test covers callback → `/auth/refresh` round-trip. Fixed environment drift first:
2. **OAuth callback never marked store authenticated** (Phase 2 introduced): `setTokens` now sets `isAuthenticated: true`. Verified safe for the refresh-interceptor caller.
3. **Stripe webhook idempotency could permanently drop failed events** (Phase 1 latent): `apply_subscription_event` now adds StripeEvent + runs handler + commits atomically; handler exception → rollback → Stripe retries. Inner `_handle_*` commits removed.
4. **Missing `/account/billing` and `/account/billing/select-plan` pages** (Phase 2 oversight): all the new billing CTAs (TrialPill, UpgradePrompt, NextStepCard, SetupChecklist) linked to non-existent routes. Built BillingPage (subscription summary + Manage-billing → portal session) and SelectPlanPage (plan cards + checkout flow).
**Backend additions (Phase I, Tasks 2731):** - Standardized backend native/dev/CI Python on 3.12.13 to match Docker.
- `BillingService.open_customer_portal` + `GET /billing/portal-session` (allowlisted for canceled/unverified users). - Added `.python-version`.
- `PATCH /users/me/onboarding-step` + `POST /users/me/onboarding-dismiss-rest`. - Rebuilt `backend/venv` from pyenv Python 3.12.13 and verified native `pytest --version` / `alembic --version` with explicit local env.
- `POST /sales-leads` (public, 5/hr/IP rate limit, fire-and-forget notification email, server-side PostHog event stub). - Updated Gitea CI backend/e2e Python setup to 3.12.
- `/admin/plan-limits` GET/PUT round-trips `plan_billing` (Stripe IDs, prices, public/archived flags) in one transaction; `BillingService.invalidate_billing_cache` no-op stub for future cache.
- `GET /config/public` (`{self_serve_enabled, oauth_providers}`); register-endpoint gate now `REQUIRE_INVITE_CODE and not SELF_SERVE_ENABLED and not invite_code`.
- `GET /accounts/invites/{code}/lookup` (Phase 36).
- OAuth callback extended to honor `account_invite_code` + `invited_email` for invitee-via-OAuth path; rejects existing-email user with `email_already_registered_use_login`.
- `GET /plans/public` (Phase 42).
- `POST /beta-signup` returns 307 to `${FRONTEND_URL}/register?from=beta`.
- `OnboardingStatus` extended with `email_verified` + `shop_setup_done`; `UserResponse` exposes `onboarding_step_completed` + `onboarding_dismissed`.
**Frontend additions:** Fixed Gitea runner assumptions next:
- `useBillingStore` (Zustand, polled 60s via `useBillingPoll` mounted in `AppLayout`), `useFeature` / `useFeatureLimit` / `useTrialBanner` hooks, `FeatureGate` / `UpgradePrompt` / `EmailVerificationGate` components.
- `RegisterPage` redesign: OAuth (Google/Microsoft) buttons + invite-code-conditional. `OAuthCallbackPage` at `/auth/{google,microsoft}/callback` with CSRF state validation. `useAppConfig` hook (consumes `/config/public`, falls back to `VITE_SELF_SERVE_ENABLED`).
- `AcceptInvitePage` at `/accept-invite?code=...` (locked email, 3 sign-in options, `/?welcome=teammate` on success).
- `EmailVerificationBanner` refactored to design-system tokens + grace-period hide; `EmailVerificationWall` polished; `VerifyEmailPage` at `/verify-email?token=...` (single-fire guard).
- `WelcomeRouter` + `WelcomeStep1/2/3` at `/welcome*`. PSA tile UI + bulk invite emails per row.
- `TrialPill` in topbar (pristine/warning/urgent/expired/paid/complimentary/past_due/canceled).
- `NextStepCard` + `SetupChecklist` (replaces orphaned `OnboardingChecklist`); unified list, no SOLO/TEAM split, no Script Builder item.
- `PricingPage` at `/pricing` (B-style, 3 plan cards, hardcoded comparison v1).
- `ContactSalesPage` at `/contact-sales`. `LandingPage` got "See pricing" CTA + replaced beta-signup form with `<Link to="/register?from=beta">`.
New env vars (Vite ARG/ENV in Dockerfile + `.env.example`): `VITE_SELF_SERVE_ENABLED`, `VITE_GOOGLE_CLIENT_ID`, `VITE_MS_CLIENT_ID`, `VITE_OAUTH_REDIRECT_BASE`, `VITE_CALENDLY_URL`. Backend env: `SALES_LEAD_RECIPIENT_EMAIL` (default `sales@resolutionflow.com`). - Added `actions/setup-node@v4` with Node 20 to Gitea frontend and e2e jobs.
- Pushed `fix(ci): set up node in gitea workflow`.
## Resume point — DO THIS NEXT Local frontend validation then exposed real lint failures in Phase 2 React code under the current lint stack. The current WIP fixes:
1. **Review the branch and open a PR.** 18 commits, all squashed-or-not at user discretion. Branch `feat/self-serve-signup-phase-2` from `main`. - `react-refresh/only-export-components` for exported pure helpers used by tests/shared invite OAuth code.
2. **Phase O — manual operational tasks** (Tasks 4547 from the plan): - `react-hooks/set-state-in-effect` warnings where local state intentionally mirrors route/config/cache state.
- **Task 45:** Stripe live-mode setup (Products, Prices, Customer Portal, webhook endpoint + signing secret) and Railway prod env vars (`STRIPE_*`, `OAUTH_REDIRECT_BASE`, `GOOGLE_CLIENT_*`, `MS_CLIENT_*`). Run `python -m scripts.sync_stripe_plan_ids` to populate `plan_billing` rows with live Stripe IDs. - `react-hooks/purity` warnings from `Date.now()` during render.
- **Task 46:** Internal validation pass via `INTERNAL_TESTER_EMAILS` allowlist (per-email bypass of `SELF_SERVE_ENABLED=false`). Backend support for the allowlist itself is NOT implemented — needs a small addition to `auth.py`/`config.py` if the validation pass requires it. Otherwise validate in test mode with the flag flipped temporarily. - Redundant loading-state write in pricing page.
- **Task 47:** Flip `SELF_SERVE_ENABLED=true` (backend) + `VITE_SELF_SERVE_ENABLED=true` (frontend rebuild). Watch PostHog funnel + Stripe webhook error rate.
3. **Followups deferred during Phase 2** (logged below).
## Followups deferred from Phase 2 Validation after those frontend changes:
- **PostHog server-side capture infrastructure missing.** `/sales-leads` calls `_capture_posthog_event` which lazy-imports `app.core.analytics.posthog` (no-op if module absent). Wire up the real PostHog server SDK before cutover — needs `app/core/analytics.py` with a configured client, plus `python-posthog` dep. - `docker exec -w /app resolutionflow_frontend npm run lint` passed.
- **Frontend `/usage/{field}` endpoint missing.** `useFeatureLimit` lazy-fetches `apiClient.get('/usage/' + field)` — endpoint doesn't exist; hook silently falls back to `used=0`. Build the backend endpoint before any UI surface relies on real usage counts. - `docker exec -w /app resolutionflow_frontend npm run test:coverage` passed (`198` tests).
- **Pre-existing dual subscription stores** (`authStore.subscription` legacy + `billingStore.subscription` new). `Subscription.status` union in `frontend/src/types/account.ts` is missing `'complimentary'`. Phase 2 didn't introduce — but worth deprecating the old store before any UI reads `complimentary` from the wrong source. - `docker exec -w /app -e NODE_OPTIONS=--max-old-space-size=4096 resolutionflow_frontend npm run build` passed.
- **`INTERNAL_TESTER_EMAILS` per-email allowlist** (Task 46): backend support not implemented. Add when Phase O Task 46 needs it.
- **`accept-invite` not gated by `self_serve_enabled`** — by design (invites work in any mode). Confirm if pilot wants stricter gating.
- **`UserResponse.onboarding_step_completed` validator** suggested by reviewer (reject `complete` without data) — skipped, spec types `data?` as optional.
- **Welcome wizard behind `EmailVerificationGate`:** Day 7+ unverified users hit the wall on `/welcome/*` and have no path back into the wizard until they verify. Per spec — wall says "Resend verification" → user verifies → wall closes → wizard resumes. If product wants a different UX, allowlist `/welcome/*` in the gate.
- **`SelectPlanPage` seat input doesn't validate against plan `max_seats`.** Stripe Checkout will reject if exceeded; nicer UX would cap client-side.
- **`BillingPage` Manage-billing button always enabled** — clicking with no `stripe_customer_id` falls back to a toast. By design; could disable + tooltip if billing-state payload were extended with that flag.
## Environment notes (carry-forward) Known local noise:
- Code-server LXC: docker-only, no native python/node/npm. Use `docker exec resolutionflow_{backend,frontend} ...`. - React `act(...)` warnings appeared in existing tests during coverage but did not fail the suite.
- Pytest: `docker exec resolutionflow_backend pytest tests/<file> -v --override-ini="addopts="`. - Vite emitted large chunk warnings during build.
- Vitest: `docker exec -w /app resolutionflow_frontend npm test -- <path> --run`. - Unrelated dirty/untracked files remain and should not be staged unless explicitly requested: `docker-compose.dev.yml`, `.env.example`, `abc-feat-self-serve-signup-phase-2-design-20260507-112020.md`, `core.*`, `docs/architecture/`, `docs/tutorials/`.
- TS build: `docker exec -w /app resolutionflow_frontend npx tsc -b`.
- Alembic: `docker exec -w /app resolutionflow_backend alembic ...`. Never `--rev-id`. ## Resume point
- No `gh` CLI — Gitea API via `$GITEA_TOKEN` for PR/issue work.
- Single alembic head: `c6cbfc534fad` (from Phase 1). Phase 2 added no migrations. 1. Commit the frontend lint fixes and `.ai/` handoff updates with the required Codex trailer.
2. Push `feat/self-serve-signup-phase-2`.
3. Poll Gitea PR #162 statuses for the new head SHA:
`curl -fsSL https://gitea.resolutionflow.com/api/v1/repos/chihlasm/resolutionflow/statuses/<sha> | python -m json.tool`
4. If statuses are still pending, report that local frontend CI is green and Gitea runner work is queued/running. If a check fails, public statuses may show only the context/description; logs require authenticated Gitea access.
## Carry-forward
- Phase O manual ops remain pending after PR review/merge: Stripe live setup, internal validation, feature-flag flip.
- Backend env: `SALES_LEAD_RECIPIENT_EMAIL`.
- Frontend env: `VITE_SELF_SERVE_ENABLED`, `VITE_GOOGLE_CLIENT_ID`, `VITE_MS_CLIENT_ID`, `VITE_OAUTH_REDIRECT_BASE`, `VITE_CALENDLY_URL`.
- Single alembic head remains `c6cbfc534fad`; Phase 2 added no migrations.

View File

@@ -26,7 +26,7 @@ Go-to-Market Validation (pre-PMF). Backend feature-complete (55+ endpoints, 100+
## Tech stack ## Tech stack
- **Backend:** Python 3.11 + FastAPI, SQLAlchemy 2.0 async (asyncpg), Alembic, Pydantic v2, JWT (python-jose + bcrypt, JTI refresh rotation), APScheduler (in-process with FastAPI lifespan). - **Backend:** Python 3.12 + FastAPI, SQLAlchemy 2.0 async (asyncpg), Alembic, Pydantic v2, JWT (python-jose + bcrypt, JTI refresh rotation), APScheduler (in-process with FastAPI lifespan).
- **Frontend:** React 19 + Vite + TypeScript, Tailwind v4 (CSS-only config in `index.css`), Zustand (immer + zundo), React Router v7, Axios (token-refresh interceptor), Lucide. - **Frontend:** React 19 + Vite + TypeScript, Tailwind v4 (CSS-only config in `index.css`), Zustand (immer + zundo), React Router v7, Axios (token-refresh interceptor), Lucide.
- **DB:** PostgreSQL 16 (RLS enabled Phase 4, pgvector). - **DB:** PostgreSQL 16 (RLS enabled Phase 4, pgvector).

View File

@@ -12,6 +12,29 @@
--- ---
## 2026-05-07 11:45 EDT — Codex — Push PR #162 CI runner setup fixes
- Inspected Gitea PR #162 via public API. PR head was `380fcf7` and all CI jobs failed quickly; pushed local commits through `4a37a47`, including Python 3.12 setup for Gitea backend/e2e jobs.
- New run on `4a37a47` showed frontend still failed quickly while backend/e2e remained pending. Root cause likely same class of runner drift: Gitea frontend/e2e jobs used `npm` without setting up Node.
- Added explicit `actions/setup-node@v4` with Node 20 to Gitea frontend and e2e jobs. This keeps CI from relying on runner ambient Node/npm.
- Files touched: `.gitea/workflows/ci.yml`, `.ai/HANDOFF.md`, `.ai/SESSION_LOG.md`.
## 2026-05-07 11:30 EDT — Codex — Standardize backend Python on 3.12
- Standardized repo declarations around Python 3.12: added `.python-version` pinned to 3.12.13, updated stale Python 3.11 docs, and added explicit Python 3.12 setup steps to Gitea CI. GitHub CI was already updated to Python 3.12 by the user.
- Installed pyenv Python 3.12.13 and created `backend/venv` from that interpreter. Installed `backend/requirements-dev.txt` into the venv.
- Verified native `python --version` and venv `python --version` both report 3.12.13. Verified native `pytest 8.4.2` and `alembic 1.18.3` with explicit safe test env vars; plain pytest import still depends on local `.env` values being valid.
- Rebuilt and restarted the dev backend container with `docker compose -f docker-compose.dev.yml build backend` and `up -d backend`; confirmed `docker exec resolutionflow_backend python --version` reports 3.12.13.
- Files touched: `.python-version`, `.gitea/workflows/ci.yml`, `.github/workflows/ci.yml`, `README.md`, `DEV-ENV.md`, `.ai/PROJECT_CONTEXT.md`, `.ai/DECISIONS.md`, `.ai/HANDOFF.md`, `.ai/SESSION_LOG.md`.
## 2026-05-07 11:14 EDT — Codex — Recheck native Python availability
- Re-ran the startup ritual and checked the host Python state after the user reported fixing the missing native Python issue.
- Verified `python` and `python3` resolve to `/config/.pyenv/shims/*` and run Python 3.12.10. `pip` and `pip3` are available as pip 25.0.1 under the same pyenv install.
- Confirmed there is no native `python3.11`, pyenv currently lists only `3.12.10`, no repo virtualenv exists under `backend/venv`, `backend/.venv`, or root `.venv`, and `python -m pytest --version` from `backend/` fails with `No module named pytest`.
- Conclusion: native Python is present, but it is not yet a ready backend dev/test environment for ResolutionFlow. Docker remains the reliable path for pytest/alembic until a Python 3.11 virtualenv with `backend/requirements*.txt` is installed.
- Files touched: `.ai/HANDOFF.md`, `.ai/SESSION_LOG.md`.
## 2026-05-06 — Claude — Self-serve signup Phase 2 (frontend + cutover code) shipped on `feat/self-serve-signup-phase-2` ## 2026-05-06 — Claude — Self-serve signup Phase 2 (frontend + cutover code) shipped on `feat/self-serve-signup-phase-2`
- Executed Tasks 2744 of `docs/superpowers/plans/2026-05-06-self-serve-signup-phase-2-frontend-cutover.md` via `superpowers:subagent-driven-development`. 18 commits on `feat/self-serve-signup-phase-2` (off `main` `f918b76`); HEAD `c75ce0c`. Each task: dispatched implementer subagent with full task text + curated context, then spec-compliance + code-quality review subagents; review issues either fixed in-flight via `git commit --amend` or noted as deferred scope. - Executed Tasks 2744 of `docs/superpowers/plans/2026-05-06-self-serve-signup-phase-2-frontend-cutover.md` via `superpowers:subagent-driven-development`. 18 commits on `feat/self-serve-signup-phase-2` (off `main` `f918b76`); HEAD `c75ce0c`. Each task: dispatched implementer subagent with full task text + curated context, then spec-compliance + code-quality review subagents; review issues either fixed in-flight via `git commit --amend` or noted as deferred scope.
@@ -313,3 +336,13 @@
- Files touched: `.ai/*.md` (created), `CLAUDE.md` (rewritten), `AGENTS.md` (created), `SESSION-HANDOFF.md` (deleted). - Files touched: `.ai/*.md` (created), `CLAUDE.md` (rewritten), `AGENTS.md` (created), `SESSION-HANDOFF.md` (deleted).
- Follow-up (same day): Codex review pass flagged stale SaaS-role claim and incomplete file-listings carried over from the pre-migration CLAUDE.md. Verified against `backend/app/core/permissions.py`, `frontend/src/hooks/usePermissions.ts`, `backend/app/api/deps.py`, `backend/app/api/router.py`, and `backend/app/services/psa/`. Corrected PROJECT_CONTEXT.md role hierarchy (`super_admin > owner > engineer > viewer`, not `team_admin`), added `require_account_owner` / `require_team_admin` to deps list, replaced stale endpoint comment with a summary pointing at `api/router.py`, added `exceptions.py` + `ticket_context.py` to the PSA file list. Also replaced seed-example content in `CURRENT_TASK.md` and `TODO.md` with clearer empty-state sentinels. - Follow-up (same day): Codex review pass flagged stale SaaS-role claim and incomplete file-listings carried over from the pre-migration CLAUDE.md. Verified against `backend/app/core/permissions.py`, `frontend/src/hooks/usePermissions.ts`, `backend/app/api/deps.py`, `backend/app/api/router.py`, and `backend/app/services/psa/`. Corrected PROJECT_CONTEXT.md role hierarchy (`super_admin > owner > engineer > viewer`, not `team_admin`), added `require_account_owner` / `require_team_admin` to deps list, replaced stale endpoint comment with a summary pointing at `api/router.py`, added `exceptions.py` + `ticket_context.py` to the PSA file list. Also replaced seed-example content in `CURRENT_TASK.md` and `TODO.md` with clearer empty-state sentinels.
- Branch cleanup (same day): committed pending test-isolation work as `b14a16a chore(tests): gate RLS tests behind RUN_RLS_TESTS flag`, new Phase 9 review doc as `b3506b5 docs(pilot): phase 9 review issues`, and `.remember/` gitignore entry as `b3be1e0 chore: ignore .remember/ skill runtime state`. Deleted `docs/landing-handoff/` (prepared for external design work, not meant to live in the repo). Working tree clean; 3 cleanup commits unpushed. - Branch cleanup (same day): committed pending test-isolation work as `b14a16a chore(tests): gate RLS tests behind RUN_RLS_TESTS flag`, new Phase 9 review doc as `b3506b5 docs(pilot): phase 9 review issues`, and `.remember/` gitignore entry as `b3be1e0 chore: ignore .remember/ skill runtime state`. Deleted `docs/landing-handoff/` (prepared for external design work, not meant to live in the repo). Working tree clean; 3 cleanup commits unpushed.
## 2026-05-07 UTC — Codex — Resolve PR #162 CI failures
- Investigated Gitea PR #162 failing checks for `feat/self-serve-signup-phase-2`. Public status metadata was available, but job logs required Gitea login and no token was present.
- Standardized backend development/CI Python on 3.12.13 to match the Docker image: added `.python-version`, updated Gitea CI Python setup, rebuilt the local backend virtualenv, and verified native `pytest` / `alembic` command availability with explicit local env.
- Added explicit Node 20 setup to Gitea frontend and e2e jobs so CI no longer depends on the runner's ambient Node installation.
- Reproduced the remaining frontend failure locally. Lint failed on Phase 2 React code because the current eslint stack flags exported pure helpers, render-time `Date.now()`, and effect-driven state synchronization.
- Patched the affected frontend surfaces narrowly: dashboard helper exports, app-config cache handling, feature-limit cache/fetch state, trial-banner time capture, invite/OAuth route error state, pricing loading state, and OAuth authorize URL helper export.
- Verified sequential frontend CI locally in Docker: `npm run lint` passed, `npm run test:coverage` passed (`198` tests), and `npm run build` passed with only Vite chunk-size warnings.
- Files touched: `.python-version`, `.gitea/workflows/ci.yml`, `.github/workflows/ci.yml`, `.ai/*`, `README.md`, `DEV-ENV.md`, and the frontend lint-fix files under `frontend/src/components/dashboard`, `frontend/src/hooks`, and `frontend/src/pages`.

View File

@@ -46,6 +46,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Cache pip - name: Cache pip
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@@ -105,6 +110,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Cache npm - name: Cache npm
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@@ -171,6 +181,16 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Cache pip - name: Cache pip
uses: actions/cache@v3 uses: actions/cache@v3
with: with:

View File

@@ -37,10 +37,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Set up Python 3.11 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
cache: pip cache: pip
cache-dependency-path: | cache-dependency-path: |
backend/requirements.txt backend/requirements.txt
@@ -143,10 +143,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Set up Python 3.11 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
cache: pip cache: pip
cache-dependency-path: | cache-dependency-path: |
backend/requirements.txt backend/requirements.txt

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12.13

View File

@@ -108,7 +108,7 @@ Run these in order. Stop at the first failure and investigate.
# Ubuntu / Debian # Ubuntu / Debian
sudo apt update && sudo apt install -y \ sudo apt update && sudo apt install -y \
git curl build-essential \ git curl build-essential \
python3.11 python3.11-venv python3-pip \ python3.12 python3.12-venv python3-pip \
postgresql-client # not the server — only if running Postgres natively postgresql-client # not the server — only if running Postgres natively
# Node 20 via nvm (survives container rebuilds if stored in a volume) # Node 20 via nvm (survives container rebuilds if stored in a volume)
@@ -236,7 +236,7 @@ REPO_ROOT=/absolute/path/to/resolutionflow
```bash ```bash
cd backend cd backend
python3.11 -m venv venv python3.12 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt

View File

@@ -11,7 +11,7 @@
## Quick Start ## Quick Start
```bash ```bash
# Prerequisites: Docker, Python 3.11+, Node.js 20+ # Prerequisites: Docker, Python 3.12, Node.js 20+
# Start PostgreSQL # Start PostgreSQL
docker start patherly_postgres docker start patherly_postgres

View File

@@ -49,6 +49,7 @@ const PLAN_GATE_STAGES: ReadonlyArray<TrialBannerStage> = [
* Pure helper — picks the highest-priority incomplete item, or `null` when * Pure helper — picks the highest-priority incomplete item, or `null` when
* all relevant items are done. Exported for direct unit testing. * all relevant items are done. Exported for direct unit testing.
*/ */
// eslint-disable-next-line react-refresh/only-export-components -- pure helper exported for focused unit tests
export function pickNextStep( export function pickNextStep(
status: OnboardingStatus, status: OnboardingStatus,
trialStage: TrialBannerStage | null, trialStage: TrialBannerStage | null,

View File

@@ -29,6 +29,7 @@ const PLAN_GATE_STAGES: ReadonlyArray<TrialBannerStage> = [
'expired', 'expired',
] ]
// eslint-disable-next-line react-refresh/only-export-components -- pure helper exported for focused unit tests
export function buildChecklistItems( export function buildChecklistItems(
status: OnboardingStatus, status: OnboardingStatus,
trialStage: TrialBannerStage | null, trialStage: TrialBannerStage | null,

View File

@@ -66,10 +66,7 @@ export function useAppConfig(): UseAppConfigResult {
const [config, setConfig] = useState<PublicConfig | null>(cached) const [config, setConfig] = useState<PublicConfig | null>(cached)
useEffect(() => { useEffect(() => {
if (cached) { if (cached) return
setConfig(cached)
return
}
let active = true let active = true
const handler = (c: PublicConfig) => { const handler = (c: PublicConfig) => {
if (active) setConfig(c) if (active) setConfig(c)

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useState } from 'react'
import { useBillingStore } from '@/store/billingStore' import { useBillingStore } from '@/store/billingStore'
import { usageApi } from '@/api/usage' import { usageApi } from '@/api/usage'
@@ -53,61 +53,38 @@ function coerceLimit(raw: unknown): number | null {
export function useFeatureLimit(field: string): FeatureLimitResult { export function useFeatureLimit(field: string): FeatureLimitResult {
const limit = useBillingStore((state) => coerceLimit(state.planLimits[field])) const limit = useBillingStore((state) => coerceLimit(state.planLimits[field]))
// Initialize from cache on first mount only; subsequent `field` changes const [state, setState] = useState(() => {
// are handled inside the effect below so the render-phase result reflects
// the new field synchronously (no stale `used`/`isLoading` for one tick).
const initialCached = useRef<CacheEntry | undefined>(undefined)
if (initialCached.current === undefined) {
initialCached.current = cache.get(field)
}
const initialFresh =
initialCached.current && Date.now() - initialCached.current.timestamp < CACHE_TTL_MS
const [used, setUsed] = useState<number>(initialFresh ? initialCached.current!.used : 0)
const [isLoading, setIsLoading] = useState<boolean>(!initialFresh)
// Track the field that the current `used`/`isLoading` state describes.
// When `field` changes, we synchronously reset state in render so callers
// never see stale data for the previous field.
const stateField = useRef<string>(field)
if (stateField.current !== field) {
stateField.current = field
const existing = cache.get(field) const existing = cache.get(field)
const freshNow = existing && Date.now() - existing.timestamp < CACHE_TTL_MS const fresh = existing && Date.now() - existing.timestamp < CACHE_TTL_MS
if (freshNow) { return {
setUsed(existing!.used) field,
setIsLoading(false) used: fresh ? existing.used : 0,
} else { isLoading: !fresh,
setUsed(0)
setIsLoading(true)
} }
} })
useEffect(() => { useEffect(() => {
const existing = cache.get(field) const existing = cache.get(field)
if (existing && Date.now() - existing.timestamp < CACHE_TTL_MS) { if (existing && Date.now() - existing.timestamp < CACHE_TTL_MS) {
setUsed(existing.used) // eslint-disable-next-line react-hooks/set-state-in-effect -- sync local hook state with fresh module cache on field change
setIsLoading(false) setState({ field, used: existing.used, isLoading: false })
return return
} }
let cancelled = false let cancelled = false
setIsLoading(true) setState({ field, used: 0, isLoading: true })
usageApi usageApi
.getCount(field) .getCount(field)
.then((result) => { .then((result) => {
if (cancelled) return if (cancelled) return
cache.set(field, { used: result.used, timestamp: Date.now() }) cache.set(field, { used: result.used, timestamp: Date.now() })
setUsed(result.used) setState({ field, used: result.used, isLoading: false })
}) })
.catch(() => { .catch(() => {
// TODO: backend /usage/{field} endpoint not yet implemented (planned). // TODO: backend /usage/{field} endpoint not yet implemented (planned).
// 404s and other errors degrade to used=0 silently — no toast. // 404s and other errors degrade to used=0 silently — no toast.
if (cancelled) return if (cancelled) return
setUsed(0) setState({ field, used: 0, isLoading: false })
})
.finally(() => {
if (cancelled) return
setIsLoading(false)
}) })
return () => { return () => {
@@ -115,6 +92,8 @@ export function useFeatureLimit(field: string): FeatureLimitResult {
} }
}, [field]) }, [field])
const used = state.field === field ? state.used : 0
const isLoading = state.field === field ? state.isLoading : true
const percentage = const percentage =
limit === null || limit <= 0 ? null : Math.min(100, Math.round((used / limit) * 100)) limit === null || limit <= 0 ? null : Math.min(100, Math.round((used / limit) * 100))
const isAtLimit = limit !== null && used >= limit const isAtLimit = limit !== null && used >= limit

View File

@@ -1,3 +1,4 @@
import { useState } from 'react'
import { useBillingStore } from '@/store/billingStore' import { useBillingStore } from '@/store/billingStore'
export type TrialBannerStage = export type TrialBannerStage =
@@ -28,6 +29,7 @@ const MS_PER_DAY = 24 * 60 * 60 * 1000
*/ */
export function useTrialBanner(): TrialBannerResult { export function useTrialBanner(): TrialBannerResult {
const subscription = useBillingStore((state) => state.subscription) const subscription = useBillingStore((state) => state.subscription)
const [now] = useState(() => Date.now())
if (!subscription) { if (!subscription) {
return { stage: null, daysRemaining: null } return { stage: null, daysRemaining: null }
@@ -51,7 +53,6 @@ export function useTrialBanner(): TrialBannerResult {
// upgrade prompt still surfaces rather than silently swallowing it. // upgrade prompt still surfaces rather than silently swallowing it.
return { stage: 'expired', daysRemaining: null } return { stage: 'expired', daysRemaining: null }
} }
const now = Date.now()
if (end <= now) { if (end <= now) {
return { stage: 'expired', daysRemaining: 0 } return { stage: 'expired', daysRemaining: 0 }
} }

View File

@@ -47,6 +47,7 @@ export function AcceptInvitePage() {
useEffect(() => { useEffect(() => {
if (!code) { if (!code) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- route changes without a code should replace stale lookup state
setLookup({ status: 'missing-code' }) setLookup({ status: 'missing-code' })
return return
} }

View File

@@ -58,6 +58,7 @@ export function OAuthCallbackPage() {
} }
if (oauthError) { if (oauthError) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- callback URL validation maps directly into local error state
setError(`OAuth error: ${oauthError}`) setError(`OAuth error: ${oauthError}`)
return return
} }

View File

@@ -182,7 +182,6 @@ export function PricingPage() {
if (!appConfig.self_serve_enabled) return if (!appConfig.self_serve_enabled) return
let cancelled = false let cancelled = false
setLoading(true)
plansApi plansApi
.getPublic() .getPublic()
.then((data) => { .then((data) => {

View File

@@ -33,7 +33,8 @@ function randomState(): string {
return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('') return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('')
} }
/** Build provider authorize URL. Exported for tests. */ /** Build provider authorize URL. Exported for tests and invite OAuth handoff. */
// eslint-disable-next-line react-refresh/only-export-components -- pure helper shared with AcceptInvitePage and unit tests
export function buildOAuthAuthorizeUrl( export function buildOAuthAuthorizeUrl(
provider: 'google' | 'microsoft', provider: 'google' | 'microsoft',
state: string, state: string,