Commit Graph
249 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.8 1f81800c37 chore(frontend): remove non-functional commitlint setup
commitlint had no config anywhere (no config file, no husky commit-msg
hook), so its rules were never loaded; the "commit" script also pointed
at an uninstalled binary. Remove @commitlint/cli and
@commitlint/config-conventional, the dead "commit"/"commitlint" scripts,
and the stale commitlint entry in README Development Requirements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 03:39:45 +07:00
Sergey KozyrenkoandClaude Opus 4.8 5523582647 chore(frontend): remove unused dependencies
Confirmed unused via depcheck + manual verification:
- anser, js-cookie, @types/js-cookie — no imports anywhere
- rehype-raw — only referenced in a vite manualChunks regex, never
  imported as a markdown plugin (markdown.tsx uses rehype-highlight/slug
  and remark-gfm); also dropped from that regex
- @graphql-codegen/{client-preset,near-operation-file-preset,typescript}
  — codegen config uses explicit plugins (typescript-operations,
  typed-document-node), not these presets/base plugin

Also drop the dead package.json "eslintConfig" block — it is ignored by
the flat config (eslint.config.mjs) and referenced an uninstalled
storybook plugin.

Verified: graphql:generate reproduces src/graphql/types.ts unchanged,
plus build, lint, 604 tests, and --frozen-lockfile all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 03:34:34 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c3f6a02f34 chore(frontend): remove unused eslint-plugin-jsx-a11y
The plugin was a direct devDependency but never enabled in
eslint.config.mjs (0 active jsx-a11y rules via --print-config). Dropping
it removes dead weight and one stale eslint-9 peer constraint. Lint still
passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 03:04:53 +07:00
Sergey KozyrenkoandClaude Opus 4.8 49d9687c75 chore(frontend): update dependencies to latest within-range
Bump ~60 minor/patch dependencies (Radix UI group, Tiptap 3.27, Apollo
4.2, vite 8.0.16, vitest 4.1.9, typescript-eslint 8.61, react 19.2.7,
react-router 7.18, graphql-codegen 7.1, etc.).

Majors intentionally held back: eslint 10, graphql 17, and @types/node 25
(kept on 24 to match the pinned Node 24.17.0 runtime).

Verified: build, lint, prettier, and 604 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:54:57 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9b073e2183 build(frontend): pin Node 24.17.0 and pnpm 11.8.0 across all builds
- add frontend/.nvmrc (24.17.0) as the single source of truth for Node;
  GitHub CI reads it via node-version-file, Dockerfile uses node:24.17.0-slim
- bump packageManager to pnpm@11.8.0; drop "corepack prepare pnpm@latest"
  so the pnpm version derives from packageManager everywhere
- migrate pnpm onlyBuiltDependencies -> allowBuilds in pnpm-workspace.yaml
  (the package.json "pnpm" field is no longer read by pnpm 11)
- add a CI step that fails if the Dockerfile Node tag drifts from .nvmrc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:50:39 +07:00
Sergey KozyrenkoandClaude Opus 4.8 11db9c2e0c feat(webui): merge file-manager folder/chevron into one hover toggle
The directory expand/collapse control now shows the folder icon by
default and crossfades to a chevron on hover (motion, reduced-motion
aware); the single element toggles expansion on click. Nesting indent
gains the icon->text gap (22px/level) so a child's icon lines up under
its parent's label, and the header expand-all control moves to the
shared Button. Folder and chevron share the text-blue-400 accent set on
the parent (header chevron picks it up on hover).

Also prunes ~46 restatement/justification comments and a dead
collectAllFilePaths export (plus its tests) to match the house style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:45:00 +07:00
Sergey KozyrenkoandClaude Fable 5 b6db0b59ad ci(webui): make all frontend CI checks blocking
Prettier / Lint / Test were `continue-on-error: true` (advisory — failures did not fail CI), the same gap that let type errors pile up. Now Prettier, Lint, Type check and Test all block the lint-and-test job (which runs on every branch push).

