Files
appRobotFileservice/public/index.html
2026-06-14 21:45:22 +02:00

325 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="Liste neu laden">⟳ Aktualisieren</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;
// ─── 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');
// ─── 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()) };
}
// ─── Programmliste rendern ───────────────────────────────────────────────────
function renderProgList(programs, activeId) {
elProgCount.textContent = programs.length;
if (!programs.length) {
elProgList.innerHTML = '<span class="empty-hint">Keine Programme gefunden</span>';
return;
}
elProgList.innerHTML = programs
.map(p => {
const isActive = p.id === activeId;
return `<div class="prog-item${isActive ? ' is-active' : ''}" data-id="${esc(p.id)}">
<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>
<button class="btn-del" data-id="${esc(p.id)}" title="Löschen">✕</button>
</div>`;
}).join('');
// Klick auf Zeile → FLoad (Programm aktivieren)
elProgList.querySelectorAll('.prog-item').forEach(el => {
el.addEventListener('click', e => {
if (e.target.classList.contains('btn-del')) return;
loadProgram(el.dataset.id);
});
});
// Klick auf Löschen-Button
elProgList.querySelectorAll('.btn-del').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
deleteProgram(btn.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);
}
}
// ─── Programm löschen ────────────────────────────────────────────────────────
async function deleteProgram(id) {
if (!confirm(`Programm '${id}' wirklich löschen?`)) return;
setStatus(elListStatus, `Lösche '${id}'…`);
try {
await apiFetch('DELETE', `/api/programs/${encodeURIComponent(id)}`);
setStatus(elListStatus, `'${id}' gelöscht`);
await refresh();
} catch (err) {
setStatus(elListStatus, `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);
}
}
// ─── Event-Listener ──────────────────────────────────────────────────────────
document.getElementById('btn-refresh').addEventListener('click', refresh);
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ─── Start ───────────────────────────────────────────────────────────────────
refresh();
setInterval(refresh, REFRESH_MS);
</script>
</body>
</html>