React 19 + Vite frontend for the Shoko Anime Management Server.
Node >=22, pnpm only. CI uses Node 24 and pnpm 11. pnpm version is not pinned locally (no
packageManager/.npmrc);mise.tomlpins 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 modeLint 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 -> stylelintDev 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/.
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.jsonis generated here at build time.tests/– Unit tests (Vitest). Top-level directory mirroringsrc/paths (e.g.tests/core/utilities/filterTree.test.ts); one test file per covered module; no colocated tests insrc/. Configured by a standalonevitest.config.mts(nodeenvironment,globalsoff,@/alias) —vite.config.mjsis never loaded by tests.
- Entry:
index.html→src/main.tsx→src/core/app.tsx→src/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
apikeyfrom Redux; all unwrapresponse.data. A 401 on an authenticated request dispatchesAUTH_LOGOUT.
- Real-time: SignalR client in
src/core/signalr, integrated as Redux middleware. Connects onMAINPAGE_LOADEDto/signalr/aggregate(feed list via query param), authenticates with the apikey, and stops onAUTH_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 onAUTH_LOGOUT. Full store persisted tosessionStorage; onlyapiSessionpersisted tolocalStorage(whenrememberUseris true). Store is throttled to persist at most once per second. Re-exports typeduseDispatch/useSelector— import from@/core/store, never fromreact-reduxdirectly. - React Query: Organized by API sub-path under
src/core/react-query/<endpoint>/, typically withqueries.ts,mutations.ts,types.ts, and optionalhelpers.ts(subsets are common; shared files live atsrc/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 requiresSENTRY_AUTH_TOKEN.version.jsonis auto-generated at build time from git hash + package version. The minimum server version gate is hardcoded invite.config.mjs(VITE_MIN_SERVER_VERSION). - Tailwind: v4 via Vite plugin. Entry point is
src/css/tailwind.css. - Path alias:
@/maps tosrc/(configured invite.config.mjsandtsconfig.json).
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).
- Formatter:
dprint(.dprint.json). Coverssrc/**andtests/**. 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
typeoverinterface. PreferT[]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 suppressno-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 byperfectionist/sort-importsand 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 insteadreact-redux:useDispatch,useSelector→ use@/core/storereact-router:useNavigate→ use@/hooks/useNavigateVoidreact-toastify:toast→ use@/core/toastusehooks-ts:useCopyToClipboard→ usecopyToClipboardfrom@/core/util
- State mutations:
no-param-reassignallowssliceStateanddraft*properties for Immer/Redux. - Console: Only
console.warnandconsole.errorare allowed. - Control flow:
for-ofandfor-inloops are allowed (no-restricted-syntaxis disabled). - React components: Nested components are allowed when passed as props.
- 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 onmasterpush),release-manual.yml,update-manifest.yml, CodeQL. - Pre-commit: Husky runs
lint-staged(configured inlint-staged.config.js), which executesdprint fmt,oxlint, andstylelinton staged files.stylelintonly coverssrc/css/*.css(flat, not recursive). - PR CI:
.github/workflows/validate-pr.ymlrunspnpm lint --quiet, thenpnpm 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.
- After every file edit, run
- Never skip pre-commit hooks. Always let Husky run — do not use
--no-verifyor equivalent.
- Do NOT modify
pnpm-lock.yaml,.oxlintrc.json, or.dprint.jsonunless explicitly asked. - Do NOT use
npmoryarn; always usepnpm 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.mdsections to keep them accurate. @/core/utilis a single file (src/core/util.ts), not a directory — do not confuse withsrc/core/utilities/.- Use
semverfor version comparisons — hand-rolling version parsing withNumber.parseInt/split('.')silently mishandles pre-release suffixes. - Use
dayjsfor date formatting/manipulation — never usenew 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 →
immeroruseImmer. Do not hand-write reducers with O(n²) object spreads. pretty-bytes— use for human-readable file sizes. Do not hand-roll byte formatting withif/elsesize thresholds.format-thousands→formatThousandfrom@/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 ascx) — use for conditional CSS class joining. Do not construct class strings with template literals or string concatenation.- Modal hotkeys (
react-hotkeys-hook): guarduseHotkeys('escape', ...)with the same pending/loading check used forModalPanel'sonRequestClose— otherwise Escape can close a modal mid-save. AddenableOnFormTags: truetouseHotkeys('enter', ...)when the modal has a focused text input, or the shortcut silently never fires (the library ignores form-tag targets by default).
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.