Implements feature request: https://community.gladysassistant.com/t/exportation-…des-donnees-au-format-csv/8750
### Description
Users asked on the forum for a way to export their history (device feature values, energy/water consumption in particular) as CSV, so they can cross-analyse it in a spreadsheet with external data and compare one year with another. Until now the only way was to script the REST API by hand.
This PR adds one complete export path, on the devices list.
**Server**
- New `device.exportStatesToCsv(deviceFeatureSelectors, start, end, { maxStates, after })` (`server/lib/device/device.exportStatesToCsv.js`). It reads the history of the selected features in one merged, date-ordered query with the columns `date,device,feature,unit,value` (long format, easy to pivot in a spreadsheet). Values containing a separator, a quote or a line break are properly escaped, and text values starting with `=`, `+`, `-` or `@` are neutralized so a spreadsheet does not read them as formulas.
- **There is no limit on the size of the exported period.** The history is read in chunks of at most 25 000 states, cursor-paginated on the `(created_at, device_feature_id)` pair — compared in microseconds (`epoch_us`), because the database stores more precision than a JavaScript date carries and a millisecond cursor could skip or duplicate states at a chunk boundary. The server never holds more than one chunk in memory, so a Raspberry Pi exports years of a chatty sensor without trouble.
- New authenticated route `GET /api/v1/device_feature/states_csv?device_features=a,b&start=&end=`:
- with `max_states`: answers one JSON chunk `{ csv, next, states }` per call — this is how the web client exports (it reassembles the file), and it works through Gladys Plus too;
- without `max_states` (direct API/scripting): answers the whole file as `text/csv; charset=utf-8` with a `Content-Disposition` filename, streamed chunk by chunk over HTTP; through the gateway (one websocket message, no streaming) a too-big non-paginated export is refused and told to paginate.
- Invalid input is rejected with clear `BadParameters` errors (no feature, invalid dates, end before start, malformed cursor), and an unknown feature returns a 404.
**Front**
- An "Export as CSV" button is added on each row of the devices list (and on each row of its mobile list), for every device with at least one feature keeping its history. It opens a dialog where the user picks the features and the period to export.
- The dialog follows the Horizon design: rendered on `<body>` through a portal, it is a bottom sheet on a phone and a centered dialog above, on a solid surface — the same grammar as the dashboard's light control panel. A dialog nested in the list card would be clipped by the card's backdrop filter.
- The file is downloaded chunk by chunk and reassembled in the browser (the chunks are handed to the Blob as-is, never concatenated into one giant string), with a progress line in the dialog ("125 000 states exported…"). Through Gladys Plus the chunks are smaller, so each one fits in one websocket message.
- The downloaded file starts with a UTF-8 BOM so spreadsheets (Excel in particular) display accented device names correctly.
- Errors are displayed in the dialog, with the server detail.
- New i18n keys under `devicesList.export` added to `en`, `fr` and `de`, and a `states_csv` fixture in the demo config so the export works in the demo too.
## Forum
Forum: https://community.gladysassistant.com/t/exportation-des-donnees-au-format-csv/8750
### Checklist
- [x] Tests pass: `cd server && npm test` (386 passing on the device lib + controller suites). The pagination is tested on the real DuckDB: chunks reassemble into the exact file (no state lost, none duplicated, order preserved), including when the chunk boundary falls inside a group of states sharing the same date; the streaming HTTP mode, the JSON chunk mode (HTTP and gateway) and the gateway size refusal are covered by controller tests. On the front, the dialog was driven in a real browser on the demo build, including the export end to end (correct filename, UTF-8 BOM, dialog closed after the download), and the chunk-reassembly loop was verified byte-exact against a fake chunked server.
- [x] Linter and prettier pass on both front and server (`npm run eslint`, `npm run prettier` / `prettier-check`), plus `npm run compare-translations` and `npm run build` on the front.
- [x] No undocumented breaking change (purely additive: one new route, one new lib function, one new UI entry point).
> This pull request was opened by an automated Claude Code run. It needs a human review before merging.