Skip to content

Latest commit

 

History

History
113 lines (92 loc) · 9.7 KB

File metadata and controls

113 lines (92 loc) · 9.7 KB

Shoko WebUI

React 19 + Vite frontend for the Shoko Anime Management Server.

Build & Development

Node >=22, pnpm only. CI uses Node 24 and pnpm 11. pnpm version is not pinned locally (no packageManager/.npmrc); mise.toml pins Node 24 for local dev.

pnpm install          # Also sets up Husky via the `prepare` script
pnpm start          # Dev server at http://localhost:3000, base /webui/
pnpm build          # Production build (dist/)
pnpm build:debug    # Development build
pnpm test           # Unit tests (Vitest, single run) — also runs in PR CI
pnpm test:watch     # Unit tests in watch mode

Lint chain (runs in this exact order):

pnpm dprint:fix     # dprint fmt (auto-fix formatting)
pnpm oxlint:fix     # oxlint --fix (auto-fix lint rules)
pnpm lint           # dprint -> oxlint -> stylelint

Dev proxy: Copy proxy.config.default.js to proxy.config.js and set the target if Shoko Server is not at http://localhost:8111. The dev server auto-opens the browser at /webui/.

Repo Structure

  • src/pages – Route-level components.
  • src/components – Reusable UI components.
  • src/core – API client (axios), Redux store, React Query, SignalR, router.
  • src/hooks – Custom React hooks.
  • src/css – Global styles and Tailwind entry.
  • public/ – Static assets; version.json is generated here at build time.
  • tests/ – Unit tests (Vitest). Top-level directory mirroring src/ paths (e.g. tests/core/utilities/filterTree.test.ts); one test file per covered module; no colocated tests in src/. Configured by a standalone vitest.config.mts (node environment, globals off, @/ alias) — vite.config.mjs is never loaded by tests.

Architecture

  • Entry: index.htmlsrc/main.tsxsrc/core/app.tsxsrc/core/router
  • State: React Query for server state; Redux Toolkit for global UI state.
  • API clients: Four axios instances in src/core/axios.ts:
    • axios — Shoko API v3 (/api/v3)
    • axiosV2 — Shoko API v2 (/api)
    • axiosPlex — Plex endpoints (/plex)
    • axiosExternal — Unconfigured base for external calls
    • v3/v2/Plex clients auto-attach apikey from Redux; all unwrap response.data. A 401 on an authenticated request dispatches AUTH_LOGOUT.
  • Real-time: SignalR client in src/core/signalr, integrated as Redux middleware. Connects on MAINPAGE_LOADED to /signalr/aggregate (feed list via query param), authenticates with the apikey, and stops on AUTH_LOGOUT. Event handlers invalidate React Query caches and dispatch slice actions.
  • Redux: Single-file store at src/core/store.ts. Root reducer clears all state on AUTH_LOGOUT. Full store persisted to sessionStorage; only apiSession persisted to localStorage (when rememberUser is true). Store is throttled to persist at most once per second. Re-exports typed useDispatch/useSelector — import from @/core/store, never from react-redux directly.
  • React Query: Organized by API sub-path under src/core/react-query/<endpoint>/, typically with queries.ts, mutations.ts, types.ts, and optional helpers.ts (subsets are common; shared files live at src/core/react-query/ top level, e.g. queryClient.ts).
  • Build: Vite 8 with Rolldown. Base path /webui/. Hidden sourcemaps. React Compiler enabled via @rolldown/plugin-babel. Sentry plugin requires SENTRY_AUTH_TOKEN. version.json is auto-generated at build time from git hash + package version. The minimum server version gate is hardcoded in vite.config.mjs (VITE_MIN_SERVER_VERSION).
  • Tailwind: v4 via Vite plugin. Entry point is src/css/tailwind.css.
  • Path alias: @/ maps to src/ (configured in vite.config.mjs and tsconfig.json).

React Patterns

This project uses the React Compiler (via @rolldown/plugin-babel). The compiler automatically memoizes components and values, so do not use useMemo, useCallback, or React.memo unless absolutely required (e.g., for a library boundary or a measured performance issue).

