@sensolus/create-snt-agent-app 0.1.1 → 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.
package/index.js CHANGED
@@ -39,7 +39,7 @@ async function substitute(dir) {
39
39
  for (const entry of await readdir(dir)) {
40
40
  const p = path.join(dir, entry)
41
41
  if ((await stat(p)).isDirectory()) { await substitute(p); continue }
42
- if (!/\.(json|js|jsx|html|md|py|txt|sh|css)$|^\.[a-z]+$/i.test(entry)) continue
42
+ if (!/\.(json|js|jsx|html|md|py|txt|sh|css|yml|yaml|ini|mako)$|^\.[a-z][a-z.]*$|^(Jenkinsfile|Dockerfile|Makefile)$/i.test(entry)) continue
43
43
  const content = await readFile(p, 'utf8')
44
44
  if (content.includes('{{APP_NAME}}')) {
45
45
  await writeFile(p, content.replaceAll('{{APP_NAME}}', appName))
@@ -53,6 +53,7 @@ Created ${appName}/
53
53
 
54
54
  Next steps:
55
55
  cd ${appName}
56
+ cp .env.example .env # then fill in MAPBOX_KEY + LOCATIONIQ_KEY (required for SntMap)
56
57
  npm install
57
58
  pip install -r backend/requirements.txt
58
59
  npm run dev # frontend on :3000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sensolus/create-snt-agent-app",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a new Sensolus agent app: React frontend wired to @sensolus/snt-agent-kit + Flask API proxy backend.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,22 @@ A modern React + Flask dashboard for querying the Sensolus public API. This appl
11
11
 
12
12
  ## Quick Start
13
13
 
14
+ ### Environment Variables (.env)
15
+
16
+ Copy `.env.example` to `.env` and fill in the map provider keys (ask your team
17
+ or check the vault). They are **required** for the map pages (`SntMap`):
18
+
19
+ ```env
20
+ MAPBOX_KEY=pk....
21
+ LOCATIONIQ_KEY=pk....
22
+ ```
23
+
24
+ The Flask backend loads `.env` at startup and serves the keys to the frontend
25
+ at runtime via `/api/config` (so they are not baked into the build, and one
26
+ Docker image can deploy across environments — in Docker, pass them with
27
+ `-e MAPBOX_KEY=... -e LOCATIONIQ_KEY=...`). The real `.env` is gitignored;
28
+ never commit keys.
29
+
14
30
  ### Local Development (Recommended)
15
31
 
16
32
  Run both the frontend dev server and Flask backend in separate terminals:
@@ -1,4 +1,14 @@
1
- # Copy to .env required for SntMap tile layers (get keys from your team vault).
2
- # Keys are loaded at app build time; never commit the real .env.
3
- VITE_LOCATIONIQ_KEY=
4
- VITE_MAPBOX_KEY=
1
+ # Copy to .env and fill in never commit the real .env (it is gitignored).
2
+ #
3
+ # Map provider keys (get them from your team vault). Required for SntMap:
4
+ # the Flask backend reads them at request time and serves them to the
5
+ # frontend via /api/config, so one Docker image works across environments.
6
+ MAPBOX_KEY=
7
+ LOCATIONIQ_KEY=
8
+
9
+ # PostgreSQL connection (defaults shown; see README "Database Configuration")
10
+ #DB_HOST=localhost
11
+ #DB_PORT=5432
12
+ #DB_NAME={{APP_NAME}}
13
+ #DB_USER=snt
14
+ #DB_PASSWORD=snt
@@ -401,6 +401,18 @@ def get_devices_by_filter():
401
401
 
402
402
 
403
403
  LOCATIONIQ_KEY = os.environ.get('LOCATIONIQ_KEY', '')
404
+ MAPBOX_KEY = os.environ.get('MAPBOX_KEY', '')
405
+
406
+
407
+ @app.route('/api/config')
408
+ def app_config():
409
+ """Runtime config served to the frontend. Lets one Docker image deploy
410
+ across dev/demo/prod by reading keys from the container's env at request
411
+ time instead of baking them into the bundle at build time."""
412
+ return jsonify({
413
+ 'mapboxKey': MAPBOX_KEY,
414
+ 'locationiqKey': LOCATIONIQ_KEY,
415
+ }), 200
404
416
 
405
417
 
406
418
  @app.route('/api/geocode')
@@ -15,7 +15,7 @@
15
15
  "url": "git+ssh://git@bitbucket.org/sensolus/{{APP_NAME}}.git"
16
16
  },
17
17
  "dependencies": {
18
- "@sensolus/snt-agent-kit": "^0.1.1",
18
+ "@sensolus/snt-agent-kit": "^0.2.0",
19
19
  "react": "^19.2.3",
20
20
  "react-dom": "^19.2.3",
21
21
  "react-router-dom": "^7.1.1"
@@ -1,14 +1,33 @@
1
- import { BrowserRouter, Routes, Route } from 'react-router-dom'
1
+ import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom'
2
+ import { SntUiProvider } from '@sensolus/snt-agent-kit'
3
+ import { LocaleProvider, messages } from './i18n'
4
+ import { AppConfigProvider } from './AppConfigContext'
2
5
  import { Home } from './pages/Home'
3
6
  import { OrganisationDetail } from './pages/OrganisationDetail'
4
7
 
8
+ // SntUiProvider decouples kit widgets from the router and backend:
9
+ // pass `navigate` (and optionally `api`) so widgets like SntPageHeader work.
10
+ // LocaleProvider sits inside it so it fetches login info via the api adapter.
11
+ function AppShell() {
12
+ const navigate = useNavigate()
13
+ return (
14
+ <SntUiProvider navigate={navigate}>
15
+ <LocaleProvider messages={messages}>
16
+ <AppConfigProvider>
17
+ <Routes>
18
+ <Route path="/" element={<Home />} />
19
+ <Route path="/organisation/:id" element={<OrganisationDetail />} />
20
+ </Routes>
21
+ </AppConfigProvider>
22
+ </LocaleProvider>
23
+ </SntUiProvider>
24
+ )
25
+ }
26
+
5
27
  function App() {
6
28
  return (
7
29
  <BrowserRouter>
8
- <Routes>
9
- <Route path="/" element={<Home />} />
10
- <Route path="/organisation/:id" element={<OrganisationDetail />} />
11
- </Routes>
30
+ <AppShell />
12
31
  </BrowserRouter>
13
32
  )
14
33
  }
@@ -0,0 +1,45 @@
1
+ import { createContext, useContext, useEffect, useState } from 'react'
2
+ import { SntLoadingOverlay } from '@sensolus/snt-agent-kit'
3
+
4
+ /**
5
+ * Fetches runtime config (map tile keys, etc.) from the Flask backend's
6
+ * /api/config endpoint and gates the app on it. Lets one Docker image
7
+ * deploy across environments — keys come from the container's env vars
8
+ * at runtime instead of being baked into the bundle at `vite build` time.
9
+ */
10
+ const AppConfigContext = createContext(null)
11
+
12
+ export function AppConfigProvider({ children }) {
13
+ const [config, setConfig] = useState(null)
14
+ const [error, setError] = useState(null)
15
+
16
+ useEffect(() => {
17
+ let cancelled = false
18
+ fetch('/api/config')
19
+ .then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
20
+ .then(data => { if (!cancelled) setConfig(data) })
21
+ .catch(err => { if (!cancelled) setError(err) })
22
+ return () => { cancelled = true }
23
+ }, [])
24
+
25
+ if (error) {
26
+ return (
27
+ <div style={{ padding: 40, color: 'var(--snt-red)' }}>
28
+ Failed to load app config from /api/config: {error.message}
29
+ </div>
30
+ )
31
+ }
32
+ if (!config) return <SntLoadingOverlay message="Loading config..." />
33
+
34
+ return (
35
+ <AppConfigContext.Provider value={config}>
36
+ {children}
37
+ </AppConfigContext.Provider>
38
+ )
39
+ }
40
+
41
+ export function useAppConfig() {
42
+ const ctx = useContext(AppConfigContext)
43
+ if (!ctx) throw new Error('useAppConfig must be used inside AppConfigProvider')
44
+ return ctx
45
+ }
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Alle auswählen',
21
- 'checkboxList.deselectAll': 'Alle abwählen',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Panel einklappen',
25
- 'sidepanel.expand': 'Panel ausklappen',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Zeitraum:',
29
- 'dateRange.from': 'Von',
30
- 'dateRange.to': 'Bis',
31
- 'dateRange.preset.this_month': 'Dieser Monat',
32
- 'dateRange.preset.last_month': 'Letzter Monat',
33
- 'dateRange.preset.last_3_months': 'Letzte 3 Monate',
34
- 'dateRange.preset.last_6_months': 'Letzte 6 Monate',
35
- 'dateRange.preset.last_12_months': 'Letzte 12 Monate',
36
- 'dateRange.preset.last_24_months': 'Letzte 24 Monate',
37
- 'dateRange.preset.last_36_months': 'Letzte 36 Monate',
38
- 'dateRange.preset.this_year': 'Dieses Jahr',
39
- 'dateRange.preset.last_year': 'Letztes Jahr',
40
- 'dateRange.preset.all_time': 'Gesamter Zeitraum',
41
- 'dateRange.preset.custom': 'Benutzerdefiniert',
42
- 'dateRange.preset.next_3_months': 'Nächste 3 Monate',
43
- 'dateRange.preset.next_6_months': 'Nächste 6 Monate',
44
- 'dateRange.preset.next_12_months': 'Nächste 12 Monate',
45
- 'dateRange.preset.this_quarter': 'Dieses Quartal',
46
- 'dateRange.preset.next_quarter': 'Nächstes Quartal',
47
- 'dateRange.preset.rolling_12_months': 'Rollende 12 Monate',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organisationen',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Select all',
21
- 'checkboxList.deselectAll': 'Deselect all',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Collapse panel',
25
- 'sidepanel.expand': 'Expand panel',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Period:',
29
- 'dateRange.from': 'From',
30
- 'dateRange.to': 'To',
31
- 'dateRange.preset.this_month': 'This month',
32
- 'dateRange.preset.last_month': 'Last month',
33
- 'dateRange.preset.last_3_months': 'Last 3 months',
34
- 'dateRange.preset.last_6_months': 'Last 6 months',
35
- 'dateRange.preset.last_12_months': 'Last 12 months',
36
- 'dateRange.preset.last_24_months': 'Last 24 months',
37
- 'dateRange.preset.last_36_months': 'Last 36 months',
38
- 'dateRange.preset.this_year': 'This year',
39
- 'dateRange.preset.last_year': 'Last year',
40
- 'dateRange.preset.all_time': 'All time',
41
- 'dateRange.preset.custom': 'Custom',
42
- 'dateRange.preset.next_3_months': 'Next 3 months',
43
- 'dateRange.preset.next_6_months': 'Next 6 months',
44
- 'dateRange.preset.next_12_months': 'Next 12 months',
45
- 'dateRange.preset.this_quarter': 'This quarter',
46
- 'dateRange.preset.next_quarter': 'Next quarter',
47
- 'dateRange.preset.rolling_12_months': 'Rolling 12 months',
48
24
 
49
25
  // Auth dialog
50
26
  'auth.dialog.title': 'Sensolus API key required',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Seleccionar todo',
21
- 'checkboxList.deselectAll': 'Deseleccionar todo',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Contraer panel',
25
- 'sidepanel.expand': 'Expandir panel',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Período:',
29
- 'dateRange.from': 'Desde',
30
- 'dateRange.to': 'Hasta',
31
- 'dateRange.preset.this_month': 'Este mes',
32
- 'dateRange.preset.last_month': 'Mes pasado',
33
- 'dateRange.preset.last_3_months': 'Últimos 3 meses',
34
- 'dateRange.preset.last_6_months': 'Últimos 6 meses',
35
- 'dateRange.preset.last_12_months': 'Últimos 12 meses',
36
- 'dateRange.preset.last_24_months': 'Últimos 24 meses',
37
- 'dateRange.preset.last_36_months': 'Últimos 36 meses',
38
- 'dateRange.preset.this_year': 'Este año',
39
- 'dateRange.preset.last_year': 'Año pasado',
40
- 'dateRange.preset.all_time': 'Todo el período',
41
- 'dateRange.preset.custom': 'Personalizado',
42
- 'dateRange.preset.next_3_months': 'Próximos 3 meses',
43
- 'dateRange.preset.next_6_months': 'Próximos 6 meses',
44
- 'dateRange.preset.next_12_months': 'Próximos 12 meses',
45
- 'dateRange.preset.this_quarter': 'Este trimestre',
46
- 'dateRange.preset.next_quarter': 'Próximo trimestre',
47
- 'dateRange.preset.rolling_12_months': '12 meses continuos',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organizaciones',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Tout sélectionner',
21
- 'checkboxList.deselectAll': 'Tout désélectionner',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Réduire le panneau',
25
- 'sidepanel.expand': 'Développer le panneau',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Période :',
29
- 'dateRange.from': 'Du',
30
- 'dateRange.to': 'Au',
31
- 'dateRange.preset.this_month': 'Ce mois-ci',
32
- 'dateRange.preset.last_month': 'Le mois dernier',
33
- 'dateRange.preset.last_3_months': '3 derniers mois',
34
- 'dateRange.preset.last_6_months': '6 derniers mois',
35
- 'dateRange.preset.last_12_months': '12 derniers mois',
36
- 'dateRange.preset.last_24_months': '24 derniers mois',
37
- 'dateRange.preset.last_36_months': '36 derniers mois',
38
- 'dateRange.preset.this_year': 'Cette année',
39
- 'dateRange.preset.last_year': 'L\'année dernière',
40
- 'dateRange.preset.all_time': 'Tout',
41
- 'dateRange.preset.custom': 'Personnalisé',
42
- 'dateRange.preset.next_3_months': '3 prochains mois',
43
- 'dateRange.preset.next_6_months': '6 prochains mois',
44
- 'dateRange.preset.next_12_months': '12 prochains mois',
45
- 'dateRange.preset.this_quarter': 'Ce trimestre',
46
- 'dateRange.preset.next_quarter': 'Trimestre prochain',
47
- 'dateRange.preset.rolling_12_months': '12 mois glissants',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organisations',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Alles selecteren',
21
- 'checkboxList.deselectAll': 'Alles deselecteren',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Paneel inklappen',
25
- 'sidepanel.expand': 'Paneel uitklappen',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Periode:',
29
- 'dateRange.from': 'Van',
30
- 'dateRange.to': 'Tot',
31
- 'dateRange.preset.this_month': 'Deze maand',
32
- 'dateRange.preset.last_month': 'Vorige maand',
33
- 'dateRange.preset.last_3_months': 'Laatste 3 maanden',
34
- 'dateRange.preset.last_6_months': 'Laatste 6 maanden',
35
- 'dateRange.preset.last_12_months': 'Laatste 12 maanden',
36
- 'dateRange.preset.last_24_months': 'Laatste 24 maanden',
37
- 'dateRange.preset.last_36_months': 'Laatste 36 maanden',
38
- 'dateRange.preset.this_year': 'Dit jaar',
39
- 'dateRange.preset.last_year': 'Vorig jaar',
40
- 'dateRange.preset.all_time': 'Alles',
41
- 'dateRange.preset.custom': 'Aangepast',
42
- 'dateRange.preset.next_3_months': 'Volgende 3 maanden',
43
- 'dateRange.preset.next_6_months': 'Volgende 6 maanden',
44
- 'dateRange.preset.next_12_months': 'Volgende 12 maanden',
45
- 'dateRange.preset.this_quarter': 'Dit kwartaal',
46
- 'dateRange.preset.next_quarter': 'Volgend kwartaal',
47
- 'dateRange.preset.rolling_12_months': 'Rollende 12 maanden',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organisaties',
@@ -1,14 +1,11 @@
1
1
  import React from 'react'
2
2
  import ReactDOM from 'react-dom/client'
3
3
  import App from './App'
4
- import { LocaleProvider, messages } from './i18n'
5
4
  import '@sensolus/snt-agent-kit/theme.css'
6
5
  import './styles/app.css'
7
6
 
8
7
  ReactDOM.createRoot(document.getElementById('root')).render(
9
8
  <React.StrictMode>
10
- <LocaleProvider messages={messages}>
11
- <App />
12
- </LocaleProvider>
9
+ <App />
13
10
  </React.StrictMode>
14
11
  )
@@ -9,10 +9,12 @@ import {
9
9
  SntSpinner,
10
10
  } from '@sensolus/snt-agent-kit'
11
11
  import { useLocale, formatNumber } from '../i18n'
12
+ import { useAppConfig } from '../AppConfigContext'
12
13
 
13
14
  export function OrganisationDetail() {
14
15
  const { id } = useParams()
15
16
  const { t, intlLocale } = useLocale()
17
+ const config = useAppConfig()
16
18
 
17
19
  // Try to get org data from sessionStorage
18
20
  const organisation = useMemo(() => {
@@ -213,6 +215,8 @@ export function OrganisationDetail() {
213
215
  </div>
214
216
  ) : (
215
217
  <SntMap
218
+ mapboxKey={config.mapboxKey}
219
+ locationiqKey={config.locationiqKey}
216
220
  height="500px"
217
221
  devices={devices}
218
222
  orgId={id}
@@ -28,6 +28,7 @@ import {
28
28
  SntToolbarSpacer,
29
29
  getDefaultDateRange,
30
30
  } from '@sensolus/snt-agent-kit'
31
+ import { useAppConfig } from '../AppConfigContext'
31
32
 
32
33
  function Section({ title, description, children }) {
33
34
  return (
@@ -88,12 +89,13 @@ const TAB_DEFS = [
88
89
  ]
89
90
 
90
91
  export function WidgetShowcase() {
92
+ const config = useAppConfig()
91
93
  const [inputValue, setInputValue] = useState('Hello world')
92
94
  const [selectValue, setSelectValue] = useState('eu')
93
95
  const [groupValue, setGroupValue] = useState('cards')
94
96
  const [switchOn, setSwitchOn] = useState(true)
95
97
  const [checkboxSelected, setCheckboxSelected] = useState(['Trackers', 'Geozones'])
96
- const [dateRange, setDateRange] = useState(() => getDefaultDateRange('last_3_months'))
98
+ const [dateRange, setDateRange] = useState(() => getDefaultDateRange('week'))
97
99
  const [dialogOpen, setDialogOpen] = useState(false)
98
100
  const [showOverlay, setShowOverlay] = useState(false)
99
101
  const [sidepanelOpen, setSidepanelOpen] = useState(true)
@@ -245,14 +247,10 @@ export function WidgetShowcase() {
245
247
  {/* ------------------------------------------------------------------ */}
246
248
  <Section
247
249
  title="SntDateRangePicker"
248
- description="Range selection with locale-aware presets. Receives ISO yyyy-MM-dd."
250
+ description="Range selection with day / week / month / custom modes. Value is { viewMode, start, end } as JS Dates."
249
251
  >
250
- <Example label={`Current: ${dateRange.from} → ${dateRange.to}`}>
251
- <SntDateRangePicker
252
- from={dateRange.from}
253
- to={dateRange.to}
254
- onChange={setDateRange}
255
- />
252
+ <Example label={`Current: ${dateRange.start.toDateString()} → ${dateRange.end.toDateString()} (${dateRange.viewMode})`}>
253
+ <SntDateRangePicker value={dateRange} onChange={setDateRange} />
256
254
  </Example>
257
255
  </Section>
258
256
 
@@ -487,6 +485,8 @@ export function WidgetShowcase() {
487
485
  description="Leaflet map with Street/Satellite layer toggle. Optionally renders geozones (by orgId or array) and device markers."
488
486
  >
489
487
  <SntMap
488
+ mapboxKey={config.mapboxKey}
489
+ locationiqKey={config.locationiqKey}
490
490
  height="320px"
491
491
  center={[50.85, 4.35]}
492
492
  zoom={6}
@@ -283,7 +283,7 @@ body {
283
283
  }
284
284
 
285
285
  .summary-stats-row .summary-stat-info .summary-value {
286
- color: var(--snt-infra);
286
+ color: var(--snt-ui-selected);
287
287
  }
288
288
 
289
289
  .summary-stats-row .summary-stat-success .summary-value {