Prerequisite: `prettier --write` on 5 pre-existing non-conformant files (pages/login.tsx, lib/report/report-pdf.tsx, 3 *.test.tsx) so the now-blocking Prettier check passes — pure formatting, no logic change. Install stays advisory (setup step); backend checks unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:48:05 +07:00
Sergey KozyrenkoandClaude Fable 5 52951ceb43 ci(webui): gate build and CI on TypeScript errors
vite build strips types and never type-checks, and the old build`s bare tsc (solution config, files:[]) checked nothing — so type errors reached main unnoticed (which is how 76 had accumulated). Now they fail fast everywhere.

- build: `tsc -b && vite build` — `pnpm build` (and the Docker image build, which runs `pnpm run build`) fails on any type error before bundling.

- add `typescript` script (`tsc -b`, checks both app + node project configs).

- ci.yml: blocking "Frontend - Type check" step in lint-and-test (runs on every branch push; docker-build only runs on main/tags).

Verified: a type error makes both `pnpm run typescript` and `pnpm build` exit non-zero (vite never runs); removing it restores a clean build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:37:18 +07:00
Sergey KozyrenkoandClaude Fable 5 895de4cadc fix(webui): resolve settings-provider type errors — frontend now 0 tsc errors
The provider form had 23 errors rooted in Zod v4: `z.preprocess` gives an `unknown` input type, which @hookform/resolvers v5 surfaces as the form field-values type, mismatching `useForm<output>` and cascading to every Control<FieldValues> site.

- Replace `z.preprocess` with explicit helper schemas (proper input/output types) and use `useForm<FormInput, unknown, FormData>`; make the reusable field components generic `<T extends FieldValues>` (Control<T> + FieldPath<T>).

- Fetch `thinking` on the model-config fragment (regenerated types.ts; backend ModelConfig.thinking exists).

- Validate the provider `type` into the ProviderType enum via `z.nativeEnum(...).parse` at the GraphQL boundary instead of an `as` cast (the form intentionally uses watch() to keep disabled fields in the payload, so its values stay input-typed).

tsc: 23 → 0; the frontend now has zero TypeScript errors. 606 tests pass; eslint + prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:05:09 +07:00
Sergey KozyrenkoandClaude Fable 5 3b6464e710 fix(webui): resolve 53 pre-existing TypeScript errors
Hack-free fixes (no `as any`/`@ts-ignore`/`as unknown as`):

- tsconfig lib es2023 for Array.findLast; tighten DocumentTitle TitleResolver to ApolloTitleComponent (the broad ComponentType was masked by findLast returning any).

- NodeJS.Timeout → ReturnType<typeof setTimeout> in browser code; models import casing (./User → ./user).

- settings prompt/api-token forms: type against the real generated fragment types and the Zod input/output split (useForm<Input, ctx, Output>); generic FormTextareaItem<T>.

- data-table / detail-nav: noUncheckedIndexedAccess guards; React vs DOM KeyboardEvent disambiguation; drop invalid user-event delay option.

tsc: 76 → 23 (the remaining 23 are one settings-provider Zod/RHF cluster). 606 tests pass; eslint + prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 21:32:21 +07:00
Sergey KozyrenkoandClaude Fable 5 d1bfe368a2 build(webui): drop deprecated baseUrl from tsconfig, rely on path mapping
TypeScript 6.0 deprecated `baseUrl` (removed in 7.0). The `@/*` alias resolves relative to each tsconfig without it, and Vite resolves `@` via its own resolve.alias, so baseUrl was vestigial. Dropping it removes the need for `ignoreDeprecations: "6.0"`, which editor-bundled TypeScript (5.x) flagged as an invalid value (TS5103).

Also drop the dead `@env`/`./env.ts` entry from tsconfig.node.json: the file does not exist and the alias is imported nowhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 17:02:11 +07:00
Sergey KozyrenkoandClaude Fable 5 962e648fe1 refactor(webui): migrate to Apollo Client v4 typed-document-node codegen
Replace the deprecated typescript-react-apollo codegen plugin (unmaintained for Apollo Client v4) with typed-document-node. All ~105 useXxxQuery/Mutation/Subscription call sites are rewritten to the generic useQuery/useMutation/useSubscription(XxxDocument, ...) form, with skipToken replacing skip + conditional-variables.

graphql-codegen.ts: plugins typescript-operations + typed-document-node; add scalars { Time: string } (was unknown); drop withHooks/apolloReact* options. tsconfig.app/node.json: ignoreDeprecations "6.0" for the baseUrl TS5101 deprecation. Regenerate src/graphql/types.ts.

Behavior-preserving: variables, fetchPolicy, error handling, and subscription onData/refetchQueries are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 16:07:57 +07:00
Sergey KozyrenkoandClaude Fable 5 8bd6c72f63 fix(knowledge): preserve document content when list query writes empty body
The /knowledges list query fetches documents with `withContent: false` to save bandwidth, writing an empty `content` to the shared normalized KnowledgeDocument cache entity. That clobbered the full body loaded by the detail query, so opening a document by direct URL (or reload) showed an empty editor.

Add a `content` field merge policy so an empty incoming value never blanks out a body already loaded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 16:07:17 +07:00
Sergey KozyrenkoandClaude Fable 5 942fb45c16 feat(knowledge): dedicated renameKnowledgeDocument mutation, lighter list
Rename a knowledge document via a dedicated mutation that rewrites only the
question in cmetadata — no re-embedding, no embedder required — mirroring the
flows renameFlow pattern instead of round-tripping the full document through
updateKnowledgeDocument.

Backend:
- renameKnowledgeDocument(id, question) mutation + resolver (admin/user split;
  ownership enforced at GetUserDocument, like the update pair)
- metadata-only query UpdateKnowledgeDocumentMetadata (no migration)
- unit + edge tests: metadata-only, missing-doc error, non-owner rejection

Frontend:
- renameKnowledge provider method; wire list and detail inline-rename to it
- drop the content "Preview" column and request the list with withContent:false
  so it no longer pulls full document bodies

Verified end to end against a local Docker backend (rename works; content and
embedding preserved) and against the remote backend (graceful failure where the
mutation is not yet deployed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 13:15:57 +07:00
Sergey KozyrenkoandClaude Fable 5 6d9453a5a2 refactor(webui): type away the any's in settings-provider
Replace 13 `any` usages with real shapes: ProviderTest / ProviderTestResults
for the test-mutation payload, Control<FieldValues> for the form control, and
inferred element types in the result maps. Collapse the two duplicated
3-level-nested error-formatting blocks into one recursive formatFormErrors
helper — validation error output is byte-identical (verified against the same
inputs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 00:35:40 +07:00
Sergey KozyrenkoandClaude Fable 5 da63ced002 refactor(webui): silence the React-Compiler incompatible-library lint
react-hooks v6 ships this rule, but it only carries signal once the React
Compiler is enabled; until then it flags every RHF watch() and useReactTable
as unactionable noise (12 hits). Turn it off project-wide and drop the
now-redundant inline disable in use-element-virtual-list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 00:35:32 +07:00
Sergey KozyrenkoandClaude Fable 5 7c90ec4f92 fix(webui): escape the CJK Unicode range in the PDF report regexes
The CJK-detection regexes embedded a literal U+3000 ideographic space inside
their character class, which ESLint flagged as no-irregular-whitespace (error).
Switch the fullwidth ranges to \u escapes ( -〿＀-￯) — identical
match behaviour (verified: Han / U+3000 / fullwidth match, ASCII does not), no
irregular whitespace in source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 00:11:38 +07:00
Sergey KozyrenkoandClaude Fable 5 c382d35a52 refactor(webui): drop the unused virtualizer escape hatch from useWindowVirtualList
No consumer reads the returned `virtualizer` (data-table destructures only the
spacer / measure fields), and exposing the raw instance leaks the abstraction —
the same dead, over-broad return just removed from useElementVirtualList. Drop it.

Also correct a stale doc claim: react-virtual does have an `enabled` option (it
gates the scroll listeners), the hook just doesn't surface it — so the
conditional-mount guidance stands without the false "no enabled option" note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 23:56:24 +07:00
Sergey KozyrenkoandClaude Fable 5 fdb5c7b86e perf(webui): virtualize the DetailNavigationSheet listbox
The sheet rendered every filtered option (1797 on a busy flows list), so the
filter re-render and the DOM weight (~1800 nodes) made filtering jank. Above a
100-item threshold the listbox now virtualizes via a new element-scroll
useElementVirtualList hook (the inner-container analogue of useWindowVirtualList):
only the ~30-node visible window is mounted. Small lists — and JSDOM tests —
keep rendering in full.

Roving focus onto an off-screen option scrolls it into view and focuses it once
its row mounts (pendingFocusId), so arrows/Home/End and open-time focus on the
current item work across the virtualized window. Options carry aria-setsize /
aria-posinset so screen readers still see the full count and position.

Measured live (chrome-devtools, test.pentagi.net, 1797-flow sheet): DOM nodes
~1800 → ~32; typing INP 258ms → ~200ms. The remaining cost is the flow.tsx page
re-render when the controller's filtered list changes (debounce flush) — a
separate concern. Open / End / Home / typing focus all verified; full suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 23:21:35 +07:00
Sergey KozyrenkoandClaude Fable 5 92a9e595e0 perf(webui): decouple DetailNavigationSheet search from the page render
Typing in the sheet's search lagged badly (INP ~600ms on a 1797-item list): the
input bound to the controller's searchQuery, whose state lives in the page-level
hook (flow.tsx), so every keystroke re-rendered the whole detail page AND rebuilt
all ~1800 option rows before the caret could echo — characters appeared in batches.

Mirror the input value in local sheet state (instant caret echo, re-renders only
the sheet) and push to the controller in a transition (filtering + prev/next stay
correct; the heavy re-render no longer blocks the keystroke). Memoize the option
rows so a keystroke that only changes the local mirror does not rebuild them.

Measured live (chrome-devtools, test.pentagi.net, 1797-flow sheet): typing INP
606ms -> 258ms; all characters land, focus stays on the input. The residual is the
DOM reconciliation when the filter changes — addressed next by virtualizing the list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 22:09:40 +07:00
Sergey KozyrenkoandClaude Fable 5 4cb25ac469 fix(webui): move DetailNavigationSheet focus to explicit navigation only
Focus was driven off `focusedId` through an effect, so a reconciliation-driven
change (the filter shrinking and re-pinning to the first survivor) also moved
DOM focus, yanking it off the search input mid-type. Two document.activeElement
guards (the second added in 5a7bdca) papered over the timing; the rAF re-check
only ever fired under JSDOM and was dead in real browsers — verified live.

Move focus imperatively only where the user navigates: opening the sheet now
uses Radix's onOpenAutoFocus (preventDefault + focus the current option, with no
rAF race against the focus trap), arrow keys focus synchronously in the key
handler, and render-phase reconciliation only updates the roving tabindex. Both
guards, the effect and the rAF are gone, and the previously-flaky focus-steal
test is deterministic (10/10 isolated).

Verified live (chrome-devtools, test.pentagi.net, 1797-flow sheet): open focuses
the current option, typing keeps focus on the input with no dropped chars, and
arrows / ArrowDown-from-input move focus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 22:08:58 +07:00
Sergey KozyrenkoandClaude Fable 5 345c26eea7 fix(webui): let account settings sections edit independently
The account page tracked a single `editing` section, so opening one editor
unmounted any other open form and silently discarded its unsaved input. Track
an open-section set instead: opening a section no longer closes the others, so
a half-typed draft survives switching between name, email and password.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 16:17:41 +07:00
Sergey KozyrenkoandClaude Fable 5 5765b05c19 feat(webui): give PasswordChangeForm horizontal/vertical layouts for the forced screen
The forced password-change screen reused the compact account-card footer, so it
rendered small right-aligned buttons that clashed with the full-width "Sign in"
CTA on the same login page. Add two orthogonal props: layout
('horizontal' default | 'vertical') drives footer direction and button width;
buttonSize ('default' default, mirroring Button) drives size. The forced screen
passes layout="vertical" (full-width stacked buttons, submit on top, matching the
login button language); the account cards pass buttonSize="sm" to stay compact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 16:04:14 +07:00
Sergey KozyrenkoandClaude Fable 5 6000bc8310 refactor(webui): drop redundant isModal/showSkip flags from account forms
isModal (gated the Cancel button) and showSkip (gated the Skip button) were
redundant with the onCancel/onSkip callbacks they always paired with: every
call site that set a flag also passed the matching handler, and none passed a
handler while wanting the button hidden. Render each button from its callback's
presence instead. Removes two props and a name that misdescribed its job
(isModal gated a button, not a modal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 15:35:08 +07:00
Sergey KozyrenkoandClaude Fable 5 1cc7d26987 refactor(webui): migrate remaining route literals to registry, add drift test
The first migration pass missed call sites where the path is nested inside a
helper rather than passed straight to navigate/to: detail-navigation getHref
helpers, mergeHrefWithSearchParams row-open handlers, the knowledge create
redirect, the flow report window.open targets (wiring up the unused
routes.flowReport builder), and the OAuth return_uri. All now build from routes.

Add routes.test.ts: every builder output is asserted to match the app.tsx route
pattern that serves it via matchPath, guarding URL<->pattern sync, plus exact
checks for query encoding and the login return-url guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 14:20:04 +07:00
Sergey KozyrenkoandClaude Fable 5 6fa9077f21 refactor(webui): migrate main navigation to lib/routes
Step 4 of the route-registry migration: dashboard/flows/templates/knowledges/resources
navigation builds from lib/routes.

- routes.ts gains newKnowledge and newTemplate (sentinel ":id=new" routes)
- main-sidebar nav links + recent-flow link -> routes.* / routes.flow(id)
- app.tsx root and catch-all redirects -> routes.dashboard
- flow/flows/new-flow, knowledge/knowledges, template/templates page navigations ->
  routes.flows / routes.flow(id, { tab }) / routes.newFlow / routes.knowledges /
  routes.newKnowledge / routes.templates / routes.newTemplate

useMatch('/x/*') patterns stay literal — they are route-matching globs, not URLs the
registry builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:53:14 +07:00
Sergey KozyrenkoandClaude Fable 5 dc6b3a9e2c refactor(webui): route auth redirects through lib/routes
Step 3 of the route-registry migration: every /login redirect and post-login landing
fallback builds from lib/routes instead of hardcoded strings.

- protected-route, axios 401/403 interceptor, user-provider (logout, session-expiry,
  /login + public-route checks) -> routes.login(...) / routes.login()
- oauth-result error redirect -> routes.login()
- login-form / public-route / login page post-login fallback -> routes.newFlow

getReturnUrlParam now has a single in-app caller (the routes.login builder) plus the
logout suffix-override; dropped its now-unused imports from axios and protected-route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:45:46 +07:00
Sergey KozyrenkoandClaude Fable 5 d71feaf854 refactor(webui): migrate settings provider/prompt page navigations to routes
Step 2 of the route-registry migration: the provider/prompt settings pages build their
navigate() targets from lib/routes instead of hardcoded strings.

- settings-providers: provider(id), newProvider({ id | type }), newProvider()
- settings-provider: routes.settings.providers (x5)
- settings-prompts: routes.settings.prompt(name)
- settings-prompt: routes.settings.prompts (x2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:37:39 +07:00
Sergey KozyrenkoandClaude Fable 5 ae45bd7c4f refactor(webui): add a central route registry; migrate the settings shell
Introduce lib/routes.ts as the single source of truth for client route paths, with typed
builders for parameterised routes (flow, provider, prompt, login return-url, ...). First
migration step covers the settings shell we own:

- app.tsx settings index/catch-all redirects -> routes.settings.account
- main-sidebar Settings/Profile links -> routes.settings.root / .account
- settings-layout menu items + the "Back to App" fallback -> routes.*

The rest (settings provider/prompt pages, auth redirects, main nav) migrate in follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 13:29:32 +07:00
Sergey KozyrenkoandClaude Fable 5 4b4e93c603 fix(webui): land /settings on Account, the first menu item
Account was added as the first settings sidebar item, but the index and catch-all
routes still redirected to /settings/providers, so the gear "Settings" link dropped
the user on the second item. Point both redirects at /settings/account.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 12:43:29 +07:00
Sergey KozyrenkoandClaude Fable 5 bc80aac985 fix(webui): derive the account avatar initial by code point
(displayName).charAt(0) returns the first UTF-16 unit, so a non-BMP first character
(emoji, rare CJK) rendered as a lone surrogate (�). Spread to iterate by code point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 12:36:47 +07:00
Sergey KozyrenkoandClaude Fable 5 86c7616efa fix(webui): return "Back to App" to the page the user came from
The settings shell's only exit was a "Back to App" link hardcoded to /flows, so opening
Settings or Profile from anywhere dropped the user at the flows list on the way out.

- the Settings and Profile entry links pass the current path as location.state.from
- SettingsLayout captures it once on entry (surviving sub-tab navigation, which drops
  state) and points "Back to App" there, falling back to /flows
- tests: SettingsLayout honors the origin / falls back / preserves it across sub-tabs;
  MainSidebar's Settings and Profile links carry the origin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 12:06:38 +07:00
Sergey KozyrenkoandClaude Fable 5 9b9a74bec0 refactor(webui): share the API-error → message mapping across account forms
The three account forms duplicated the getApiErrorCode + ERROR_BY_CODE lookup pattern
verbatim, and each carried a dead `AuthRequired` entry that the axios interceptor
already handles (hard redirect) before the form's catch can render it.

- add resolveApiErrorMessage(err, map, fallback) next to the axios error helpers
- email/name/password forms use it and drop the unreachable AuthRequired entry
- unit-test the helper's precedence (mapped code > server msg > fallback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:40:39 +07:00
Sergey KozyrenkoandClaude Fable 5 789caf0dfe fix(webui): keep the session on a transient /info failure; update user cache optimistically
A network blip or 5xx on an /info refresh — after an email/name change or on plain
navigation — wrongly cleared local auth and bounced the user to /login, even though the
session cookie was still valid (the axios interceptor already handles real 401/403).

- refreshAuthInfo and the navigation refresh now keep the current session on a transient
  /info failure instead of clearing auth and redirecting to /login
- add patchUser so the email/name forms reflect the change immediately, dropping the
  dependency on a successful /info round-trip for the visible field
- tests: UserProvider keeps the session on transient failure (refresh and navigation
  paths); forms call patchUser with the new value

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:18:39 +07:00
Sergey KozyrenkoandClaude Fable 5 96f346e6ec test(webui): cover account page OAuth gating and the name-change form
- settings-account: local accounts expose name/email/password; OAuth accounts hide
  the password card and email editing, keep the name editable, and label the provider
  (known label, raw fallback, then generic); renders nothing without a user
- name-change-form (previously untested): seeds the current name, submits the trimmed
  value and refreshes auth, blocks an empty name, maps Users.NotFound to friendly copy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 10:42:58 +07:00
Sergey KozyrenkoandClaude Fable 5 c14ba936fb fix(auth): accept mixed-case and long-TLD emails; normalize on change
The vmail validator rejected addresses the frontend's z.string().email() accepts
(uppercase, TLDs longer than 4 chars like .cloud), so users saw a confusing 400.

- relax the vmail regex to allow uppercase and TLDs of 2+ chars
- lowercase + trim the new address in ChangeEmailCurrentUser and in the form schema
  so storage, the duplicate check, and login lookups stay case-stable
- validator + change-email tests for mixed case and long TLDs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:26:44 +07:00
Sergey KozyrenkoandClaude Fable 5 06178bf868 fix(server): link OAuth logins to existing accounts by email; harden email change
- authLoginCallback matches users by email alone so an OAuth login links into an
  existing (incl. local) account instead of 500-ing on users_mail_unique
- relink on a create-branch unique-violation race instead of returning 500
- issue the session with the linked account's actual role privileges
- clear the stale OAuth provider link when a user changes their email
- map the email-change unique-violation race to 409 instead of 500
- isUniqueViolation matches Postgres and SQLite case-insensitively
- tests for link/create/blocked/role-inheritance/race, email 409, provider reset
- update auth form test selectors after the form refactor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:00:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 901753e8d1 feat(webui): show the Profile menu shortcut for OAuth users too
The account page is already available to OAuth users, so drop the
local-only gate on the user-menu "Profile" item to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 03:00:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 273592ec29 feat(webui): open account page to OAuth users, edit display name, consistent forms
- Keep /settings/account available to OAuth users: drop the redirect and
  the local-only sidebar gate. Show the auth provider (Google/GitHub) on
  the summary badge, render the email read-only, and hide the password
  card for OAuth accounts. Fix the `provide` -> `provider` typo in User.
- Add a "Display name" card + NameChangeForm (PUT /user/name) for all users.
- Make the email/password forms consistent: lead with Current Password,
  unify placeholders ("Enter your ..."), drop the redundant Current Email
  field, and size form buttons to match the "Change" buttons.
- Rename the user-menu item "My Profile" -> "Profile".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 02:56:38 +07:00
Sergey KozyrenkoandClaude Opus 4.8 82b82c5f49 chore(webui): remove dead monaco-terminal component and monaco deps
monaco-terminal.tsx had no importers; drop it together with the now-unused
monaco-editor and @monaco-editor/react dependencies (~3 MB). The component is
preserved on branch feature/frontend-monaco-terminal for later work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:44:17 +07:00
Sergey KozyrenkoandClaude Fable 5 5a7bdca4ac fix(webui): re-check focus before stealing it in DetailNavigationSheet
Stabilizes a flaky test ("does not yank focus onto a listbox option when
filteredItems shrinks"): the rAF that moves roving focus now re-checks
document.activeElement before calling focus(), so a focus bounce between
the effect's gate check and the next frame no longer steals focus.

Provisional: the race only reproduces under JSDOM focus simulation (not in
real browsers), so the proper fix likely belongs in the test layer — to be
revisited separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:40:41 +07:00
Sergey KozyrenkoandClaude Fable 5 ed55924873 refactor(webui): drop space-y/space-x for flex gap; fix calendar month offset
Replace the few remaining space-y-*/space-x-* utilities with flex + gap to
match the codebase convention (only the vendored calendar primitive used
them outside this set).

