358 lines
16 KiB
HTML
358 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>appRobotFileservice</title>
|
||
<link rel="stylesheet" href="/index.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app-header">
|
||
<h1>appRobotFileservice</h1>
|
||
<span class="subtitle">Datei-Browser · GCodeFiles/</span>
|
||
</div>
|
||
|
||
<div class="sections">
|
||
|
||
<!-- ===== LINKE SPALTE: Programm-Liste ===== -->
|
||
<div class="section">
|
||
<h2>
|
||
Programme
|
||
<span class="badge" id="prog-count">–</span>
|
||
</h2>
|
||
|
||
<div class="controls">
|
||
<button id="btn-refresh" title="Aktualisieren">⟳</button>
|
||
<button id="btn-new-folder" title="Neuen Ordner erstellen">📁+</button>
|
||
<button id="btn-delete-selected" title="Ausgewähltes löschen" disabled>🗑</button>
|
||
</div>
|
||
|
||
<div class="status-line" id="list-status"></div>
|
||
<div id="program-list"><span class="empty-hint">Lade…</span></div>
|
||
</div>
|
||
|
||
<!-- ===== RECHTE SPALTE: Aktives Programm + Inhalt ===== -->
|
||
<div class="section">
|
||
<h2>
|
||
Aktives Programm
|
||
<span class="badge" id="active-name">–</span>
|
||
</h2>
|
||
|
||
<!-- Info-Zeile -->
|
||
<div class="active-bar" id="active-bar" style="display:none">
|
||
<div class="kv"><span class="k">ID</span><span class="v" id="ai-id">–</span></div>
|
||
<div class="sep"></div>
|
||
<div class="kv"><span class="k">Zeilen</span><span class="v" id="ai-count">–</span></div>
|
||
<div class="sep"></div>
|
||
<div class="kv"><span class="k">Cursor</span><span class="v" id="ai-cursor">–</span></div>
|
||
<div class="sep"></div>
|
||
<div class="kv"><span class="k">Version</span><span class="v" id="ai-version">–</span></div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button id="btn-first" disabled title="Erste Zeile">|◀ First</button>
|
||
<button id="btn-prev" disabled title="Schritt zurück">◀ Prev</button>
|
||
<button id="btn-next" disabled title="Schritt vor">Next ▶</button>
|
||
<button id="btn-last" disabled title="Letzte Zeile">Last ▶|</button>
|
||
<button id="btn-clear" disabled title="Programm leeren">✕ Clear</button>
|
||
<a id="btn-download" class="dl-link" style="display:none" download>⬇ .gcode</a>
|
||
</div>
|
||
|
||
<div class="status-line" id="active-status"></div>
|
||
|
||
<div class="table-wrap">
|
||
<table id="lines-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="idx">#</th>
|
||
<th class="line">G-Code (gespeichert in Grad)</th>
|
||
<th class="ts">Zeitstempel</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="lines-body">
|
||
<tr><td colspan="3" class="empty-hint" style="padding:10px">Kein aktives Programm</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- .sections -->
|
||
|
||
<script>
|
||
// ─── Konfiguration ───────────────────────────────────────────────────────────
|
||
const API = ''; // leer → gleicher Origin; überschreib mit 'http://…:2100'
|
||
const REFRESH_MS = 5000; // Auto-Refresh-Intervall
|
||
|
||
// ─── Zustand ─────────────────────────────────────────────────────────────────
|
||
let currentActiveId = null;
|
||
let selectedId = null; // in der Liste ausgewähltes Item (unabhängig vom aktiven Programm)
|
||
let selectedType = null; // 'file' | 'folder'
|
||
|
||
// ─── DOM-Referenzen ───────────────────────────────────────────────────────────
|
||
const elProgCount = document.getElementById('prog-count');
|
||
const elProgList = document.getElementById('program-list');
|
||
const elListStatus = document.getElementById('list-status');
|
||
const elActiveName = document.getElementById('active-name');
|
||
const elActiveBar = document.getElementById('active-bar');
|
||
const elAiId = document.getElementById('ai-id');
|
||
const elAiCount = document.getElementById('ai-count');
|
||
const elAiCursor = document.getElementById('ai-cursor');
|
||
const elAiVersion = document.getElementById('ai-version');
|
||
const elLinesBody = document.getElementById('lines-body');
|
||
const elActStatus = document.getElementById('active-status');
|
||
const elBtnFirst = document.getElementById('btn-first');
|
||
const elBtnPrev = document.getElementById('btn-prev');
|
||
const elBtnNext = document.getElementById('btn-next');
|
||
const elBtnLast = document.getElementById('btn-last');
|
||
const elBtnClear = document.getElementById('btn-clear');
|
||
const elBtnDownload = document.getElementById('btn-download');
|
||
const elBtnDeleteSelected = document.getElementById('btn-delete-selected');
|
||
|
||
// ─── API-Helpers ─────────────────────────────────────────────────────────────
|
||
async function apiFetch(method, path, body) {
|
||
const opts = { method, headers: {} };
|
||
if (body !== undefined) {
|
||
opts.headers['content-type'] = 'application/json';
|
||
opts.body = JSON.stringify(body);
|
||
}
|
||
const res = await fetch(API + path, opts);
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw Object.assign(new Error(data.message || res.statusText), { code: data.code, status: res.status });
|
||
return data;
|
||
}
|
||
|
||
function setStatus(el, msg, isErr = false) {
|
||
el.textContent = msg;
|
||
el.className = 'status-line' + (isErr ? ' err' : '');
|
||
}
|
||
|
||
// ─── Zeitstempel lesbar machen ───────────────────────────────────────────────
|
||
function fmtTs(raw) {
|
||
if (!raw) return '';
|
||
const n = Number(raw);
|
||
if (!Number.isFinite(n) || n < 1e12) return raw;
|
||
return new Date(n).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'medium' });
|
||
}
|
||
|
||
// ─── Zeile in Code + Timestamp zerlegen ──────────────────────────────────────
|
||
function parseLine(line) {
|
||
const idx = line.indexOf(';');
|
||
if (idx === -1) return { code: line.trim(), ts: '' };
|
||
return { code: line.slice(0, idx).trim(), ts: fmtTs(line.slice(idx + 1).trim()) };
|
||
}
|
||
|
||
// ─── Selection-State aktualisieren ──────────────────────────────────────────
|
||
function setSelected(id, type) {
|
||
selectedId = id;
|
||
selectedType = type;
|
||
// Alle Items neu markieren
|
||
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
||
el.classList.toggle('is-selected', el.dataset.id === id);
|
||
});
|
||
elBtnDeleteSelected.disabled = !id;
|
||
}
|
||
|
||
// ─── Programmliste rendern ───────────────────────────────────────────────────
|
||
function renderProgList(programs, folders, activeId) {
|
||
const total = folders.length + programs.length;
|
||
elProgCount.textContent = total;
|
||
if (!total) {
|
||
elProgList.innerHTML = '<span class="empty-hint">Keine Programme gefunden</span>';
|
||
elBtnDeleteSelected.disabled = true;
|
||
return;
|
||
}
|
||
|
||
// Ordner zuerst, dann Dateien
|
||
const folderHtml = folders.map(f => `
|
||
<div class="prog-item is-folder" data-id="${esc(f.id)}" data-type="folder">
|
||
<span class="prog-name" title="${esc(f.name)}">📁 ${esc(f.name)}</span>
|
||
</div>`).join('');
|
||
|
||
const fileHtml = programs.map(p => {
|
||
const isActive = p.id === activeId;
|
||
const isSel = p.id === selectedId;
|
||
return `<div class="prog-item${isActive ? ' is-active' : ''}${isSel ? ' is-selected' : ''}" data-id="${esc(p.id)}" data-type="file">
|
||
<span class="prog-name" title="${esc(p.name)}">📄 ${esc(p.name)}</span>
|
||
<span class="prog-id">${esc(p.id)}</span>
|
||
<span class="prog-count">${p.lineCount} Z.</span>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
elProgList.innerHTML = folderHtml + fileHtml;
|
||
|
||
// Einfachklick → auswählen; Doppelklick → Programm laden
|
||
elProgList.querySelectorAll('.prog-item').forEach(el => {
|
||
el.addEventListener('click', () => setSelected(el.dataset.id, el.dataset.type));
|
||
el.addEventListener('dblclick', () => {
|
||
if (el.dataset.type === 'file') loadProgram(el.dataset.id);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── Zeilen-Tabelle rendern ──────────────────────────────────────────────────
|
||
function renderLines(state) {
|
||
const { programId, cursor, lineCount, lines = [], version } = state;
|
||
currentActiveId = programId;
|
||
|
||
elActiveName.textContent = programId || '–';
|
||
const hasProgram = !!programId;
|
||
|
||
// Info-Bar
|
||
if (hasProgram) {
|
||
elActiveBar.style.display = 'flex';
|
||
elAiId.textContent = programId;
|
||
elAiCount.textContent = lineCount;
|
||
elAiCursor.textContent = cursor;
|
||
elAiVersion.textContent = version;
|
||
} else {
|
||
elActiveBar.style.display = 'none';
|
||
}
|
||
|
||
// Stepping-Buttons
|
||
const hasLines = lineCount > 0;
|
||
elBtnFirst.disabled = !hasProgram || !hasLines || cursor === 0;
|
||
elBtnPrev.disabled = !hasProgram || !hasLines || cursor === 0;
|
||
elBtnNext.disabled = !hasProgram || !hasLines || cursor >= lineCount - 1;
|
||
elBtnLast.disabled = !hasProgram || !hasLines || cursor >= lineCount - 1;
|
||
elBtnClear.disabled = !hasProgram;
|
||
|
||
// Download-Link
|
||
if (programId) {
|
||
elBtnDownload.href = `/api/programs/${encodeURIComponent(programId)}/download`;
|
||
elBtnDownload.download = `${programId}.gcode`;
|
||
elBtnDownload.style.display = '';
|
||
} else {
|
||
elBtnDownload.style.display = 'none';
|
||
}
|
||
|
||
// Tabelle
|
||
if (!lines.length) {
|
||
elLinesBody.innerHTML = `<tr><td colspan="3" class="empty-hint" style="padding:10px">${
|
||
hasProgram ? 'Programm ist leer' : 'Kein aktives Programm — links ein Programm anklicken'
|
||
}</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
elLinesBody.innerHTML = lines.map((line, i) => {
|
||
const { code, ts } = parseLine(line);
|
||
const isCursor = i === cursor;
|
||
return `<tr class="${isCursor ? 'cursor-row' : ''}">
|
||
<td class="idx">${i}</td>
|
||
<td class="line">${isCursor ? '▶ ' : ''}${esc(code)}</td>
|
||
<td class="ts">${esc(ts)}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
// Zur Cursor-Zeile scrollen
|
||
const cursorRow = elLinesBody.querySelector('.cursor-row');
|
||
if (cursorRow) cursorRow.scrollIntoView({ block: 'nearest' });
|
||
}
|
||
|
||
// ─── Daten laden ─────────────────────────────────────────────────────────────
|
||
async function refresh() {
|
||
try {
|
||
const [programs, active] = await Promise.all([
|
||
apiFetch('GET', '/api/programs'),
|
||
apiFetch('GET', '/api/active'),
|
||
]);
|
||
renderProgList(programs.programs || [], [], active.programId);
|
||
renderLines(active);
|
||
setStatus(elListStatus, '');
|
||
setStatus(elActStatus, '');
|
||
} catch (err) {
|
||
setStatus(elListStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Programm laden (FLoad-Äquivalent) ───────────────────────────────────────
|
||
async function loadProgram(id) {
|
||
setStatus(elActStatus, `Lade '${id}'…`);
|
||
try {
|
||
const state = await apiFetch('PUT', '/api/active', { id });
|
||
renderLines(state);
|
||
await refresh();
|
||
setStatus(elActStatus, `'${id}' geladen`);
|
||
} catch (err) {
|
||
setStatus(elActStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Stepping ────────────────────────────────────────────────────────────────
|
||
async function step(endpoint) {
|
||
setStatus(elActStatus, '…');
|
||
try {
|
||
const r = await apiFetch('POST', `/api/active/${endpoint}`);
|
||
setStatus(elActStatus, `Cursor → ${r.cursor}`);
|
||
await refresh();
|
||
} catch (err) {
|
||
setStatus(elActStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Clear ───────────────────────────────────────────────────────────────────
|
||
async function clearProgram() {
|
||
if (!currentActiveId) return;
|
||
if (!confirm(`Programm '${currentActiveId}' wirklich leeren?`)) return;
|
||
try {
|
||
const r = await apiFetch('POST', '/api/active/clear');
|
||
renderLines(r);
|
||
setStatus(elActStatus, 'Programm geleert');
|
||
} catch (err) {
|
||
setStatus(elActStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Programm löschen (aus Toolbar) ─────────────────────────────────────────
|
||
async function deleteSelected() {
|
||
if (!selectedId) return;
|
||
if (selectedType === 'folder') {
|
||
alert(`Ordner-Löschung ist noch nicht implementiert.\n('${selectedId}')`);
|
||
return;
|
||
}
|
||
if (!confirm(`Programm '${selectedId}' wirklich löschen?`)) return;
|
||
setStatus(elListStatus, `Lösche '${selectedId}'…`);
|
||
try {
|
||
await apiFetch('DELETE', `/api/programs/${encodeURIComponent(selectedId)}`);
|
||
setStatus(elListStatus, `'${selectedId}' gelöscht`);
|
||
selectedId = null;
|
||
selectedType = null;
|
||
elBtnDeleteSelected.disabled = true;
|
||
await refresh();
|
||
} catch (err) {
|
||
setStatus(elListStatus, `Fehler: ${err.message}`, true);
|
||
}
|
||
}
|
||
|
||
// ─── Neuen Ordner anlegen ────────────────────────────────────────────────────
|
||
async function createFolder() {
|
||
alert('Ordner-Verwaltung ist noch nicht implementiert.\n(kommt in Phase 3 des Roadmaps)');
|
||
}
|
||
|
||
// ─── Event-Listener ──────────────────────────────────────────────────────────
|
||
document.getElementById('btn-refresh').addEventListener('click', refresh);
|
||
document.getElementById('btn-new-folder').addEventListener('click', createFolder);
|
||
elBtnDeleteSelected.addEventListener('click', deleteSelected);
|
||
elBtnFirst.addEventListener('click', () => step('first'));
|
||
elBtnPrev .addEventListener('click', () => step('prev'));
|
||
elBtnNext .addEventListener('click', () => step('next'));
|
||
elBtnLast .addEventListener('click', () => step('last'));
|
||
elBtnClear.addEventListener('click', clearProgram);
|
||
|
||
// Collapse-Verhalten (wie im appRobotHoming)
|
||
document.querySelectorAll('.section h2').forEach(h2 => {
|
||
h2.addEventListener('click', () => h2.closest('.section').classList.toggle('collapsed'));
|
||
});
|
||
|
||
// ─── HTML escapen ────────────────────────────────────────────────────────────
|
||
function esc(s) {
|
||
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ─── Start ───────────────────────────────────────────────────────────────────
|
||
refresh();
|
||
setInterval(refresh, REFRESH_MS);
|
||
</script>
|
||
</body>
|
||
</html>
|