@live-change/frontend-template 0.9.201 → 0.9.204

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +49 -1
  2. package/.claude/rules/live-change-backend-architecture.md +5 -5
  3. package/.claude/rules/live-change-backend-event-sourcing.md +2 -2
  4. package/.claude/rules/live-change-backend-models-and-relations.md +65 -8
  5. package/.claude/rules/live-change-frontend-e2e-lifecycle.md +42 -0
  6. package/.claude/rules/live-change-frontend-i18n-locales.md +25 -0
  7. package/.claude/rules/live-change-frontend-vue-primevue.md +100 -4
  8. package/.claude/rules/live-change-node-toolchain-fnm.md +39 -0
  9. package/.claude/rules/live-change-service-structure.md +2 -2
  10. package/.claude/settings.json +3 -1
  11. package/.claude/skills/create-skills-and-rules/SKILL.md +23 -0
  12. package/.claude/skills/live-change-backend-change-triggers/SKILL.md +15 -0
  13. package/.claude/skills/live-change-dao-protocol/SKILL.md +46 -0
  14. package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +116 -0
  15. package/.claude/skills/live-change-design-models-relations/SKILL.md +63 -5
  16. package/.claude/skills/live-change-frontend-command-forms/SKILL.md +6 -5
  17. package/.claude/skills/live-change-frontend-data-views/SKILL.md +75 -4
  18. package/.claude/skills/live-change-frontend-e2e-lifecycle/SKILL.md +82 -0
  19. package/.claude/skills/live-change-frontend-range-list/SKILL.md +90 -0
  20. package/.claude/skills/live-change-frontend-synchronized/SKILL.md +101 -0
  21. package/.claude/skills/live-change-node-toolchain-fnm/SKILL.md +44 -0
  22. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +58 -4
  23. package/.cursor/rules/live-change-backend-architecture.mdc +1 -1
  24. package/.cursor/rules/live-change-backend-event-sourcing.mdc +1 -1
  25. package/.cursor/rules/live-change-backend-models-and-relations.mdc +69 -7
  26. package/.cursor/rules/live-change-backend-views-vs-triggers-for-reads-writes.mdc +28 -0
  27. package/.cursor/rules/live-change-dao-protocol.mdc +47 -0
  28. package/.cursor/rules/live-change-frontend-e2e-lifecycle.mdc +15 -0
  29. package/.cursor/rules/live-change-frontend-i18n-locales.mdc +26 -0
  30. package/.cursor/rules/live-change-frontend-views-not-commands-for-reads.mdc +30 -0
  31. package/.cursor/rules/live-change-frontend-vue-primevue.mdc +91 -0
  32. package/.cursor/rules/live-change-node-toolchain-fnm.mdc +40 -0
  33. package/.cursor/rules/live-change-service-structure.mdc +1 -1
  34. package/.cursor/skills/create-skills-and-rules.md +23 -0
  35. package/.cursor/skills/live-change-backend-change-triggers.md +15 -0
  36. package/.cursor/skills/live-change-design-actions-views-triggers.md +57 -0
  37. package/.cursor/skills/live-change-design-models-relations.md +23 -5
  38. package/.cursor/skills/live-change-frontend-command-forms.md +6 -5
  39. package/.cursor/skills/live-change-frontend-data-views.md +17 -0
  40. package/.cursor/skills/live-change-frontend-e2e-lifecycle.md +82 -0
  41. package/.cursor/skills/live-change-frontend-range-list.md +45 -0
  42. package/.cursor/skills/live-change-frontend-synchronized.md +101 -0
  43. package/.cursor/skills/live-change-node-toolchain-fnm.md +44 -0
  44. package/.node-version +1 -0
  45. package/.nvmrc +1 -0
  46. package/README.md +18 -0
  47. package/e2e/client-session.test.ts +17 -0
  48. package/e2e/e2eSuite.ts +12 -0
  49. package/e2e/env.ts +130 -0
  50. package/e2e/homepage.test.ts +17 -0
  51. package/e2e/steps.ts +3 -0
  52. package/e2e/withBrowser.ts +18 -0
  53. package/opencode.json +4 -1
  54. package/package.json +55 -53
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: live-change-node-toolchain-fnm
3
+ description: Run node, npm, npx, tsx and framework CLI with fnm exec so .node-version and .nvmrc are respected
4
+ ---
5
+
6
+ # Skill: Node toolchain with fnm
7
+
8
+ Use this skill whenever you run **Node**, **npm**, **npx**, **tsx**, or **corepack** in this repo (tests, `describe`, dev servers, scripts). Agents must not use the default sandbox Node; use **fnm exec** so the version from dotfiles applies.
9
+
10
+ ## When to use
11
+
12
+ - Running `npm test`, `npm run …`, linters, builds
13
+ - `node server/start.js describe` or any framework entry
14
+ - `tsx` for TypeScript scripts
15
+ - Any subprocess that would invoke `node` or `npm` for a project with `.node-version` / `.nvmrc`
16
+
17
+ ## Step 1 – Find the right directory
18
+
19
+ Locate the nearest project root that has `.node-version` or `.nvmrc` for the task (often the app folder, e.g. `auto-firma/app/`).
20
+
21
+ `cd` to that directory before running commands so fnm reads the correct file.
22
+
23
+ ## Step 2 – Prefix with fnm exec
24
+
25
+ ```bash
26
+ fnm exec -- node server/start.js describe --service myService --output yaml
27
+ fnm exec -- npm test
28
+ fnm exec -- npx vitest
29
+ fnm exec -- tsx ./tools/something.ts
30
+ ```
31
+
32
+ The part after `--` is the same command you would run manually with the correct Node active.
33
+
34
+ ## Step 3 – Nested monorepo paths
35
+
36
+ If you are in the repo root but the dotfile is only under `some-app/`:
37
+
38
+ ```bash
39
+ cd some-app && fnm exec -- npm test
40
+ ```
41
+
42
+ ## If fnm is missing
43
+
44
+ Do not fall back to bare `node` / `npm` for framework work. Report that fnm is required (or document a one-off alternative the user approved).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Rules for implementing actions, views, and triggers in LiveChange services
3
- globs: **/services/**/*.js
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
4
  alwaysApply: false