In calendar.tsx the naive space-y→gap conversion shifted the date grid 16px
right: the months flex container holds a zero-width nav wrapper (its prev/
next buttons are absolutely positioned), so gap-4 spaced the (empty) nav from
the month. Drop the gap on months; keep it on month (caption ↔ grid, both
real children).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:40:21 +07:00
Sergey KozyrenkoandClaude Fable 5 ddb654651e feat(webui): account settings page for email & password
Replace the "My Profile" modal with a dedicated /settings/account page in
the existing settings sidebar (first nav item, gated to local accounts —
OAuth users are redirected). Email and password are edited inline via a
read-view → form pattern with a summary card.

- Extract a reusable InputPassword control built on input-group; use it in
  login and both account forms (consistent focus ring + invalid border).
- Add getApiErrorCode and a per-form code→message map so backend error
  codes surface friendly copy (the prior mapping was dead — msg always won).
- Co-locate form tests next to their source files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:40:05 +07:00
Sergey Kozyrenko 182642ba6b Merge pull request #340 from Akalanka1337/user-profile-email-update
Replace Change Password with My Profile (email and password update)
2026-06-11 19:35:53 +07:00
Sergey KozyrenkoandClaude Fable 5 99c8b6786c fix: recover from stale-chunk and DOM-desync SPA crashes
Two crashes seen in production, both reproduced on the live app:

