You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
22 KiB
Markdown

# Frontend Audit Report v2 -- SolidJS + Tailwind CSS
**Date**: 2026-03-27
**Scope**: Pages (ThemeManager, Settings, GenerateSynthesis, Home, SynthesisDetail), settings components (SettingsSchedule, SettingsBraveSearch, SettingsRateLimit), ApiKeyManager, API layer, types, utilities
**Codebase snapshot**: commit `7c8a196`
---
## Executive Summary
The frontend is well-structured for a learning project. SolidJS primitives are used correctly and idiomatically in most places. The API layer is clean, the type system is comprehensive, and the i18n approach is sound. The main areas for improvement are: oversized page components that mix too many concerns, inconsistent feedback patterns (inline messages vs. toasts), a few SolidJS reactivity subtleties, and opportunities for better component reuse.
---
## 1. SolidJS Patterns
### 1.1 Reactive Primitives -- Correct Usage
Signals, effects, and resources are used appropriately throughout. Notable good patterns:
- **`createResource`** in `Settings.tsx` for providers/API keys -- idiomatic for async data that should auto-refetch.
- **`createMemo`** in `Home.tsx` (`groupedByTheme`) -- correctly memoizes an expensive grouping computation.
- **`createSSEConnection`** in `utils/sse.ts` -- elegantly wraps EventSource into reactive signals with `onCleanup`. This is textbook SolidJS.
- **Optimistic updates** in `ThemeManager.tsx` (`handleTogglePreferred`) -- updates local state first, reverts on error. Exactly the right pattern.
### 1.2 Reactive Primitives -- Issues
**ISSUE (Medium): `createEffect` triggering async side effects in `ThemeManager.tsx` (line 123-138)**
```tsx
createEffect(() => {
const theme = selectedTheme();
if (theme) {
// ...populate signals...
fetchSources(theme.id); // async fire-and-forget inside effect
}
});
```
Calling an async function inside `createEffect` without tracking the returned promise means errors in `fetchSources` are silently swallowed by the effect (the `try/catch` inside `fetchSources` helps, but the pattern itself is fragile). More importantly, if `selectedTheme()` changes rapidly, multiple concurrent `fetchSources` calls can race and the final state may reflect a stale theme's sources.
**Recommendation**: Debounce or cancel previous fetches. Consider using `createResource` keyed on `selectedThemeId()` instead, which natively handles this:
```tsx
const [sources] = createResource(selectedThemeId, (id) => sourcesApi.list(id));
```
**ISSUE (Low): Duplicate `createEffect` for auto-selecting provider in `Settings.tsx` (lines 76-88 and 122-131)**
Two separate effects handle provider-related logic. The first checks if the saved provider is still available; the second auto-selects when only one exists. These could be unified into a single effect for clarity.
### 1.3 Show/For Usage -- Generally Correct
- `<Show when={...}>` with the callback form `{(accessor) => ...}` is used correctly in most places (e.g., Settings.tsx line 232, ThemeManager.tsx line 489).
- `<For each={...}>` is used everywhere lists are rendered -- correct.
**ISSUE (Low): Multiple adjacent `<Show>` blocks with inverted conditions in `GenerateSynthesis.tsx` (lines 393-401)**
```tsx
<Show when={status() === 'done'}>...</Show>
<Show when={status() === 'in-progress'}>...</Show>
<Show when={status() === 'pending'}>...</Show>
```
Three mutually exclusive `<Show>` blocks evaluate the same reactive expression three times. SolidJS's `<Switch>`/`<Match>` is the idiomatic pattern for exclusive branching:
```tsx
<Switch>
<Match when={status() === 'done'}>...</Match>
<Match when={status() === 'in-progress'}>...</Match>
<Match when={status() === 'pending'}>...</Match>
</Switch>
```
**ISSUE (Low): Nested `<Show>` chains in `SynthesisDetail.tsx` provenance section (lines 494-538)**
Three nested `<Show>` blocks for loading/empty/data states. A `<Switch>` would be cleaner.
### 1.4 onCleanup -- Mostly Good
- `ThemeManager.tsx` clears both `deleteTimer` and `deleteThemeTimer` on cleanup -- good.
- `Home.tsx` clears all delete timers from the record -- good.
- `SynthesisDetail.tsx` clears the email success timer -- good.
- `createSSEConnection` properly closes EventSource and clears retry timeout on cleanup -- excellent.
**ISSUE (Medium): Missing `onCleanup` for navigation timer in `GenerateSynthesis.tsx`**
The `onCleanup` on line 199 is placed inside a `createEffect`, which is correct for that specific timer. However, the SSE connection itself is only cleaned up via `onCleanup` inside `createSSEConnection`. If the component is destroyed while a generation is in flight and the user never calls `handleRetry`, the connection is properly cleaned up by the SSE utility's own `onCleanup`. This is fine -- just noting the implicit dependency.
**ISSUE (Low): `emailTimer` stored as a signal in `SynthesisDetail.tsx` (line 134)**
```tsx
const [emailTimer, setEmailTimer] = createSignal<ReturnType<typeof setTimeout> | undefined>();
```
Timer IDs do not need to be reactive (nothing in the UI depends on the timer value). A plain `let` variable would be simpler, as done in `ThemeManager.tsx` and `Home.tsx`. The signal causes an unnecessary subscription.
---
## 2. Component Architecture
### 2.1 Oversized Components
**ISSUE (High): `ThemeManager.tsx` -- 935 lines, ~30 signals, single monolithic component**
This is the largest and most complex page. It manages:
- Theme CRUD (list, create, save, delete)
- Content settings editing (name, topic, categories, max age, max items, summary length)
- Source CRUD (add, delete, toggle preferred)
- Bulk import (text-based)
- CSV import/export
- Schedule delegation (via `SettingsSchedule`)
The file defines ~30 `createSignal` calls (lines 42-81). This is a strong signal that the component should be decomposed. Suggested extraction:
| Extracted Component | Signals Moved | Lines Saved |
|---|---|---|
| `ThemeContentForm` | editName, editThemeTopic, editCategories, editMaxAge, editMaxItems, editSummaryLength, newCategory, savingTheme, themeMessage | ~180 |
| `ThemeSourceList` | sources, loadingSources, newTitle, newUrl, adding, addError, confirmingDeleteId, deleteTimer | ~230 |
| `ThemeBulkImport` | bulkText, importing, importError | ~50 |
| `ThemeCsvImport` | csvError, fileInputRef, importing | ~40 |
**ISSUE (Medium): `SynthesisDetail.tsx` -- 548 lines, manages email, export, delete, provenance, and display level**
This component could benefit from extracting:
- `SynthesisEmailSection` (email state + send logic)
- `SynthesisExportSection` (markdown + PDF export)
- `SynthesisProvenanceSection` (provenance data + table)
The `NewsItemCard` and `Section` sub-components are already well-extracted within the file (lines 36-105) -- good pattern.
**ISSUE (Medium): `Settings.tsx` -- 694 lines**
Already partially decomposed via `SettingsBraveSearch`, `SettingsRateLimit`, and `ApiKeyManager`. However, the remaining provider selection card (lines 356-512) is ~160 lines of dense JSX with deeply nested `<Show>` blocks. It could become a `SettingsProviderCard` component.
**ISSUE (Low): `GenerateSynthesis.tsx` -- 471 lines**
Acceptable size. The `completedSteps()` computation (lines 135-169) is somewhat complex but well-documented. The step checklist UI could be a separate `StepChecklist` component for readability.
### 2.2 State Management
The application uses a lightweight approach: `AuthContext` for global auth state, `ToastProvider` for notifications, and local signals within each page. This is appropriate for the app's complexity level. There is no global state management library, and none is needed.
**Positive**: The `useToast` context is used consistently in newer components (`SettingsSchedule`, `SettingsBraveSearch`, `ApiKeyManager`).
### 2.3 Reusability
**Good reuse patterns:**
- `Button` component with variant/loading/icon props -- used across pages.
- `LoadingSpinner` with optional `fullPage` prop -- used everywhere.
- `SettingsSchedule`, `SettingsBraveSearch`, `SettingsRateLimit` -- well-scoped settings sub-components.
**ISSUE (Medium): Duplicated inline button styles**
Several pages define raw `<button>` elements with long Tailwind class strings instead of using the `Button` component:
- `ThemeManager.tsx` lines 799-808 (bulk import submit)
- `GenerateSynthesis.tsx` lines 449-461 (generate button)
- `SynthesisDetail.tsx` lines 372-386 (send email button), 410-439 (export buttons)
- `Home.tsx` lines 229-235 (new synthesis button)
These should all use `<Button>` for visual consistency and reduced duplication.
**ISSUE (Medium): Duplicated CSV Export/Import buttons in `ThemeManager.tsx` (lines 744-763)**
The CSV import button uses a raw `<label>` + hidden `<input>` pattern with long inline styles. This pattern also appears in `Settings.tsx` (lines 637-655). A reusable `FileUploadButton` component would eliminate this duplication.
**ISSUE (Low): `SynthesisCard` defined as a function, not a component, in `Home.tsx` (line 133)**
```tsx
const SynthesisCard = (synth: SynthesisListItem) => (...)
```
This is a plain function returning JSX, called as `SynthesisCard(synth)`. In SolidJS this works but bypasses the component lifecycle -- it won't get its own error boundary or Suspense tracking. For a list card that's fine, but it's inconsistent with the rest of the codebase which uses `Component<>`. More importantly, calling it as `SynthesisCard(synth)` instead of `<SynthesisCard synth={synth} />` means SolidJS treats it as an inline expression, not a tracked component boundary. This makes debugging harder in dev tools.
---
## 3. Type Safety
### 3.1 TypeScript Configuration -- Excellent
`tsconfig.json` has `strict: true`, `isolatedModules: true`, and `forceConsistentCasingInFileNames: true`. No escape hatches.
### 3.2 Type Definitions -- Good
`types.ts` is comprehensive with 303 lines covering all domain entities. Types are used consistently across API clients, pages, and components.
**Positive patterns:**
- `satisfies` used in `syntheses.ts` line 86 (`{ email } satisfies SendEmailRequest`) -- ensures the object conforms to the type without widening.
- Union types for `ApiError` fields (`field_errors?: Record<string, string>`).
- Proper nullability (`theme_id: string | null`, `date?: string | null`).
### 3.3 Type Safety Issues
**ISSUE (Medium): `isApiError` type guard is too loose (types.ts lines 163-170)**
```tsx
export function isApiError(error: unknown): error is ApiError {
return (
typeof error === 'object' &&
error !== null &&
'status' in error &&
'message' in error
);
}
```
Any object with `status` and `message` properties will pass this guard -- including native `Response` objects, DOM errors, or other library errors. Adding a type discriminator field (e.g., `kind: 'api_error'`) or checking `typeof error.status === 'number'` would improve safety.
**ISSUE (Low): Theme types defined in `api/themes.ts` rather than `types.ts`**
`ThemeResponse`, `CreateThemeRequest`, and `UpdateThemeRequest` are defined in `api/themes.ts`. All other domain types (Source, Synthesis, UserSettings, etc.) live in `types.ts`. This inconsistency means consumers must import from two different locations. The same applies to `ScheduleResponse` and `UpsertScheduleRequest` in `api/schedules.ts`.
**ISSUE (Low): Non-null assertion in `Settings.tsx` line 543**
```tsx
<ApiKeyManager providers={providers()!} />
```
The `!` assertion is safe here because it's inside a `<Show when={providers() && providers()!.length > 0}>` block, but it would be cleaner to use the callback form of `<Show>` to get a guaranteed-non-null accessor.
**ISSUE (Low): Non-null assertion in `ThemeManager.tsx` line 913**
```tsx
<SettingsSchedule themeId={selectedThemeId()!} />
```
Same pattern -- safe because it's inside `<Show when={selectedTheme()}>`, but the `!` could be avoided by threading the theme's ID through the callback accessor.
### 3.4 API Contracts
The API client (`client.ts`) is well-designed:
- Generics on all methods (`get<T>`, `post<T>`, etc.) provide return-type safety.
- `FormData` is handled transparently (no `Content-Type` header set, letting the browser add the multipart boundary).
- 401 redirect is centralized.
- 204 handling returns `undefined as T` -- this is a reasonable trade-off; the alternative would be a separate `deleteVoid` method.
**ISSUE (Low): `API_BASE` defined in two places**
`API_BASE` is defined as a constant in both `client.ts` (line 3) and `syntheses.ts` (line 4). The `syntheses.ts` copy is only used for `fetchFile` and `progressUrl`, which bypass the `ApiClient`. If `fetchFile` were added to `ApiClient`, the duplication would be eliminated.
---
## 4. Code Organization
### 4.1 File Structure -- Good
The project follows clear conventions:
- `pages/` for route-level components
- `components/` for reusable pieces, with `ui/` for atomic elements and `settings/` for domain-specific composites
- `api/` for HTTP clients, one file per resource
- `contexts/` for global state
- `utils/` for pure helpers
- `i18n/` for translations
### 4.2 Import Conventions -- Consistent
All imports use the `~/` alias. No relative path chaos. Named imports are used everywhere (no `import *`).
### 4.3 Issues
**ISSUE (Low): `normalizeUrl` and `isValidUrl` exported from `pages/Sources.tsx` and imported by `pages/ThemeManager.tsx` (line 23)**
```tsx
import { normalizeUrl, isValidUrl } from '~/pages/Sources';
```
Page components should not export utility functions consumed by other pages. These helpers belong in `~/utils/url.ts` (or similar). The Sources page is currently redirected to `/themes` in the router (App.tsx line 67), suggesting it may be deprecated soon, which would break the import.
**ISSUE (Low): `fetchFile` and `triggerDownload` exported from `api/syntheses.ts` and imported by `api/sources.ts`**
General-purpose file download utilities live inside a domain-specific API module. They should be in `api/client.ts` or a dedicated `utils/download.ts`.
---
## 5. Page-by-Page Analysis
### 5.1 ThemeManager.tsx
**Strengths:**
- JSDoc block comment at the top explains the page's purpose.
- Proper `onCleanup` for timers.
- Optimistic updates for preferred source toggling.
- Delegates schedule management to `SettingsSchedule`.
**Weaknesses:**
- Largest file in the frontend (935 lines). Should be decomposed (see section 2.1).
- Mixes content settings, source CRUD, bulk import, and CSV import in one render tree.
- The two-click delete pattern (with timer-based confirmation reset) is duplicated for both themes and sources -- could be a reusable hook (`createConfirmDelete`).
### 5.2 Settings.tsx
**Strengths:**
- Excellent JSDoc documenting the page's key behaviors.
- Import/Export as a collapsed `<details>` section -- good UX.
- Provider auto-detection when only one provider exists.
- `DEFAULT_SETTINGS` used as a merge base for imported settings -- robust.
- Sticky save button at the bottom.
**Weaknesses:**
- Provider card section is large and deeply nested.
- Two separate `createEffect` blocks for provider logic.
### 5.3 GenerateSynthesis.tsx
**Strengths:**
- Clean SSE state machine documented in the JSDoc.
- `STEPS` array as a module-level constant -- easy to extend.
- `createEffect` for auto-redirect on completion is clean.
- Good UX: "you can leave this page" note, stop button, retry button.
**Weaknesses:**
- `completedSteps()` has O(steps * events) complexity -- fine for 3 steps, but the logic is hard to follow.
- Three adjacent `<Show>` blocks for step status should be `<Switch>/<Match>`.
- `handleStop` makes a raw `fetch` call instead of using the API client.
### 5.4 Home.tsx
**Strengths:**
- Sort toggle (date vs. theme) is a nice UX addition.
- `groupedByTheme` memo is efficient and well-commented.
- Delete confirmation with auto-cancel timers, one per card -- well-implemented.
**Weaknesses:**
- `SynthesisCard` is a function, not a component (see 2.3).
- Inline SVG for LLM logs icon (lines 191-193) -- should use a Lucide icon or a reusable component.
- The `deleteTimers` signal stores a `Record<string, timeout>` -- every timer update creates a new object. For many cards, a `Map` stored as a `let` variable would be more efficient (timer IDs don't need to be reactive).
### 5.5 SynthesisDetail.tsx
**Strengths:**
- `NewsItemCard` and `Section` are well-extracted sub-components.
- Display level slider for article detail density -- creative UX.
- `truncateSummary` is a pure function outside the component -- good.
- Pre-fills email from auth context.
**Weaknesses:**
- 548 lines -- should extract email/export/provenance sections (see 2.1).
- Hardcoded French strings in `NewsItemCard` (lines 75, 83: "Lire la suite" and "Reduire"). These bypass the i18n system.
- Provenance table has no pagination -- could be large.
---
## 6. Consistency Issues
### 6.1 Feedback Patterns -- Inconsistent
| Component | Feedback Mechanism |
|---|---|
| ThemeManager | Inline message banner (`themeMessage` signal) |
| Settings | Inline message banner (`message` signal) |
| SettingsSchedule | Toast notifications (`useToast`) |
| SettingsBraveSearch | Toast notifications (`useToast`) |
| ApiKeyManager | Toast notifications (`useToast`) |
| GenerateSynthesis | Inline error/success banners |
| Home | Inline error banner |
| SynthesisDetail | Inline error/success banners per section |
Newer components (SettingsSchedule, SettingsBraveSearch, ApiKeyManager) use the Toast system. Older pages use inline message banners with manual state management. **Recommendation**: Migrate all feedback to the Toast system for consistency, except for blocking errors that prevent the user from proceeding (those should remain inline).
### 6.2 Delete Confirmation Patterns -- Duplicated
Three different implementations:
1. **Two-click with timer** (ThemeManager sources, ThemeManager theme): first click enters confirm state, auto-resets after 3-5 seconds.
2. **Two-click with timer via signal record** (Home): same concept, but uses a `Record<string, timeout>` signal.
3. **Modal-style banner** (SynthesisDetail): `showDeleteConfirm` signal shows a banner with Cancel/Confirm buttons.
A reusable `createDeleteConfirmation(timeoutMs)` primitive could unify all three.
### 6.3 Error Handling Patterns -- Consistent
The `isApiError(err)` guard is used consistently across all pages and components for try/catch blocks. The fallback pattern `t('some.errorKey')` is applied uniformly. This is good.
---
## 7. Accessibility
**ISSUE (Medium): No `aria-label` on many interactive elements**
- Star toggle buttons in ThemeManager (line 833): `title` attribute is set but no `aria-label`.
- Delete buttons in source list (line 869): same issue.
- Sort toggle buttons in Home (lines 289-308): no accessible name.
**ISSUE (Low): `<details>/<summary>` in Settings (line 619) may not announce state changes to screen readers**
Native `<details>` has good built-in accessibility, but the `<p>` inside `<summary>` may cause unexpected behavior in some screen readers.
---
## 8. Performance Considerations
**ISSUE (Low): Sorting inside `<For each={...}>` in Home.tsx (line 314)**
```tsx
<For each={[...syntheses()].sort(...)}>
```
Every reactive update to `syntheses()` will re-sort the array. This should use `createMemo`:
```tsx
const sortedByDate = createMemo(() =>
[...syntheses()].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
);
```
The `groupedByTheme` already does this correctly via `createMemo`.
**ISSUE (Low): Growing events array in SSE connection**
`createSSEConnection` appends every event to the `events` signal array. For long-running generation jobs, this array grows unboundedly. The `completedSteps()` function in GenerateSynthesis iterates over all events on every reactive update. For typical jobs this is negligible, but for very long jobs with many progress events it could become noticeable.
---
## 9. Summary of Recommendations
### Priority High
1. **Decompose `ThemeManager.tsx`** into 3-4 focused sub-components (ThemeContentForm, ThemeSourceList, ThemeBulkImport, ThemeCsvImport).
### Priority Medium
2. **Unify feedback patterns**: Migrate inline message banners to the Toast system where appropriate.
3. **Extract a reusable delete confirmation primitive** to replace the three duplicated implementations.
4. **Replace raw `<button>` elements** with the existing `<Button>` component across all pages.
5. **Move `normalizeUrl`/`isValidUrl`** from `pages/Sources.tsx` to `utils/url.ts`.
6. **Move `fetchFile`/`triggerDownload`** from `api/syntheses.ts` to a shared location.
7. **Use `createResource`** instead of `createEffect` + manual fetch for theme sources in ThemeManager.
8. **Strengthen `isApiError` type guard** with `typeof` checks on `status` and `message`.
9. **Hardcoded French strings** in SynthesisDetail NewsItemCard ("Lire la suite", "Reduire") must use `t()`.
### Priority Low
10. **Use `<Switch>/<Match>`** instead of multiple adjacent `<Show>` blocks for mutually exclusive states.
11. **Consolidate theme/schedule types** into `types.ts` alongside all other domain types.
12. **Memoize the date-sorted synthesis list** in Home.tsx instead of sorting inline in the `<For>`.
13. **Convert `SynthesisCard`** from a plain function to a proper `Component<>`.
14. **Use `let` instead of `createSignal`** for timer IDs that are not rendered in the UI.
---
## 10. What's Done Well
- **SSE utility** (`createSSEConnection`) is an exemplary SolidJS utility: wraps imperative APIs into reactive signals, handles reconnection with exponential backoff, and cleans up properly.
- **API client** is clean, type-safe, and handles all edge cases (FormData, 204, 401 redirect).
- **i18n system** is simple, type-safe (translation keys are a union type), and consistently used.
- **Toast system** with Portal rendering and auto-dismiss timers is well-implemented.
- **Button component** with variant/loading/icon props is a good reusable primitive.
- **Auth context** is minimal and correct -- derived signals like `isAdmin()` avoid redundant state.
- **Route-level code splitting** via `lazy()` in App.tsx -- good for initial load performance.
- **JSDoc comments** on pages and utilities are thorough and explain *why*, not just *what*.