@live-change/frontend-template 0.9.198 → 0.9.200

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 (71) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +246 -0
  2. package/.claude/rules/live-change-backend-architecture.md +126 -0
  3. package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
  4. package/.claude/rules/live-change-backend-models-and-relations.md +260 -0
  5. package/.claude/rules/live-change-frontend-vue-primevue.md +317 -0
  6. package/.claude/rules/live-change-service-structure.md +89 -0
  7. package/.claude/settings.json +32 -0
  8. package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
  9. package/.claude/skills/create-skills-and-rules.md +196 -0
  10. package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
  11. package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
  12. package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
  13. package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
  14. package/.claude/skills/live-change-design-models-relations.md +173 -0
  15. package/.claude/skills/live-change-design-service/SKILL.md +133 -0
  16. package/.claude/skills/live-change-design-service.md +132 -0
  17. package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
  18. package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
  19. package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
  20. package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
  21. package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
  22. package/.claude/skills/live-change-frontend-action-form.md +143 -0
  23. package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
  24. package/.claude/skills/live-change-frontend-analytics.md +146 -0
  25. package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
  26. package/.claude/skills/live-change-frontend-command-forms.md +215 -0
  27. package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
  28. package/.claude/skills/live-change-frontend-data-views.md +182 -0
  29. package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
  30. package/.claude/skills/live-change-frontend-editor-form.md +177 -0
  31. package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
  32. package/.claude/skills/live-change-frontend-locale-time.md +171 -0
  33. package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
  34. package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
  35. package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
  36. package/.claude/skills/live-change-frontend-range-list.md +128 -0
  37. package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
  38. package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
  39. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +290 -0
  40. package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
  41. package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
  42. package/.cursor/rules/live-change-backend-models-and-relations.mdc +256 -0
  43. package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
  44. package/.cursor/rules/live-change-service-structure.mdc +107 -0
  45. package/.cursor/skills/create-skills-and-rules.md +248 -0
  46. package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
  47. package/.cursor/skills/live-change-design-actions-views-triggers.md +296 -0
  48. package/.cursor/skills/live-change-design-models-relations.md +230 -0
  49. package/.cursor/skills/live-change-design-service.md +76 -0
  50. package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
  51. package/.cursor/skills/live-change-frontend-action-buttons.md +129 -0
  52. package/.cursor/skills/live-change-frontend-action-form.md +149 -0
  53. package/.cursor/skills/live-change-frontend-analytics.md +147 -0
  54. package/.cursor/skills/live-change-frontend-command-forms.md +216 -0
  55. package/.cursor/skills/live-change-frontend-data-views.md +183 -0
  56. package/.cursor/skills/live-change-frontend-editor-form.md +240 -0
  57. package/.cursor/skills/live-change-frontend-locale-time.md +172 -0
  58. package/.cursor/skills/live-change-frontend-page-list-detail.md +201 -0
  59. package/.cursor/skills/live-change-frontend-range-list.md +129 -0
  60. package/.cursor/skills/live-change-frontend-ssr-setup.md +120 -0
  61. package/README.md +71 -0
  62. package/front/src/router.js +2 -1
  63. package/opencode.json +10 -0
  64. package/package.json +52 -50
  65. package/server/app.config.js +35 -0
  66. package/server/services.list.js +2 -0
  67. package/.nx/workspace-data/file-map.json +0 -195
  68. package/.nx/workspace-data/nx_files.nxt +0 -0
  69. package/.nx/workspace-data/project-graph.json +0 -8
  70. package/.nx/workspace-data/project-graph.lock +0 -0
  71. package/.nx/workspace-data/source-maps.json +0 -1
