mirror of
https://github.com/rommapp/romm.git
synced 2026-10-11 22:40:04 +00:00
Consolidate agent instructions into AGENTS.md and add a shared dev environment setup (#4869)
This commit is contained in:
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# SessionStart hook for cloud sessions: runs dev-setup.sh, which also restarts
|
||||
# MariaDB since environment snapshots keep files but not running processes.
|
||||
|
||||
if [[ ${CLAUDE_CODE_REMOTE-} != "true" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n ${CLAUDE_ENV_FILE-} ]]; then
|
||||
echo "export PATH=\"${HOME}/.local/bin:\$PATH\"" >>"${CLAUDE_ENV_FILE}"
|
||||
fi
|
||||
|
||||
LOG="${TMPDIR:-/tmp}/romm-dev-setup.log"
|
||||
if bash "${CLAUDE_PROJECT_DIR}/scripts/dev-setup.sh" --db >"${LOG}" 2>&1; then
|
||||
tail -n 1 "${LOG}"
|
||||
else
|
||||
echo "scripts/dev-setup.sh --db failed; see ${LOG}"
|
||||
fi
|
||||
@@ -8,6 +8,15 @@
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/setup-code-signing.sh\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "startup|resume",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/cloud-setup.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -89,7 +89,7 @@ Give each finding one verdict, backed by evidence you checked yourself:
|
||||
Weigh each claim against what the PR deliberately does. A finding that would
|
||||
undo a behaviour the maintainer asked for is a decline, with that intent as the
|
||||
reason. Repo rules are a valid source: Greptile enforces the comment discipline
|
||||
from `CLAUDE.md` and `review-polish`, and those findings stand.
|
||||
from `AGENTS.md` and `review-polish`, and those findings stand.
|
||||
|
||||
A valid finding that is pattern-shaped is a missing check: name one as a
|
||||
follow-up in the reply (see `review-polish`, "Encode what you fixed twice").
|
||||
|
||||
@@ -19,7 +19,7 @@ reading code, not as a substitute for reading it.
|
||||
|
||||
Skip it when you already know the file or symbol (read that directly), and when
|
||||
the question is about work on the current branch, since the server cannot see
|
||||
the checkout. For the rules on how to write code, `CLAUDE.md`, the focused
|
||||
the checkout. For the rules on how to write code, `AGENTS.md`, the focused
|
||||
skills and `docs/*_ARCHITECTURE.md` are authoritative.
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -21,7 +21,7 @@ fmt && trunk check` comes last of all, so nothing lands unformatted. If
|
||||
|
||||
## A. Comments and docstrings: the most-corrected thing in this repo
|
||||
|
||||
`CLAUDE.md` already says keep comments short, don't restate the code, don't
|
||||
`AGENTS.md` already says keep comments short, don't restate the code, don't
|
||||
explain a change. In practice contributions still ship multi-paragraph
|
||||
rationale, and it gets cut. Cut it yourself.
|
||||
|
||||
|
||||
@@ -1,6 +1,55 @@
|
||||
# Repository Instructions
|
||||
# RomM - Repository Guide for Contributors & Agents
|
||||
|
||||
Read and follow [CLAUDE.md](CLAUDE.md) for all repository instructions.
|
||||
RomM is a self-hosted ROM manager and player: scan a game library off disk, enrich it with metadata from 10+ providers, browse it in a web UI, and play in the browser.
|
||||
|
||||
---
|
||||
|
||||
The frontend talks to the backend over `/api/*` (REST) and `/ws` (Socket.IO). TypeScript types are **generated** from the backend's OpenAPI schema into `frontend/src/__generated__/` - the backend is the single source of truth for API shapes.
|
||||
|
||||
## Deep-dive references
|
||||
|
||||
- **`docs/BACKEND_ARCHITECTURE.md`** - directory map, ER diagram, every endpoint, auth/scopes, tasks.
|
||||
- **`docs/FRONTEND_ARCHITECTURE.md`** - routing, stores, services, theming, build tooling.
|
||||
- **`DEVELOPER_SETUP.md`** - Docker and manual local setup (mock library, `.env`, services).
|
||||
- **`CONTRIBUTING.md`** - contribution flow, **AI-assistance disclosure**, translations.
|
||||
|
||||
---
|
||||
|
||||
## Stack-specific guidance
|
||||
|
||||
`frontend/AGENTS.md` (v1 vs v2, frontend commands) and `backend/AGENTS.md` (backend commands) load when you work in those directories. New frontend work goes in v2 (`frontend/src/v2/`); v1 is frozen.
|
||||
|
||||
---
|
||||
|
||||
## Skills - load the focused guide for your task
|
||||
|
||||
These live in `.claude/skills/` and carry the detailed rules. Invoke the one that matches what you're doing. `pr-ready`, `address-bot-reviews`, and `draft-announcement` are run by the user, not auto-invoked.
|
||||
|
||||
---
|
||||
|
||||
## Repo-wide rules
|
||||
|
||||
**Disclose AI assistance in the PR.** RomM requires it (see `CONTRIBUTING.md`): state that AI was used and to what extent. This is mandatory and non-negotiable for agent-written contributions.
|
||||
**Branch off `master`; open PRs against `master`.** Fork → feature branch → PR. Don't push to `master`.
|
||||
**Linting is via [Trunk](https://trunk.io)** (`trunk fmt && trunk check`), which wraps ruff, black, isort, ESLint, Prettier, and more, and runs in CI on every PR. **Never commit with `--no-verify`.**
|
||||
**The backend owns the API contract.** Changed a response schema or route? Regenerate frontend types (`npm run generate`) and re-typecheck.
|
||||
**Tests travel with code.** New logic gets a test; new endpoints get endpoint tests; new v2 primitives get a Storybook story (+ `play()` if interactive).
|
||||
**Verify before handoff.** Don't say "done" on UI work without testing it in the browser in both themes and all input modalities. See `review-polish`.
|
||||
**English first.** Outside of language files, all code, comments, identifiers, `.md` files, and commit/PR messages are in English.
|
||||
**No em-dashes.** Never use em-dashes (U+2014) when writing comments or text. Use commas, parentheses, or separate sentences instead. An em-dash string literal is code (an empty-value glyph) and stays allowed.
|
||||
**Prefer a check to a model review.** A mechanical rule (an import, a character, a CSS pattern, an SFC shape) belongs in ESLint, `tsconfig`, or Trunk. When a review finding repeats, propose a check as a named follow-up; see `review-polish`.
|
||||
**Keep comments short.** Comments should be concise, and focus on the "why" rather than the "what" (the code itself is the "what"). A comment is one or two lines; a docstring is one sentence plus `Args:`/`Returns:` when the signature needs it. Three or more lines of prose means you are explaining, not commenting. See `review-polish` for what to cut.
|
||||
**Don't restate the code.** The code is self-documenting, so skip comments that narrate what a reader can already see (which button sits where, that something is disabled when empty, obvious ordering). Comment only non-obvious "why" that the code can't convey on its own.
|
||||
**One home per value.** Before declaring a constant, type, limit, regex, or store getter, search for an existing one and import it. Validation limits live on the model and are imported by endpoints; a type lives in the component that owns it and is exported. Don't add a near-duplicate that differs only by sorting, and don't invent an identity scheme where a foreign key already exists.
|
||||
**Never commit secrets.** Never commit secrets (API keys, passwords, tokens, etc.) to the repo. Use environment variables or secret management tools instead.
|
||||
**Don't explain a change.** Avoid comments that explain why a change was made to the code, or that record what the code used to do. Focus instead on the current behaviour of the code and how it works. Reasons for a change go in the commit message and the PR body.
|
||||
**Python tools live in `backend/tools/`.** Standalone dev/test utilities and scripts (not part of the app runtime) go in `backend/tools/`, not scattered across `backend/`.
|
||||
**Link PRs to issues.** In the PR description, use `Fixes #XXXX` for issue/bug fixes and `Closes #XXXX` for feature implementations.
|
||||
**Use the PR template.** Base every PR description on `.github/PULL_REQUEST_TEMPLATE.md`.
|
||||
**Show UI changes in the PR.** A PR that touches anything visible ships with at least one screenshot under the template's `Screenshots` heading, captured from the running app during the `review-polish` browser pass. Enough shots to convey what changed, not a catalogue of every state. Save the files outside the repo and never commit them, and upload them with `gh pr create --attach` (or `gh pr edit --attach` on an existing PR) rather than handing the paths to the user.
|
||||
**Diagram architectural changes in the PR.** When a change moves a boundary (a new service, task, or provider in a path, a model relationship, a different call path across layers, an auth or socket flow), the PR description carries a `mermaid` block showing it. GitHub renders them. Draw what changed, not the whole system. See `review-polish`.
|
||||
|
||||
---
|
||||
|
||||
## Never run the full backend test suite locally
|
||||
|
||||
@@ -23,3 +72,19 @@ callers of anything whose signature or behavior you changed (`grep` for the symb
|
||||
|
||||
CI runs the whole suite on the PR (`pytest.yml`, MariaDB + PostgreSQL). That is the
|
||||
place for full-suite coverage; local runs stay scoped.
|
||||
|
||||
---
|
||||
|
||||
## Environment setup
|
||||
|
||||
Each tool version is pinned in one file: Python in `.python-version`, uv as `ARG UV_VERSION` in `docker/Dockerfile`, and Node in `frontend/.nvmrc`. Don't install these by hand. Run `scripts/dev-setup.sh`, which installs the pinned toolchain, the backend and frontend dependencies, and the Trunk launcher, and skips anything already in place. Add `--db` to also install and start MariaDB with the pytest database (Debian/Ubuntu only). Rerun it whenever a version is wrong or a dependency is missing.
|
||||
|
||||
In Claude Code cloud sessions, `.claude/hooks/cloud-setup.sh` runs `scripts/dev-setup.sh --db` at session start. Setting the environment's setup script to `bash scripts/dev-setup.sh --db` gets the install cached in the environment snapshot, so the hook only has to restart MariaDB. Other agents should use the same command as their setup step.
|
||||
|
||||
---
|
||||
|
||||
## Quick command reference
|
||||
|
||||
**Setup:** see `DEVELOPER_SETUP.md`. Docker path is `cp env.template .env` → `docker compose build` → `docker compose up -d` (app at `http://localhost:3000`).
|
||||
|
||||
**Lint (both stacks):** `trunk fmt && trunk check`.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# RomM - Repository Guide for Contributors & Agents
|
||||
|
||||
RomM is a self-hosted ROM manager and player: scan a game library off disk, enrich it with metadata from 10+ providers, browse it in a web UI, and play in the browser.
|
||||
|
||||
---
|
||||
|
||||
The frontend talks to the backend over `/api/*` (REST) and `/ws` (Socket.IO). TypeScript types are **generated** from the backend's OpenAPI schema into `frontend/src/__generated__/` - the backend is the single source of truth for API shapes.
|
||||
|
||||
## Deep-dive references
|
||||
|
||||
- **`docs/BACKEND_ARCHITECTURE.md`** - directory map, ER diagram, every endpoint, auth/scopes, tasks.
|
||||
- **`docs/FRONTEND_ARCHITECTURE.md`** - routing, stores, services, theming, build tooling.
|
||||
- **`DEVELOPER_SETUP.md`** - Docker and manual local setup (mock library, `.env`, services).
|
||||
- **`CONTRIBUTING.md`** - contribution flow, **AI-assistance disclosure**, translations.
|
||||
|
||||
---
|
||||
|
||||
## Stack-specific guidance
|
||||
|
||||
`frontend/CLAUDE.md` (v1 vs v2, frontend commands) and `backend/CLAUDE.md` (backend commands) load when you work in those directories. New frontend work goes in v2 (`frontend/src/v2/`); v1 is frozen.
|
||||
|
||||
---
|
||||
|
||||
## Skills - load the focused guide for your task
|
||||
|
||||
These live in `.claude/skills/` and carry the detailed rules. Invoke the one that matches what you're doing. `pr-ready`, `address-bot-reviews`, and `draft-announcement` are run by the user, not auto-invoked.
|
||||
|
||||
---
|
||||
|
||||
## Repo-wide rules
|
||||
|
||||
**Disclose AI assistance in the PR.** RomM requires it (see `CONTRIBUTING.md`): state that AI was used and to what extent. This is mandatory and non-negotiable for agent-written contributions.
|
||||
**Branch off `master`; open PRs against `master`.** Fork → feature branch → PR. Don't push to `master`.
|
||||
**Linting is via [Trunk](https://trunk.io)** (`trunk fmt && trunk check`), which wraps ruff, black, isort, ESLint, Prettier, and more, and runs in CI on every PR. **Never commit with `--no-verify`.**
|
||||
**The backend owns the API contract.** Changed a response schema or route? Regenerate frontend types (`npm run generate`) and re-typecheck.
|
||||
**Tests travel with code.** New logic gets a test; new endpoints get endpoint tests; new v2 primitives get a Storybook story (+ `play()` if interactive).
|
||||
**Verify before handoff.** Don't say "done" on UI work without testing it in the browser in both themes and all input modalities. See `review-polish`.
|
||||
**English first.** Outside of language files, all code, comments, identifiers, `.md` files, and commit/PR messages are in English.
|
||||
**No em-dashes.** Never use em-dashes (U+2014) when writing comments or text. Use commas, parentheses, or separate sentences instead. An em-dash string literal is code (an empty-value glyph) and stays allowed.
|
||||
**Prefer a check to a model review.** A mechanical rule (an import, a character, a CSS pattern, an SFC shape) belongs in ESLint, `tsconfig`, or Trunk. When a review finding repeats, propose a check as a named follow-up; see `review-polish`.
|
||||
**Keep comments short.** Comments should be concise, and focus on the "why" rather than the "what" (the code itself is the "what"). A comment is one or two lines; a docstring is one sentence plus `Args:`/`Returns:` when the signature needs it. Three or more lines of prose means you are explaining, not commenting. See `review-polish` for what to cut.
|
||||
**Don't restate the code.** The code is self-documenting, so skip comments that narrate what a reader can already see (which button sits where, that something is disabled when empty, obvious ordering). Comment only non-obvious "why" that the code can't convey on its own.
|
||||
**One home per value.** Before declaring a constant, type, limit, regex, or store getter, search for an existing one and import it. Validation limits live on the model and are imported by endpoints; a type lives in the component that owns it and is exported. Don't add a near-duplicate that differs only by sorting, and don't invent an identity scheme where a foreign key already exists.
|
||||
**Never commit secrets.** Never commit secrets (API keys, passwords, tokens, etc.) to the repo. Use environment variables or secret management tools instead.
|
||||
**Don't explain a change.** Avoid comments that explain why a change was made to the code, or that record what the code used to do. Focus instead on the current behaviour of the code and how it works. Reasons for a change go in the commit message and the PR body.
|
||||
**Python tools live in `backend/tools/`.** Standalone dev/test utilities and scripts (not part of the app runtime) go in `backend/tools/`, not scattered across `backend/`.
|
||||
**Link PRs to issues.** In the PR description, use `Fixes #XXXX` for issue/bug fixes and `Closes #XXXX` for feature implementations.
|
||||
**Use the PR template.** Base every PR description on `.github/PULL_REQUEST_TEMPLATE.md`.
|
||||
**Show UI changes in the PR.** A PR that touches anything visible ships with at least one screenshot under the template's `Screenshots` heading, captured from the running app during the `review-polish` browser pass. Enough shots to convey what changed, not a catalogue of every state. Save the files outside the repo and never commit them, and upload them with `gh pr create --attach` (or `gh pr edit --attach` on an existing PR) rather than handing the paths to the user.
|
||||
**Diagram architectural changes in the PR.** When a change moves a boundary (a new service, task, or provider in a path, a model relationship, a different call path across layers, an auth or socket flow), the PR description carries a `mermaid` block showing it. GitHub renders them. Draw what changed, not the whole system. See `review-polish`.
|
||||
|
||||
---
|
||||
|
||||
## Quick command reference
|
||||
|
||||
**Setup:** see `DEVELOPER_SETUP.md`. Docker path is `cp env.template .env` → `docker compose build` → `docker compose up -d` (app at `http://localhost:3000`).
|
||||
|
||||
**Lint (both stacks):** `trunk fmt && trunk check`.
|
||||
@@ -95,6 +95,8 @@ cp ./bin64/RAHasher /usr/bin/RAHasher
|
||||
|
||||
#### - Install python dependencies
|
||||
|
||||
`scripts/dev-setup.sh` installs the pinned uv, Python, and Node versions plus the backend and frontend dependencies in one step, and is safe to rerun. On systems without `apt-get`, install the MariaDB and PostgreSQL client headers yourself first. To do it by hand instead, follow the steps below.
|
||||
|
||||
You'll need uv installed
|
||||
|
||||
<https://docs.astral.sh/uv/getting-started/installation/>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// UserMenu — the avatar pill in the navbar that opens the v2 quick
|
||||
// UserMenu: the avatar pill in the navbar that opens the v2 quick
|
||||
// navigator. The dropdown mirrors the SettingsSidebar's information
|
||||
// architecture so the user has the same mental model in both places:
|
||||
//
|
||||
// • Account — Notifications, Profile, User interface
|
||||
// • Library — Library management, Scan settings, Metadata sources,
|
||||
// • Account: Notifications, Profile, User interface
|
||||
// • Library: Library management, Scan settings, Metadata sources,
|
||||
// Client API tokens
|
||||
// • System — Administration, Server stats
|
||||
// • Tools — Jukebox, Controller debug
|
||||
// • Actions — Scan, Upload (librarian actions, not settings)
|
||||
// • About / Changelog — kept as dialogs (no dedicated views)
|
||||
// • System: Administration, Server stats
|
||||
// • Tools: Jukebox, Controller debug
|
||||
// • Actions: Scan, Upload (librarian actions, not settings)
|
||||
// • About / Changelog: kept as dialogs (no dedicated views)
|
||||
// • Log out
|
||||
//
|
||||
// Items inherit the same scope/role gates as their target views so
|
||||
@@ -62,7 +62,7 @@ const avatarSrc = computed(() =>
|
||||
|
||||
const isAdmin = useCan("app.admin");
|
||||
// `library.scan` is an editor-up capability, so it stands in for
|
||||
// "editor or admin" without an inline role check (see CLAUDE.md §VI.G).
|
||||
// "editor or admin" without an inline role check (see the `frontend-v2-patterns` skill).
|
||||
const canSeeChangelog = useCan("library.scan");
|
||||
|
||||
const canSeeProfile = computed(
|
||||
@@ -155,7 +155,7 @@ async function onLogout() {
|
||||
</RBtn>
|
||||
</template>
|
||||
|
||||
<!-- User header — inlined; the layout is feature-specific so it
|
||||
<!-- User header, inlined. The layout is feature-specific so it
|
||||
lives at the call site, not in the lib. -->
|
||||
<div class="r-v2-user-menu__header">
|
||||
<RAvatar :image="avatarSrc" size="30" />
|
||||
@@ -323,7 +323,7 @@ async function onLogout() {
|
||||
|
||||
<!-- About is visible to everyone; Changelog is gated to editor+ (it
|
||||
surfaces librarian-oriented release notes). Both remain dialogs
|
||||
(no dedicated views) — see CLAUDE.md. -->
|
||||
(no dedicated views). -->
|
||||
<RMenuItem
|
||||
icon="mdi-help-circle-outline"
|
||||
:label="t('common.about')"
|
||||
@@ -383,7 +383,7 @@ html[data-bp~="xs"] .r-v2-user__chevron {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Group section inside the dropdown — small uppercase label above each
|
||||
/* Group section inside the dropdown: a small uppercase label above each
|
||||
cluster so the IA mirrors SettingsSidebar exactly. The label is omitted
|
||||
for the trailing Actions/About/Logout regions where dividers already
|
||||
communicate the boundary. */
|
||||
@@ -403,7 +403,7 @@ html[data-bp~="xs"] .r-v2-user__chevron {
|
||||
padding: 4px 12px 2px;
|
||||
}
|
||||
|
||||
/* Inlined header — was RMenuHeader before. Identity card at the top
|
||||
/* Inlined header. Identity card at the top
|
||||
of the dropdown: avatar + username + role pill. */
|
||||
.r-v2-user-menu__header {
|
||||
display: flex;
|
||||
|
||||
@@ -53,7 +53,7 @@ const coverFileInput = ref<HTMLInputElement | null>(null);
|
||||
// the user sees the action is running. v1 leaned on a global
|
||||
// `showLoadingDialog` event for the same feedback, but v2 has no
|
||||
// listener for it (intentionally, inline `:loading` on the control
|
||||
// itself is the v2 pattern, see CLAUDE.md §VI.B), so the emit was a
|
||||
// itself is the v2 pattern, see the `frontend-v2-patterns` skill), so the emit was a
|
||||
// no-op and the dialog appeared frozen during slow uploads / SGDB
|
||||
// fetches.
|
||||
const saving = ref(false);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// useGalleryVirtualItems — turns the gallery's structural state (mode,
|
||||
// useGalleryVirtualItems turns the gallery's structural state (mode,
|
||||
// total, charIndex, columns) into a flat list of body `GalleryItem`s
|
||||
// that an RVirtualScroller can render with one slot per kind.
|
||||
//
|
||||
// Header and toolbar are NOT in this list — they live in the
|
||||
// Header and toolbar are NOT in this list. They live in the
|
||||
// scroller's `#prepend` and `#sticky` slots so `position: sticky`
|
||||
// handles pinning natively (no JS scroll tracking, no jitter).
|
||||
//
|
||||
// Performance contract: this composable is STRUCTURAL — it only
|
||||
// Performance contract: this composable is STRUCTURAL; it only
|
||||
// depends on layout / groupBy / total / charIndex / columns /
|
||||
// loadingInitial. It does NOT read the loaded ROMs (`byPosition`).
|
||||
// Per-row slot data is resolved by the view at render time via
|
||||
@@ -18,11 +18,11 @@
|
||||
// Re-pack handling: a measured cover ratio change bumps `ratioVersion`, which
|
||||
// re-runs the build. Two things keep that cheap so a fast scroll through a
|
||||
// huge library doesn't thrash:
|
||||
// * Structural sharing — unchanged item objects are reused from the previous
|
||||
// * Structural sharing: unchanged item objects are reused from the previous
|
||||
// build (keyed by their stable `key`), so a bump only allocates the rows
|
||||
// that actually reflowed (paired with the scroller's `get-item-key`, Vue
|
||||
// then moves the rest instead of remounting their cards).
|
||||
// * Linear letter assignment — flat rows get their letters in one merge pass
|
||||
// * Linear letter assignment: flat rows get their letters in one merge pass
|
||||
// over the sorted ranges (`assignFlatRowLetters`), not a per-row scan.
|
||||
//
|
||||
// AlphaStrip integration is index-based: `letterToIndex` maps each
|
||||
@@ -53,12 +53,12 @@ const ROW_CHROME_PX = 41;
|
||||
// Reference card-art width the row height is derived from when the caller
|
||||
// doesn't pass one (md gallery card). Height = width / ratio + chrome.
|
||||
const REFERENCE_COVER_WIDTH_PX = 158;
|
||||
// Box-art default — matches v1's `getAspectRatio` for `cover_path`.
|
||||
// Box-art default, matching v1's `getAspectRatio` for `cover_path`.
|
||||
const DEFAULT_COVER_RATIO = 2 / 3;
|
||||
|
||||
// Fixed heights for the item kinds that DON'T depend on the boxart
|
||||
// aspect ratio. Row / skeleton-row are computed per-style — see
|
||||
// `getItemHeight`. Values match the rendered geometry in the gallery
|
||||
// aspect ratio. Row / skeleton-row are computed per-style (see
|
||||
// `getItemHeight`). Values match the rendered geometry in the gallery
|
||||
// views' CSS:
|
||||
// * letter-header: 20px top + 16px text + 12px bottom = 48 (→ 56 for air).
|
||||
// * load-more / empty: rendered button / centered text.
|
||||
@@ -120,7 +120,7 @@ interface Options {
|
||||
/** Total count of ROMs in the active gallery (server-provided). */
|
||||
total: Ref<number> | ComputedRef<number>;
|
||||
/** Letter → first-position map from the server. Backend ships letters
|
||||
* lowercase / digits — `availableLetters` and the row letter sets
|
||||
* lowercase / digits: `availableLetters` and the row letter sets
|
||||
* normalise to AlphaStrip's bucket shape. */
|
||||
charIndex: Ref<Record<string, number>> | ComputedRef<Record<string, number>>;
|
||||
/** Current column count for grid modes (responsive). */
|
||||
@@ -129,7 +129,7 @@ interface Options {
|
||||
loadingInitial: ComputedRef<boolean>;
|
||||
/** Empty-state message used when the page resolves with zero results. */
|
||||
emptyMessage: Ref<string> | ComputedRef<string>;
|
||||
/** "Not found" — overrides every body kind with a single empty row. */
|
||||
/** "Not found" overrides every body kind with a single empty row. */
|
||||
notFound?: Ref<boolean> | ComputedRef<boolean>;
|
||||
/** Override the not-found message (defaults to emptyMessage). */
|
||||
notFoundMessage?: Ref<string> | ComputedRef<string>;
|
||||
@@ -146,7 +146,7 @@ interface Options {
|
||||
* `cardHeight * ratioAt(p)`). Falls back to `fallbackRatio` until the
|
||||
* image is measured. */
|
||||
ratioAt?: (position: number) => number;
|
||||
/** Ratio to pack an unmeasured position at — must be the ratio its card
|
||||
/** Ratio to pack an unmeasured position at. Must be the ratio its card
|
||||
* actually paints, which is the active boxart style's box ratio: a card
|
||||
* with no artwork paints its placeholder at that ratio and never
|
||||
* measures, and one waiting on its image paints there until it loads.
|
||||
@@ -187,7 +187,7 @@ function buildLetterRanges(
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const [letter, start] = entries[i];
|
||||
const end = entries[i + 1]?.[1] ?? total;
|
||||
// Backend may collapse multiple raw letters into "#" (digits, etc.) —
|
||||
// Backend may collapse multiple raw letters into "#" (digits, etc.);
|
||||
// merge into the previous range rather than emit duplicates.
|
||||
const last = ranges[ranges.length - 1];
|
||||
if (last && last.letter === letter) {
|
||||
@@ -243,7 +243,7 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
return r > 0 ? r : DEFAULT_COVER_RATIO;
|
||||
};
|
||||
|
||||
/** Per-build ratio lookup — resolves the fallback once so the packer's
|
||||
/** Per-build ratio lookup. Resolves the fallback once so the packer's
|
||||
* hot path (one call per position) stays a single map read. */
|
||||
const makeRatioAt = () => {
|
||||
const fallback = fallbackRatio();
|
||||
@@ -268,8 +268,8 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
// Only the run of rows from the first re-measured cover onward actually
|
||||
// reflows; everything before stays identical. So we REUSE the previous
|
||||
// build's item objects whenever their content matches (looked up by stable
|
||||
// key). A reused object keeps its reference, so — paired with the scroller's
|
||||
// content-derived `get-item-key` — Vue moves the untouched rows instead of
|
||||
// key). A reused object keeps its reference, so (paired with the scroller's
|
||||
// content-derived `get-item-key`) Vue moves the untouched rows instead of
|
||||
// re-rendering them, and only the reflowed rows allocate. This turns a
|
||||
// per-bump O(total) re-allocation into O(reflowed rows). `prevByKey` holds
|
||||
// the last build and survives across evaluations as closure state; if it
|
||||
@@ -304,7 +304,7 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
};
|
||||
}
|
||||
function shareHeader(letter: string): GalleryItem {
|
||||
// A header's whole content is its letter, which the key encodes — same key
|
||||
// A header's whole content is its letter, which the key encodes. Same key
|
||||
// ⇒ identical, always reusable.
|
||||
const prev = prevByKey.get(`lh-${letter}`);
|
||||
if (prev?.kind === "letter-header") return prev;
|
||||
@@ -332,13 +332,13 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// List layout — one virtual item per ROM position. The view template
|
||||
// List layout: one virtual item per ROM position. The view template
|
||||
// resolves each row via `getRomAt(position)` and renders skeleton vs real
|
||||
// inside the same kind. Group-by-letter in list mode is deferred (see
|
||||
// CLAUDE.md §X — list-mode MVP first).
|
||||
// inside the same kind. Group-by-letter in list mode is deferred
|
||||
// (list-mode MVP first).
|
||||
if (opts.layout.value === "list") {
|
||||
if (opts.loadingInitial.value && opts.total.value === 0) {
|
||||
// Bootstrap phase — placeholder rows give the scroller a shape while
|
||||
// Bootstrap phase: placeholder rows give the scroller a shape while
|
||||
// metadata is in flight. Enough to fill a typical viewport.
|
||||
const skeletonListRows = Math.max(skeletonRows * 4, 12);
|
||||
for (let i = 0; i < skeletonListRows; i++) {
|
||||
@@ -375,7 +375,7 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Grid + first-window-loading — skeleton rows until the server returns
|
||||
// Grid + first-window-loading: skeleton rows until the server returns
|
||||
// `total` and `charIndex`.
|
||||
if (opts.loadingInitial.value && opts.total.value === 0) {
|
||||
for (let i = 0; i < skeletonRows; i++) {
|
||||
@@ -424,7 +424,7 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Flat — flow-pack the whole list, then tag every row with its
|
||||
// Flat: flow-pack the whole list, then tag every row with its
|
||||
// overlapping letters in one linear pass (not a per-row scan).
|
||||
const rows = packFlowRows(0, total, rowWidth, cardHeight, gap, ratioAt);
|
||||
const lettersPerRow = assignFlatRowLetters(rows, ranges);
|
||||
@@ -438,7 +438,7 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
|
||||
const virtualItems = computed<GalleryItem[]>(() => {
|
||||
const items = buildItems();
|
||||
// Refresh the reuse cache from this build for the next re-pack — cheap
|
||||
// Refresh the reuse cache from this build for the next re-pack. Cheap
|
||||
// O(items) bookkeeping; the win is the avoided re-allocation in buildItems.
|
||||
const nextByKey = new Map<string, GalleryItem>();
|
||||
for (const it of items) nextByKey.set(it.key, it);
|
||||
@@ -446,7 +446,7 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
return items;
|
||||
});
|
||||
|
||||
// AlphaStrip available letters — every letter in the server's charIndex,
|
||||
// AlphaStrip available letters: every letter in the server's charIndex,
|
||||
// independent of which windows are currently loaded.
|
||||
const availableLetters = computed<Set<string>>(() => {
|
||||
const set = new Set<string>();
|
||||
@@ -459,10 +459,10 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
const map = new Map<string, number>();
|
||||
const items = virtualItems.value;
|
||||
|
||||
// List layout — one virtual item per position. Walk once and pin
|
||||
// List layout: one virtual item per position. Walk once and pin
|
||||
// each letter to its first list-row index. Bootstrap phase paints
|
||||
// skeleton-list-rows that aren't tied to a letter, so the map is
|
||||
// empty until `total` resolves — same pattern as grid.
|
||||
// empty until `total` resolves, same pattern as grid.
|
||||
if (opts.layout.value === "list") {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i];
|
||||
@@ -473,7 +473,7 @@ export function useGalleryVirtualItems(opts: Options) {
|
||||
return map;
|
||||
}
|
||||
|
||||
// Pass 1 — grouped mode: letter-header anchors are exact.
|
||||
// Pass 1, grouped mode: letter-header anchors are exact.
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i];
|
||||
if (it.kind === "letter-header" && !map.has(it.letter)) {
|
||||
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs the pinned toolchain and project dependencies, skipping satisfied steps.
|
||||
# --db also installs and starts MariaDB with the pytest database (Debian/Ubuntu).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOCAL_BIN="${HOME}/.local/bin"
|
||||
export PATH="${LOCAL_BIN}:${PATH}:/usr/sbin:/sbin"
|
||||
|
||||
with_db=false
|
||||
for arg in "$@"; do
|
||||
case "${arg}" in
|
||||
--db) with_db=true ;;
|
||||
*)
|
||||
echo "usage: scripts/dev-setup.sh [--db]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log() { printf '==> %s\n' "$*"; }
|
||||
|
||||
as_root() {
|
||||
if [[ ${EUID} -eq 0 ]]; then "$@"; else sudo "$@"; fi
|
||||
}
|
||||
|
||||
apt_install() {
|
||||
local missing=() pkg
|
||||
for pkg in "$@"; do
|
||||
dpkg-query -W -f='${Status}' "${pkg}" 2>/dev/null | grep -q "ok installed" ||
|
||||
missing+=("${pkg}")
|
||||
done
|
||||
[[ ${#missing[@]} -eq 0 ]] && return 0
|
||||
if ! command -v apt-get >/dev/null; then
|
||||
log "Skipping ${missing[*]}: install them with your package manager"
|
||||
return 0
|
||||
fi
|
||||
log "Installing ${missing[*]}"
|
||||
as_root apt-get update -qq >/dev/null
|
||||
as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${missing[@]}" >/dev/null
|
||||
}
|
||||
|
||||
setup_uv() {
|
||||
local want have
|
||||
want="$(sed -n 's/^ARG UV_VERSION=//p' "${ROOT}/docker/Dockerfile")"
|
||||
have="$(uv --version 2>/dev/null || true)"
|
||||
have="${have#uv }"
|
||||
have="${have%% *}"
|
||||
if [[ ${have} == "${want}" ]]; then return 0; fi
|
||||
log "Installing uv ${want} (found ${have:-none})"
|
||||
curl -LsSf "https://astral.sh/uv/${want}/install.sh" |
|
||||
env UV_UNMANAGED_INSTALL="${LOCAL_BIN}" sh >/dev/null
|
||||
hash -r
|
||||
}
|
||||
|
||||
setup_backend() {
|
||||
# Build headers for the mariadb and psycopg[c] drivers.
|
||||
apt_install libmariadb-dev libpq-dev
|
||||
log "Syncing backend dependencies"
|
||||
(cd "${ROOT}" && uv python install --quiet && uv sync --frozen --all-extras --dev --quiet)
|
||||
}
|
||||
|
||||
setup_node() {
|
||||
local want have full os arch dest
|
||||
want="$(tr -d '[:space:]' <"${ROOT}/frontend/.nvmrc")"
|
||||
have="$(node --version 2>/dev/null || true)"
|
||||
if [[ ${have} == "v${want}."* ]]; then return 0; fi
|
||||
full="$(curl -fsSL https://nodejs.org/dist/index.json | grep -o "\"v${want}\.[0-9]*\"" | head -1 | tr -d '"')"
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
arch="$(uname -m)"
|
||||
case "${arch}" in
|
||||
x86_64) arch=x64 ;;
|
||||
aarch64) arch=arm64 ;;
|
||||
*) ;;
|
||||
esac
|
||||
dest="${HOME}/.local/share/node/${full}"
|
||||
log "Installing Node ${full} (found ${have:-none})"
|
||||
mkdir -p "${dest}" "${LOCAL_BIN}"
|
||||
curl -fsSL "https://nodejs.org/dist/${full}/node-${full}-${os}-${arch}.tar.xz" |
|
||||
tar -xJ -C "${dest}" --strip-components=1
|
||||
ln -sf "${dest}/bin/node" "${dest}/bin/npm" "${dest}/bin/npx" "${LOCAL_BIN}/"
|
||||
hash -r
|
||||
}
|
||||
|
||||
setup_frontend() {
|
||||
local stamp="${ROOT}/frontend/node_modules/.dev-setup-stamp" node_version lock_sum
|
||||
node_version="$(node --version)"
|
||||
lock_sum="$(cksum <"${ROOT}/frontend/package-lock.json")"
|
||||
if [[ -f ${stamp} && "$(<"${stamp}")" == "${node_version} ${lock_sum}" ]]; then return 0; fi
|
||||
log "Installing frontend dependencies"
|
||||
(cd "${ROOT}/frontend" && npm ci --no-audit --no-fund --no-update-notifier --loglevel=error)
|
||||
echo "${node_version} ${lock_sum}" >"${stamp}"
|
||||
}
|
||||
|
||||
setup_trunk() {
|
||||
command -v trunk >/dev/null && return 0
|
||||
log "Installing Trunk and its linters"
|
||||
mkdir -p "${LOCAL_BIN}"
|
||||
curl -fsSL https://trunk.io/releases/trunk -o "${LOCAL_BIN}/trunk"
|
||||
chmod +x "${LOCAL_BIN}/trunk"
|
||||
hash -r
|
||||
(cd "${ROOT}" && trunk install --ci >/dev/null 2>&1) ||
|
||||
log "Trunk could not download its linters (network policy?); trunk check will retry on first use"
|
||||
}
|
||||
|
||||
setup_db() {
|
||||
apt_install mariadb-server
|
||||
if ! mariadb-admin ping --silent >/dev/null 2>&1; then
|
||||
log "Starting MariaDB"
|
||||
as_root service mariadb start >/dev/null
|
||||
fi
|
||||
as_root mariadb <"${ROOT}/backend/romm_test/setup.sql"
|
||||
}
|
||||
|
||||
setup_uv
|
||||
setup_backend
|
||||
setup_node
|
||||
setup_frontend
|
||||
setup_trunk
|
||||
if [[ ${with_db} == true ]]; then setup_db; fi
|
||||
|
||||
uv_version="$(uv --version)"
|
||||
python_version="$(cd "${ROOT}" && uv run --quiet python --version)"
|
||||
node_version="$(node --version)"
|
||||
log "Toolchain ready: ${uv_version%% (*}, ${python_version}, Node ${node_version}"
|
||||
Reference in New Issue
Block a user