Skip to content

New frontend#392

Open
ezio-melotti wants to merge 27 commits into
masterfrom
new-frontend
Open

New frontend#392
ezio-melotti wants to merge 27 commits into
masterfrom
new-frontend

Conversation

@ezio-melotti

Copy link
Copy Markdown
Collaborator

This PR adds a new minimal frontend, as described in the included zero_frontend.md (which might be removed before the PR is merged).

@ezio-melotti
ezio-melotti requested a review from Copilot July 5, 2026 16:53
@ezio-melotti ezio-melotti self-assigned this Jul 5, 2026
@ezio-melotti ezio-melotti added the frontend Issues related to the frontend label Jul 5, 2026
Comment thread src/simoc_sam/api.py Dismissed
Comment thread src/simoc_sam/api.py Dismissed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread frontend/app.js
Comment thread frontend/app.js
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 thread frontend/app.js Outdated
Comment thread frontend/app.js Outdated
Comment thread src/simoc_sam/api.py
Comment thread src/simoc_sam/api.py
Comment thread src/simoc_sam/api.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 18 changed files in this pull request and generated 4 comments.

Comment thread src/simoc_sam/defaults.py
Comment thread src/simoc_sam/api.py
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 thread src/simoc_sam/api.py
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 thread configs/flaskapi.service

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 21 changed files in this pull request and generated 9 comments.

Comment thread src/simoc_sam/api.py
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 thread src/simoc_sam/api.py
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 thread src/simoc_sam/api.py
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 thread src/simoc_sam/api.py
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 thread configs/flaskapi.service
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 thread frontend/app.js
Comment thread frontend/app.js
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 thread frontend/app.js
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 thread frontend/app.js
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>`;
Comment thread src/simoc_sam/db.py Dismissed
Comment thread src/simoc_sam/db.py Dismissed
Comment thread src/simoc_sam/db.py Dismissed
Comment thread src/simoc_sam/db.py Dismissed
Comment thread src/simoc_sam/db.py Dismissed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 introduces config.api_port and templates nginx to proxy to that configured port. If api_port is changed from the default, nginx and the systemd service can easily get out of sync. Consider templating the systemd unit (similar to how setup_nginx() renders simoc_live from simoc_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 thread src/simoc_sam/api.py
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 thread frontend/app.js
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);
}
Comment thread src/simoc_sam/db.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

frontend Issues related to the frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants