## Summary
- Closes [#73](#73)
- Remove per-message `Tokens in … · out …` under assistant bubbles
- Remove dashboard `UsageSummaryBar` (plan / prompts left / period tokens)
- Add Account **Usage limit** card (Gemini-style): plan name, prompt progress meter with rolling-window reset hint, monthly tokens meter when capped (else period in/out), upgrade hint pointing at Billing
## Test plan
- [ ] Chat dashboard: no usage bar above messages
- [ ] Assistant messages: no token footer
- [ ] Account: Usage limit card shows plan + prompt bar (+ token bar when monthly quota set)
- [ ] `npm test -- --watchAll=false --testPathPattern=ConversationDetailCard` passes
Related follow-up: [#75](#75) upgrade/change/cancel subscription.Reviewed-on: #74
## Summary
- Closes [#33](#33)
- Add Billing glass cards on `/account/` (`Account2`): current plan summary + billing history from `GET /finance/invoices/` (and payments fallback)
- **Manage subscription** CTA → `POST /finance/portal/` → redirect to Stripe hosted Customer Portal (return URL `/account/`)
- Empty / unpaid users get **Complete payment** → Checkout instead of a dead portal button
- Visible errors for list/portal failures; fire `BILLING_PORTAL_OPENED`
- Depends on backend portal endpoint: [chat_backend#35](ai_ml_operations/chat_backend#35)
## Test plan
- [ ] `npm test -- --watchAll=false --testPathPattern='(BillingSection|finance\\.test)'`
- [ ] Signed-in user with paid invoices: plan + history render; Manage subscription redirects to portal URL
- [ ] User with no invoices: empty copy + Complete payment starts Checkout
- [ ] Portal/list API errors show alert text (no silent fail)
- [ ] Return from Stripe portal lands on `/account/`
- [ ] No card fields collected in-appReviewed-on: #71
## Summary
- Closes#67
- Renames Android/Capacitor identity from `com.aimloperations.chat` → `ai.hesychia.chat`
- Updates `appId`, Gradle `applicationId`/`namespace`, `MainActivity` package path, `strings.xml` scheme, and Android README
## Notes
- New application ID installs as a separate app beside any old-ID install (expected for early builds).
- App already built and run on a physical device with this identity work in progress.
## Test plan
- [ ] `npx cap sync android` after web build
- [ ] Debug install on device — package shows `ai.hesychia.chat`
- [ ] App launches and reaches login / chat
- [ ] Confirm old `com.aimloperations.chat` install (if present) is separate and can be uninstalledReviewed-on: #68
## Summary
- Closes#64
- Beta FE was still calling `beta.chatbackend.aimloperations.com`, which no longer resolves, so login failed with Axios `ERR_NETWORK`
- Point REST/WS env URLs at the live backend: `beta.backend.hesychia.ai`
## Test plan
- [ ] Redeploy beta FE with updated `.env.beta`
- [ ] Confirm built bundle `baseURL` is `https://beta.backend.hesychia.ai/api/`
- [ ] Sign in on https://beta.hesychia.ai succeeds (no `ERR_NETWORK`)
- [ ] Chat WebSocket connects via `wss://beta.backend.hesychia.ai/ws/chat_again/`Reviewed-on: #65
## Summary
- Closes#61
- Replaces #62, which was opened from a branch that still carried the drawer commit already merged in #60 and therefore conflicted. Same change, cherry-picked cleanly onto current `master`.
- **Makes the mobile white screen diagnosable.** The app had **no error boundary anywhere**, so any throw in the dashboard unmounted the whole tree and left an empty `#root`. That matches the reported behavior exactly: brief flash, white page, other routes fine on a fresh load, blank again when navigating back to `/`. `AppErrorBoundary` renders the error name, message, stack, URL and user agent with Reload / Copy details buttons, so the real exception can be read and copied **from the phone**. It is keyed on the route path, so navigating away clears a crash.
- **Responsive layout fixes** for the clipping visible in mobile emulation: `100vw` → `100%`, `100dvh` for the chat shell so mobile URL bars can't push the input row off screen, `min-width: 0` / `flex-shrink` on the input row so attach, model select, textarea and send compress instead of overflowing, `env(safe-area-inset-*)` padding on header, drawer and input area, and message bubbles that wrap long tokens (`overflow-wrap: anywhere`, `max-width` on `pre`/`img`/`table`).
- **Perf:** particle canvas pauses while the tab is hidden, honors `prefers-reduced-motion`, caps particle count for the O(n²) link pass, and re-seeds only on real size changes. Chat auto-scroll now fires on message change rather than on every render.
## Test plan
- [x] `npm run test:ci` — 25 suites / 112 tests pass (adds `AppErrorBoundary` coverage for the pass-through and crash paths)
- [x] `npm run build` — clean
- [ ] Phone browser: sign in and land on `/` — chat renders, or the error card shows the real exception (paste into #61)
- [ ] Phone browser: no horizontal scroll; attach, model select, textarea and send all visible
- [ ] Phone browser: input row still reachable with the keyboard open
- [ ] Long code block / unbroken string stays inside the bubble
- [ ] Desktop: sidebar, chat and input unchanged; background animation still runs
- [ ] Background the tab and return — animation resumes
## Notes
The crash is not reproducible in desktop mobile emulation, so this ships the boundary as the diagnostic path instead of guessing at the throwing call site. Once the error card appears on the phone, the stack should point straight at the cause. Worth landing before the Capacitor Android wrap (#20), since the same failure mode is much harder to inspect inside a WebView.Reviewed-on: #63
## Summary
- Closes#59
- Mobile conversation drawer is now a fixed sheet with stronger stacking over dashboard content
- Adds explicit close (X) control and empty-state copy when there are no conversations
- Keeps existing close-on-select / New Chat behavior
## Test plan
- [ ] Mobile: open Conversations drawer — sheet + overlay appear above content
- [ ] Tap X or overlay — drawer closes
- [ ] Empty conversation list shows guidance text
- [ ] Select conversation / New Chat closes drawer and chat still works
- [ ] Desktop sidebar layout unchangedReviewed-on: #60
## Summary
- Follow-up for #54 (localStorage guard in #55 did not fix mobile white screen)
- Confirmed in production bundle: `ProtectedRoutes` rendered `<Outlet key={Date.now()} />`
- That remounts the entire authenticated tree on **every** parent re-render (account fetch, WebSocket connect/status, conversations, messages)
- On mobile this shows as: brief UI flash → solid white page (matches screen recording)
- Also memoize styled-components theme in `GlobalThemeWrapper` so `ParticleBackground` does not tear down/restart its canvas on unrelated re-renders
## Evidence
Production JS currently contains:
```js
e?(0,ma.jsx)(a.sv,{},Date.now()):(0,ma.jsx)(a.C5,{to:"/signin/",replace:!0})
```
i.e. authenticated → `<Outlet key={Date.now()} />`
## Test plan
- [ ] Mobile Vivaldi/Chrome: sign in on hesychia.ai → dashboard stays visible (header, conversations toggle, input)
- [ ] Pull-to-refresh while logged in → UI returns and stays
- [ ] Desktop login still works; switching conversations / WS reconnect does not blank the page
- [ ] Open mobile menu / Conversations sidebar still worksReviewed-on: #56
## Summary
- Closes#54
- Add `getLocalStorageSafe()` helper in `tokenStorage.js` to handle browsers that expose `localStorage` but throw `SecurityError` on access
- Wrap all JWT token get/set/remove calls in try/catch so auth bootstrap no longer crashes React on mobile
## Problem
On mobile browsers, hesychia.ai flashes the UI briefly after login then goes white. Auth startup reads tokens from `localStorage` without guarding against storage access errors (common in private mode or blocked storage).
## Test plan
- [ ] Open hesychia.ai on mobile Safari/Chrome (normal mode) and sign in — dashboard should render
- [ ] Repeat in private/incognito mode — app should not white-screen (may require re-login each session if storage blocked)
- [ ] Verify existing web login/logout still works on desktop
- [ ] Run `npm test -- tokenStorage` in `llm-fe`Reviewed-on: #55
## Summary
- Closes#51
- Trim prompt Yup validation; refuse empty/whitespace in submit handlers and `sendMessage`
- Add WebSocketContext test for empty send rejection
## Test plan
- [x] `WebSocketContext.test.js` empty/whitespace send cases
- [ ] Manually confirm whitespace-only input cannot submit on desktop + mobile viewport
- [ ] Confirm normal messages still send
Related BE: https://git.aimloperations.com/ai_ml_operations/chat_backend/issues/31Reviewed-on: #52
## Summary
Closes#49.
- Show **Hesychia.ai** wordmark with the brand mark on the sign-in page
- Regenerate brand PNGs with true transparent corners (source soft-glow RGBA had no fully clear pixels)
- Update SignIn test for the new heading
## Test plan
- [ ] Open `/signin/` — mark + **Hesychia.ai** above Sign In
- [ ] Confirm no black square around mark on dark particle background
- [ ] `npm test -- --testPathPattern=SignIn.test --watchAll=false`Reviewed-on: #50
## Summary
- Fixes wrong relative imports in `AnalyticsSession.tsx` (`../` → `../../`) so `npm run build:beta` can resolve contexts/utils.
- Unblocks beta deploy; empty docroot was causing `https://beta.hesychia.ai/` nginx 403.
Fixes#47
## Test plan
- [ ] CI unit tests pass
- [ ] Beta deploy workflow succeeds after merge
- [ ] `https://beta.hesychia.ai/` returns 200 with the SPAReviewed-on: #48
## Summary
Closes#30.
- Replace old infinity brand mark with the cyan Greek-key (meander) Hesychia emblem
- Update web favicons / PWA icons (`public/`), Capacitor source art (`assets/`), and regenerate Android launcher + splash densities
- Wire mark into Header / Header2, Sign In / Sign Up, and Sidenav branding
## Test plan
- [ ] Favicon + apple-touch icon show new mark in browser tab / home screen
- [ ] Header and auth screens show mark + Hesychia wordmark
- [ ] `npm run assets:generate` already run; Android launcher / splash use new art
- [ ] Old infinity artwork no longer present in `assets/` / `public/` brand slotsReviewed-on: #46
Version consent in localStorage (v1 + legacy migrate), point banner at Terms analytics section, and document the always-on vs optional policy. Banner/provider shipped in #42.
## Summary
Closes#39.
- Shared `TermsOfServiceDocument` used by public + acknowledge flows
- `/terms_of_service/` reachable without auth; `/legal/terms-of-service` alias
- Expanded clauses including **Analytics and Cookies** (always-on page views vs consent-gated events)
- Acknowledge still posts `/user/acknowledge_tos/` when signed in
- Links from Sign In, Sign Up, and Footer
## Test plan
- [ ] Visit `/terms_of_service/` logged out — document renders, no Acknowledge
- [ ] Visit `/legal/terms-of-service` — same public content
- [ ] Sign in as user needing ToS — Acknowledge posts and navigates home
- [ ] Confirm Sign In / Sign Up / Footer / consent banner link to ToS
- [ ] `npm run test:ci -- --testPathPattern='TermsOfService|Footer'`Reviewed-on: #43
## Summary
- Frontend half of ai_ml_operations/chat_backend#24
- SSO buttons on Sign In / Sign Up (shown when `/public/settings/` reports provider configured)
- New `/auth/callback/` route stores JWTs, loads account, sends new SSO users through Stripe Checkout
- Error states for denied consent, unverified/missing email, link conflict, etc.
- README note on Capacitor / native redirect URI limits
## Pair with
- Backend PR: `chat_backend` branch `feature/sso-oauth-24`
## Test plan
- [ ] `npm test -- --watchAll=false --testPathPattern="SignIn.test|SignUp.test|AuthCallback.test"`
- [ ] With backend OAuth configured, Continue with Google/Microsoft from Sign In works
- [ ] First-time SSO register lands in Stripe Checkout when `needs_checkout=1`
- [ ] Existing linked user signs in and reaches app / TOS
- [ ] Error query (`email_unverified`, `access_denied`) shows message + back linkReviewed-on: #41
## Summary
- Pair with chat_backend password reset (#1)
- Fix forgot-password request: execute invisible reCAPTCHA, POST `user/reset_password/` with await, correct `/signin/` back link
- Restyle confirmation + set-password pages to match Sign In glass UI
- Set-password: validate link via GET, await POST, enforce ≥8 chars + number + special char
- Sign In “Forgot Password?” navigates to `/password_reset/`
## Test plan
- [ ] `npm test -- --watchAll=false --testPathPattern=PasswordReset.test`
- [ ] From Sign In → Forgot Password → submit email → confirmation page
- [ ] Open email link `/set_password/?slug=…` → set password → redirected to Sign In
- [ ] Invalid/expired slug redirects to Sign In
- [ ] End-to-end with backend `feature/password-reset-1`Reviewed-on: #40
## Summary
- Closes#31
- Public `/signup/` page (Sign In–matched UI): register → JWT → `POST /api/finance/checkout/` → redirect to Stripe hosted Checkout
- `/billing/success` and `/billing/cancel` return URLs
- Sign In ↔ Sign Up links; Sign Up link only when backend `enable_account_registration` is true
Depends on backend PR: registration flag + public settings + gated `/user/create/`
## Test plan
- [ ] `cd llm-fe && CI=true npm run test:ci -- --testPathPattern='SignIn|SignUp'`
- [ ] With registration disabled: `/signup/` shows unavailable; Sign In has no Sign up link
- [ ] With registration enabled: form validates, happy path redirects to Checkout URL
- [ ] Cancel return explains retry; success prompts continue / sign-inReviewed-on: #34
## Summary
- Closes#29
- Rename user-facing product from AIML Operations chat to **Hesychia** (headers, sidenav brand, footer, ToS welcome, sample page)
- Update page metadata (`title`, description, Open Graph, `manifest.json` app name)
- Point README / env comments at **hesychia.ai**; keep live API hosts and deploy webroots until infra cutover
## Android display name
Capacitor tree is not on `master` yet ([#28](#28)). Display name / `appName` → **Hesychia** pushed on `ship-android-capacitor-20` so it lands with that wrap. Package id stays `com.aimloperations.chat` (Play identity; rename later if desired).
## Out of scope (per issue)
- New logo artwork (#30)
- Backend system-prompt identity (`chat_backend` #20)
## Test plan
- [ ] Web UI shows **Hesychia** in header / sidenav / footer (no old product name)
- [ ] Browser tab title and PWA manifest name are Hesychia
- [ ] Footer still credits AI ML Operations, LLC as developer
- [ ] `npm run test:ci` (Footer test updated)
- [ ] After #28 merges: Android launcher / activity title shows HesychiaReviewed-on: #32
## Summary
- Closes#24
- Introduce `AppRouter` + `getAppRouter()` so Capacitor uses `HashRouter` (filesystem bundle, no nginx rewrite) while web keeps `BrowserRouter`
- Isolate platform checks in `nativePlatform` (no `Capacitor.isNativePlatform()` sprinkled through the route tree)
- Auth forced redirects (`axiosApi` → `/signin/`) use hash-safe `redirectToAppPath` on native
- Leave CRA `homepage` / absolute `PUBLIC_URL` asset paths unchanged so `/var/www/{env}.chat.aimloperations/html` deploy stays intact; HashRouter keeps document URL at the bundle root so assets resolve
## Test plan
- [x] Unit tests: `AppRouter` + `nativePlatform` (`npm run test:ci`)
- [ ] Capacitor build: open nested route (`/#/account/`), force WebView reload — no blank/404
- [ ] Browser back/forward + Android hardware back with HashRouter
- [ ] Web deploy smoke: deep link reload at `/account/` still works via nginx rewrite
- [ ] Confirm login redirects after 401 still land on sign-in (web path + native hash)Reviewed-on: #27
## Summary
- Closes#23
- Add WebSocket reconnect with exponential backoff + jitter (capped attempts), heartbeat keepalives, and reconnect on browser visibility/`online` plus Capacitor `App`/`Network` plugins when present
- Refresh JWT before reconnect (coordinates with #22) so conversation refetch after a drop still works
- Mid-stream drop recovery: mark interrupted + refetch conversation; surface connecting/reconnecting/disconnected state and disable send while down
## Test plan
- [x] Unit tests: `websocketReconnect` helpers + `WebSocketProvider` reconnect/resume/send (`npm run test:ci`)
- [ ] Background app ~30s and ~10min, then resume — chat reconnects without reload
- [ ] Airplane mode on/off and cellular↔WiFi — reconnect indicator then restored send
- [ ] Drop mid-stream response — interrupted banner + conversation refetch
- [ ] Confirm nginx/`/ws/` proxy idle timeout stays above heartbeat (25s) or allows ping payloadsReviewed-on: #25
## Summary
- Closes#18
- Replace stale `favicon.jpg` / old ICO with multi-size `favicon.ico`, PNG variants (`16x16`, `32x32`), `logo192.png` / `logo512.png`, and `apple-touch-icon.png` generated from the new brand mark
- Point `index.html` and `manifest.json` at the new assets; drop the CRA placeholder app name in the manifest
## Test plan
- [ ] Hard-refresh the app (or clear site data) and confirm the new logo appears in the browser tab
- [ ] Check DevTools Network for `favicon.ico` / PNG icons — no 404s and no requests for `favicon.jpg`
- [ ] On mobile Safari / iOS home-screen add, confirm apple-touch-icon looks correct
- [ ] Confirm PWA/manifest icons resolve to the new 192/512 assetsReviewed-on: #19
## Summary
- Closes#16
- Clear unused imports/bindings, empty `{}` prop patterns, `==` vs `===`, and hook dependency gaps across App, contexts, and pages
- Replace invalid `calc(3rem + unset)` in `index.css` so Css Minimizer no longer fails
## Test plan
- [x] `npm run build` in `llm-fe` succeeds (no ESLint / postcss-calc errors)
- [ ] Spot-check SignIn, dashboard, account, feedback, document storage pages still render
- [ ] Confirm WebSocket/message flows still work after MessageContext dep cleanupReviewed-on: #17
## Summary
Implements [chat_web_app#13](#13) Part A: wire the CRA frontend into the same **node-static** deploy path as `dta_webapp` (no Docker for serving).
- `build:prod` / `build:beta` write into `/var/www/{env}.chat.aimloperations/html`
- API/WS URLs come from `REACT_APP_*` (`.env.production` / `.env.beta`) instead of hardcoded localhost
- Gitea unit tests + auto deploy-prod on `master`; deploy-beta via `workflow_dispatch`
- README documents local dev, env files, and webroot layout
Branch includes the #11 unit-test commit (`test:ci`) so CI has a green suite.
Companion server-infra branch (Part B): `issue-13-register-chat-web-app` on `ai_ml_operations/server-infra`.
## Test plan
- [ ] `cd llm-fe && npm run test:ci`
- [ ] Merge + ensure Unit Tests workflow passes on PR
- [ ] Merge server-infra `issue-13-register-chat-web-app`, then sync/pull on runner
- [ ] Redeploy web-static (or full host provision) so nginx listens on **8082**
- [ ] Manual: `server-infra/scripts/deploy.sh --app chat_web_app --env prod --ref <sha>`
- [ ] NPM: point `chat.aimloperations.com` at `adama:8082` + `roslin:8082` + `ai-server-4080:8082`
- [ ] Confirm built assets hit `https://chatbackend.aimloperations.com`Reviewed-on: #15