5
5
  ---
6
6
 
@@ -54,9 +54,9 @@ definition.action({
54
54
 
55
55
  ## Widoki – ogólne zasady
56
56
 
57
- - Widok ma być prostym, czytelnym wejściem do danych:
58
- - korzystaj z indeksów (`indexObjectGet`, `indexRangeGet`),
59
- - nie implementuj skomplikowanej logiki w widokach, jeśli można ją przenieść do akcji.
57
+ - Widok ma być **kanałem odczytu** (DAO, indeksy, agregacje, wyliczenia). Logika odczytu należy do widoku (ew. funkcji pomocniczej wywołanej z `daoPath` / `get`), a nie do akcji „tylko po to, żeby zwrócić dane bez zmiany stanu”.
58
+ - W implementacji ścieżek korzystaj z indeksów (`indexObjectGet`, `indexRangeGet`) zamiast pełnych skanów.
59
+ - Odczyt vs zapis (CQRS-like): [live-change-backend-views-vs-triggers-for-reads-writes.mdc](./live-change-backend-views-vs-triggers-for-reads-writes.mdc).
60
60
 
61
61
  ## Widoki – reguła: `get` + `observable` zawsze razem
62
62
 
@@ -138,6 +138,54 @@ definition.view({
138
138
  })
139
139
  ```
140
140
 
141
+ ### Widoki zakresowe: prefiksy indeksów i opcjonalne filtry
142
+
143
+ W `sortedIndexRangePath(indexName, keyPrefix, range)` najpierw działa `keyPrefix`, a dopiero potem `range` (`gt/gte/lt/lte`) na pełnym kluczu indeksu.
144
+
145
+ To oznacza:
146
+
147
+ - nie przekazuj surowych wartości pola (np. samej daty miesiąca) do `gt/lt`, jeśli klucz zaczyna się od innych części
148
+ - filtr domenowy (`month`, `state`, `company`) przekazuj jako osobny parametr widoku
149
+ - `range` zostaw do paginacji/cursora (RangeViewer i podobne mechanizmy)
150
+
151
+ ```js
152
+ definition.view({
153
+ name: 'bankTransactionsByBankAccountAndDate',
154
+ properties: {
155
+ bankAccount: { type: String },
156
+ month: { type: String },
157
+ ...App.rangeProperties
158
+ },
159
+ async daoPath({ bankAccount, month, ...props }) {
160
+ const range = App.extractRange(props)
161
+ if(month) {
162
+ const prefix = [bankAccount, month].map(v => JSON.stringify(v)).join(':')
163
+ return BankTransaction.rangePath(App.utils.prefixRange(range, prefix, prefix + ':'))
164
+ }
165
+ return BankTransaction.sortedIndexRangePath('byBankAccountAndDate', [bankAccount], range)
166
+ }
167
+ })
168
+ ```
169
+
170
+ Jeśli filtr po miesiącu jest częsty, lepiej dodać indeks z bucketem miesiąca (`byBankAccountAndMonthAndDate`) i użyć:
171
+
172
+ ```js
173
+ BankTransaction.sortedIndexRangePath('byBankAccountAndMonthAndDate', [bankAccount, month], range)
174
+ ```
175
+
176
+ ### Guardrails dla widoków pod RangeViewer/rangeBuckets
177
+
178
+ - Dla list paginowanych po indeksie preferuj `Model.sortedIndexRangePath(indexName, keyPrefix, App.extractRange(props))`.
179
+ - Nie stosuj semantyki `indexRangePath` dla widoków konsumowanych przez bucketowe UI zakresowe.
180
+ - `gt/gte/lt/lte` zostaw wyłącznie jako kursor paginacji, nie jako filtry domenowe.
181
+ - Dla filtrów typu miesiąc/rok/status najpierw projektuj indeks z odpowiednim prefiksem.
182
+ - `App.utils.prefixRange` traktuj jako fallback backendowy, gdy zmiana indeksu nie jest możliwa.
183
+
184
+ ### Guardrail dla indeksów standalone
185
+
186
+ - Gdy indeks łączy równorzędne strumienie danych (union wielu tabel), definiuj go jako serwisowy `definition.index(...)` (najlepiej w osobnym `indexes.js`), a nie jako `model.indexes` przypisany do przypadkowego modelu.
187
+ - `model.indexes` stosuj tylko wtedy, gdy semantycznym właścicielem indeksu jest jeden model.
188
+
141
189
  ## Triggery – online/offline i batchowanie
142
190
 
143
191
  - Triggery są do reakcji na zdarzenia (np. zmiana stanu sesji, start serwera).
@@ -236,6 +284,12 @@ definition.trigger({
236
284
 
237
285
  Sprawdzaj `data`/`oldData`: oba obecne = update, tylko `data` = create, tylko `oldData` = delete.
238
286
 
287
+ ## Cron-service — harmonogramy, interwały i UI admina
288
+
289
+ - Dla wykonywania **triggerów** wg **czasu ściennego** lub **stałego interwału** używaj **`@live-change/cron-service`** (**Schedule** / **Interval**) wraz z **task-service** — nie planuj „tylko timera” bez modeli cron i cyklu życia **`changeCron_Schedule`** / **`changeCron_Interval`**.
290
+ - Wzorzec admina (jak **task-frontend**): **`setSchedule`** / **`setInterval`** przez **`ActionForm`**, listy przez **`path.cron.schedules`** / **`path.cron.intervals`**, wzbogacanie wierszy **`.with()`** o **`scheduleInfo`** / **`intervalInfo`**, **`runState`** (`jobType` **`cron_Schedule`** lub **`cron_Interval`**), **`task.tasksByCauseAndCreatedAt`**; usuwanie przez **`deleteSchedule`** / **`deleteInterval`**.
291
+ - Pola czasu **Schedule** (**minute**, **hour**, **day**, **dayOfWeek**, **month**): **`NaN`** = „każdy” na danym poziomie — szczegóły w **`15-cron-and-intervals.md`** (sekcja **API used by task-frontend**).
292
+
239
293
  ## Wzorzec „pending + resolve” (asynchroniczny wynik)
240
294
 
241
295
  - Używaj, gdy akcja w serwisie musi poczekać na wynik z zewnętrznego procesu (np. urządzenie, worker).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Rules for LiveChange backend service architecture and directory structure
3
- globs: **/services/**/*.js
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
4
  alwaysApply: false
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Event-sourcing data flow rules — emit events for DB writes, use triggerService for cross-service writes
3
- globs: **/services/**/*.js
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
4
  alwaysApply: false
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Rules for defining models, relations, indexes and access control in LiveChange
3
- globs: **/services/**/*.js
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
4
  alwaysApply: false
5
5
  ---
6
6
 
@@ -36,6 +36,29 @@ properties: {
36
36
  }
37
37
  ```
38
38
 
39
+ ## Arity relacji (bardzo ważne)
40
+
41
+ Rozróżniaj dwa poziomy:
42
+
43
+ - **arity adnotacji** — czy można podać listę konfiguracji
44
+ - **arity rodziców w konfiguracji** — `what: [A, B]` lub `to: ['owner', 'topic']`
45
+
46
+ | Relacja | Arity adnotacji | Arity rodziców |
47
+ |---|---|---|
48
+ | `propertyOf` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
49
+ | `itemOf` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
50
+ | `boundTo` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
51
+ | `relatedTo` | jedna lub wiele konfiguracji | w każdej konfiguracji `what` może być pojedyncze lub tablica modeli |
52
+ | `propertyOfAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
53
+ | `itemOfAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
54
+ | `boundToAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
55
+ | `relatedToAny` | jedna lub wiele konfiguracji | w każdej konfiguracji `to` może mieć jedną lub wiele nazw |
56
+
57
+ Przykłady:
58
+
59
+ - poprawnie: `propertyOf: { what: [A, B] }`
60
+ - niepoprawnie: `propertyOf: [configA, configB]`
61
+
39
62
  ## `userItem` – zasób należący do użytkownika
40
63
 
41
64
  Używaj, gdy model należy do zalogowanego użytkownika.
@@ -123,7 +146,7 @@ Wtedy:
123
146
 
124
147
  - **nie** przechowuj „drugiej strony” jako zwykłego `contractorId` (albo ogólnie `someId`) w `properties`
125
148
  - **nie** dodawaj ręcznie pól `...Id` w modelu relacyjnym, jeśli to ma być relacja – to utrudnia generatorowi CRUD/relacji poprawne wnioskowanie o powiązaniach
126
- - zamiast tego zdefiniuj relacje jako `propertyOf` do **każdego** z modeli, które mają być rodzicami relacji
149
+ - zamiast tego zdefiniuj jedną relację `propertyOf` z `what: [ModelA, ModelB, ...]`
127
150
 
128
151
  To jest istotne, bo generator CRUD/relacji rozumie wtedy, że encja jest powiązaniem pomiędzy dwiema encjami, a nie „zwykłym obiektem z polem id”.
129
152
 
@@ -138,10 +161,9 @@ definition.model({
138
161
  properties: {
139
162
  // dodatkowe pola relacji (opcjonalnie)
140
163
  },
141
- propertyOf: [
142
- { what: CostInvoice },
143
- { what: Contractor }
144
- ]
164
+ propertyOf: {
165
+ what: [CostInvoice, Contractor]
166
+ }
145
167
  })
146
168
  ```
147
169
 
@@ -186,7 +208,7 @@ Relacje automatycznie dodają **pola identyfikatorów** i **indeksy** do modelu.
186
208
  | `propertyOfAny: { ownerTypes: [...] }` | `ownerType`, `owner` | `byOwner` (hash) |
187
209
  | `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
188
210
 
189
- Dla relacji z wieloma rodzicami (np. `propertyOf: [{ what: A }, { what: B }]`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
211
+ Dla relacji z wieloma rodzicami (np. `propertyOf: { what: [A, B] }`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
190
212
 
191
213
  ## `propertyOfAny` — typy rodzica (`{name}Types`)
192
214
 
@@ -249,6 +271,46 @@ indexes: {
249
271
 
250
272
  Poza serwisem indeks bywa widoczny pod nazwą z prefiksem serwisu, np. `myService_Model_byDeviceAndStatus`.
251
273
 
274
+ ### Kiedy indeks ma być poza modelem
275
+
276
+ Jeśli indeks jest unią/projekcją danych z kilku równorzędnych tabel (bez jednego naturalnego właściciela), nie wciskaj go do `model.indexes`.
277
+
278
+ - Użyj serwisowego `definition.index(...)`, najlepiej w osobnym `indexes.js`.
279
+ - To dotyczy szczególnie indeksów łączących różne typy encji w jeden strumień odczytu.
280
+
281
+ ### Indeksy `function` dla pól wyliczanych
282
+
283
+ Gdy część klucza indeksu nie istnieje jako zwykłe pole modelu (np. `month` wyliczany z `date`), użyj indeksu `function` zamiast `property`.
284
+
285
+ Zasady:
286
+
287
+ - buduj stabilny klucz jako `JSON.stringify(part1):JSON.stringify(part2):... + '_' + id`
288
+ - zwracaj obiekty `{ id, to }`, gdzie `to` wskazuje źródłowy rekord
289
+ - preferuj `table.map(mapper).to(output)` zamiast ręcznego `.onChange(...output.change...)`
290
+ - `map()` automatycznie odfiltrowuje `null`, więc mapper może zwracać tylko docelowy obiekt
291
+
292
+ ```js
293
+ indexes: {
294
+ byBankAccountAndMonthAndDate: {
295
+ function: async (input, output, { tableName }) => {
296
+ const table = await input.table(tableName)
297
+ const mapper = obj => ({
298
+ id: [
299
+ obj.bankAccount,
300
+ obj.date?.slice(0, 7),
301
+ obj.date
302
+ ].map(v => JSON.stringify(v)).join(':') + '_' + obj.id,
303
+ to: obj.id
304
+ })
305
+ await table.map(mapper).to(output)
306
+ },
307
+ parameters: {
308
+ tableName: definition.name + '_BankTransaction'
309
+ }
310
+ }
311
+ }
312
+ ```
313
+
252
314
  ## Access control na relacjach
253
315
 
254
316
  - Zawsze ustawiaj `readAccessControl` i `writeAccessControl` na relacjach (`userItem`, `itemOf`, `propertyOf`), zamiast polegać na domyślnym zachowaniu.
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: LiveChange backend — views/viewGet for reads; actions/triggers + emit for writes (CQRS-like)
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange backend — widoki do odczytu, akcja/trigger do zapisu
8
+
9
+ Na serwerze **inne ścieżki niż na froncie**: nie ma `live` / `useFetch` — odczyt definiujesz jako **`definition.view`** i ewentualnie wywołujesz go z kodu przez **`app.viewGet`** / **`app.serviceViewGet`**.
10
+
11
+ ## Odczyt
12
+
13
+ - **Widok** (`definition.view`) — jedyny kanał wystawienia danych do klienta (DAO, indeksy, wyliczenia, `fetch` zewnętrzny). Może zwracać dane złożone lub wyliczone (np. podgląd „następnego numeru”).
14
+ - **Z kodu serwera** (trigger, akcja, batch):
15
+ - `await app.viewGet('viewName', { ...properties })` — widok **w tym samym** serwisie co `app`.
16
+ - `await app.serviceViewGet('otherService', 'viewName', { ...properties })` — widok w **innym** serwisie.
17
+ - Bezpośredni odczyt modelu (`Model.get`, `indexObjectGet`, …) jest OK tam, gdzie nie potrzebujesz warstwy widoku/access control widoku — ale **nie** zamieniaj tego na „akcję tylko po to, żeby coś zwrócić”.
18
+
19
+ ## Zapis / zmiana stanu
20
+
21
+ - **Akcja** (`definition.action`) i **trigger** (`definition.trigger`) — walidacja, orchestracja, **`emit`** do eventów, **`trigger`** / **`triggerService`** do zapisu zgodnie z [live-change-backend-event-sourcing.mdc](./live-change-backend-event-sourcing.mdc).
22
+ - Nie dodawaj `definition.action`, której jedynym celem jest zwrócenie danych **bez** trwałej zmiany stanu — użyj **widoku**.
23
+
24
+ ## Antywzorzec
25
+
26
+ - Akcja/command po stronie klienta tylko po to, żeby pobrać podgląd — błąd projektu po obu stronach: na backendzie powinien być **widok**, na froncie `live`/`useFetch` (reguła frontendowa).
27
+
28
+ Szczegóły: `live-change-stack/docs/docs/server/07-views.md`, `06-actions.md`, `08-triggers.md`. API klienta (Vue) — `live-change-stack/docs/docs/frontend/04-logic-and-data-layer.md`, nie ta reguła.
@@ -0,0 +1,47 @@
1
+ ---
2
+ description: Zasady komunikacji przez surowy protokół DAO (C++, Python, inne klienty)
3
+ globs: *.cpp, *.py, *.rs, *.go
4
+ ---
5
+
6
+ # LiveChange DAO Protocol Arguments
7
+
8
+ When communicating with the LiveChange framework using the raw `@live-change/dao` protocol (e.g., from C++, Python, Rust, Go, or any other non-JS client), you MUST ALWAYS pass arguments as an **array**.
9
+
10
+ The framework treats DAO request and observable arguments like function arguments and uses the spread operator (`...args`) to pass them to the underlying action or view functions. If you pass an object instead of an array, the server will throw a `TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function`.
11
+
12
+ Even if an action or view expects a single object as its parameter, that object MUST be wrapped in a single-element array.
13
+
14
+ ## C++ Example (using nlohmann/json)
15
+
16
+ ### Incorrect ❌
17
+ ```cpp
18
+ nlohmann::json args = {
19
+ {"pairingKey", "123"},
20
+ {"connectionType", "device"}
21
+ };
22
+ connection->request({"serviceName", "actionName"}, args, settings);
23
+ ```
24
+
25
+ ### Correct ✅
26
+ ```cpp
27
+ // Wrap the object in an array
28
+ auto args = {
29
+ nlohmann::json::object({
30
+ {"pairingKey", "123"},
31
+ {"connectionType", "device"}
32
+ })
33
+ };
34
+ connection->request({"serviceName", "actionName"}, args, settings);
35
+ ```
36
+
37
+ Or explicitly:
38
+ ```cpp
39
+ nlohmann::json args = nlohmann::json::array({
40
+ nlohmann::json::object({
41
+ {"pairingKey", "123"},
42
+ {"connectionType", "device"}
43
+ })
44
+ });
45
+ ```
46
+
47
+ Always double-check that your `args` payload is an array before sending it over the DAO connection.
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: Stable node:test E2E lifecycle with env, withBrowser and e2eSuite
3
+ globs: **/e2e/**/*.{js,ts}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Frontend E2E lifecycle
8
+
9
+ - Keep teardown out of shared `e2e/env.ts`.
10
+ - Never use `after(...process.exit(...))` in `env.ts`.
11
+ - Add `disposeTestEnv()` in `env.ts` and call it from `process.on('beforeExit', ...)`.
12
+ - Put `after(async () => { await disposeTestEnv(); process.exit(0) })` in `e2eSuite.ts`.
13
+ - Wrap each `e2e/*.test.ts` file in one `e2eSuite('<suite-name>', () => { ... })`.
14
+ - Keep startup failure `process.exit(1)` in `getTestEnv()` catch.
15
+ - Run test commands with `fnm exec -- ...`.
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Keep all locale files under front/locales in sync when adding or changing i18n strings
3
+ globs: **/front/locales/**/*.{json,js,ts}, **/front/src/**/*.{vue,js,ts}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Frontend i18n – every file in `front/locales`
8
+
9
+ When you add or change **user-visible strings** (labels, messages, buttons, errors, page titles, etc.) that go through **vue-i18n** or the project’s locale files, you must update **every** locale resource in that app’s **`front/locales/`** directory — not only one language.
10
+
11
+ ## Required workflow
12
+
13
+ 1. **Identify the app root** for the frontend you are editing (the folder whose tree contains `front/src` and `front/locales`).
14
+ 2. **List all locale files** in `front/locales/` for that app (`en.json`, `pl.json`, `en.js`, `pl.js`, `landing-en.json`, … — whatever exists).
15
+ 3. **Add or update the same keys** in **each** of those files. Structure and nesting must stay consistent across languages (same key paths).
16
+ 4. **Do not** add a new key to a single locale file and leave others missing — that breaks builds or shows raw keys at runtime.
17
+
18
+ ## Translations you are unsure about
19
+
20
+ - Prefer a correct string in the primary locales (e.g. `en` + `pl`) when the product supports them.
21
+ - For other files, use a sensible placeholder, copy from English, or add a clearly marked temporary string — but **still add the key** everywhere so nothing is omitted.
22
+
23
+ ## Scope
24
+
25
+ - Applies to any work under `front/src` that introduces or changes translated text, and to direct edits in `front/locales`.
26
+ - If a project uses multiple locale formats (`.json` and `.js`), update **all** files that participate in i18n for that app, not only one extension.
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: LiveChange frontend — use views (live/useFetch) for reads; commands only for mutations
3
+ globs: **/front/**/*.{vue,js,ts}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange frontend — odczyt przez widoki, nie przez command
8
+
9
+ Ten sam **model mentalny** co na backendzie (odczyt vs zapis), ale **inne API**: na froncie nie ma `app.viewGet` — klient korzysta ze **ścieżek widoków** z `usePath()`.
10
+
11
+ ## Odczyt danych
12
+
13
+ - Buduj ścieżkę: `usePath()` (tylko synchronicznie w `setup`) → `path.<service>.<viewName>({ ...params })`; w `computed` używaj już utworzonego obiektu `path`, nie wywołuj ponownie `usePath()` / `path()`.
14
+ - Subskrypcja: `live(computed(() => path...))` albo `await Promise.all([live(...), ...])`.
15
+ - Jednorazowo: `useFetch(path.service.view({ ... }))` (np. w handlerze, po uploadzie).
16
+ - Dla niezależnych odczytów uruchamiaj `live(...)` równolegle (`Promise.all`), a sekwencyjnie tylko gdy drugi path zależy od wyniku pierwszego.
17
+ - W `Path.with(...)` buduj deklaratywnie ścieżki; do branchingu po typie/polu używaj `$switch`, nie runtime `if` na callbacku `.with`.
18
+
19
+ Nie używaj `api.command` ani `useActions()` do „załadowania” danych, podglądu, wyliczenia tylko do UI, ani „jakiego będzie następny numer” — to **zawsze** powinno iść przez **widok** (`definition.view` po stronie serwera) i `live` / `useFetch` po stronie klienta.
20
+
21
+ ## Mutacja stanu
22
+
23
+ - `api.command(['service', 'actionName'], props)` lub `useActions()` → wywołanie **akcji** zmieniającej trwały stan (create/update/delete przez eventy itd.).
24
+ - `workingZone` i spinner przy commandach mają sens przy **zapisie**, nie przy odczycie.
25
+
26
+ ## Antywzorzec
27
+
28
+ - Wywołanie akcji/command tylko po to, żeby dostać zwrócony string (np. podgląd numeru faktury) **bez** zmiany bazy — **źle**. Zamiast tego dodaj **widok** zwracający ten podgląd i pobierz go przez `live` / `useFetch`.
29
+
30
+ Szczegóły API: `live-change-stack/docs/docs/frontend/04-logic-and-data-layer.md`. Backend (`app.viewGet`, triggery): reguła `live-change-backend-views-vs-triggers-for-reads-writes.mdc` — nie stosuj jej w plikach Vue.
@@ -19,6 +19,8 @@ alwaysApply: false
19
19
  - **Nie** używaj `ref(null)` + `onMounted` do ładowania danych.
20
20
  - Dane ładuj w `setup`/`script setup` przez `await Promise.all(...)` na `live(path()....)`.
21
21
  - Rodzic powinien owijać stronę w `<Suspense>` (w projektach live-change robi to zazwyczaj `ViewRoot` globalnie).
22
+ - Sekwencyjnego `await live(...)` używaj tylko wtedy, gdy drugi path zależy od wyniku pierwszego.
23
+ - Gdy zależność da się opisać przez `.with(...)`, preferuj jedno zapytanie Path DSL zamiast imperatywnego łączenia wyników.
22
24
 
23
25
  Przykład:
24
26
 
@@ -64,6 +66,50 @@ Schemat decyzji:
64
66
  2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
65
67
  3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
66
68
 
69
+ ## Autosave helpery – `synchronized` i `synchronizedList`
70
+
71
+ Używaj helperów dla danych z `live(...)`:
72
+
73
+ - `synchronized` dla pojedynczego obiektu do edycji.
74
+ - `synchronizedList` dla edytowalnych list (wierszy).
75
+ - Kontekst wspólny przekazuj przez `identifiers`, a identyfikatory wiersza przez `objectIdentifiers`.
76
+ - Dla draftów z payloadem zagnieżdżonym używaj `updateDataProperty: 'data'`.
77
+
78
+ Kiedy traktować ekran jako listę:
79
+
80
+ - UI ma `v-for` z wieloma edytowalnymi wierszami (tabela/lista konfiguracji/admin).
81
+ - Użytkownik edytuje wiele rekordów inline, a zapis ma działać per wiersz.
82
+ - Akcje backendowe potrzebują wspólnego kontekstu listy i kluczy konkretnego wiersza.
83
+
84
+ W takich przypadkach stosuj jeden `synchronizedList(...)` i edytuj dane bezpośrednio przez `syncList.value`.
85
+ Dla takich list nie buduj osobnej mapy `id -> synchronized(...)`.
86
+
87
+ ```js
88
+ const sync = synchronized({
89
+ source: sourceRef,
90
+ update: actions.service.updateThing,
91
+ identifiers: { thing: thingId },
92
+ recursive: true,
93
+ autoSave: true,
94
+ debounce: 600
95
+ })
96
+
97
+ const syncList = synchronizedList({
98
+ source: rowsRef,
99
+ update: actions.service.updateRow,
100
+ delete: actions.service.deleteRow,
101
+ identifiers: { object, objectType },
102
+ objectIdentifiers: row => ({ row: row.to, object, objectType }),
103
+ recursive: true
104
+ })
105
+ ```
106
+
107
+ Przykłady ekranów, gdzie to dotyczy:
108
+
109
+ - listy uprawnień i ról,
110
+ - słowniki i konfiguracje wielowierszowe,
111
+ - tabele administracyjne z inline edycją pól.
112
+
67
113
  ### `api.command`
68
114
 
69
115
  ```js
@@ -183,6 +229,8 @@ const articlePath = computed(() => path.blog.article({ article: unref(articleId)
183
229
  const [article] = await Promise.all([live(articlePath)])
184
230
  ```
185
231
 
232
+ `usePath()` wywołuj synchronicznie w `setup` i zapisz wynik w zmiennej `path`. W getterze `computed` buduj tylko `path.<service>.<view>(...)` — nie wywołuj ponownie `usePath()` ani aliasu `path()` w środku `computed` (brak aktywnej instancji → błąd przy `appContext`).
233
+
186
234
  Dla warunkowego ładowania (np. tylko gdy zalogowany), zwróć wartość falsy:
187
235
 
188
236
  ```js
@@ -207,6 +255,49 @@ path.blog.articles({})
207
255
 
208
256
  Dostęp: `article.authorProfile?.firstName`. Działa zarówno z `live()` jak i `RangeViewer`.
209
257
 
258
+ ### Guardrails Path DSL (`.with`, `$switch`)
259
+
260
+ - Callback `.with(item => ...)` traktuj jako deklarację query DSL, nie jako kod biznesowy wykonywany na realnym rekordzie.
261
+ - W callbacku `.with` nie rób side effects, `api.command`, ani imperatywnego `if/else` porównującego pola proxy.
262
+ - Dla warunkowego wyboru ścieżki używaj `item.field.$switch({...}).$bind('target')`.
263
+
264
+ ```js
265
+ path.accounting.settlementsByTransaction({ transactionType, transaction, range })
266
+ .with(settlement => settlement.subjectType.$switch({
267
+ invoice_CostInvoice: path.invoice.costInvoice({ costInvoice: settlement.subject }),
268
+ invoice_IncomeInvoice: path.invoice.incomeInvoice({ incomeInvoice: settlement.subject }),
269
+ hr_CivilContract: path.hr.civilContract({ civilContract: settlement.subject })
270
+ }).$bind('subjectDoc'))
271
+ ```
272
+
273
+ ## Listy zakresowe z reaktywnymi filtrami
274
+
275
+ Jeśli `pathFunction` dla listy zakresowej zależy od reaktywnych filtrów (np. miesiąc/status/szukaj), preferuj `ReactiveRangeViewer`.
276
+
277
+ Zasady:
278
+
279
+ - nie polegaj na samej zmianie `pathFunction` w `RangeViewer`
280
+ - nie rozrzucaj workaroundów typu ręczne `:key` po stronach
281
+ - użyj `sourceKey` jako jawnego triggera przeładowania
282
+ - gdy UX wymaga stabilności układu, ustaw `preserveHeightOnReload`
283
+
284
+ ```vue
285
+ <ReactiveRangeViewer
286
+ :pathFunction="transactionsPathRange"
287
+ :sourceKey="JSON.stringify({ accountId, month: filterByMonth ? month : null })"
288
+ :preserveHeightOnReload="true"
289
+ :canLoadTop="false"
290
+ canDropBottom
291
+ />
292
+ ```
293
+
294
+ ## Guardrails dla kursora zakresu (`RangeViewer` / `rangeBuckets`)
295
+
296
+ - Dla list opartych o indeks backend powinien udostępniać widoki oparte o `sortedIndexRangePath`.
297
+ - Nigdy nie nadpisuj `range.gt/gte/lt/lte` we frontendowym `pathFunction`.
298
+ - Pole `range` zostaw jako kursor paginacji; filtry domenowe (`month`, `year`, `status`) przekazuj osobno.
299
+ - Jeśli pojawia się pokusa ręcznego przepisywania kursora, przenieś logikę do projektu indeksu po stronie backendu (ew. fallback `prefixRange`), nie do hacków we froncie.
300
+
210
301
  ## WorkingZone dla akcji asynchronicznych
211
302
 
212
303
  `ViewRoot` opakowuje każdą stronę w `<WorkingZone>`. Używaj `inject('workingZone')` dla akcji przycisków poza formularzami:
@@ -0,0 +1,40 @@
1
+ ---
2
+ description: Run Node, npm, npx, tsx with fnm exec using project .node-version or .nvmrc
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Node.js toolchain – use `fnm exec`
7
+
8
+ Do **not** rely on whatever Node.js version the agent sandbox or shell happens to use. That often differs from the project and breaks the LiveChange stack, tests, and `describe`.
9
+
10
+ Use **fnm** so the version from the project dotfiles (`.node-version` or `.nvmrc`) is selected.
11
+
12
+ ## Required pattern
13
+
14
+ Work in the directory that contains the relevant `.node-version` or `.nvmrc` (app or package root). Prefix **every** `node`, `npm`, `npx`, `corepack`, or `tsx` invocation with:
15
+
16
+ ```bash
17
+ fnm exec -- <command and arguments>
18
+ ```
19
+
20
+ Examples:
21
+
22
+ ```bash
23
+ fnm exec -- node server/start.js describe --service myService --output yaml
24
+ fnm exec -- npm test
25
+ fnm exec -- npm run build
26
+ fnm exec -- npx eslint .
27
+ fnm exec -- tsx scripts/example.ts
28
+ ```
29
+
30
+ If the dotfile lives only under a subfolder (e.g. `auto-firma/app/`), `cd` there first, then run `fnm exec -- ...`.
31
+
32
+ ## When this applies
33
+
34
+ - Tests, CI scripts, linters, builds
35
+ - Framework CLI: `describe`, dev servers, migrations, anything touching `@live-change/*`
36
+ - Any `npm` / `node` / `npx` / `tsx` for this monorepo or its subprojects
37
+
38
+ ## If `fnm` is unavailable
39
+
40
+ Tell the user to install fnm or align the environment; do not proceed assuming an arbitrary `node` on `PATH` is correct.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: LiveChange service file structure — every service must be a directory with separate definition.js, index.js, etc.
3
- globs: server/**/*.js
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
4
  alwaysApply: true
5
5
  ---
6
6
 
@@ -137,6 +137,22 @@ globs: **/front/src/**/*.{vue,js,ts}
137
137
  | `description` | What this rule covers (used for matching) |
138
138
  | `globs` | File patterns that trigger this rule (e.g. `**/*.js`, `**/services/**/*.js`) |
139
139
 
140
+ ### Backend / LiveChange service rules — standard `globs`
141
+
142
+ Rules for **server-side** LiveChange code (models, actions, triggers, service layout) should use a **comma-separated** `globs` line so they still match when a project stores services under different folder names (many teams copy `.cursor/rules` into other repos):
143
+
144
+ ```yaml
145
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
146
+ ```
147
+
148
+ | Pattern | Covers |
149
+ |---|---|
150
+ | `**/services/**/*.js` | Trees such as `live-change-stack/services/<service>/` |
151
+ | `**/server/**/*.js` | Any nested `server/` directory (e.g. `app/server/`, `packages/foo/server/`) |
152
+ | `server/**/*.js` | Backend rooted at top-level `server/` |
153
+
154
+ Frontend rules keep their own globs (e.g. `**/front/src/**/*.{vue,js,ts}`). Do not drop the `server` variants for backend-only rules — otherwise Cursor will miss files after a layout change.
155
+
140
156
  ### Body structure
141
157
 
142
158
  ```markdown
@@ -181,6 +197,7 @@ alwaysApply: false
181
197
  - Do NOT quote glob patterns in frontmatter
182
198
  - Keep rules short (target 25 lines, max 50 lines for best Cursor performance)
183
199
  - The `.mdc` extension is required for Cursor
200
+ - For **backend** LiveChange rules, use the same **`globs`** standard as in Claude rules: `**/services/**/*.js, **/server/**/*.js, server/**/*.js`
184
201
 
185
202
  ## Step 5 – Register rules in OpenCode (`opencode.json`)
186
203
 
@@ -198,8 +215,12 @@ OpenCode reads `.claude/skills/<name>/SKILL.md` natively for skills (no extra st
198
215
 
199
216
  When you **create a new rule**, add its path to the `instructions` array in `opencode.json`.
200
217
 
218
+ Project rule **`live-change-node-toolchain-fnm`** should stay **first** (or early) in `instructions` so agents always load the requirement to run `node`, `npm`, `npx`, and `tsx` via `fnm exec` using `.node-version` / `.nvmrc`.
219
+
201
220
  When you **create a new skill**, no `opencode.json` change is needed — OpenCode discovers skills from `.claude/skills/<name>/SKILL.md` automatically.
202
221
 
222
+ When a skill or rule shows shell examples that invoke **`node`**, **`npm`**, **`npx`**, or **`tsx`**, use the **`fnm exec -- …`** form (see `.claude/rules/live-change-node-toolchain-fnm.md` and skill `live-change-node-toolchain-fnm`).
223
+
203
224
  **Important:** OpenCode ignores the `globs` frontmatter from Claude Code rules. All instructions listed in `opencode.json` are always loaded.
204
225
 
205
226
  ## Step 6 – Mirror Cursor skills (`.cursor/skills/<name>.md`)
@@ -239,10 +260,12 @@ done
239
260
 
240
261
  ## Checklist
241
262
 
263
+ - [ ] Shell examples for Node/npm use `fnm exec --` per `live-change-node-toolchain-fnm`
242
264
  - [ ] Directory created: `.claude/skills/<name>/SKILL.md`
243
265
  - [ ] Frontmatter has both `name` (matching dir) and `description`
244
266
  - [ ] `.cursor/skills/<name>.md` mirrored (flat file, same content)
245
267
  - [ ] `.claude/rules/*.md` created (if rule)
246
268
  - [ ] `.cursor/rules/*.mdc` created with `globs` + `alwaysApply` (if rule)
269
+ - [ ] Backend rules use standard `globs`: `**/services/**/*.js, **/server/**/*.js, server/**/*.js` (when the rule targets LiveChange server code)
247
270
  - [ ] `opencode.json` `instructions` array updated (if new rule)
248
271
  - [ ] Sub-projects updated (automation, auto-firma)