Imported from Team-Haruki/Haruki-Toolbox (
AGENTS.md). Install upstream withnpx skills add Team-Haruki/Haruki-Toolbox. Copyright stays with the author.
Haruki Toolbox Agent Guide
Project Summary
Haruki Toolbox is a frontend-only project built with:
- Vue 3
- TypeScript
- Vite
- Bun
- Pinia
- Vue Router
- Vue I18n
- Tailwind CSS 4
- vite-plugin-pwa (Service Worker: precache + runtime image caches)
- Playwright for E2E smoke tests
This repository is the web UI for the Haruki ecosystem. It integrates with Ory Kratos/Hydra/Oathkeeper, but it is not the backend repository.
Source Map
src/main.ts: app bootstrap, Pinia setup, auth/session bootstrap, i18n initializationsrc/App.vue: top-level app shell and user settings sync retry flowsrc/pwa.ts: Service Worker registration, update prompt, build-info polling, old-cache cleanupsrc/core/: app-wide router and HTTP infrastructuresrc/shared/: shared stores, i18n, shared components, and the Sekai game-data layer (src/shared/sekai/: master-data loading/caching via a web worker, catalog helpers, asset endpoint/URL resolution, Service-Worker image cache recovery)src/components/ui/: reusable UI primitivessrc/composables/,src/lib/,src/config/: cross-feature composables, pure helpers, and app configsrc/modules/<feature>/: feature-localapi,components,composables,lib,views, androutessrc/types/: shared API and domain typingsscripts/: repo tooling (check-imports.mjsimport guard,sync-i18n-zh-tw.mjszh-TW locale fill)tests/e2e/: Playwright browser tests
Architecture Conventions
- Keep feature code inside the owning module under
src/modules/<feature>/. - Keep
views/thin. Move async logic, API orchestration, and reusable state intocomposables/orlib/. - Put app-wide infrastructure in
src/core/only when it truly spans multiple features. - Put reusable cross-feature state in
src/shared/stores/. - Reuse
src/components/ui/*and shared components before introducing new UI primitives. - Use
@/imports instead of deep relative paths. - Match the formatting/style of the file you are editing instead of reformatting unrelated code.
Sekai Catalog Conventions
The catalog pages (/cards, /events, /gachas, /music list + detail) share one foundation; new catalog-like pages must build on it rather than re-implementing the pieces.
- Shells and sections:
src/shared/components/catalog/—CatalogPageShell(list column: title row, toolbar, filters, results, footer),CatalogDetailShell(back/breadcrumb row, title row, loading/error/not-found states, document-title override),CatalogDetailSection(titled Card;collapsiblesections emitopenonce — heavy/lazy data loads on that event, never on mount),CatalogFilterPanel(collapsed by default on phones, remembers its state perpageKey, shows removableactiveChipswhile collapsed),CatalogResultsBar,CatalogPagination,CatalogRegionSelect,CatalogCharacterPicker,CatalogStatusBadge,CatalogCountdown,CatalogEmptyState,CatalogErrorState,CatalogInfoList/CatalogInfoRow,CatalogEntityGrid. - Entity visuals:
src/shared/components/Sekai*.vue(SekaiAssetImagefor any ordered-candidate image,SekaiCardThumbnail,SekaiCharacterAvatar,SekaiUnitLogo,SekaiAttrIcon,SekaiRarityStars) andImageLightbox. They all route errors through@/shared/sekai/image-recovery;handleSekaiImageErrorreturnsfalseonce retries are exhausted — only then switch to a placeholder. - URL is the state: list filters/sort/page/size live in
route.querythroughuseRouteQueryState(codec, …)(src/composables/), with the page's codec inlib/<module>-query.ts(+ tests) built from@/lib/query-codecreaders/writers. Layout preferences (grid/list view, art mode, filter panel open) are per-device localStorage values viauseCatalogViewPreference, never query keys. - Scroll memory: list routes carry
meta.scrollMemory; the router then leaves scrolling to the page, which callsuseCatalogScrollMemory(ready)and restores its position once real results rendered. - Data:
useCatalogResource(region, key, files, build, options)overuseSekaiCatalogStorecaches one built value per region + master version and keeps a small LRU of raw master arrays. Canonical index resources are the only code allowed to read the big files:useCharactersIndex(@/shared/sekai/catalog-resources),useCardsIndex(@/modules/cards),useEventsIndex(@/modules/events),useGachasIndex(@/modules/gachas),useMusicsIndex(@/modules/music-library). Other resources declare their own"<module>/<name>"key exactly once with their file list and builder; builders return plain data (no reactivity, no asset URLs, not()); region-conditional files go inoptional. - Labels: enum values from master data go through
resolveSekai*Label({ t, te }, value)(@/shared/sekai/labels) so unknown members render as raw text instead of i18n keys. - Cross-module imports between catalog modules go through the public barrels (
@/modules/<feature>) orlib/files only, never another module'scomponents/orcomposables/.
Import Guard
scripts/check-imports.mjs runs before vue-tsc in bun run build and bun run typecheck; violations fail both. It bans legacy paths (@/components/pages/*, @/api/*, @/store, @/settingsStore, @/router, @/lib/ticket-display, old WebLayout.vue/Turnstile.vue locations) in favor of the module/shared/core layout, and enforces barrel rules:
- A module's internals must not import its own
index.tsbarrel (@/modules/<self>or relative equivalents) — import the concrete subpath (./api/user,./composables/list). - A module's non-
api/internals must not import their ownapibarrel (@/modules/<self>/api) — import concrete files like./api/user. - Consumers in other modules may import the public
@/modules/<feature>or@/modules/<feature>/apibarrels.
See CLAUDE.md for the full banned-token table with replacements.
HTTP, Auth, and Session Rules
- For business API requests, use
request()fromsrc/core/http/call-api.ts. - Do not create ad-hoc Axios instances or hardcode API base URLs in feature code.
- API base URL is resolved centrally from
useSettingsStore().currentEndpoint. - Kratos browser flows belong in
src/modules/auth/lib/kratos.tsand related auth composables. - Do not replace Kratos browser flow logic with generic
request()calls unless the existing auth layer is being intentionally redesigned. request()defaultsskipErrorToasttotrue; features should opt into local, user-meaningful toasts where appropriate.src/main.ts,src/App.vue, andsrc/shared/stores/user.tsare a sensitive cluster. Partial Kratos session data is not a full substitute for synced toolbox user data.- When hydrating from a fallback Kratos session, preserve cached user context unless the session is definitely gone. Otherwise post-login sync paths that depend on
userIdcan break.
State and Persistence
useUserStore()is the source of truth for current user/session state.settingsSyncStateis meaningful. If you change bootstrap or sync flows, verify how it affectsApp.vue.useSettingsStore()owns endpoint selection, theme, and locale.- Theme and locale are persisted; do not add duplicate persistence elsewhere without a strong reason.
- Avoid clearing the user store on recoverable bootstrap errors unless you are certain the session is invalid.
Routing Rules
- Route definitions live with the owning feature and are assembled through the web route tree.
- Use route
meta.titleKeyvalues for page titles when adding routes. - Respect
requiresAuth,requiresAdmin,requiresSuperAdmin, andguestOnlysemantics already handled bysrc/core/router/guards.ts. - If you change auth routing, review existing auth flow code in
src/modules/auth/carefully first.
UI and UX Expectations
- Prefer
<script setup lang="ts">for Vue components. - Use existing composables and UI patterns before creating new abstractions.
- Keep forms, dialogs, and toasts consistent with surrounding modules.
- Use
vue-sonnerfor toast feedback. - Reuse existing i18n keys/patterns where possible rather than embedding strings inline.
- Maintain responsive behavior for both desktop and mobile.
Internationalization
- Three locales:
zh-CN(default),zh-TW,en-US. Messages are split into lazy per-feature bundles (core,catalog,deck,rank,tools,user-settings,admin,tickets,public-pages); files live atsrc/shared/i18n/messages/<locale>/<locale>-<bundle>.ts. Thecatalogbundle (routes/cards,/events,/gachas,/music) holds the page-level namespacescardCatalog,eventCatalog,gachaCatalog,musicCatalog; the shared shell strings (catalog.*) and every game enum label (cards.unit/attr/rarity/supply,events.type,gachas.type,musicLibrary.difficulty, …) stay incorebecause other features render them. - All user-facing text must exist in all three locales, in the same bundle file. Write zh-CN and en-US by hand; fill zh-TW with
bun scripts/sync-i18n-zh-tw.mjs(OpenCC, only fills missing keys). coreloads at boot; other bundles load per route prefix viasrc/shared/i18n/bundles.ts. A new top-level route prefix needs a mapping there, or its non-core strings won't load.- Do not leave new UI strings hardcoded in components unless there is a very strong project-specific reason.
- Keep translation key structure aligned with the owning module.
Testing and Validation
Run the smallest relevant set, and prefer the full set for broad changes:
bun run lintbun run typecheck(includes the import guard)bun run testbun run e2ebun run quality— lint + typecheck + test in one command
Guidelines:
- Add or update
*.test.tsfiles when changing pure helpers, normalizers, or domain mapping logic. - Consider Playwright coverage when changing routing, auth redirects, bootstrap behavior, or page-level flows.
bun run buildis a good final confidence check for larger UI or type-heavy changes.- The Playwright webServer runs Vite under node on purpose (
bunx vite --mode e2e ..., no--bun): vite under the bun runtime hangs before listening on linux-x64 CI runners. Do not reintroduce--bunthere.
Common Pitfalls
- Do not hardcode auth or API origins when helpers/stores already resolve them.
- Do not clear cached user data in partial-session bootstrap paths unless logout is explicit or the session is confirmed invalid.
- Do not add new business logic directly to route views if it belongs in a composable or
lib/helper. - Do not update only some of the three locale bundles (zh-CN, zh-TW, en-US).
- Do not bypass
request()response handling unless you are working inside the auth/browser-flow integration layer. - Do not hand-roll
<img>error handling for Sekai/CDN images: the Service Worker caches them CacheFirst with opaque responses (errors included), so recovery must go through@/shared/sekai/image-recovery(purge + cache-busted retry). - Do not commit generated artifacts like
dist/or ephemeral Playwright output.
Recommended Change Checklist
Before finishing a non-trivial change, verify:
- The change lives in the correct module or shared layer.
- New strings exist in all three locales (zh-TW via the sync script is fine).
- API calls use the shared request/auth patterns.
- Session/bootstrap behavior still preserves valid user context.
- Relevant lint, typecheck, and test commands pass.
Commit Message Format
All commits must follow [Type] Short description:
- Allowed types:
[Feat](new feature),[Fix](bug fix),[Chore](maintenance, refactor, deps, build),[Docs](docs-only). - Description must start with a capital letter.
- Use imperative mood ("Add X", not "Added X").
- Do not end the subject line with a period.
- Keep it short.
- When an agent authors the commit, append a
Co-Authored-By:trailer identifying the agent (blank line between subject and trailer). - Version bumps (
versioninpackage.json,major.minor.patch): patch for[Fix], minor for[Feat], major for breaking changes.[Chore]/[Docs]commits normally do not bump. When several commits ship together, one bump for the batch is enough.
Examples (from this repo's history):
[Feat] Add hydra oauth2 flow[Fix] Sidebar option big gap issue in Safari[Chore] Add AGENTS.md and copilot-instructions.md[Docs] Update CLAUDE.md with commit format