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

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