Code Style

  • Formatter: dprint (.dprint.json). Covers src/** and tests/**. Line width 120, single quotes (double quotes in JSX), always semicolons.
  • Linter: Oxlint (.oxlintrc.json). Migrated from ESLint. Uses built-in plugins (eslint, typescript, react, import) and JS plugins (@tanstack/query, better-tailwindcss, sort-destructure-keys, @stylistic, react-hooks, perfectionist).
  • TypeScript: Prefer type over interface. Prefer T[] syntax. Use consistent type imports. Multiline type members use semicolons; single-line members use commas.
  • Functions: Arrow-function expressions only (const Foo = () => ...). Omit parens for single parameters; require them for block bodies.
  • Identifiers: Minimum 3 characters. Exceptions: cx, ID, id, to, TV, _, __. Object properties are exempt.
  • Unused variables: Prefix with _ to suppress no-unused-vars (applies to args, vars, and caught errors).
  • Nullish coalescing: Use ?? instead of ||, except for boolean values where || is acceptable.
  • Imports: Use @/ alias instead of relative ../ paths. Don't hand-order imports — import order is enforced by perfectionist/sort-imports and auto-fixed by oxlint (see the Agent lint workflow; write imports in any order and let the fixer sort them).
  • Destructuring: Object destructuring keys must be sorted alphabetically.
  • Restricted imports (will error if imported directly):
    • ../* (relative parent imports) → use @/ alias instead
    • react-redux: useDispatch, useSelector → use @/core/store
    • react-router: useNavigate → use @/hooks/useNavigateVoid
    • react-toastify: toast → use @/core/toast
    • usehooks-ts: useCopyToClipboard → use copyToClipboard from @/core/util
  • State mutations: no-param-reassign allows sliceState and draft* properties for Immer/Redux.
  • Console: Only console.warn and console.error are allowed.
  • Control flow: for-of and for-in loops are allowed (no-restricted-syntax is disabled).
  • React components: Nested components are allowed when passed as props.

Verification & CI

  • Verification is pnpm test (Vitest unit tests) + pnpm lint (typecheck: pnpm tscheck). Test coverage is deliberately limited to regression protection of high-risk modules (filterTree.ts, auto-match logic/regexes); never add coverage tooling or UI/DOM assertions.
  • Other CI workflows: release-dev-auto.yml (auto build on master push), release-manual.yml, update-manifest.yml, CodeQL.
  • Pre-commit: Husky runs lint-staged (configured in lint-staged.config.js), which executes dprint fmt, oxlint, and stylelint on staged files. stylelint only covers src/css/*.css (flat, not recursive).
  • PR CI: .github/workflows/validate-pr.yml runs pnpm lint --quiet, then pnpm test.
  • Agent lint workflow:
    • After every file edit, run ./node_modules/.bin/dprint fmt <file> to format just that file.
    • After completing edits on a file, run ./node_modules/.bin/oxlint --fix <file> to catch lint errors early (auto-fixes import order and other fixable rules) — fix any remaining errors before moving on.
  • Never skip pre-commit hooks. Always let Husky run — do not use --no-verify or equivalent.

Guardrails

  • Do NOT modify pnpm-lock.yaml, .oxlintrc.json, or .dprint.json unless explicitly asked.
  • Do NOT use npm or yarn; always use pnpm add / pnpm remove.
  • Do not add explicit type annotations where TS inference is sufficient.
  • Treat changes to src/core/axios.ts, src/core/store.ts, and auth-related logic with extra scrutiny.
  • If you modify files, styles, structures, configurations, or workflows mentioned in this file, update the corresponding AGENTS.md sections to keep them accurate.
  • @/core/util is a single file (src/core/util.ts), not a directory — do not confuse with src/core/utilities/.
  • Use semver for version comparisons — hand-rolling version parsing with Number.parseInt/split('.') silently mishandles pre-release suffixes.
  • Use dayjs for date formatting/manipulation — never use new Date() / .toLocaleString() for display. Always import from @/core/util: import { dayjs } from '@/core/util'. Plugins and locale are pre-configured there.
  • lodash — before writing custom utility functions for grouping, sorting, filtering, debouncing, throttling, or deep equality, check if lodash already provides it.
  • Immutable state → immer or useImmer. Do not hand-write reducers with O(n²) object spreads.
  • pretty-bytes — use for human-readable file sizes. Do not hand-roll byte formatting with if/else size thresholds.
  • format-thousandsformatThousand from @/core/util — use for number formatting with thousands separators. Do not use .toLocaleString() or string concatenation.
  • fast-json-patch — use for JSON Patch (RFC 6902) operations. Do not write custom diff/patch logic for API settings updates.
  • classnames (imported as cx) — use for conditional CSS class joining. Do not construct class strings with template literals or string concatenation.
  • Modal hotkeys (react-hotkeys-hook): guard useHotkeys('escape', ...) with the same pending/loading check used for ModalPanel's onRequestClose — otherwise Escape can close a modal mid-save. Add enableOnFormTags: true to useHotkeys('enter', ...) when the modal has a focused text input, or the shortcut silently never fires (the library ignores form-tag targets by default).

Specifications

Implementation specs and planning artifacts may exist under specs/. Use them as historical/reference material when relevant, but prefer the current source code and API contracts as the source of truth.