- "Failed to fetch dynamically imported module": after a redeploy rotates the
  hashed chunk filenames, an open tab imports a deleted one. The server answered a
  missing /assets/* with 301 -> index.html (HTML for a JS module -> a MIME
  failure); it now returns 404 + no-store. The client listens for Vite's
  vite:preloadError and reloads once (debounced) to pull the current build. Hashed
  assets are served immutable; index.html and SPA routes no-cache.

- "Failed to execute 'removeChild' ... not a child of this node": an external
  agent (a browser extension or auto-translation) mutates the DOM React owns,
  desyncing reconciliation. A root react-router errorElement catches this
  commit-phase crash and self-heals with a debounced reload, instead of React
  Router's dead default error screen. translate="no" opts the English-only UI out
  of the one trigger it can prevent (browser translation); the errorElement covers
  the rest regardless of source.

Verified by reproducing both on the live old build (missing-chunk 301->HTML;
extension/translation DOM mutation -> the exact removeChild crash) and confirming
the fixed build recovers from each. Adds chunk-reload + RouteErrorBoundary unit
tests and a static-serving integration test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:32:34 +07:00
Akalanka1337 82bca05ac3 feat(webui): replace Change Password with My Profile (email and password updates) 2026-06-05 23:51:36 +05:30
Sergey KozyrenkoandClaude Opus 4.8 831f0e213e refactor(frontend): extract DataTable virtualization into a headless hook
Lifts the inline useWindowVirtualizer wiring from a8c3fbc into a reusable
useWindowVirtualList hook plus dedicated VirtualizedTableBody / DataTableRow
/ PaddingRow components, mounted only past the row threshold so
non-virtualized tables pay no window/ResizeObserver listener cost.

Corrects two latent issues from the inline version:
- Scroll anchor moved from the table wrapper (above <thead>) to <tbody>,
  removing a header-height offset in the virtualizer's coordinate math that
  overscan was masking. Overscan dropped 10 -> 5.
- Rows forward the stable virtualizer.measureElement ref instead of a
  per-row arrow recreated each render, so React no longer detaches and
  re-measures every visible row on every render.

Hardening:
- data-index + measure ref bundled into one `measurement` prop so the
  "both or neither" invariant lives in the type, not a comment.
- a11y: virtualized tables expose aria-rowcount and per-row aria-rowindex,
  so assistive tech sees the true total instead of only the ~20 rows in the
  DOM.
- Hook observes only document.body for above-anchor layout shifts; the
  anchor's own resize can't move its top edge, so observing it was dead.

Tests cover threshold gating, the renderSubComponent opt-out, the aria-row
attributes, and that data-index survives the Radix asChild context-menu
wrapper (the live /flows path).

Verified in-browser on /flows (1116 rows): 16 rows in the DOM,
aria-rowcount 1117, correct recycling top -> middle -> bottom, context menu
intact, zero console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:25:23 +07:00
Sergey KozyrenkoandClaude Opus 4.7 a8c3fbcf74 feat(frontend): virtualize DataTable rows past 50
Adds an opt-in `isVirtualized` prop on DataTable, wired through
@tanstack/react-virtual's useWindowVirtualizer. Threshold gates short
tables (preserves Find-in-page, screen-reader enumeration). Padding <tr>
sentinels keep native HTML table semantics. Skipped when
`renderSubComponent` is set — expanded rows would need per-row
remeasurement we don't wire here.

Enabled on /flows. Measured with pageSize=All (1116 rows):
  DOM elements:      41,657 → 1,121  (37x less)
  tbody <tr>:         1,116 → 22
  a11y tree size:     ~485 KB → ~80 KB

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:36:10 +07:00
Sergey KozyrenkoandClaude Opus 4.7 19d53887b7 refactor(frontend): use Unicode script escapes for CJK detection
The old char-range regex missed hiragana, katakana, hangul, halfwidth
katakana and CJK Ext B+ ideographs, so Japanese/Korean reports would
not trigger NotoSansSC registration. Unicode property escapes cover
all CJK scripts and stay correct as Unicode adds new extensions.

Verified with 18 script samples (zh-Hans/Hant, ja kana, ko hangul/jamo,
fullwidth, CJK Ext B) — all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:03:12 +07:00
Sergey KozyrenkoandClaude Opus 4.7 02ef55cb30 perf(frontend): lazy-register CJK font in PDF generator
Skips loading NotoSansSC.otf (8.1 MB) for reports without CJK chars.
Measured win on flow #50 download: 22.6 MB → 13.8 MB total traffic.
Adds `u` flag to CJK regexes so emoji surrogate pairs don't trigger
false positives (was matching U+D83D high surrogate on flow #41).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:55:44 +07:00