@@ -0,0 +1,256 @@
1
+ ---
2
+ description: Rules for defining models, relations, indexes and access control in LiveChange
3
+ globs: **/services/**/*.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange backend – modele i relacje
8
+
9
+ ## Ogólne zasady
10
+
11
+ - Modele zapisuj w plikach domenowych (`<domain>.js`) importowanych w `index.js` serwisu.
12
+ - Dbaj o czytelność definicji – **właściwości wieloliniowo**, z jasnymi polami `type`, `default`, `validation` itd.
13
+ - Tam, gdzie to możliwe, używaj relacji (`userItem`, `itemOf`, `propertyOf`, `foreignModel`) zamiast ręcznego klepania CRUD i widoków.
14
+
15
+ ## Styl definicji `properties`
16
+
17
+ - Każde pole opisane wieloliniowo, jedna rzecz na linię.
18
+ - Unikaj upychania typu i walidacji w jednej linii, jeśli zmniejsza to czytelność.
19
+
20
+ ```js
21
+ properties: {
22
+ name: {
23
+ type: String,
24
+ validation: ['nonEmpty']
25
+ },
26
+ status: {
27
+ type: String,
28
+ default: 'offline'
29
+ },
30
+ capabilities: {
31
+ type: Array,
32
+ of: {
33
+ type: String
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## `userItem` – zasób należący do użytkownika
40
+
41
+ Używaj, gdy model należy do zalogowanego użytkownika.
42
+
43
+ ```js
44
+ definition.model({
45
+ name: 'Device',
46
+ properties: {
47
+ name: {
48
+ type: String
49
+ }
50
+ },
51
+ userItem: {
52
+ readAccessControl: { roles: ['owner', 'admin'] },
53
+ writeAccessControl: { roles: ['owner', 'admin'] },
54
+ writeableProperties: ['name']
55
+ }
56
+ })
57
+ ```
58
+
59
+ Skutek:
60
+
61
+ - automatyczne widoki typu „moje X”,
62
+ - automatyczne akcje create/update/delete dla właściciela.
63
+
64
+ ## `itemOf` – dziecko należy do rodzica
65
+
66
+ Używaj, gdy model jest listą elementów powiązanych z innym modelem.
67
+
68
+ ```js
69
+ definition.model({
70
+ name: 'DeviceConnection',
71
+ properties: {
72
+ connectionType: {
73
+ type: String
74
+ },
75
+ status: {
76
+ type: String,
77
+ default: 'offline'
78
+ }
79
+ },
80
+ itemOf: {
81
+ what: Device,
82
+ readAccessControl: { roles: ['owner', 'admin'] },
83
+ writeAccessControl: { roles: ['owner', 'admin'] }
84
+ }
85
+ })
86
+ ```
87
+
88
+ Zasady:
89
+
90
+ - rodzic `what` to model zadeklarowany wcześniej w tym lub innym serwisie,
91
+ - relacja generuje standardowe widoki/akcje dla listy i elementów.
92
+
93
+ ## `propertyOf` – właściwość z ID równym rodzicowi
94
+
95
+ Używaj, gdy model przechowuje „stan” jednego obiektu (ID dziecka = ID rodzica).
96
+
97
+ ```js
98
+ definition.model({
99
+ name: 'DeviceCursorState',
100
+ properties: {
101
+ x: { type: Number },
102
+ y: { type: Number }
103
+ },
104
+ propertyOf: {
105
+ what: Device,
106
+ readAccessControl: { roles: ['owner', 'admin'] },
107
+ writeAccessControl: { roles: ['owner', 'admin'] }
108
+ }
109
+ })
110
+ ```
111
+
112
+ Skutek:
113
+
114
+ - prosty dostęp: `DeviceCursorState.get(deviceId)`,
115
+ - bez potrzeby dodatkowych indeksów po polu `device`.
116
+
117
+ ## `propertyOf` dla wielu modeli (relacja 1:1 do każdego z nich)
118
+
119
+ W niektórych domenach model jest „łącznikiem 1:1” pomiędzy rekordami (np. faktura ↔ kontrahent w roli dostawcy/klienta).
120
+ Najczęściej są to 2 modele, ale `propertyOf` może wskazywać na **dowolnie wiele** modeli (np. relacja łącząca 3+ encje), jeśli taka jest semantyka domeny.
121
+
122
+ Wtedy:
123
+
124
+ - **nie** przechowuj „drugiej strony” jako zwykłego `contractorId` (albo ogólnie `someId`) w `properties`
125
+ - **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
127
+
128
+ 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
+
130
+ Przykład (schematycznie):
131
+
132
+ ```js
133
+ const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
134
+ const Contractor = definition.foreignModel('company', 'Contractor')
135
+
136
+ definition.model({
137
+ name: 'Supplier',
138
+ properties: {
139
+ // dodatkowe pola relacji (opcjonalnie)
140
+ },
141
+ propertyOf: [
142
+ { what: CostInvoice },
143
+ { what: Contractor }
144
+ ]
145
+ })
146
+ ```
147
+
148
+ ## `foreignModel` – relacja do modelu z innego serwisu
149
+
150
+ Używaj, gdy `itemOf`/`propertyOf` ma wskazywać na model spoza aktualnego serwisu.
151
+
152
+ ```js
153
+ const Device = definition.foreignModel('deviceManager', 'Device')
154
+
155
+ definition.model({
156
+ name: 'BotSession',
157
+ properties: {
158
+ // ...
159
+ },
160
+ itemOf: {
161
+ what: Device,
162
+ readAccessControl: { roles: ['owner', 'admin'] }
163
+ }
164
+ })
165
+ ```
166
+
167
+ Zasady:
168
+
169
+ - pierwszy argument to nazwa serwisu,
170
+ - drugi to nazwa modelu w tamtym serwisie.
171
+
172
+ ## Automatycznie dodawane pola z relacji
173
+
174
+ Relacje automatycznie dodają **pola identyfikatorów** i **indeksy** do modelu. **Nie definiuj ich ponownie** w `properties`.
175
+
176
+ **Konwencja nazw:** nazwa pola = nazwa modelu rodzica z małą pierwszą literą (`Device` → `device`, `CostInvoice` → `costInvoice`).
177
+
178
+ | Relacja | Dodane pole/pola | Dodane indeksy |
179
+ |---|---|---|
180
+ | `itemOf: { what: Device }` | `device` | `byDevice` |
181
+ | `propertyOf: { what: Device }` | `device` | `byDevice` |
182
+ | `userItem` | `user` | `byUser` |
183
+ | `userProperty` | `user` | `byUser` |
184
+ | `sessionOrUserProperty` | `sessionOrUserType`, `sessionOrUser` | `bySessionOrUser` (hash) |
185
+ | `sessionOrUserProperty: { extendedWith: ['object'] }` | + `objectType`, `object` | indeksy złożone |
186
+ | `propertyOfAny: { ownerTypes: [...] }` | `ownerType`, `owner` | `byOwner` (hash) |
187
+ | `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
188
+
189
+ Dla relacji z wieloma rodzicami (np. `propertyOf: [{ what: A }, { what: B }]`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
190
+
191
+ ## `propertyOfAny` — typy rodzica (`{name}Types`)
192
+
193
+ `propertyOfAny` jest relacją polimorficzną. Lista dozwolonych typów jest podawana w polach `{name}Types`, gdzie `{name}` pochodzi z `to`.
194
+
195
+ - Gdy `to` nie jest podane, domyślnie jest `['owner']`, więc użyj `ownerTypes`.
196
+ - Gdy `to: ['invoice']`, użyj `invoiceTypes`.
197
+
198
+ ```js
199
+ // domyślnie to: ['owner']
200
+ propertyOfAny: {
201
+ ownerTypes: ['invoice_CostInvoice', 'invoice_IncomeInvoice']
202
+ }
203
+
204
+ // jawnie
205
+ propertyOfAny: {
206
+ to: ['invoice'],
207
+ invoiceTypes: ['invoice_CostInvoice', 'invoice_IncomeInvoice']
208
+ }
209
+ ```
210
+
211
+ ```js
212
+ // ✅ Poprawnie — definiuj tylko SWOJE pola
213
+ definition.model({
214
+ name: 'Connection',
215
+ properties: {
216
+ status: { type: String } // 'device' NIE jest tutaj — dodaje go itemOf
217
+ },
218
+ itemOf: { what: Device } // automatycznie dodaje pole 'device' + indeks 'byDevice'
219
+ })
220
+
221
+ // ❌ Źle — redundantne pole
222
+ definition.model({
223
+ name: 'Connection',
224
+ properties: {
225
+ device: { type: String }, // ❌ już dodane przez itemOf
226
+ status: { type: String }
227
+ },
228
+ itemOf: { what: Device }
229
+ })
230
+ ```
231
+
232
+ Użyj `node server/start.js describe --service myService --model MyModel --output yaml` żeby zobaczyć wszystkie pola łącznie z automatycznie dodanymi.
233
+
234
+ ## Indeksy
235
+
236
+ - Definiuj indeksy jawnie w modelu, gdy będziesz często wyszukiwać po danym polu lub kombinacji pól.
237
+ - Nazwy indeksów powinny być opisowe, bez skrótów trudnych do odczytania.
238
+
239
+ ```js
240
+ indexes: {
241
+ bySessionKey: {
242
+ property: ['sessionKey']
243
+ },
244
+ byDeviceAndStatus: {
245
+ property: ['device', 'status']
246
+ }
247
+ }
248
+ ```
249
+
250
+ Poza serwisem indeks bywa widoczny pod nazwą z prefiksem serwisu, np. `myService_Model_byDeviceAndStatus`.
251
+
252
+ ## Access control na relacjach
253
+
254
+ - Zawsze ustawiaj `readAccessControl` i `writeAccessControl` na relacjach (`userItem`, `itemOf`, `propertyOf`), zamiast polegać na domyślnym zachowaniu.
255
+ - Traktuj to jako część modelu, nie dodatek na końcu.
256
+
@@ -0,0 +1,290 @@
1
+ ---
2
+ description: Rules for Vue 3, PrimeVue, Tailwind frontend development on LiveChange
3
+ globs: **/front/src/**/*.{vue,js,ts}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Frontend na live-change-stack – Vue 3 + PrimeVue + Tailwind
8
+
9
+ ## Stack
10
+
11
+ - Vue 3 + TypeScript
12
+ - PrimeVue 4 – główna biblioteka komponentów UI
13
+ - Tailwind CSS – podstawowe stylowanie (unikaj zbędnego custom CSS)
14
+ - vite-plugin-pages – file-based routing w `src/pages/`
15
+ - @live-change/vue3-ssr – integracja z backendem LiveChange
16
+
17
+ ## Pobieranie danych – `live` + `Promise.all`
18
+
19
+ - **Nie** używaj `ref(null)` + `onMounted` do ładowania danych.
20
+ - Dane ładuj w `setup`/`script setup` przez `await Promise.all(...)` na `live(path()....)`.
21
+ - Rodzic powinien owijać stronę w `<Suspense>` (w projektach live-change robi to zazwyczaj `ViewRoot` globalnie).
22
+
23
+ Przykład:
24
+
25
+ ```js
26
+ import { path, live, api as useApi } from '@live-change/vue3-ssr'
27
+
28
+ const api = useApi()
29
+
30
+ const [devices] = await Promise.all([
31
+ live(path().deviceManager.myUserDevices({}))
32
+ ])
33
+
34
+ const [device, connections] = await Promise.all([
35
+ live(path().deviceManager.myUserDevice({ device: deviceId })),
36
+ live(path().deviceManager.deviceOwnedDeviceConnections({ device: deviceId }))
37
+ ])
38
+ ```
39
+
40
+ Dostęp w template:
41
+
42
+ ```vue
43
+ <template>
44
+ <div v-if="device.value">
45
+ {{ device.value.name }}
46
+ </div>
47
+ </template>
48
+ ```
49
+
50
+ ## Komendy i formularze – wybór wzorca
51
+
52
+ Są 4 sposoby wywoływania akcji backendowych. Używaj właściwego:
53
+
54
+ | Wzorzec | Kiedy używać |
55
+ |---|---|
56
+ | `editorData` | **Edycja rekordów modelu** (create/update). Drafty, walidacja, `AutoField`. Ustawienia, edytory, profile. |
57
+ | `actionData` | **Jednorazowe formularze akcji** (nie CRUD). Submit → done. Publikacja, zaproszenie, import. |
58
+ | `api.command` | **Pojedynczy przycisk lub wywołanie z kodu** (bez pól formularza). Usuwanie, toggle, akcje z kodu. |
59
+ | `<command-form>` | **Unikaj.** Legacy, tylko trywialne prototypy. Preferuj `editorData` lub `actionData`. |
60
+
61
+ Schemat decyzji:
62
+
63
+ 1. Użytkownik wypełnia pola formularza? → **Nie**: użyj `api.command` (owijaj w `workingZone.addPromise` dla przycisków).
64
+ 2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
65
+ 3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
66
+
67
+ ### `api.command`
68
+
69
+ ```js
70
+ await api.command(['deviceManager', 'createMyUserDevice'], {
71
+ name: 'Moje urządzenie'
72
+ })
73
+
74
+ await api.command(['deviceManager', 'deleteMyUserDevice'], {
75
+ device: id
76
+ })
77
+ ```
78
+
79
+ Format komendy:
80
+
81
+ - tablica `[serviceName, actionName]`,
82
+ - payload jako zwykły obiekt.
83
+
84
+ ## Routing – blok `<route>` i meta `signedIn`
85
+
86
+ - Każda strona w `src/pages/` może mieć blok `<route>` z metadanymi.
87
+ - Używaj `meta.signedIn` do oznaczania stron wymagających logowania.
88
+
89
+ ```vue
90
+ <route>
91
+ { "name": "devices", "meta": { "signedIn": true } }
92
+ </route>
93
+ ```
94
+
95
+ Dynamiczne ścieżki:
96
+
97
+ - plik `[id].vue` odpowiada ścieżce `/devices/:id`.
98
+
99
+ ```js
100
+ import { useRoute } from 'vue-router'
101
+
102
+ const route = useRoute()
103
+ const deviceId = route.params.id
104
+ ```
105
+
106
+ ## Confirm + Toast – wzorzec dla akcji destrukcyjnych
107
+
108
+ ```js
109
+ import { useConfirm } from 'primevue/useconfirm'
110
+ import { useToast } from 'primevue/usetoast'
111
+
112
+ const confirm = useConfirm()
113
+ const toast = useToast()
114
+
115
+ function deleteDevice(id) {
116
+ confirm.require({
117
+ message: 'Czy na pewno chcesz usunąć to urządzenie?',
118
+ header: 'Potwierdzenie',
119
+ icon: 'pi pi-exclamation-triangle',
120
+ accept: async () => {
121
+ await api.command(['deviceManager', 'deleteMyUserDevice'], { device: id })
122
+ toast.add({
123
+ severity: 'success',
124
+ summary: 'Usunięto',
125
+ life: 2000
126
+ })
127
+ }
128
+ })
129
+ }
130
+ ```
131
+
132
+ ## Wzorce UI – lista i szczegóły
133
+
134
+ ### Lista
135
+
136
+ ```vue
137
+ <template>
138
+ <div class="container mx-auto p-4">
139
+ <div class="flex items-center justify-between mb-6">
140
+ <h1 class="text-2xl font-bold">Urządzenia</h1>
141
+ <Button label="Dodaj" icon="pi pi-plus" @click="openDialog" />
142
+ </div>
143
+
144
+ <Card v-if="devices.value?.length === 0">
145
+ <template #content>
146
+ <p class="text-center text-gray-500">
147
+ Brak urządzeń
148
+ </p>
149
+ </template>
150
+ </Card>
151
+
152
+ <div class="grid gap-4">
153
+ <Card v-for="device in devices.value" :key="device.id">
154
+ <template #content>
155
+ <!-- zawartość -->
156
+ </template>
157
+ </Card>
158
+ </div>
159
+ </div>
160
+ </template>
161
+ ```
162
+
163
+ ### Tag statusu
164
+
165
+ ```vue
166
+ <Tag
167
+ :value="conn.status"
168
+ :severity="conn.status === 'online' ? 'success' : 'secondary'"
169
+ />
170
+ ```
171
+
172
+ ## Computed paths – reaktywne parametry
173
+
174
+ Gdy ścieżki zależą od reaktywnych wartości (route params, props), owijaj je w `computed()`:
175
+
176
+ ```js
177
+ import { computed, unref } from 'vue'
178
+ import { usePath, live } from '@live-change/vue3-ssr'
179
+
180
+ const path = usePath()
181
+
182
+ const articlePath = computed(() => path.blog.article({ article: unref(articleId) }))
183
+ const [article] = await Promise.all([live(articlePath)])
184
+ ```
185
+
186
+ Dla warunkowego ładowania (np. tylko gdy zalogowany), zwróć wartość falsy:
187
+
188
+ ```js
189
+ import { useClient } from '@live-change/vue3-ssr'
190
+ const client = useClient()
191
+
192
+ const myDataPath = computed(() => client.value.user && path.blog.myArticles({}))
193
+ const [myData] = await Promise.all([live(myDataPath)])
194
+ ```
195
+
196
+ ## Powiązane dane – `.with()`
197
+
198
+ Dołączaj powiązane obiekty do elementów w jednym reaktywnym zapytaniu:
199
+
200
+ ```js
201
+ path.blog.articles({})
202
+ .with(article => path.userIdentification.identification({
203
+ sessionOrUserType: article.authorType,
204
+ sessionOrUser: article.author
205
+ }).bind('authorProfile'))
206
+ ```
207
+
208
+ Dostęp: `article.authorProfile?.firstName`. Działa zarówno z `live()` jak i `RangeViewer`.
209
+
210
+ ## WorkingZone dla akcji asynchronicznych
211
+
212
+ `ViewRoot` opakowuje każdą stronę w `<WorkingZone>`. Używaj `inject('workingZone')` dla akcji przycisków poza formularzami:
213
+
214
+ ```js
215
+ import { inject } from 'vue'
216
+ const workingZone = inject('workingZone')
217
+
218
+ function doAction() {
219
+ workingZone.addPromise('actionName', (async () => {
220
+ await actions.blog.publishArticle({ article: id })
221
+ toast.add({ severity: 'success', summary: 'Opublikowano', life: 2000 })
222
+ })())
223
+ }
224
+ ```
225
+
226
+ Aktywuje globalny spinner/blur na czas trwania promisy.
227
+
228
+ ## Kontrola dostępu z `useClient`
229
+
230
+ ```js
231
+ import { useClient } from '@live-change/vue3-ssr'
232
+ const client = useClient()
233
+ ```
234
+
235
+ - `client.value.user` – truthy gdy zalogowany
236
+ - `client.value.roles` – tablica ról (np. `['admin', 'owner']`)
237
+
238
+ W template:
239
+
240
+ ```vue
241
+ <Button v-if="client.roles.includes('admin')" label="Admin" />
242
+ <div v-if="!client.user">Zaloguj się</div>
243
+ ```
244
+
245
+ ## Locale i czas
246
+
247
+ - Wywołaj `useLocale().captureLocale()` w `App.vue` żeby zapisać locale przeglądarki do backendu.
248
+ - Używaj `locale.localTime(date)` z `d()` z vue-i18n do wyświetlania dat bezpiecznego dla SSR.
249
+ - `currentTime` z `@live-change/frontend-base` to reaktywny ref który tykuje co 500ms.
250
+ - `useTimeSynchronization()` z `@live-change/vue3-ssr` koryguje różnicę zegarów – używaj gdy timing jest krytyczny (odliczanie, eventy real-time).
251
+
252
+ ## Analityka
253
+
254
+ Używaj `analytics` z `@live-change/vue3-components`:
255
+
256
+ ```js
257
+ import { analytics } from '@live-change/vue3-components'
258
+ analytics.emit('article:published', { articleId: id })
259
+ ```
260
+
261
+ Podpinaj providery (PostHog, GA4) w osobnym pliku importowanym z `App.vue`.
262
+
263
+ ## SSR i konfiguracja frontendu
264
+
265
+ - Utrzymuj standardowy układ entry points:
266
+
267
+ ```js
268
+ // entry-client.js
269
+ import { clientEntry } from '@live-change/frontend-base/client-entry.js'
270
+ export default clientEntry(App, createRouter, config)
271
+
272
+ // entry-server.js
273
+ import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
274
+ export const render = serverEntry(App, createRouter, config)
275
+ export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
276
+ ```
277
+
278
+ - Konfigurację PrimeVue trzymaj w jednym miejscu (`config.js`) i używaj `definePreset` do nadpisania motywu.
279
+
280
+ ## Odkrywanie widoków i akcji z `describe`
281
+
282
+ Komenda CLI `describe` pozwala znaleźć dostępne widoki (do `live()`) i akcje (do `api.command` / `editorData` / `actionData`):
283
+
284
+ ```bash
285
+ node server/start.js describe --service blog
286
+ node server/start.js describe --service blog --view articlesByCreatedAt --output yaml
287
+ node server/start.js describe --service blog --action createMyUserArticle --output yaml
288
+ ```
289
+
290
+ To najszybszy sposób na odkrycie jakie ścieżki i akcje są dostępne, w tym te wygenerowane automatycznie przez relacje.
@@ -0,0 +1,107 @@
1
+ ---
2
+ description: LiveChange service file structure — every service must be a directory with separate definition.js, index.js, etc.
3
+ globs: server/**/*.js
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # LiveChange Service Structure
8
+
9
+ Every LiveChange service **must** be a directory, not a single file.
10
+
11
+ ## Required structure
12
+
13
+ ```
14
+ server/<serviceName>/
15
+ definition.js # creates app.createServiceDefinition({ name }) – nothing else
16
+ index.js # imports definition, imports all domain files, exports definition
17
+ config.js # required – resolves definition.config, sets definition.clientConfig, exports plain config object
18
+ <domain>.js # one file per domain area (models, views, actions, triggers)
19
+ ```
20
+
21
+ ## definition.js
22
+
23
+ Only creates and exports the definition. No models, no actions here.
24
+
25
+ ```js
26
+ import App from '@live-change/framework'
27
+ const app = App.app()
28
+
29
+ const definition = app.createServiceDefinition({ name: 'myService' })
30
+
31
+ export default definition
32
+ ```
33
+
34
+ ## index.js
35
+
36
+ Imports definition and all domain files (side-effect imports), then re-exports definition.
37
+
38
+ ```js
39
+ import App from '@live-change/framework'
40
+ const app = App.app()
41
+
42
+ import definition from './definition.js'
43
+
44
+ import './authenticator.js'
45
+ import './myModel.js'
46
+ import './otherModel.js'
47
+
48
+ export default definition
49
+ ```
50
+
51
+ ## config.js (required)
52
+
53
+ Reads `definition.config` (passed from `app.config.js`), resolves defaults, sets `definition.clientConfig` (for frontend),
54
+ and exports a plain config object used by other domain files.
55
+
56
+ ```js
57
+ import definition from './definition.js'
58
+
59
+ const {
60
+ defaultAccess = {
61
+ writeAccessControl: ['owner', 'admin'],
62
+ readAllAccess: ['admin']
63
+ }
64
+ } = definition.config || {}
65
+
66
+ definition.clientConfig = {
67
+ // values exposed to frontend, if needed
68
+ }
69
+
70
+ export default { defaultAccess }
71
+ ```
72
+
73
+ ## Domain files (e.g. myModel.js)
74
+
75
+ Each file imports `definition` (and `config` if needed) and registers models/views/actions/triggers.
76
+
77
+ ```js
78
+ import App from '@live-change/framework'
79
+ const app = App.app()
80
+
81
+ import definition from './definition.js'
82
+ import config from './config.js'
83
+
84
+ export const MyModel = definition.model({ name: 'MyModel', ... })
85
+
86
+ definition.view({ name: 'myList', ... })
87
+ definition.action({ name: 'doThing', ... })
88
+ ```
89
+
90
+ Rule of thumb:
91
+
92
+ - Domain files should read configuration from `config` (exported from `config.js`), not directly from `definition.config`.
93
+
94
+ ## services.list.js import
95
+
96
+ Always import from the directory index, not a flat file:
97
+
98
+ ```js
99
+ import myService from './myService/index.js' // ✓
100
+ import myService from './myService.js' // ✗
101
+ ```
102
+
103
+ ## Why
104
+
105
+ A flat single-file service makes it hard to add new domain files (auth, models, triggers, etc.)
106
+ without the file growing unmanageably. The directory structure mirrors the pattern used in
107
+ `@live-change/live-change-stack/services/*`.