Line Sender

This commit is contained in:
chk
2026-06-15 09:22:41 +02:00
parent b7d742b9a4
commit 635b8bd1f3
14 changed files with 1050 additions and 217 deletions

View File

@@ -0,0 +1,421 @@
# Commands from GCode File — Konzept & Implementierungsplan
## Ausgangslage
Der Browser-Datei-Browser (`appRobotFileservice:2100`) kann Zeilen im aktiven
Programm navigieren. Der **Driver** (`appRobotDriver`) führt G-Code aus und hat
den Roboter angeschlossen.
Aktuell sind die zwei Pfade strikt getrennt:
| Pfad | Wer | Effekt |
|------|-----|--------|
| Browser → `POST /api/active/next` (Fileservice) | nur Browser | Cursor bewegt sich, Datei gespeichert — **kein Roboter** |
| WS-Client → Driver → FCode → Fileservice | externer Controller | Cursor bewegt sich **und** Zeile wird ausgeführt |
Das Ziel: der Browser bekommt einen Modus-Schalter. Im Modus **Senden**
wandert der Cursor nicht nur, sondern der Roboter fährt auch dorthin.
---
## Sicherheit und Netzwerk-Topologie
```
Internet / User
appRobotFileservice (Port 2100, erreichbar von aussen)
│ Browser spricht nur hier hin (HTTP/REST)
│ lokales Netz (server-seitig)
appRobotDriver (Port 2095, nur im LAN erreichbar)
GRBL/FluidNC-Controller → Roboter-Hardware
```
Der Browser verbindet sich **niemals** direkt mit dem Driver. Der Fileservice
ist die einzige Aussenschnittstelle. Die Verbindung zum Driver läuft
server-seitig im lokalen Netz.
---
## UI-Konzept
### Drei Button-Gruppen in einer Zeile (Links · Mitte · Rechts)
```
[ |◀ First ◀ Prev Next ▶ Last ▶| ] [ ✕ Clear ⬇ .gcode ] [ ↕ Navigieren | ▶ Senden ]
Gruppe Links Gruppe Mitte Gruppe Rechts
```
CSS-Ansatz: `.controls-row { display: flex; justify-content: space-between; }`,
jede Gruppe ist ein `<div class="ctrl-group">`.
### Schalter Navigieren / Senden
Zwei Buttons als Radiogruppe (einer immer aktiv):
```html
<div class="toggle-group">
<button id="btn-mode-nav" class="toggle active">↕ Navigieren</button>
<button id="btn-mode-send" class="toggle" >▶ Senden</button>
</div>
```
CSS-State `.toggle.active` hebt den aktiven Modus hervor (z. B. Hintergrund
`var(--active)`, Rand `var(--accent)`). Der Senden-Button bleibt ausgegraut,
solange keine `DRIVER_WS_URL` konfiguriert ist.
---
## Architektur im Modus "Senden"
### Warum kein FCode an den Driver?
Wenn der Fileservice dem Driver ein FCode schickte (z. B. `FPlus`), würde
der Driver seinerseits über `FCodeClient` wieder `POST /api/active/next` auf
den Fileservice aufrufen — der Cursor bewegt sich zweimal.
### Richtiger Weg: G-Code-Zeile direkt an den Driver-WS
Der Driver-WebSocket (`InputWS.js`) akzeptiert rohe G-Code-Befehle
(`G90 G1 X… Y… A… F…`) direkt, ohne Fileservice-Rückruf. Die Fileservice-Logik:
1. Cursor-Schritt wie bisher (`_gotoIndex()`), liefert die saubere G-Code-Zeile
2. Diese Zeile wird über eine **server-seitige WS-Verbindung** an den Driver gesendet
3. Driver führt Inverse Kinematik aus, schickt an GRBL-Controller, broadcast M114
### Ablauf im Modus "Senden"
```
Browser klickt Next ▶
├─[Navigieren]─► POST /api/active/next (Fileservice)
│ Cursor +1, Datei schreibt ;!, Browser rendert Cursor-Position
└─[Senden]──────► POST /api/active/next?execute=true (Fileservice)
Fileservice: Cursor +1, holt line = 'G90 G1 X250 Y0 A45 F1000'
Fileservice: sendet line über lokale WS-Verbindung an Driver
Driver: Inverse Kinematik → GRBL-Controller → Roboter fährt
Driver: broadcast M114 (Position) an alle Driver-WS-Clients
Fileservice antwortet mit { cursor, line } — Browser rendert Cursor
```
---
## Nötige Code-Änderungen
### 1. `public/index.html` — JS
**Neuer Zustand (kein WS im Browser):**
```js
let sendMode = false; // false = Navigieren, true = Senden
```
**`step()`-Funktion — Buttons sperren während Ausführung, Fehler anzeigen:**
```js
const STEP_BUTTONS = ['btn-first', 'btn-prev', 'btn-next', 'btn-last'];
function setStepButtonsEnabled(enabled) {
STEP_BUTTONS.forEach(id => {
document.getElementById(id).disabled = !enabled;
});
}
async function step(endpoint) {
setStatus(elActStatus, sendMode ? '⏳ Sende an Roboter…' : '…');
if (sendMode) setStepButtonsEnabled(false); // während Roboterfahrt sperren
try {
const url = sendMode
? `/api/active/${endpoint}?execute=true`
: `/api/active/${endpoint}`;
const r = await apiFetch('POST', url);
setStatus(elActStatus, sendMode
? `✓ Ausgeführt → Cursor ${r.cursor}`
: `Cursor → ${r.cursor}`);
await refresh();
} catch (err) {
// err.message enthält den Driver-Fehlertext (z. B. 'inverse kinematics failed')
setStatus(elActStatus, `Fehler: ${err.message}`, true);
} finally {
if (sendMode) setStepButtonsEnabled(true); // immer wieder freigeben
}
}
```
**Toggle-Buttons:**
```js
document.getElementById('btn-mode-nav').addEventListener('click', () => {
sendMode = false;
document.getElementById('btn-mode-nav').classList.add('active');
document.getElementById('btn-mode-send').classList.remove('active');
});
document.getElementById('btn-mode-send').addEventListener('click', () => {
sendMode = true;
document.getElementById('btn-mode-send').classList.add('active');
document.getElementById('btn-mode-nav').classList.remove('active');
});
```
**Senden-Button beim Start deaktivieren, wenn kein Driver konfiguriert:**
```js
// Beim Start prüfen ob Driver-URL gesetzt ist
const cfg = await apiFetch('GET', '/api/config');
if (!cfg.driverWsUrl) {
document.getElementById('btn-mode-send').disabled = true;
document.getElementById('btn-mode-send').title = 'Driver WS nicht konfiguriert';
}
```
**Layout — `controls` aufteilen:**
```html
<div class="controls-row">
<div class="ctrl-group">
<button id="btn-first" disabled>|◀ First</button>
<button id="btn-prev" disabled>◀ Prev</button>
<button id="btn-next" disabled>Next ▶</button>
<button id="btn-last" disabled>Last ▶|</button>
</div>
<div class="ctrl-group">
<button id="btn-clear" disabled>✕ Clear</button>
<a id="btn-download" class="dl-link" style="display:none" download>⬇ .gcode</a>
</div>
<div class="ctrl-group">
<div class="toggle-group">
<button id="btn-mode-nav" class="toggle active">↕ Navigieren</button>
<button id="btn-mode-send" class="toggle">▶ Senden</button>
</div>
</div>
</div>
```
### 2. `public/index.css`
```css
/* Drei-Gruppen-Zeile */
.controls-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.ctrl-group {
display: flex;
gap: 6px;
align-items: center;
}
/* Toggle-Schalter */
.toggle-group {
display: flex;
border: 1px solid #334155;
border-radius: 6px;
overflow: hidden;
}
.toggle-group .toggle {
border: none;
border-radius: 0;
border-right: 1px solid #334155;
padding: 6px 12px;
}
.toggle-group .toggle:last-child { border-right: none; }
.toggle-group .toggle.active {
background: var(--active);
color: var(--accent);
border-color: var(--accent);
}
```
### 3. Driver-Protokoll — Bestätigung und Fehler
`InputWS.js` sendet nach G-Code-Verarbeitung:
| Ergebnis | Empfänger | Format |
|----------|-----------|--------|
| **Erfolg** | Broadcast an **alle** WS-Clients | `{"position":{…}, "motorCounts":{…}}` (M114) |
| **Fehler** | Nur an den **anfragenden** Client | `{ "type": "error", "code": "GCODE_ERROR", "message": "…", "input": "…" }` |
Da `driverClient.js` der anfragende Client ist, empfängt er **beides** — Erfolg
(M114-Broadcast) und Fehler (targeted). Damit kann der Fileservice-Endpoint auf
die Driver-Antwort warten bevor er dem Browser antwortet.
**Folge für das UI:** Der Browser muss keine separate Bestätigungslogik
implementieren. Die Pfeil-Buttons sperren einfach für die Dauer des
HTTP-POST — Entsperrung und Fehleranzeige kommen mit der HTTP-Antwort.
```
Browser POST /api/active/next?execute=true
│ Fileservice: Cursor +1, holt line, sendet an Driver WS
│ ┌─ wartet auf nächste WS-Nachricht (max. DRIVER_TIMEOUT_MS) ─┐
│ │ M114 empfangen → HTTP 200 { cursor, line, driverPos } │
│ │ error empfangen → HTTP 502 { error.code, error.message } │
│ │ Timeout → HTTP 504 'Driver timeout' │
│ └──────────────────────────────────────────────────────────────┘
Browser: buttons gesperrt während fetch() läuft
buttons aktiv nach Antwort
bei 5xx: Fehlermeldung anzeigen
```
### 4. `src/driverClient.js` — neues Modul im Fileservice
Hält eine persistente WS-Verbindung zum Driver. `send()` gibt ein Promise zurück,
das mit der nächsten Driver-Antwort (M114 oder Fehler) resolved/rejected wird:
```js
// src/driverClient.js — liest URL + Timeout aus cfg (src/config.js)
const WebSocket = require('ws');
const log = require('./log');
const cfg = require('./config');
let ws = null;
function getOrCreateWs() {
if (ws && ws.readyState !== WebSocket.CLOSED) return ws;
ws = new WebSocket(cfg.driverWsUrl, { rejectUnauthorized: false });
ws.on('error', (e) => log.warn('driverClient WS Fehler:', e.message));
ws.on('close', () => log.info('driverClient WS geschlossen'));
ws.on('open', () => log.info('driverClient WS verbunden mit ' + cfg.driverWsUrl));
return ws;
}
function send(line) {
if (!cfg.driverWsUrl) {
return Promise.reject(Object.assign(
new Error('DRIVER_WS_URL nicht konfiguriert'), { driverCode: 'NOT_CONFIGURED' }
));
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(Object.assign(
new Error(`Keine Antwort vom Driver innerhalb von ${cfg.driverTimeoutMs} ms`),
{ driverCode: 'TIMEOUT' }
));
}, cfg.driverTimeoutMs);
const conn = getOrCreateWs();
const handler = (data) => {
clearTimeout(timer);
const text = data.toString();
try {
const parsed = JSON.parse(text);
if (parsed.type === 'error') {
reject(Object.assign(new Error(parsed.message), { driverCode: parsed.code || 'DRIVER_ERROR' }));
} else {
resolve(parsed); // M114: { position: {…}, motorCounts: {…} }
}
} catch {
resolve({ raw: text }); // unbekanntes Format → als Erfolg werten
}
};
if (conn.readyState === WebSocket.OPEN) {
conn.once('message', handler);
conn.send(line);
} else {
conn.once('open', () => { conn.once('message', handler); conn.send(line); });
}
});
}
module.exports = { send, configured: () => !!cfg.driverWsUrl };
```
### 5. `src/active/activeState.js` — `execute`-Parameter
Ein interner `_step()`-Helper enthält die gemeinsame Logik inkl. `_ensureActive()`.
Die öffentlichen Methoden delegieren nur noch:
```js
const driverClient = require('../driverClient');
async _step(index, execute) {
await this._ensureActive();
const result = await this._gotoIndex(index);
if (execute) {
const driverPos = await driverClient.send(result.line); // wartet auf ACK
return { ...result, driverPos };
}
return result;
}
async next(execute = false) { return this._step(this.cursor + 1, execute); }
async prev(execute = false) { return this._step(this.cursor - 1, execute); }
async first(execute = false) { return this._step(0, execute); }
async last(execute = false) { return this._step(this.lines.length - 1, execute); }
async goto(index, execute = false) { await this._ensureActive(); return this._step(Number(index), execute); }
```
### 6. `src/routes/active.js` — `execute`-Flag + Fehlerweiterleitung
Alle Step-Routen sind auth-geschützt (`requireAuth`). Driver-Fehler (`err.driverCode`) → HTTP 502:
```js
router.post('/next', requireAuth, asyncH(async (req, res) => {
const execute = req.query.execute === 'true';
try { res.json(await active.next(execute)); }
catch (err) { if (err.driverCode) return res.status(502).json({ error: err.driverCode, message: err.message }); throw err; }
}));
// analog für /prev, /first, /last, /goto
```
// analog für /prev, /first, /last
```
### 6. `src/config.js` — neue Option
```js
driverWsUrl: process.env.DRIVER_WS_URL || '',
```
Beim Start in `server.js`:
```js
const driverClient = require('./driverClient');
if (cfg.driverWsUrl) driverClient.configure(cfg.driverWsUrl);
```
### 7. `GET /api/config` — Browser-Endpoint
```js
app.get('/api/config', (req, res) => res.json({
driverWsUrl: cfg.driverWsUrl ? 'configured' : '', // URL nicht exponieren
}));
```
Die echte WS-URL wird dem Browser nicht mitgeteilt — nur ob ein Driver
konfiguriert ist (boolean-ähnlich). Das verhindert, dass Clients die Driver-
Adresse kennen.
### 8. Env-Variable `DRIVER_WS_URL` im Fileservice
```
DRIVER_WS_URL=wss://appRobotDriver:2095
```
Im Docker-Stack entspricht `appRobotDriver` dem Dienstnamen im Portainer-Stack
(analog zu `FILESERVICE_URL=http://appRobot_Fileservice:2100` im Driver).
---
## Keine Änderungen am Driver nötig
`InputWS.js` verarbeitet rohe G-Code-Befehle (`G90 G1 X… A… F…`) bereits
korrekt — der neue Sende-Modus nutzt exakt diesen bestehenden Pfad.
---
## Offene Fragen / Risiken
| Thema | Hinweis |
|-------|---------|
| **WSS / Zertifikat** | Driver läuft über HTTPS/WSS mit selbstsigniertem Zertifikat — `ws`-Client braucht `rejectUnauthorized: false` (nur im LAN akzeptabel) |
| **Timeout** | `DRIVER_TIMEOUT_MS` (Default 10 s) muss länger sein als die längste Roboterfahrt; langsame Moves oder blockierte Controller könnten Timeouts auslösen |
| **WS-Gleichzeitigkeit** | `driverClient.js` verwendet `ws.once('message', …)` — funktioniert nur wenn jeweils **ein** Step auf einmal läuft. Buttons werden während des laufenden Steps gesperrt, daher sicher |
| **Reconnect** | `driverClient.js` öffnet WS neu bei `CLOSED`; kurze Unterbrechungen im LAN werden so automatisch überbrückt |
| **Fehlermeldung im Browser** | Driver-Fehlertext (`err.message`) wird 1:1 angezeigt — kann englisch oder intern sein; ggf. spätere Übersetzungstabelle ergänzen |
| **FPlay / FStop** | Analog zu Step könnten auch Playback-Befehle über diesen Kanal laufen — ist aber ein separates Feature |

