@sensolus/create-snt-agent-app 0.1.0 → 0.1.1

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 (42) hide show
  1. package/package.json +1 -1
  2. package/template/CLAUDE.md +218 -0
  3. package/template/Dockerfile +32 -0
  4. package/template/Jenkinsfile +28 -0
  5. package/template/README.md +477 -16
  6. package/template/_env.example +4 -0
  7. package/template/backend/app.py +630 -49
  8. package/template/backend/db_config.py +16 -0
  9. package/template/backend/extensions.py +3 -0
  10. package/template/backend/init_db.py +75 -0
  11. package/template/backend/migrations/README +1 -0
  12. package/template/backend/migrations/alembic.ini +50 -0
  13. package/template/backend/migrations/env.py +113 -0
  14. package/template/backend/migrations/script.py.mako +24 -0
  15. package/template/backend/migrations/versions/001_add_favourite_organisations.py +31 -0
  16. package/template/backend/migrations/versions/002_add_org_daily_stats.py +36 -0
  17. package/template/backend/models.py +31 -0
  18. package/template/backend/requirements.txt +8 -2
  19. package/template/eslint.config.js +6 -2
  20. package/template/index.html +11 -8
  21. package/template/infra/docker-compose.yml +15 -0
  22. package/template/openapi.json +40357 -0
  23. package/template/package.json +8 -1
  24. package/template/scripts/create-ecr-repo.sh +42 -0
  25. package/template/src/App.jsx +12 -34
  26. package/template/src/hooks/useFavourites.js +44 -0
  27. package/template/src/i18n/index.js +3 -0
  28. package/template/src/i18n/messages.js +8 -14
  29. package/template/src/i18n/translations/de.js +96 -0
  30. package/template/src/i18n/translations/en.js +103 -0
  31. package/template/src/i18n/translations/es.js +96 -0
  32. package/template/src/i18n/translations/fr.js +96 -0
  33. package/template/src/i18n/translations/nl.js +96 -0
  34. package/template/src/main.jsx +2 -3
  35. package/template/src/pages/Home.jsx +170 -0
  36. package/template/src/pages/OrganisationDetail.jsx +259 -0
  37. package/template/src/pages/OrganisationList.jsx +457 -0
  38. package/template/src/pages/Overview.jsx +199 -0
  39. package/template/src/pages/WidgetShowcase.jsx +522 -0
  40. package/template/src/styles/app.css +543 -4
  41. package/template/start-backend.sh +4 -0
  42. package/template/start-frontend.sh +3 -0
