@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.
- package/.claude/rules/live-change-backend-actions-views-triggers.md +246 -0
- package/.claude/rules/live-change-backend-architecture.md +126 -0
- package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +260 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +317 -0
- package/.claude/rules/live-change-service-structure.md +89 -0
- package/.claude/settings.json +32 -0
- package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
- package/.claude/skills/create-skills-and-rules.md +196 -0
- package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
- package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
- package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
- package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
- package/.claude/skills/live-change-design-models-relations.md +173 -0
- package/.claude/skills/live-change-design-service/SKILL.md +133 -0
- package/.claude/skills/live-change-design-service.md +132 -0
- package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
- package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
- package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
- package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
- package/.claude/skills/live-change-frontend-action-form.md +143 -0
- package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
- package/.claude/skills/live-change-frontend-analytics.md +146 -0
- package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
- package/.claude/skills/live-change-frontend-command-forms.md +215 -0
- package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
- package/.claude/skills/live-change-frontend-data-views.md +182 -0
- package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
- package/.claude/skills/live-change-frontend-editor-form.md +177 -0
- package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
- package/.claude/skills/live-change-frontend-locale-time.md +171 -0
- package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
- package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-range-list.md +128 -0
- package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
- package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +290 -0
- package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
- package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +256 -0
- package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
- package/.cursor/rules/live-change-service-structure.mdc +107 -0
- package/.cursor/skills/create-skills-and-rules.md +248 -0
- package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +296 -0
- package/.cursor/skills/live-change-design-models-relations.md +230 -0
- package/.cursor/skills/live-change-design-service.md +76 -0
- package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +129 -0
- package/.cursor/skills/live-change-frontend-action-form.md +149 -0
- package/.cursor/skills/live-change-frontend-analytics.md +147 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +216 -0
- package/.cursor/skills/live-change-frontend-data-views.md +183 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +240 -0
- package/.cursor/skills/live-change-frontend-locale-time.md +172 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +201 -0
- package/.cursor/skills/live-change-frontend-range-list.md +129 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +120 -0
- package/README.md +71 -0
- package/front/src/router.js +2 -1
- package/opencode.json +10 -0
- package/package.json +52 -50
- package/server/app.config.js +35 -0
- package/server/services.list.js +2 -0
- package/.nx/workspace-data/file-map.json +0 -195
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +0 -8
- package/.nx/workspace-data/project-graph.lock +0 -0
- package/.nx/workspace-data/source-maps.json +0 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for defining models, relations, indexes and access control in LiveChange
|
|
3
|
+
globs: **/services/**/*.js
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LiveChange backend – models and relations (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use these rules when defining or editing models and relations in LiveChange services.
|
|
9
|
+
|
|
10
|
+
## General style for models
|
|
11
|
+
|
|
12
|
+
- Put models in domain files imported from the service `index.js`.
|
|
13
|
+
- Prefer **readable, multi-line** property definitions.
|
|
14
|
+
- Avoid squeezing `type`, `default`, `validation` into a single unreadable line.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
properties: {
|
|
18
|
+
name: {
|
|
19
|
+
type: String,
|
|
20
|
+
validation: ['nonEmpty']
|
|
21
|
+
},
|
|
22
|
+
status: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: 'offline'
|
|
25
|
+
},
|
|
26
|
+
capabilities: {
|
|
27
|
+
type: Array,
|
|
28
|
+
of: {
|
|
29
|
+
type: String
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## `userItem` – belongs to the signed-in user
|
|
36
|
+
|
|
37
|
+
Use when the model is owned by the currently signed-in user.
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
definition.model({
|
|
41
|
+
name: 'Device',
|
|
42
|
+
properties: {
|
|
43
|
+
name: {
|
|
44
|
+
type: String
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
userItem: {
|
|
48
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
49
|
+
writeAccessControl: { roles: ['owner', 'admin'] },
|
|
50
|
+
writeableProperties: ['name']
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This automatically generates:
|
|
56
|
+
|
|
57
|
+
- “my X” views (list + single),
|
|
58
|
+
- basic CRUD actions for the owner.
|
|
59
|
+
|
|
60
|
+
## `itemOf` – child belongs to a parent
|
|
61
|
+
|
|
62
|
+
Use for lists of items related to another model.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
definition.model({
|
|
66
|
+
name: 'DeviceConnection',
|
|
67
|
+
properties: {
|
|
68
|
+
connectionType: {
|
|
69
|
+
type: String
|
|
70
|
+
},
|
|
71
|
+
status: {
|
|
72
|
+
type: String,
|
|
73
|
+
default: 'offline'
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
itemOf: {
|
|
77
|
+
what: Device,
|
|
78
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
79
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Guidelines:
|
|
85
|
+
|
|
86
|
+
- `what` must point to a model defined earlier (in this or another service).
|
|
87
|
+
- The relation generates standard views/actions for listing and managing children.
|
|
88
|
+
|
|
89
|
+
## `propertyOf` – one-to-one property, id = parent id
|
|
90
|
+
|
|
91
|
+
Use when the model represents a single “state object” for a parent, with the same id.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
definition.model({
|
|
95
|
+
name: 'DeviceCursorState',
|
|
96
|
+
properties: {
|
|
97
|
+
x: { type: Number },
|
|
98
|
+
y: { type: Number }
|
|
99
|
+
},
|
|
100
|
+
propertyOf: {
|
|
101
|
+
what: Device,
|
|
102
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
103
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Effects:
|
|
109
|
+
|
|
110
|
+
- You can fetch it directly as `DeviceCursorState.get(deviceId)`.
|
|
111
|
+
- No need for an extra `device` field or index for lookups by parent id.
|
|
112
|
+
|
|
113
|
+
## `propertyOf` with multiple parents (1:1 link to each)
|
|
114
|
+
|
|
115
|
+
Sometimes a model is a dedicated 1:1 link between entities (for example: invoice ↔ contractor in a specific role).
|
|
116
|
+
Most commonly this is 1–2 parents, but `propertyOf` can point to **any number** of parent models (including 3+), if that matches the domain semantics.
|
|
117
|
+
|
|
118
|
+
In that case:
|
|
119
|
+
|
|
120
|
+
- avoid storing the “other side id” as a plain `contractorId` / `someId` property
|
|
121
|
+
- avoid adding ad-hoc `...Id` fields in a relation model just to “join” entities — CRUD/relations generators won’t treat it as a relation
|
|
122
|
+
- instead, define the relation as `propertyOf` to **each** parent so the relations/CRUD generator understands the model is connecting entities.
|
|
123
|
+
|
|
124
|
+
Example (schematic):
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
|
|
128
|
+
const Contractor = definition.foreignModel('company', 'Contractor')
|
|
129
|
+
|
|
130
|
+
definition.model({
|
|
131
|
+
name: 'Supplier',
|
|
132
|
+
properties: {
|
|
133
|
+
// optional extra fields
|
|
134
|
+
},
|
|
135
|
+
propertyOf: [
|
|
136
|
+
{ what: CostInvoice },
|
|
137
|
+
{ what: Contractor }
|
|
138
|
+
]
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## `foreignModel` – parent from another service
|
|
143
|
+
|
|
144
|
+
Use when `itemOf` / `propertyOf` refers to a model from a different service.
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
const Device = definition.foreignModel('deviceManager', 'Device')
|
|
148
|
+
|
|
149
|
+
definition.model({
|
|
150
|
+
name: 'BotSession',
|
|
151
|
+
properties: {
|
|
152
|
+
// ...
|
|
153
|
+
},
|
|
154
|
+
itemOf: {
|
|
155
|
+
what: Device,
|
|
156
|
+
readAccessControl: { roles: ['owner', 'admin'] }
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Notes:
|
|
162
|
+
|
|
163
|
+
- First argument is the service name.
|
|
164
|
+
- Second is the model name in that service.
|
|
165
|
+
|
|
166
|
+
## Auto-added fields from relations
|
|
167
|
+
|
|
168
|
+
Relations automatically add **identifier fields** and **indexes** to the model. Do **not** re-declare these in `properties`.
|
|
169
|
+
|
|
170
|
+
**Naming convention:** field name = parent model name with first letter lowercased (`Device` → `device`, `CostInvoice` → `costInvoice`).
|
|
171
|
+
|
|
172
|
+
| Relation | Field(s) auto-added | Index(es) auto-added |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `itemOf: { what: Device }` | `device` | `byDevice` |
|
|
175
|
+
| `propertyOf: { what: Device }` | `device` | `byDevice` |
|
|
176
|
+
| `userItem` | `user` | `byUser` |
|
|
177
|
+
| `userProperty` | `user` | `byUser` |
|
|
178
|
+
| `sessionOrUserProperty` | `sessionOrUserType`, `sessionOrUser` | `bySessionOrUser` (hash) |
|
|
179
|
+
| `sessionOrUserProperty: { extendedWith: ['object'] }` | + `objectType`, `object` | composite indexes |
|
|
180
|
+
| `propertyOfAny: { to: ['owner'] }` | `ownerType`, `owner` | `byOwner` (hash) |
|
|
181
|
+
| `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
|
|
182
|
+
|
|
183
|
+
For multi-parent relations (e.g. `propertyOf: [{ what: A }, { what: B }]`), all index combinations are created (`byA`, `byB`, `byAAndB`).
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// ✅ Correct — only define YOUR fields
|
|
187
|
+
definition.model({
|
|
188
|
+
name: 'Connection',
|
|
189
|
+
properties: {
|
|
190
|
+
status: { type: String } // 'device' is NOT here — auto-added by itemOf
|
|
191
|
+
},
|
|
192
|
+
itemOf: { what: Device } // adds 'device' field + 'byDevice' index
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// ❌ Wrong — redundant field
|
|
196
|
+
definition.model({
|
|
197
|
+
name: 'Connection',
|
|
198
|
+
properties: {
|
|
199
|
+
device: { type: String }, // ❌ already added by itemOf
|
|
200
|
+
status: { type: String }
|
|
201
|
+
},
|
|
202
|
+
itemOf: { what: Device }
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Use `node server/start.js describe --service myService --model MyModel --output yaml` to see all fields including auto-added ones.
|
|
207
|
+
|
|
208
|
+
## Indexes
|
|
209
|
+
|
|
210
|
+
- Declare indexes explicitly when you frequently query by a field or field combination.
|
|
211
|
+
- Use descriptive names like `bySessionKey`, `byDeviceAndStatus`, etc.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
indexes: {
|
|
215
|
+
bySessionKey: {
|
|
216
|
+
property: ['sessionKey']
|
|
217
|
+
},
|
|
218
|
+
byDeviceAndStatus: {
|
|
219
|
+
property: ['device', 'status']
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
In other services, the same index may be visible with a prefixed name such as `myService_Model_byDeviceAndStatus`.
|
|
225
|
+
|
|
226
|
+
## Access control on relations
|
|
227
|
+
|
|
228
|
+
- Always set `readAccessControl` and `writeAccessControl` on relations (`userItem`, `itemOf`, `propertyOf`).
|
|
229
|
+
- Treat access control as part of the model definition, not an afterthought.
|
|
230
|
+
|
|
231
|
+
## `entity` models – granting access on creation
|
|
232
|
+
|
|
233
|
+
Models with `entity` and `writeAccessControl` / `readAccessControl` check roles on every CRUD operation, but do **not** auto-grant roles to the creator. You must add a change trigger to grant the creator `'owner'` (or other roles) after creation:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
definition.trigger({
|
|
237
|
+
name: 'changeMyService_MyModel',
|
|
238
|
+
properties: {
|
|
239
|
+
object: { type: MyModel, validation: ['nonEmpty'] },
|
|
240
|
+
data: { type: Object },
|
|
241
|
+
oldData: { type: Object }
|
|
242
|
+
},
|
|
243
|
+
async execute({ object, data, oldData }, { client, triggerService }) {
|
|
244
|
+
if (!data || oldData) return // only on create
|
|
245
|
+
if (!client?.user) return
|
|
246
|
+
|
|
247
|
+
await triggerService({ service: 'accessControl', type: 'accessControl_setAccess' }, {
|
|
248
|
+
objectType: 'myService_MyModel',
|
|
249
|
+
object,
|
|
250
|
+
roles: ['owner'],
|
|
251
|
+
sessionOrUserType: 'user_User',
|
|
252
|
+
sessionOrUser: client.user,
|
|
253
|
+
lastUpdate: new Date()
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Without this trigger, the creator cannot read or modify their own object. The `objectType` format is `serviceName_ModelName`.
|
|
260
|
+
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for Vue 3, PrimeVue, Tailwind frontend development on LiveChange
|
|
3
|
+
globs: **/front/src/**/*.{vue,js,ts}
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend on live-change-stack – Vue 3 + PrimeVue + Tailwind (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use these rules when working on frontends that talk to LiveChange backends.
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
- Vue 3 + TypeScript
|
|
13
|
+
- PrimeVue 4 for UI components
|
|
14
|
+
- Tailwind CSS for styling (prefer utility classes, avoid unnecessary custom CSS)
|
|
15
|
+
- vite-plugin-pages for file-based routing in `src/pages/`
|
|
16
|
+
- `@live-change/vue3-ssr` for integration with the backend
|
|
17
|
+
|
|
18
|
+
## Data loading – `live` + `Promise.all` (Suspense)
|
|
19
|
+
|
|
20
|
+
- **Do not** use `ref(null)` + `onMounted` to fetch data.
|
|
21
|
+
- Always fetch data using `await Promise.all([...])` and `live(path()...)` in `script setup`.
|
|
22
|
+
- The root app should wrap pages with `<Suspense>` (usually handled by `ViewRoot` in live-change frontends).
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { path, live, api as useApi } from '@live-change/vue3-ssr'
|
|
28
|
+
|
|
29
|
+
const api = useApi()
|
|
30
|
+
|
|
31
|
+
const [devices] = await Promise.all([
|
|
32
|
+
live(path().deviceManager.myUserDevices({}))
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
const [device, connections] = await Promise.all([
|
|
36
|
+
live(path().deviceManager.myUserDevice({ device: deviceId })),
|
|
37
|
+
live(path().deviceManager.deviceOwnedDeviceConnections({ device: deviceId }))
|
|
38
|
+
])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In templates, use the `.value` of these refs:
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<template>
|
|
45
|
+
<div v-if="device.value">
|
|
46
|
+
{{ device.value.name }}
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Commands and forms – choosing the right pattern
|
|
52
|
+
|
|
53
|
+
There are 4 ways to execute backend actions. Use the right one:
|
|
54
|
+
|
|
55
|
+
| Pattern | When to use |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `editorData` | **Editing model records** (create/update). Drafts, validation, `AutoField`. Use for settings, editors, profiles. |
|
|
58
|
+
| `actionData` | **One-shot action forms** (not CRUD). Submit once → done. Use for publish, invite, import. |
|
|
59
|
+
| `api.command` | **Single button or programmatic calls** (no form fields). Use for delete, toggle, code-triggered actions. |
|
|
60
|
+
| `<command-form>` | **Avoid.** Legacy, only for trivial prototypes. Prefer `editorData` or `actionData`. |
|
|
61
|
+
|
|
62
|
+
Decision flow:
|
|
63
|
+
|
|
64
|
+
1. Does the user fill in form fields? → **No**: use `api.command` (wrap in `workingZone.addPromise` for buttons).
|
|
65
|
+
2. Is it editing a model record (create/update)? → **Yes**: use `editorData`. **No**: use `actionData`.
|
|
66
|
+
3. Only use `<command-form>` for the simplest throwaway cases.
|
|
67
|
+
|
|
68
|
+
## Form validation feedback
|
|
69
|
+
|
|
70
|
+
Every field in a form using `editorData` or `actionData` **must** show validation errors. Never use bare `InputText`, `Dropdown`, or other PrimeVue inputs without error feedback.
|
|
71
|
+
|
|
72
|
+
Three approaches (pick whichever fits the layout):
|
|
73
|
+
|
|
74
|
+
1. **AutoField without slot** — auto-picks input and shows errors. Simplest, use by default.
|
|
75
|
+
2. **AutoField with slot** — wrap a custom input inside `<AutoField>`. Still renders label + error automatically.
|
|
76
|
+
3. **Manual `Message`** — add `<Message v-if="editor.propertiesErrors?.field" severity="error" variant="simple" size="small">` below the input. Use when AutoField wrapper doesn't fit.
|
|
77
|
+
|
|
78
|
+
Always pass `:error="editor.propertiesErrors?.fieldName"` (or `formData.propertiesErrors?.fieldName` for `actionData`).
|
|
79
|
+
|
|
80
|
+
## Form element requirement
|
|
81
|
+
|
|
82
|
+
Forms using `editorData` or `actionData` with `EditorButtons` or `ActionButtons` **must** be wrapped in a `<form>` element with submit/reset handlers:
|
|
83
|
+
|
|
84
|
+
```vue
|
|
85
|
+
<!-- editorData -->
|
|
86
|
+
<form @submit.prevent="editor.save()" @reset.prevent="editor.reset()">
|
|
87
|
+
|
|
88
|
+
<!-- actionData -->
|
|
89
|
+
<form @submit.prevent="formData.submit()" @reset.prevent="formData.reset()">
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`EditorButtons` and `ActionButtons` use `type="submit"` / `type="reset"` on their internal buttons. Without a `<form>` parent, these buttons do nothing.
|
|
93
|
+
|
|
94
|
+
### `api.command`
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
await api.command(['deviceManager', 'createMyUserDevice'], {
|
|
98
|
+
name: 'My device'
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], {
|
|
102
|
+
device: id
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Format:
|
|
107
|
+
|
|
108
|
+
- `['serviceName', 'actionName']` as the first argument,
|
|
109
|
+
- payload object as the second argument.
|
|
110
|
+
|
|
111
|
+
## Routing – `<route>` block and `meta.signedIn`
|
|
112
|
+
|
|
113
|
+
- Each page in `src/pages/` can declare its route meta in a `<route>` block.
|
|
114
|
+
- Use `meta.signedIn` for pages that require authentication.
|
|
115
|
+
|
|
116
|
+
```vue
|
|
117
|
+
<route>
|
|
118
|
+
{ "name": "devices", "meta": { "signedIn": true } }
|
|
119
|
+
</route>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Dynamic routes:
|
|
123
|
+
|
|
124
|
+
- `[id].vue` corresponds to `/devices/:id`.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { useRoute } from 'vue-router'
|
|
128
|
+
|
|
129
|
+
const route = useRoute()
|
|
130
|
+
const deviceId = route.params.id
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Confirm + Toast for destructive actions
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
137
|
+
import { useToast } from 'primevue/usetoast'
|
|
138
|
+
|
|
139
|
+
const confirm = useConfirm()
|
|
140
|
+
const toast = useToast()
|
|
141
|
+
|
|
142
|
+
function deleteDevice(id) {
|
|
143
|
+
confirm.require({
|
|
144
|
+
message: 'Are you sure you want to delete this device?',
|
|
145
|
+
header: 'Confirmation',
|
|
146
|
+
icon: 'pi pi-exclamation-triangle',
|
|
147
|
+
accept: async () => {
|
|
148
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], { device: id })
|
|
149
|
+
toast.add({
|
|
150
|
+
severity: 'success',
|
|
151
|
+
summary: 'Deleted',
|
|
152
|
+
life: 2000
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Common UI patterns – list and detail
|
|
160
|
+
|
|
161
|
+
### List page
|
|
162
|
+
|
|
163
|
+
```vue
|
|
164
|
+
<template>
|
|
165
|
+
<div class="container mx-auto p-4">
|
|
166
|
+
<div class="flex items-center justify-between mb-6">
|
|
167
|
+
<h1 class="text-2xl font-bold">Devices</h1>
|
|
168
|
+
<Button label="Add" icon="pi pi-plus" @click="openDialog" />
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<Card v-if="devices.value?.length === 0">
|
|
172
|
+
<template #content>
|
|
173
|
+
<p class="text-center text-gray-500">
|
|
174
|
+
No devices yet
|
|
175
|
+
</p>
|
|
176
|
+
</template>
|
|
177
|
+
</Card>
|
|
178
|
+
|
|
179
|
+
<div class="grid gap-4">
|
|
180
|
+
<Card v-for="device in devices.value" :key="device.id">
|
|
181
|
+
<template #content>
|
|
182
|
+
<!-- content -->
|
|
183
|
+
</template>
|
|
184
|
+
</Card>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</template>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Status tag
|
|
191
|
+
|
|
192
|
+
```vue
|
|
193
|
+
<Tag
|
|
194
|
+
:value="conn.status"
|
|
195
|
+
:severity="conn.status === 'online' ? 'success' : 'secondary'"
|
|
196
|
+
/>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Computed paths – reactive parameters
|
|
200
|
+
|
|
201
|
+
When paths depend on reactive values (route params, props), wrap them in `computed()`:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
import { computed, unref } from 'vue'
|
|
205
|
+
import { usePath, live } from '@live-change/vue3-ssr'
|
|
206
|
+
|
|
207
|
+
const path = usePath()
|
|
208
|
+
|
|
209
|
+
const articlePath = computed(() => path.blog.article({ article: unref(articleId) }))
|
|
210
|
+
const [article] = await Promise.all([live(articlePath)])
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
For conditional loading (e.g. only when logged in), return a falsy value:
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
217
|
+
const client = useClient()
|
|
218
|
+
|
|
219
|
+
const myDataPath = computed(() => client.value.user && path.blog.myArticles({}))
|
|
220
|
+
const [myData] = await Promise.all([live(myDataPath)])
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Related data – `.with()`
|
|
224
|
+
|
|
225
|
+
Attach related objects to items in a single reactive query:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
path.blog.articles({})
|
|
229
|
+
.with(article => path.userIdentification.identification({
|
|
230
|
+
sessionOrUserType: article.authorType,
|
|
231
|
+
sessionOrUser: article.author
|
|
232
|
+
}).bind('authorProfile'))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Access: `article.authorProfile?.firstName`. Works with both `live()` and `RangeViewer`.
|
|
236
|
+
|
|
237
|
+
## WorkingZone for async actions
|
|
238
|
+
|
|
239
|
+
`ViewRoot` wraps every page in `<WorkingZone>`. Use `inject('workingZone')` for non-form button actions:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
import { inject } from 'vue'
|
|
243
|
+
const workingZone = inject('workingZone')
|
|
244
|
+
|
|
245
|
+
function doAction() {
|
|
246
|
+
workingZone.addPromise('actionName', (async () => {
|
|
247
|
+
await actions.blog.publishArticle({ article: id })
|
|
248
|
+
toast.add({ severity: 'success', summary: 'Published', life: 2000 })
|
|
249
|
+
})())
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This activates the global loading spinner/blur while the promise is pending.
|
|
254
|
+
|
|
255
|
+
## Auth guards with `useClient`
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
259
|
+
const client = useClient()
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- `client.value.user` – truthy when logged in
|
|
263
|
+
- `client.value.roles` – array of roles (e.g. `['admin', 'owner']`)
|
|
264
|
+
|
|
265
|
+
Use in templates:
|
|
266
|
+
|
|
267
|
+
```vue
|
|
268
|
+
<Button v-if="client.roles.includes('admin')" label="Admin" />
|
|
269
|
+
<div v-if="!client.user">Please sign in</div>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Locale and time
|
|
273
|
+
|
|
274
|
+
- Call `useLocale().captureLocale()` in `App.vue` to save browser locale to backend.
|
|
275
|
+
- Use `locale.localTime(date)` with vue-i18n's `d()` for SSR-safe date display.
|
|
276
|
+
- `currentTime` from `@live-change/frontend-base` is a reactive ref that ticks every 500ms.
|
|
277
|
+
- `useTimeSynchronization()` from `@live-change/vue3-ssr` corrects clock skew – use when timing is critical (countdowns, real-time events).
|
|
278
|
+
|
|
279
|
+
## Analytics
|
|
280
|
+
|
|
281
|
+
Use `analytics` from `@live-change/vue3-components`:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
import { analytics } from '@live-change/vue3-components'
|
|
285
|
+
analytics.emit('article:published', { articleId: id })
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Wire providers (PostHog, GA4) in a separate file imported from `App.vue`.
|
|
289
|
+
|
|
290
|
+
## SSR and frontend configuration
|
|
291
|
+
|
|
292
|
+
- Keep separate entry points for client and server:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
// entry-client.js
|
|
296
|
+
import { clientEntry } from '@live-change/frontend-base/client-entry.js'
|
|
297
|
+
export default clientEntry(App, createRouter, config)
|
|
298
|
+
|
|
299
|
+
// entry-server.js
|
|
300
|
+
import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
|
|
301
|
+
export const render = serverEntry(App, createRouter, config)
|
|
302
|
+
export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- Configure PrimeVue theme in one place (e.g. `config.js`) using `definePreset` and keep dark mode options consistent.
|
|
306
|
+
|
|
307
|
+
## Discovering views and actions with `describe`
|
|
308
|
+
|
|
309
|
+
Use the CLI `describe` command to find available views (for `live()`) and actions (for `api.command` / `editorData` / `actionData`):
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
node server/start.js describe --service blog
|
|
313
|
+
node server/start.js describe --service blog --view articlesByCreatedAt --output yaml
|
|
314
|
+
node server/start.js describe --service blog --action createMyUserArticle --output yaml
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
This is the fastest way to discover what paths and actions are available, including those auto-generated by relations.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for LiveChange service directory structure and file organization
|
|
3
|
+
globs: **/services/**/*.js
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LiveChange Service Structure
|
|
7
|
+
|
|
8
|
+
Every LiveChange service **must** be a directory, not a single file.
|
|
9
|
+
|
|
10
|
+
## Required structure
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
server/services/<serviceName>/
|
|
14
|
+
definition.js # creates app.createServiceDefinition({ name }) – nothing else
|
|
15
|
+
index.js # imports definition, imports all domain files, exports definition
|
|
16
|
+
config.js # optional – reads definition.config, exports resolved config object
|
|
17
|
+
<domain>.js # one file per domain area (models, views, actions, triggers)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## definition.js
|
|
21
|
+
|
|
22
|
+
Only creates and exports the definition. No models, no actions here.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import App from '@live-change/framework'
|
|
26
|
+
const app = App.app()
|
|
27
|
+
|
|
28
|
+
const definition = app.createServiceDefinition({ name: 'myService' })
|
|
29
|
+
|
|
30
|
+
export default definition
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## index.js
|
|
34
|
+
|
|
35
|
+
Imports definition and all domain files (side-effect imports), then re-exports definition.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import App from '@live-change/framework'
|
|
39
|
+
const app = App.app()
|
|
40
|
+
|
|
41
|
+
import definition from './definition.js'
|
|
42
|
+
|
|
43
|
+
import './authenticator.js'
|
|
44
|
+
import './myModel.js'
|
|
45
|
+
import './otherModel.js'
|
|
46
|
+
|
|
47
|
+
export default definition
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## config.js (if needed)
|
|
51
|
+
|
|
52
|
+
Reads `definition.config` set from `app.config.js`, resolves defaults, exports plain object.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import definition from './definition.js'
|
|
56
|
+
|
|
57
|
+
const { someOption = 'default' } = definition.config
|
|
58
|
+
|
|
59
|
+
export default { someOption }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Domain files (e.g. myModel.js)
|
|
63
|
+
|
|
64
|
+
Each file imports `definition` (and `config` if needed) and registers models/views/actions/triggers.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import App from '@live-change/framework'
|
|
68
|
+
const app = App.app()
|
|
69
|
+
|
|
70
|
+
import definition from './definition.js'
|
|
71
|
+
|
|
72
|
+
export const MyModel = definition.model({ name: 'MyModel', ... })
|
|
73
|
+
|
|
74
|
+
definition.view({ name: 'myList', ... })
|
|
75
|
+
definition.action({ name: 'doThing', ... })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## services.list.js import
|
|
79
|
+
|
|
80
|
+
Always import from the directory index, not a flat file:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import myService from './services/myService/index.js' // correct
|
|
84
|
+
import myService from './services/myService.js' // wrong
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Reference implementation
|
|
88
|
+
|
|
89
|
+
See `/home/m8/IdeaProjects/live-change/live-change-stack/services/stripe-service/` as the canonical example.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(ls -d /home/m8/IdeaProjects/live-change/*/.claude/skills/)",
|
|
5
|
+
"Bash(ls -d /home/m8/IdeaProjects/live-change/*/.cursor/skills/)",
|
|
6
|
+
"Bash(node -e \"const p = require.resolve\\(''''@live-change/vue3-components''''\\); console.log\\(p\\)\")",
|
|
7
|
+
"Bash(find /home/m8/IdeaProjects/live-change/live-change-stack -path *vue3-components* -name *.js)",
|
|
8
|
+
"Read(//home/m8/IdeaProjects/**)",
|
|
9
|
+
"Bash(find /home/m8/IdeaProjects/live-change -name \"*.vue\" -type f ! -path \"*/node_modules/*\" ! -path \"*/.history/*\" -exec grep -l \"AutoField\" {})",
|
|
10
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/skills/live-change-frontend-editor-form/SKILL.md /home/m8/IdeaProjects/live-change/.cursor/skills/live-change-frontend-editor-form.md)",
|
|
11
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/skills/live-change-frontend-action-form/SKILL.md /home/m8/IdeaProjects/live-change/.cursor/skills/live-change-frontend-action-form.md)",
|
|
12
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-frontend-vue-primevue.md /home/m8/IdeaProjects/live-change/auto-firma/.claude/rules/live-change-frontend-vue-primevue.md)",
|
|
13
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-frontend-vue-primevue.md /home/m8/IdeaProjects/live-change/automation/.claude/rules/live-change-frontend-vue-primevue.md)",
|
|
14
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-frontend-vue-primevue.md /home/m8/IdeaProjects/live-change/live-change-stack/frontend/frontend-template/.claude/rules/live-change-frontend-vue-primevue.md)",
|
|
15
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/skills/live-change-design-models-relations/SKILL.md /home/m8/IdeaProjects/live-change/.cursor/skills/live-change-design-models-relations.md)",
|
|
16
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/skills/live-change-design-actions-views-triggers/SKILL.md /home/m8/IdeaProjects/live-change/.cursor/skills/live-change-design-actions-views-triggers.md)",
|
|
17
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-backend-actions-views-triggers.md /home/m8/IdeaProjects/live-change/auto-firma/.claude/rules/live-change-backend-actions-views-triggers.md)",
|
|
18
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-backend-models-and-relations.md /home/m8/IdeaProjects/live-change/auto-firma/.claude/rules/live-change-backend-models-and-relations.md)",
|
|
19
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-backend-actions-views-triggers.md /home/m8/IdeaProjects/live-change/automation/.claude/rules/live-change-backend-actions-views-triggers.md)",
|
|
20
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-backend-models-and-relations.md /home/m8/IdeaProjects/live-change/automation/.claude/rules/live-change-backend-models-and-relations.md)",
|
|
21
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-backend-event-sourcing.md /home/m8/IdeaProjects/live-change/auto-firma/.claude/rules/live-change-backend-event-sourcing.md)",
|
|
22
|
+
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-backend-event-sourcing.md /home/m8/IdeaProjects/live-change/automation/.claude/rules/live-change-backend-event-sourcing.md)",
|
|
23
|
+
"Bash(find /home/m8/IdeaProjects/live-change/live-change-stack/framework -type f \\\\\\(-name *.ts -o -name *.js \\\\\\))",
|
|
24
|
+
"Bash(find /home/m8/IdeaProjects/live-change/live-change-stack/services -type f -name *.js)",
|
|
25
|
+
"Bash(grep -r \"userItem\\\\|userProperty\" /home/m8/IdeaProjects/live-change/live-change-stack/services/user-service --include=*.js)"
|
|
26
|
+
],
|
|
27
|
+
"additionalDirectories": [
|
|
28
|
+
"/home/m8/IdeaProjects/live-change/.claude/skills/create-skills-and-rules",
|
|
29
|
+
"/home/m8/IdeaProjects/live-change/.claude/rules"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|