New frontend#392
Open
ezio-melotti wants to merge 27 commits into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a minimal, offline-first “SIMOC Live” frontend backed by a new read-only Flask API that serves data from the existing SQLite sensor database, with nginx proxying /api/* and serving static assets.
Changes:
- Added a new Flask API (
src/simoc_sam/api.py) with endpoints for sensor metadata, latest readings, historical query, and export (CSV/JSON), plus pytest coverage. - Added a new static frontend (
frontend/) using vendored JS/CSS (Chart.js, flatpickr, chartjs-adapter-date-fns) for live and historical views. - Added deployment wiring: nginx template update and a new systemd unit + CLI commands to set up/tear down the frontend + Flask API.
Reviewed changes
Copilot reviewed 11 out of 17 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| zero_frontend.md | Architecture/plan document for the minimal frontend + API. |
| tests/test_config.py | Updates config default-vars test for new API host/port settings. |
| tests/test_api.py | Adds unit tests for the new Flask API endpoints and helpers. |
| src/simoc_sam/defaults.py | Adds default api_host / api_port configuration. |
| src/simoc_sam/api.py | Implements the new read-only Flask API (plus static serving for dev). |
| simoc-sam.py | Adds CLI commands to install/remove frontend files and manage the Flask systemd unit; updates nginx templating to pass api_port. |
| requirements.txt | Adds Flask dependency. |
| frontend/vendor/VERSIONS | Pins vendored frontend library versions and source URLs. |
| frontend/vendor/flatpickr.js | Vendored flatpickr JS. |
| frontend/vendor/flatpickr.css | Vendored flatpickr CSS. |
| frontend/vendor/chartjs-adapter-date-fns.js | Vendored Chart.js date adapter. |
| frontend/style.css | Adds styling for the new minimal frontend. |
| frontend/index.html | Adds the minimal frontend HTML shell (live + history views). |
| frontend/app.js | Implements frontend logic: polling, selection UI, queries, chart/table rendering, exports. |
| configs/simoc_live.tmpl | Updates nginx config to proxy /api/ to the Flask API port. |
| configs/flaskapi.service | Adds a systemd service for running the Flask API. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+233
to
+257
| if (state.viewMode === 'plot') { | ||
| for (const metric of metrics) { | ||
| container.appendChild(makeChartBox(sensor, metric, data)); | ||
| } | ||
| } else { | ||
| container.appendChild(makeTableBox(sensor, metrics, data)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function makeChartBox(sensor, metric, data) { | ||
| const info = state.sensors[sensor]; | ||
| const meta = info.metrics[metric]; | ||
| const label = meta.unit ? `${meta.label} (${meta.unit})` : meta.label; | ||
| const box = document.createElement('div'); | ||
| box.className = 'chart-box'; | ||
| box.innerHTML = `<h3>${info.name} — ${label}</h3><div class="chart-wrap"><canvas></canvas></div>`; | ||
| const dataset = { | ||
| label, | ||
| data: data.timestamps.map((ts, i) => ({x: ts, y: data[metric][i]})), | ||
| borderColor: CHART_COLORS[0], | ||
| backgroundColor: CHART_COLORS[0], | ||
| pointRadius: 0, | ||
| borderWidth: 1.5, | ||
| }; |
Comment on lines
+88
to
+94
| for sensor, metrics in selection.items(): | ||
| if sensor not in SENSOR_DATA: | ||
| raise ValueError(f'Unknown sensor: {sensor!r}') | ||
| valid_metrics = set(SENSOR_DATA[sensor].data.keys()) | ||
| for metric in metrics: | ||
| if metric not in valid_metrics: | ||
| raise ValueError(f'Unknown metric for {sensor}: {metric!r}') |
Comment on lines
+115
to
+120
| result[sensor] = { | ||
| 'timestamps': [to_unix_ms(ts) for ts in readings['timestamp']], | ||
| } | ||
| for metric in metrics: | ||
| result[sensor][metric] = readings[metric] | ||
| return result |
Comment on lines
+88
to
+94
| for sensor, metrics in selection.items(): | ||
| if sensor not in SENSOR_DATA: | ||
| raise ValueError(f'Unknown sensor: {sensor!r}') | ||
| valid_metrics = set(SENSOR_DATA[sensor].data.keys()) | ||
| for metric in metrics: | ||
| if metric not in valid_metrics: | ||
| raise ValueError(f'Unknown metric for {sensor}: {metric!r}') |
Comment on lines
+115
to
+119
| result[sensor] = { | ||
| 'timestamps': [to_unix_ms(ts) for ts in readings['timestamp']], | ||
| } | ||
| for metric in metrics: | ||
| result[sensor][metric] = readings[metric] |
Comment on lines
+175
to
+179
| try: | ||
| start, end, selection, limit = parse_selection(request.get_json()) | ||
| except ValueError as err: | ||
| app.logger.debug('Bad request to /api/query: %s', err) | ||
| return jsonify({'error': str(err)}), 400 |
Comment on lines
+186
to
+196
| def api_export(): | ||
| """Export readings as CSV or JSON (no decimation).""" | ||
| payload = request.get_json() | ||
| fmt = (payload or {}).get('format', 'csv') | ||
| if fmt not in ('csv', 'json'): | ||
| return jsonify({'error': f'Invalid format: {fmt!r}'}), 400 | ||
| try: | ||
| start, end, selection, _ = parse_selection(payload) | ||
| except ValueError as err: | ||
| app.logger.debug('Bad request to /api/export: %s', err) | ||
| return jsonify({'error': str(err)}), 400 |
Comment on lines
+16
to
+20
| ExecStart=/home/pi/simoc-sam/venv/bin/gunicorn \ | ||
| --workers 2 \ | ||
| --bind 127.0.0.1:8082 \ | ||
| --access-logfile - \ | ||
| "simoc_sam.api:create_app()" |
Comment on lines
+148
to
+152
| const btn = document.createElement('button'); | ||
| btn.className = 'toggle'; | ||
| btn.textContent = meta.label; | ||
| btn.disabled = true; | ||
| btn.addEventListener('click', () => { |
Comment on lines
+324
to
+328
| if (state.viewMode === 'plot') { | ||
| for (const metric of metrics) { | ||
| container.appendChild(makeChartBox(sensor, metric, data, xMin, xMax)); | ||
| } | ||
| } else { |
Comment on lines
+111
to
+123
| const rows = Object.entries(info.metrics).map(([metric, meta]) => { | ||
| const value = reading?.[metric]; | ||
| const display = (value === null || value === undefined) ? '--' | ||
| : (typeof value === 'number' ? value.toFixed(1) : value); | ||
| const unit = meta.unit ? ` ${meta.unit}` : ''; | ||
| return `<tr><td>${meta.label}</td> | ||
| <td class="value">${display}${unit}</td></tr>`; | ||
| }).join(''); | ||
| const ts = reading ? formatLocal(Date.parse(reading.timestamp)) : 'no data'; | ||
| card.innerHTML = ` | ||
| <h2><span class="dot"></span>${info.name}</h2> | ||
| <table>${rows}</table> | ||
| <div class="ts">${ts}</div>`; |
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 21 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
configs/flaskapi.service:20
- This unit binds Gunicorn to a hard-coded port (
--bind 127.0.0.1:8082), but the rest of the PR introducesconfig.api_portand templates nginx to proxy to that configured port. Ifapi_portis changed from the default, nginx and the systemd service can easily get out of sync. Consider templating the systemd unit (similar to howsetup_nginx()renderssimoc_livefromsimoc_live.tmpl) or otherwise deriving the bind port from the same config source.
ExecStart=/home/pi/simoc-sam/venv/bin/gunicorn \
--workers 2 \
--bind 127.0.0.1:8082 \
--access-logfile - \
"simoc_sam.api:create_app()"
Comment on lines
+104
to
+109
| start = payload.get('start') | ||
| end = payload.get('end') | ||
| limit = payload.get('limit') | ||
| if limit is not None and (not isinstance(limit, int) or limit <= 0): | ||
| raise ValueError(f'"limit" must be a positive integer') | ||
| return start, end, selection, limit |
Comment on lines
+414
to
+422
| function downloadBlob(content, filename, mimetype) { | ||
| const blob = new Blob([content], {type: mimetype}); | ||
| const url = URL.createObjectURL(blob); | ||
| const a = document.createElement('a'); | ||
| a.href = url; | ||
| a.download = filename; | ||
| a.click(); | ||
| URL.revokeObjectURL(url); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds a new minimal frontend, as described in the included
zero_frontend.md(which might be removed before the PR is merged).