@@ -0,0 +1,522 @@
1
+ import { useState } from 'react'
2
+ import {
3
+ SntBadge,
4
+ SntButton,
5
+ SntButtonGroup,
6
+ SntCard,
7
+ SntCheckboxList,
8
+ SntColors,
9
+ SntDateRangePicker,
10
+ SntDialog,
11
+ SntGrid,
12
+ SntGridItem,
13
+ SntHistogram,
14
+ SntInput,
15
+ SntLoadingOverlay,
16
+ SntMap,
17
+ SntProgressBar,
18
+ SntSelect,
19
+ SntSidepanel,
20
+ SntFilterSection,
21
+ SntSpinner,
22
+ SntSummaryStat,
23
+ SntSwitch,
24
+ SntTable,
25
+ SntTabs,
26
+ SntTabPanel,
27
+ SntToolbar,
28
+ SntToolbarSpacer,
29
+ getDefaultDateRange,
30
+ } from '@sensolus/snt-agent-kit'
31
+
32
+ function Section({ title, description, children }) {
33
+ return (
34
+ <SntCard title={title}>
35
+ {description && (
36
+ <p style={{ color: 'var(--snt-grey)', marginBottom: 16, fontSize: 14 }}>
37
+ {description}
38
+ </p>
39
+ )}
40
+ <div className="showcase-section-body">{children}</div>
41
+ </SntCard>
42
+ )
43
+ }
44
+
45
+ function Example({ label, children }) {
46
+ return (
47
+ <div className="showcase-example">
48
+ <div className="showcase-example-label">{label}</div>
49
+ <div className="showcase-example-body">{children}</div>
50
+ </div>
51
+ )
52
+ }
53
+
54
+ const BADGE_VARIANTS = [
55
+ 'primary', 'secondary', 'success', 'warning', 'danger',
56
+ 'info', 'light', 'dark', 'orange', 'salmon', 'purple', 'emerald',
57
+ ]
58
+
59
+ const BUTTON_VARIANTS = ['primary', 'secondary', 'success', 'danger', 'warning', 'info']
60
+
61
+ const TABLE_DATA = [
62
+ { id: 1, name: 'Alpha Logistics', trackers: 142, status: 'ACTIVE' },
63
+ { id: 2, name: 'Beta Transport', trackers: 56, status: 'ACTIVE' },
64
+ { id: 3, name: 'Gamma Couriers', trackers: 8, status: 'INACTIVE' },
65
+ { id: 4, name: 'Delta Freight', trackers: 231, status: 'ACTIVE' },
66
+ { id: 5, name: 'Epsilon Shipping', trackers: 17, status: 'PENDING' },
67
+ ]
68
+
69
+ const STATUS_VARIANT = {
70
+ ACTIVE: 'success',
71
+ INACTIVE: 'secondary',
72
+ PENDING: 'warning',
73
+ }
74
+
75
+ const HISTOGRAM_BUCKETS = [
76
+ { start: 0, end: 10, count: 4 },
77
+ { start: 10, end: 20, count: 12 },
78
+ { start: 20, end: 30, count: 28 },
79
+ { start: 30, end: 40, count: 19 },
80
+ { start: 40, end: 50, count: 9 },
81
+ { start: 50, end: 60, count: 3 },
82
+ ]
83
+
84
+ const TAB_DEFS = [
85
+ { key: 'one', label: 'Tab one' },
86
+ { key: 'two', label: 'Tab two' },
87
+ { key: 'three', label: 'Tab three' },
88
+ ]
89
+
90
+ export function WidgetShowcase() {
91
+ const [inputValue, setInputValue] = useState('Hello world')
92
+ const [selectValue, setSelectValue] = useState('eu')
93
+ const [groupValue, setGroupValue] = useState('cards')
94
+ const [switchOn, setSwitchOn] = useState(true)
95
+ const [checkboxSelected, setCheckboxSelected] = useState(['Trackers', 'Geozones'])
96
+ const [dateRange, setDateRange] = useState(() => getDefaultDateRange('last_3_months'))
97
+ const [dialogOpen, setDialogOpen] = useState(false)
98
+ const [showOverlay, setShowOverlay] = useState(false)
99
+ const [sidepanelOpen, setSidepanelOpen] = useState(true)
100
+ const [showcaseTab, setShowcaseTab] = useState('one')
101
+
102
+ return (
103
+ <div className="page-container widget-showcase">
104
+ <SntCard>
105
+ <p style={{ margin: 0, color: 'var(--snt-grey)' }}>
106
+ Live reference of every Sensolus widget shipped with this app. Each card
107
+ shows the widget, what it&apos;s for, and a handful of common configurations.
108
+ Source lives in <code>src/widgets/</code>.
109
+ </p>
110
+ </SntCard>
111
+
112
+ {/* ------------------------------------------------------------------ */}
113
+ <Section
114
+ title="SntButton"
115
+ description="Primary action button. Use variant to express intent (primary, success, danger, ...)."
116
+ >
117
+ <Example label="Variants">
118
+ <div className="showcase-row">
119
+ {BUTTON_VARIANTS.map((v) => (
120
+ <SntButton key={v} variant={v}>{v}</SntButton>
121
+ ))}
122
+ </div>
123
+ </Example>
124
+ <Example label="Disabled">
125
+ <SntButton variant="primary" disabled>Disabled</SntButton>
126
+ </Example>
127
+ <Example label="With icon">
128
+ <SntButton variant="primary" icon={<span>+</span>}>Add item</SntButton>
129
+ </Example>
130
+ </Section>
131
+
132
+ {/* ------------------------------------------------------------------ */}
133
+ <Section
134
+ title="SntBadge"
135
+ description="Compact status / label pill. onChange receives a value, not an event."
136
+ >
137
+ <Example label="Variants">
138
+ <div className="showcase-row">
139
+ {BADGE_VARIANTS.map((v) => (
140
+ <SntBadge key={v} variant={v} text={v} />
141
+ ))}
142
+ </div>
143
+ </Example>
144
+ <Example label="Compact">
145
+ <SntBadge variant="success" text="ACTIVE" compact />
146
+ </Example>
147
+ </Section>
148
+
149
+ {/* ------------------------------------------------------------------ */}
150
+ <Section
151
+ title="SntInput"
152
+ description="Text input. onChange receives the value directly."
153
+ >
154
+ <Example label="Text">
155
+ <SntInput value={inputValue} onChange={setInputValue} placeholder="Type something..." />
156
+ </Example>
157
+ <Example label="Password">
158
+ <SntInput type="password" value="hunter2" onChange={() => {}} />
159
+ </Example>
160
+ <Example label="Disabled / read-only">
161
+ <div className="showcase-row">
162
+ <SntInput value="disabled" onChange={() => {}} disabled />
163
+ <SntInput value="read-only" onChange={() => {}} readOnly />
164
+ </div>
165
+ </Example>
166
+ </Section>
167
+
168
+ {/* ------------------------------------------------------------------ */}
169
+ <Section
170
+ title="SntSelect"
171
+ description="Native dropdown. options = [{ value, label }]."
172
+ >
173
+ <Example label="Single select">
174
+ <SntSelect
175
+ value={selectValue}
176
+ onChange={setSelectValue}
177
+ options={[
178
+ { value: 'eu', label: 'Europe' },
179
+ { value: 'us', label: 'United States' },
180
+ { value: 'apac', label: 'Asia-Pacific' },
181
+ ]}
182
+ />
183
+ </Example>
184
+ <Example label="With placeholder">
185
+ <SntSelect
186
+ value=""
187
+ onChange={() => {}}
188
+ placeholder="Pick a region..."
189
+ options={[
190
+ { value: 'eu', label: 'Europe' },
191
+ { value: 'us', label: 'United States' },
192
+ ]}
193
+ />
194
+ </Example>
195
+ </Section>
196
+
197
+ {/* ------------------------------------------------------------------ */}
198
+ <Section
199
+ title="SntButtonGroup"
200
+ description="Segmented control for exclusive choices. Great for view toggles."
201
+ >
202
+ <Example label="Toggle">
203
+ <SntButtonGroup
204
+ value={groupValue}
205
+ onChange={setGroupValue}
206
+ options={[
207
+ { value: 'cards', label: 'Cards' },
208
+ { value: 'table', label: 'Table' },
209
+ { value: 'map', label: 'Map' },
210
+ ]}
211
+ />
212
+ </Example>
213
+ </Section>
214
+
215
+ {/* ------------------------------------------------------------------ */}
216
+ <Section
217
+ title="SntSwitch"
218
+ description="On/off toggle. checked state is controlled."
219
+ >
220
+ <Example label="With label">
221
+ <SntSwitch checked={switchOn} onChange={setSwitchOn} label="Show inactive devices" />
222
+ </Example>
223
+ <Example label="Disabled">
224
+ <SntSwitch checked={true} onChange={() => {}} label="Locked" disabled />
225
+ </Example>
226
+ </Section>
227
+
228
+ {/* ------------------------------------------------------------------ */}
229
+ <Section
230
+ title="SntCheckboxList"
231
+ description="Multi-select filter with select-all. options is a plain array of strings."
232
+ >
233
+ <Example label="Multi-select">
234
+ <div style={{ maxWidth: 320 }}>
235
+ <SntCheckboxList
236
+ label="Resources"
237
+ options={['Organisations', 'Trackers', 'Users', 'Geozones', 'Alerts']}
238
+ selected={checkboxSelected}
239
+ onChange={setCheckboxSelected}
240
+ />
241
+ </div>
242
+ </Example>
243
+ </Section>
244
+
245
+ {/* ------------------------------------------------------------------ */}
246
+ <Section
247
+ title="SntDateRangePicker"
248
+ description="Range selection with locale-aware presets. Receives ISO yyyy-MM-dd."
249
+ >
250
+ <Example label={`Current: ${dateRange.from} → ${dateRange.to}`}>
251
+ <SntDateRangePicker
252
+ from={dateRange.from}
253
+ to={dateRange.to}
254
+ onChange={setDateRange}
255
+ />
256
+ </Example>
257
+ </Section>
258
+
259
+ {/* ------------------------------------------------------------------ */}
260
+ <Section
261
+ title="SntCard"
262
+ description="Surface for grouping content. Optional image, title, badge, title button."
263
+ >
264
+ <SntGrid minItemWidth={260}>
265
+ <SntGridItem>
266
+ <SntCard title="Simple card">
267
+ <p>Card body content.</p>
268
+ </SntCard>
269
+ </SntGridItem>
270
+ <SntGridItem>
271
+ <SntCard
272
+ title="With badge"
273
+ badge={{ text: 'NEW', variant: 'success' }}
274
+ >
275
+ <p>Cards can carry a status badge.</p>
276
+ </SntCard>
277
+ </SntGridItem>
278
+ <SntGridItem>
279
+ <SntCard
280
+ title="Clickable"
281
+ onClick={() => alert('Card clicked')}
282
+ titleButton={<SntButton variant="primary">Open</SntButton>}
283
+ >
284
+ <p>onClick makes the whole card interactive.</p>
285
+ </SntCard>
286
+ </SntGridItem>
287
+ </SntGrid>
288
+ </Section>
289
+
290
+ {/* ------------------------------------------------------------------ */}
291
+ <Section
292
+ title="SntGrid + SntGridItem"
293
+ description="Responsive equal-height grid. Items wrap based on minItemWidth."
294
+ >
295
+ <SntGrid minItemWidth={140} gap={12}>
296
+ {Array.from({ length: 6 }).map((_, i) => (
297
+ <SntGridItem key={i}>
298
+ <div className="showcase-grid-cell">Item {i + 1}</div>
299
+ </SntGridItem>
300
+ ))}
301
+ </SntGrid>
302
+ </Section>
303
+
304
+ {/* ------------------------------------------------------------------ */}
305
+ <Section
306
+ title="SntToolbar"
307
+ description="Horizontal row for grouping actions, with optional spacer."
308
+ >
309
+ <SntToolbar>
310
+ <SntButton variant="primary">Save</SntButton>
311
+ <SntButton>Cancel</SntButton>
312
+ <SntToolbarSpacer />
313
+ <div style={{ width: 220 }}>
314
+ <SntInput value="" onChange={() => {}} placeholder="Search..." />
315
+ </div>
316
+ <SntButton variant="info">Filter</SntButton>
317
+ </SntToolbar>
318
+ </Section>
319
+
320
+ {/* ------------------------------------------------------------------ */}
321
+ <Section
322
+ title="SntTabs + SntTabPanel"
323
+ description="Standard horizontal tab strip. Controlled via activeTab."
324
+ >
325
+ <SntTabs tabs={TAB_DEFS} activeTab={showcaseTab} onChange={setShowcaseTab}>
326
+ <SntTabPanel tabKey="one" activeTab={showcaseTab}>
327
+ <p style={{ padding: '16px 0' }}>Content for tab one.</p>
328
+ </SntTabPanel>
329
+ <SntTabPanel tabKey="two" activeTab={showcaseTab}>
330
+ <p style={{ padding: '16px 0' }}>Content for tab two.</p>
331
+ </SntTabPanel>
332
+ <SntTabPanel tabKey="three" activeTab={showcaseTab}>
333
+ <p style={{ padding: '16px 0' }}>Content for tab three.</p>
334
+ </SntTabPanel>
335
+ </SntTabs>
336
+ </Section>
337
+
338
+ {/* ------------------------------------------------------------------ */}
339
+ <Section
340
+ title="SntTable"
341
+ description="Sortable, paginated data table. Columns define key, header, optional render."
342
+ >
343
+ <SntTable
344
+ data={TABLE_DATA}
345
+ rowKey="id"
346
+ defaultPageSize={25}
347
+ columns={[
348
+ { key: 'name', header: 'Name' },
349
+ { key: 'trackers', header: 'Trackers' },
350
+ {
351
+ key: 'status',
352
+ header: 'Status',
353
+ render: (row, val) => (
354
+ <SntBadge variant={STATUS_VARIANT[val] || 'secondary'} text={val} />
355
+ ),
356
+ },
357
+ ]}
358
+ />
359
+ </Section>
360
+
361
+ {/* ------------------------------------------------------------------ */}
362
+ <Section
363
+ title="SntSummaryStat"
364
+ description="Big-number stat tile. variant adds a coloured value."
365
+ >
366
+ <div className="summary-stats-row">
367
+ <SntSummaryStat value="142" label="Trackers" variant="info" />
368
+ <SntSummaryStat value="36" label="Users" variant="success" />
369
+ <SntSummaryStat value="3" label="Alerts" variant="warning" />
370
+ <SntSummaryStat value="1" label="Outages" variant="danger" />
371
+ </div>
372
+ </Section>
373
+
374
+ {/* ------------------------------------------------------------------ */}
375
+ <Section
376
+ title="SntProgressBar"
377
+ description="Inline percentage bar. variant=auto colours by value."
378
+ >
379
+ <Example label="Variants (25 / 60 / 90)">
380
+ <div className="showcase-stack">
381
+ <SntProgressBar value={25} variant="danger" />
382
+ <SntProgressBar value={60} variant="warning" />
383
+ <SntProgressBar value={90} variant="success" />
384
+ </div>
385
+ </Example>
386
+ <Example label="Auto colour by value">
387
+ <div className="showcase-stack">
388
+ <SntProgressBar value={20} variant="auto" />
389
+ <SntProgressBar value={55} variant="auto" />
390
+ <SntProgressBar value={85} variant="auto" />
391
+ </div>
392
+ </Example>
393
+ </Section>
394
+
395
+ {/* ------------------------------------------------------------------ */}
396
+ <Section
397
+ title="SntHistogram"
398
+ description="Microchart showing a distribution. Pass an array of {start, end, count}."
399
+ >
400
+ <SntHistogram buckets={HISTOGRAM_BUCKETS} height={80} />
401
+ </Section>
402
+
403
+ {/* ------------------------------------------------------------------ */}
404
+ <Section
405
+ title="SntSpinner + SntLoadingOverlay"
406
+ description="Indeterminate loading indicators."
407
+ >
408
+ <Example label="Spinner sizes">
409
+ <div className="showcase-row" style={{ alignItems: 'center' }}>
410
+ <SntSpinner size="small" />
411
+ <SntSpinner size="medium" />
412
+ <SntSpinner size="large" />
413
+ </div>
414
+ </Example>
415
+ <Example label="Loading overlay (toggle)">
416
+ <SntButton variant="primary" onClick={() => {
417
+ setShowOverlay(true)
418
+ setTimeout(() => setShowOverlay(false), 1500)
419
+ }}>
420
+ Show overlay for 1.5s
421
+ </SntButton>
422
+ {showOverlay && (
423
+ <div style={{ position: 'relative', height: 120, marginTop: 12 }}>
424
+ <SntLoadingOverlay message="Loading sample data..." />
425
+ </div>
426
+ )}
427
+ </Example>
428
+ </Section>
429
+
430
+ {/* ------------------------------------------------------------------ */}
431
+ <Section
432
+ title="SntDialog"
433
+ description="Modal dialog. open + onClose are controlled by the caller."
434
+ >
435
+ <SntButton variant="primary" onClick={() => setDialogOpen(true)}>
436
+ Open dialog
437
+ </SntButton>
438
+ <SntDialog
439
+ open={dialogOpen}
440
+ onClose={() => setDialogOpen(false)}
441
+ title="Example dialog"
442
+ size="small"
443
+ >
444
+ <p>This is a small modal dialog. Click the backdrop or × to close.</p>
445
+ <div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
446
+ <SntButton onClick={() => setDialogOpen(false)}>Cancel</SntButton>
447
+ <SntButton variant="primary" onClick={() => setDialogOpen(false)}>OK</SntButton>
448
+ </div>
449
+ </SntDialog>
450
+ </Section>
451
+
452
+ {/* ------------------------------------------------------------------ */}
453
+ <Section
454
+ title="SntSidepanel + SntFilterSection"
455
+ description="Collapsible filter rail used alongside list views."
456
+ >
457
+ <div className="showcase-sidepanel-demo">
458
+ <SntSidepanel
459
+ title="Filters"
460
+ open={sidepanelOpen}
461
+ onToggle={() => setSidepanelOpen((v) => !v)}
462
+ width={240}
463
+ >
464
+ <SntFilterSection label="Status">
465
+ <SntSwitch checked onChange={() => {}} label="Active only" />
466
+ </SntFilterSection>
467
+ <SntFilterSection label="Region">
468
+ <SntSelect
469
+ value="eu"
470
+ onChange={() => {}}
471
+ options={[
472
+ { value: 'eu', label: 'Europe' },
473
+ { value: 'us', label: 'United States' },
474
+ ]}
475
+ />
476
+ </SntFilterSection>
477
+ </SntSidepanel>
478
+ <div className="showcase-sidepanel-content">
479
+ <p>Sidepanel content area. Toggle the chevron to collapse.</p>
480
+ </div>
481
+ </div>
482
+ </Section>
483
+
484
+ {/* ------------------------------------------------------------------ */}
485
+ <Section
486
+ title="SntMap"
487
+ description="Leaflet map with Street/Satellite layer toggle. Optionally renders geozones (by orgId or array) and device markers."
488
+ >
489
+ <SntMap
490
+ height="320px"
491
+ center={[50.85, 4.35]}
492
+ zoom={6}
493
+ showGeozones={false}
494
+ showGeozoneSelector={false}
495
+ devices={[
496
+ { id: 'd1', name: 'Demo tracker A', lastLat: 50.8503, lastLng: 4.3517 },
497
+ { id: 'd2', name: 'Demo tracker B', lastLat: 51.2194, lastLng: 4.4025 },
498
+ { id: 'd3', name: 'Demo tracker C', lastLat: 51.0543, lastLng: 3.7174 },
499
+ ]}
500
+ />
501
+ </Section>
502
+
503
+ {/* ------------------------------------------------------------------ */}
504
+ <Section
505
+ title="SntColors"
506
+ description="JavaScript colour constants matching the CSS variables."
507
+ >
508
+ <div className="showcase-color-grid">
509
+ {Object.entries(SntColors).map(([name, value]) => (
510
+ <div key={name} className="showcase-color-swatch">
511
+ <div className="showcase-color-chip" style={{ background: value }} />
512
+ <div className="showcase-color-meta">
513
+ <div className="showcase-color-name">{name}</div>
514
+ <div className="showcase-color-value">{value}</div>
515
+ </div>
516
+ </div>
517
+ ))}
518
+ </div>
519
+ </Section>
520
+ </div>
521
+ )
522
+ }