# Pinia Store Guidance Read this before adding or changing frontend stores, their callers, or realtime integration. These conventions apply across board, shared and system stores. The board and card store refactors establish the pattern for future work; use the examples below to understand the responsibilities, rather than copying every detail of an existing store. General frontend rules remain in [Frontend.md](Frontend.md). Validation workflows remain in [Testing.md](Testing.md). ## Ownership and Boundaries Give each kind of shared data one authoritative store. Put the operations that maintain it in that store, and have callers use those operations. Current examples: | Owner | Responsibility | | --- | --- | | `authStore` | Session lifecycle, bootstrap, role checks and CSRF setup. | | `boardCatalogueStore` | Signed-in user’s board catalogue and its load/create/clone/import/save/delete requests. | | `boardStore` | Active board metadata and columns, board context initialization/cleanup, realtime connection and event routing. | | `cardStore` | Card entities, column membership, card ordering and card operations. | | `tagStore`, `cardTypeStore`, `slickStore` | Shared board catalogues and their operations. | | `attachmentStore` | Open attachment context, uploads/deletions and coordinating attachment changes with thumbnails. | | `cardAttachmentThumbnailStore` | Board thumbnail state and refreshes. | | `commentStore` | Card comment state and operations. | | `uiFeedbackStore` | Shared user-facing errors, warnings and toasts. | - Keep transient interaction state in the component or composable that owns the interaction. For example, `useBoardCardDragDrop` owns dragging state and passes an explicit card ID to `cardStore.moveCard`. - Derive values from their authoritative state where possible. `boardStore.currentBoardId` owns board selection: initialization sets it before loading, and disposal clears it immediately. `boardShell` is the loaded snapshot for that selection; responses must not select a board. - A child store may need its own active context identity to scope requests and reject stale responses. That has a separate lifecycle purpose; do not replace it with a parent-store dependency merely to eliminate an ID field. - Compose read models instead of maintaining duplicate mutable copies. `boardStore` combines its column metadata with cards from `cardStore` when exposing the board. - Keep dependencies directed: the context owner coordinates child stores; child stores own their data. Do not introduce circular dependencies to recover context or trigger cleanup. ## API Actions and State Updates The usual flow is: 1. A view or composable calls a store action with explicit inputs. 2. The action calls the typed API client. 3. After success, it applies the response through shared internal mutations, which own the relevant context checks. 4. Failures use the established feedback path. - Keep request actions, state-update actions and pure helpers recognisable as separate responsibilities. A server delete action and a local removal action do different work. - Local API responses and realtime events should use the same upsert/removal implementation. Avoid parallel `...FromRealtime` implementations of the same state changes. - Use upsert for insertion and replacement when their state work is the same. A distinct creation action is justified when it owns additional work, such as loading attachment thumbnails. - Keep actions explicit and predictable: load, create, update, delete, move, upsert and remove. Prefer names that describe the actual scope; `unlinkCardsFromColumns` returns a column membership map excluding the specified cards. It leaves card entities and attachments untouched. - Pass primitive IDs when identity is all an operation needs, or an ID collection for a collection operation. Do not introduce single-field removal models. - Preserve the return contract callers use to decide whether to close an editor, display validation or continue another operation. - When the API returns the authoritative entity, apply that response locally. Do not reload the collection just to reflect the write. Completing a write must not select or restore the board the request originally targeted. - Use small helpers for genuine shared work. First check whether callers can use an existing action directly before adding a generic request/orchestration wrapper. ## Internal Mutation Convention - Name internal state-mutation helpers in uppercase `VERB_NOUN` form, for example `UPSERT_MEMBER` and `REMOVE_MEMBER`. - Keep them strictly internal to the store. In setup stores, omit them from the returned object; callers must not invoke uppercase helpers through the store API. - Group them together at the top of the store body, immediately after state and dependency declarations, before public actions and other helpers. - Keep public actions and other helpers in camelCase, including request actions such as `addMember`, `updateMemberRole` and `deleteMember`, and lifecycle actions such as `loadMembers` and `dispose`. - Internal mutations synchronously apply local state changes. Keep API requests and asynchronous orchestration in actions. This is a naming and organisation convention for ordinary functions; use direct calls without a Vuex-style `commit` or mutation registry. - Pass the operation's board/card identity to a scoped mutation. The mutation owns the check against the store's current context and ignores data for a different context. Callers share that check by delegating to the mutation. - Card state-update actions take an explicit board ID and delegate to guarded internal mutations. Those mutations report whether the update was accepted so actions can preserve their return contracts and skip related slick/attachment/thumbnail work for a different board. Snapshot replacement establishes the selected board; it is a lifecycle operation. - Reuse the same internal mutation wherever the local state change is the same. For example, successful addition and role update both call `UPSERT_MEMBER(boardId, member)`; successful deletion calls `REMOVE_MEMBER(boardId, userId)`. - When realtime routing or another store needs to apply a change, expose a camelCase action that delegates to the internal mutation and owns any related orchestration. Keep the uppercase helper private. Apply this convention as stores are changed or reviewed. Existing camelCase public state-update actions remain public contracts; do not rename them to uppercase or migrate unrelated stores as part of a scoped change. ## Collection Operations - Implement collection upserts/removals once. Single-entity state-update wrappers, where useful, pass `[entity]` or `[id]` to the collection implementation. - Bulk operations call collection state updates directly. Do not loop over single-entity actions that clone and publish the full maps for every entity. - Build the next maps once, apply the collection, and replace each affected state map once. Preserve prior state and input arrays when constructing replacements. - Update membership for the whole collection and sort each destination collection once. In `cardStore`, unlink incoming card IDs from their old columns before adding them to their destination columns. - Give ordering one owner. `boardStore` sorts columns; `cardStore` sorts cards during snapshot replacement and incremental updates. Do not sort cards again in a board mapper before passing them to the card store. - Deduplicate IDs and handle empty collections at the action that performs the work. Thin delegates such as `bulkMoveCards` leave that preparation to `bulkEditCards`. - Where the collection API supports the single-item operation, route single-item callers through it with a collection of one. The card editor uses `deleteCards([cardId])` and `archiveCards([cardId])`; redundant single-card store and frontend API methods were removed, including their demo equivalents. - Check endpoint semantics when consolidating callers, including authorization, missing entities and return values. Bulk-delete idempotency is a known deferred bug (#891); routing card deletion through the bulk action was explicitly approved before that fix. Do not generalise that bug into the desired deletion contract. ## Related State and Side Effects The action that applies a domain change owns its related state updates. Views and realtime registrations should not repeat a sequence of calls across stores to complete that change. - Card removal owns card-cache removal, column membership changes, thumbnail cleanup and notification to the attachment store. Delete, archive, transfer and realtime deletion share this path. - Attachment additions/deletions go through `attachmentStore.added`/`removed`. These update the matching open context and coordinate board thumbnail updates. Thumbnail work must still happen when no card is open or a different card is open. - `cardStore.applyCreatedCard` combines upsert with thumbnail refresh. Realtime creation, duplication and archive restoration use it. Ordinary local creation uses upsert because it has no copied attachments to fetch; do not introduce extra requests solely to make callers look uniform. - Keep cleanup context-aware. Removing a live card must not clear attachment state for another board/card or an archived snapshot. - Board settings views call `boardStore.saveBoard`/`deleteBoard`. These delegate catalogue requests, then apply the saved summary through the guarded board mutation or dispose the workspace only if the deleted board is still selected. The catalogue remains valid across board switches; do not scope its writes to the currently selected board. - System info message saves go through `systemInfoMessageStore.save`, which applies the API response to the header's shared message through the same mutation as loads and realtime. The editor owns its draft, preview and save feedback; saving must update the header without a board realtime connection. - `userProfileStore.saveOwnProfile` applies the saved profile and updates `authStore` through `setOwnProfile`. The profile view owns drafts and feedback and watches account ID for loading, so applying a saved display name does not trigger another profile request. - Tag deletion is coordinated by `boardStore.deleteTag`: capture the tag before deletion, call `tagStore.deleteTag`, then clear matching card tags through the board-guarded card action. The editor calls this one action. - Slick deletion is coordinated by `boardStore.deleteSlick`: it calls `slickStore.deleteSlick`, then clears matching card membership only while that board is current. Editors call this one action. Keep the existing `cardStore` → `slickStore` dependency directed; do not add a reverse dependency for deletion cleanup. - Await related asynchronous work when the caller depends on its completion. A wrapper must return the delegated promise so its caller can await it. ## Catalogue Authority and API Contracts - Distinguish authoritative catalogue data from convenient embedded read data. A rich entity response does not become a second authority for shared labels, metadata or styling. - Document which store drives live rendering when a contract exposes both embedded metadata and a shared catalogue. Use catalogue updates to avoid rewriting every entity that embeds the metadata. - `slickStore` owns the slick catalogue. Card read payloads embed a full `slick`; card responses and slick create/update responses use `upsertSlick` to insert or replace catalogue entries by ID. Card writes continue to use `slickName`. - Keep full-replacement edits easy to round-trip. Preserve unchanged fields without projection-heavy conversion simply because the read model is richer. - Follow the API trust rules in [Frontend.md](Frontend.md): rely on backend contracts and avoid speculative fallbacks or re-deriving guaranteed fields. Keep necessary context/stale-response guards narrowly tied to the lifecycle they protect. ## Context Lifecycle and Loading - The shared board layout owns board workspace initialization. Reuse the initialized context when moving between board and archive views for the same board. - A child view should not reload a shared snapshot or catalogue just because it remounts. Realtime remains connected while visiting the archive, so shared metadata continues to receive resync updates. - Run independent loads together. The tag, card-type and slick catalogue loads use `Promise.all`; keep genuinely dependent stages ordered. - Capture the operation's board ID before awaiting a request and guard its results against the selected board. Board initialization, snapshot loading and layout watchers use board IDs without version counters. Same-board out-of-order responses are accepted; do not add request generations to distinguish them. Assess other lifecycles against their actual requirements. - The context owner owns teardown. Board selection changes clear the previous shared context before loading. `boardStore.dispose` clears selection and child state before awaiting realtime disconnect, so pending work cannot repopulate disposed state. Cleanup covers cards, comments, members, attachment state, thumbnails and board catalogues. App-level cleanup calls that owner instead of separately disposing the same child stores. - Member lookups are shared between the Members view and card editor. `boardStore` clears them when changing boards or clearing board context; leaving the Members view does not dispose them. Failed member loads clear their context so the next editor lookup retries. - System-admin membership uses its own `systemBoardMembersStore` context, owned by `SystemBoardMembersView`. Its actions take explicit board IDs, apply write responses through guarded mutations without reloading, and scope busy/feedback by board ID. The view disposes this context on exit and captures it before removal confirmation; confirmation cannot delete from a different or disposed context. - `boardStore` establishes the comment store's board context when applying a board snapshot. Comment loads and posts do not select a board; API responses and realtime additions pass their board ID to the shared guarded mutations. - The card editor loads comments when opening a card or when a board snapshot resets their cached state. Ordinary card updates retain loaded comments and use incremental comment events. Post completion clears the submitted draft only while the originating editor route and draft text are unchanged. - Child-store disposal clears owned data and invalidates pending loads so late responses cannot repopulate a disposed context. Initial board snapshot failures and loss of access clear the owned context through the shared cleanup path. Temporary background refresh failures retain the loaded board and active drafts while retrying; board changes and disposal cancel pending retries. - `scheduledJobsStore` owns a page-entry snapshot and refreshes after successful Run now requests. Its responses apply in arrival order; disposal clears state but late responses/feedback are accepted. Keep per-schedule submission protection without request versions or replacement lifecycle tracking. See [Jobs.md](Jobs.md). - `systemJobsStore` shares history/details by job ID. Components use actions to set visibility and the open detail; realtime refreshes use the requested pagination. Responses apply in arrival order, and late responses after disposal are accepted. Timestamp equality only determines whether cached logs still match a summary; do not add response revisions or stale-detail retries. See [Jobs.md](Jobs.md). - Keep board-scoped and application-scoped lifecycles distinct. Leaving a board workspace does not imply clearing every store in the application. - Do not add extra snapshot reloads or event buffering to cover hypothetical startup races. Introduce recovery work for a concrete requirement or observed failure; the rare initial-load/realtime race was explicitly left outside these refactors. - The card-type editor uses the catalogue initialized by the board layout. A missing type redirects to the list without reloading the catalogue or tracking watcher cancellation. - Keep board-ID guards for shared state. Do not add route snapshots, unmount flags or watcher cancellation solely to prevent late editor navigation after a pending save/delete; that navigation edge case is explicitly accepted. Assess each additional guard against the behaviour it protects rather than replacing one tracking mechanism with another. ## Realtime Routing - `boardStore` owns realtime connect/disconnect for the board workspace. - Guard board-scoped callbacks at registration with the shared `forCurrentBoard` helper, then delegate to the owning action. Reuse the same handler when events have the same effect, such as card updated and moved events. - Keep registrations small. A callback should not become a second home for card, attachment or thumbnail orchestration. - Keep genuine multi-step recovery explicit. Resync reloads the board snapshot and board catalogues, checks context, and reloads related open state as needed; reconnect uses the resync path. - Apply ordinary entity events incrementally. Reserve snapshot replacement for initialization and recovery rather than every mutation or navigation. - `TagsChanged` reloads only the authoritative tag catalogue through the guarded `tagStore.loadTags` action. Implicit tag creation and explicit creation/style/emoji edits use it. Ordering renormalisation, tag renames and tag deletion still request a full resync: ordering affects other cards, and card read models carry tag names for filters and editor saves. A full resync covers a simultaneous catalogue change without a second notification. ## Busy State and Feedback - Expose operation progress through store busy/loading state and restore it in `finally`. Keep initial context loading distinct where it controls the workspace loading UI. - Reuse the store's established request/feedback helper. Shared failures normally go through `uiFeedbackStore`; preserve deliberate inline-validation or operation-specific feedback paths. - Shared errors carry a source: use `setError(message, source)` and `clearError(source)` with the same stable store or operation name. A successful request may clear only its source's error. When composing loaders, let the loaders report their own failures rather than adding duplicate error toasts in the orchestration layer. - Scope request feedback and busy-state completion to the captured board ID. Switching boards or clearing context resets busy state; an old-board response must not clear a current-board request’s busy state or change its feedback. - The shared error banner displays the latest reported error; it is not an error queue. Different sources cannot clear one another's errors, even when their message text matches. Requests within one source retain arrival-order semantics. `resetError()` is reserved for context teardown, such as clearing the board workspace, rather than request success. - Keep session and permission checks centralised through auth/store/router integration. ## Validation and Reference Implementations - Use [Testing.md](Testing.md) for commands and test scope. Store behaviour belongs in Vitest; browser tests cover relevant user journeys and browser integration. - When changing batch updates, cover ordering, column membership, retained unrelated entities, input/prior-state preservation and related cleanup. Verify one publication per affected map when batching is the behaviour being changed. - When changing context lifecycle, cover disposal and late responses as well as normal loads. When moving interaction ownership, cover cancellation and the next interaction. - Reuse existing coverage where it already exercises the behaviour. Avoid tests that merely mirror a helper's implementation. - Keep HTTP and demo implementations aligned with the shared `BoardApi` contract. Remove unused methods and update affected tests when consolidating API callers. Reference files: - [boardMembersStore.ts](../BoardOil.Web/src/board/stores/boardMembersStore.ts): internal uppercase mutations grouped at the top, shared by public request actions, with board checks inside the mutations. - [boardStore.ts](../BoardOil.Web/src/board/stores/boardStore.ts): context ownership, explicit board selection, guarded realtime routing and catalogue loading. - [cardStore.ts](../BoardOil.Web/src/board/stores/cardStore.ts): collection updates, ordering and shared card side effects. - [attachmentStore.ts](../BoardOil.Web/src/board/stores/attachmentStore.ts): open-context handling and thumbnail coordination. - [useBoardCardDragDrop.ts](../BoardOil.Web/src/board/composables/useBoardCardDragDrop.ts): interaction ownership with explicit store-action inputs.