View File

@@ -1,153 +1,127 @@
# ROADMAP — appRobotFileservice Web-UI (Datei-Browser)
# appRobotFileservice Web-UI (Datei-Browser)
## Ziel
Einfache Web-Oberfläche direkt auf Port 2100, die GCode-Programme verwaltet.
Kein Framework, kein Build-Schritt — reines HTML/CSS/JS, serviert als static files.
> Stand: 2026-06-15
> Beschreibt den implementierten Ist-Zustand der Browser-Oberfläche auf Port 2100.
---
## Phase 1 — Datei-Browser (aktuell implementiert)
## Implementierter Funktionsumfang
**Dateien:** `public/index.html`, `public/index.css`
### Funktionen
### Programm-Liste (linke Spalte)
| Feature | Status | Endpunkt |
|---|---|---|
| Programme auflisten | ✅ | `GET /api/programs?dir=` |
| Unterordner auflisten | ✅ | `GET /api/folders?dir=` |
| Breadcrumb-Navigation | ✅ | — |
| In Unterordner navigieren (Klick) | ✅ | — |
| Programm laden (Einzelklick) | ✅ | `PUT /api/active` |
| Neue Datei anlegen (📄+) | ✅ | `POST /api/programs` |
| Neuen Ordner anlegen (📁+) | ✅ | `POST /api/folders` |
| Auswahl löschen (🗑) — Datei oder Ordner | ✅ | `DELETE /api/programs/:id` / `DELETE /api/folders/:name` |
| Ordner-Löschen rekursiv | ✅ | `DELETE /api/folders/:name?dir=` |
| Auto-Refresh (5 s Polling) | ✅ | — |
### Aktives Programm (rechte Spalte)
| Feature | Status | Endpunkt |
|---|---|---|
| Programm-Liste laden | ✅ | `GET /api/programs` |
| Aktives Programm anzeigen (Zeilenliste in Grad) | ✅ | `GET /api/active` |
| Programm als aktiv setzen (FLoad) | ✅ | `PUT /api/active` |
| Programm löschen | ✅ | `DELETE /api/programs/:id` |
| Cursor in Tabelle hervorheben + auto-scrollen | ✅ | — |
| Stepping: First / Prev / Next / Last | ✅ | `POST /api/active/first|prev|next|last` |
| Programm leeren (FClear) | ✅ | `POST /api/active/clear` |
| Cursor in Tabelle hervorheben + auto-scrollen | ✅ | — |
| Auto-Refresh (5 s Polling) | ✅ | — |
| Collapse-Karten (wie appRobotHoming) | ✅ | — |
| Download `.gcode` | ✅ | `GET /api/programs/:id/download` |
| Zeilen löschen (🗑 pro Zeile, pending-State) | ✅ | `DELETE /api/active/lines/:index` |
| Abbrechen / Speichern Edit-Bar | ✅ | — |
### Was Phase 1 noch fehlt
### Navigieren / Senden Toggle (rechts in Controls-Zeile)
- **Download `.gcode`** — Endpunkt `GET /api/programs/:id/download` muss im Server
hinzugefügt werden (liefert die rohe Datei mit `Content-Disposition: attachment`).
- **Upload `.gcode`** — Datei vom Desktop in den Service laden (Phase 2).
- **Umbenennen** — `PUT /api/programs/:id` mit neuem `name`.
| Feature | Status | Details |
|---|---|---|
| Modus-Schalter `↕ Navigieren` / `▶ Senden` | ✅ | Drei-Gruppen-Layout |
| Navigieren-Modus: nur Cursor bewegen | ✅ | `POST /api/active/next` |
| Senden-Modus: G-Code-Zeile an Driver senden | ✅ | `POST /api/active/next?execute=true` |
| Pfeil-Buttons während Fahrt gesperrt | ✅ | Bis HTTP-Antwort kommt |
| Driver-Fehlermeldung im UI anzeigen | ✅ | HTTP 502 → Statuszeile |
| Senden-Button deaktiviert wenn kein Driver | ✅ | `GET /api/config` beim Start |
---
## Phase 2 — Datei-Verwaltung
### Download-Endpunkt (kleines Backend-Feature für Phase 1.5)
## Architektur
```
GET /api/programs/:id/download
→ Content-Type: text/plain
→ Content-Disposition: attachment; filename="<id>.gcode"
→ Body: rohe .gcode-Datei
Browser (index.html)
├──[Navigieren]─► POST /api/active/next (Fileservice)
│ Cursor +1, ;! in .gcode gespeichert
└──[Senden]──────► POST /api/active/next?execute=true (Fileservice)
Fileservice: Cursor +1, sendet G-Code-Zeile
│ server-seitig (lokales Netz)
└──► appRobotDriver WS
Inverse Kinematik → GRBL → Roboter
M114-Broadcast / Fehler zurück an Fileservice
Fileservice → HTTP 200 (OK) oder 502 (Driver-Fehler)
```
Implementierung: 1 Route in `src/routes/programs.js` + `store.gcodePath(id)`.
### Upload
```
POST /api/programs/upload
Body: multipart/form-data { file: <gcode-Datei> }
→ speichert unter slugify(filename) in GCodeFiles/
```
Im Frontend: `<input type="file" accept=".gcode,.ngc">` + `FormData` fetch.
### Umbenennen / Metadaten bearbeiten
`PUT /api/programs/:id` ist bereits vorhanden — nur das Frontend-Formular fehlt.
Inline-Eingabefeld in der Programm-Zeile, Enter → PUT.
Der Browser verbindet sich **niemals direkt** mit dem Driver — der Fileservice
ist die einzige Aussenschnittstelle.
---
## Phase 3 — Verzeichnis-Navigation
## Cursor-Persistenz
Aktuell: alles flach im `GCodeFiles/`-Ordner.
Später: Unterordner für Projekte (z. B. `GCodeFiles/greifen/`, `GCodeFiles/ablegen/`).
### Backend
Neue Konfiguration: `storageDir` bleibt Wurzel; zusätzlicher optionaler `subDir`-Parameter.
Der Cursor wird als `;!`-Marker direkt **in der `.gcode`-Datei** gespeichert
(primäre Quelle, sichtbar in jedem Texteditor). Das `.json`-Sidecar enthält `cursor`
nur noch als Fallback für Altdateien ohne Marker.
```
GET /api/programs?dir=greifen → listet GCodeFiles/greifen/
PUT /api/active { id, dir } → lädt GCodeFiles/<dir>/<id>.gcode
POST /api/dirs { name } → legt Unterordner an
DELETE /api/dirs/:name → löscht (leer) Unterordner
G90 G1 x0 y300 z0 a90.00 b-90.00 c0.00 e0.00 f1000 ;1
G90 G1 x10 y300 z0 a0.00 b-90.00 c0.00 e0.00 f1000 ;2! ← Cursor-Zeile
```
Wichtig: Pfad-Traversal verhindern — `assertValidId` auf jede Pfad-Komponente anwenden.
### Frontend
Breadcrumb-Leiste oberhalb der Programmliste:
```
GCodeFiles/ > greifen/ > [zurück]
```
Doppelklick auf Ordner-Zeile → navigiert hinein.
Klick auf Breadcrumb-Segment → navigiert zurück.
`store.read()` liefert immer saubere Zeilen (kein `!`) und `cursor` als Index.
---
## Phase 4 — Live-Updates (WebSocket oder SSE)
## Verzeichnis-Navigation
Aktuell: Polling alle 5 s.
Besser: Server-Sent Events (SSE) oder WebSocket, damit die UI sofort
reagiert wenn der Driver per FPoint einen neuen Punkt schreibt.
### Option A — Server-Sent Events (einfach, read-only)
```
GET /api/events
→ text/event-stream
→ event: active-changed\ndata: <ActiveState-JSON>\n\n
```
Der Fileservice emittiert nach jedem `_persist()` ein SSE-Event.
Frontend: `new EventSource('/api/events')` statt `setInterval`.
### Option B — WebSocket (bidirektional, für spätere Steuerung)
Erlaubt auch WS-basierte FCode-Befehle direkt von der Web-UI.
Aufwändiger, aber konsistent mit dem Driver-Pattern.
**Empfehlung für Phase 4:** SSE — eine Zeile Mehraufwand gegenüber Polling,
kein zusätzliches Protokoll.
- Echte Unterverzeichnisse in `GCodeFiles/` (max. 5 Ebenen tief)
- Segmente validiert mit `/^[a-z0-9_-]+$/`
- `dir`-Parameter in allen relevanten Endpoints (`?dir=training/run1`)
- Breadcrumb: klickbare Segmente, Root = `GCodeFiles`
---
## Phase 5 — Inline-Editor
## Noch nicht implementiert / offene Punkte
Einzelne Zeilen direkt im Browser bearbeiten.
```
PUT /api/active/lines/:index { line: "G90 G1 x0 y300 z0 a90.00 …" }
DELETE /api/active/lines/:index
POST /api/active/lines { line, atIndex }
```
Alle drei Endpunkte sind bereits im Backend implementiert.
Frontend: Klick auf Tabellenzeile → `<input>` erscheint inline; Escape abbricht, Enter speichert.
| Feature | Priorität | Hinweis |
|---|---|---|
| Upload `.gcode` vom Desktop | P2 | `POST /api/programs/upload` (multipart) |
| Programm umbenennen | P2 | `PUT /api/programs/:id` (Backend vorhanden, UI fehlt) |
| Zeile inline bearbeiten | P3 | `PUT /api/active/lines/:index` (Backend vorhanden) |
| Zeile einfügen | P3 | `POST /api/active/lines { atIndex }` (Backend vorhanden) |
| Live-Updates via SSE | P4 | statt 5s-Polling; `GET /api/events` |
| `FPlay` / `FStop` im Browser | P4 | Playback-Modus |
---
## Datei-Übersicht (nach Phase 1)
## Datei-Übersicht
```
appRobotFileservice/
public/
index.html HTML-Struktur + JavaScript (inline)
index.css Design-System (identisch mit appRobotHoming/styles.css)
doc/
fileBrowser_ROADMAP.md diese Datei
src/
server.js +express.static('public/')
routes/
programs.js TODO Phase 2: /download-Endpunkt
active.js komplett
public/
index.html HTML + JavaScript (inline)
index.css Design-System (dark, CSS-Variablen)
src/
server.js GET /api/config → driverConfigured
driverClient.js WS-Client zum Driver (für Senden-Modus)
routes/
programs.js /api/programs* inkl. /download
active.js /api/active* mit ?execute=true für Step-Routes
folders.js /api/folders*
doc/
fileBrowser.md diese Datei (Ist-Zustand)
commandsFromGCodeFile.md Senden-Modus Konzept + Implementierungsplan
```