From 7ec9b418329c2bfe7e5ada4048a37f6f54eb506a Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 29 Jul 2026 11:35:52 +0000 Subject: [PATCH 1/8] feat: add parsEO dashboard (filename builder + schema tools) --- tools/parseo-dashboard/README.md | 37 ++ tools/parseo-dashboard/dashboard.html | 550 ++++++++++++++++++++++++++ 2 files changed, 587 insertions(+) create mode 100644 tools/parseo-dashboard/README.md create mode 100644 tools/parseo-dashboard/dashboard.html diff --git a/tools/parseo-dashboard/README.md b/tools/parseo-dashboard/README.md new file mode 100644 index 00000000..193f8d42 --- /dev/null +++ b/tools/parseo-dashboard/README.md @@ -0,0 +1,37 @@ +# parsEO Dashboard + +Web-based dashboard for the parsEO CLMS Filename API. + +## Setup + +The parsEO API runs as a Docker container on **nucleus** at `http://nucleus:8000`. + +```bash +cd ~/parseo-api +docker compose up -d +``` + +## API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/health` | GET | Health check | +| `/api/info` | GET | parsEO version info | +| `/api/families` | GET | List all schema families | +| `/api/families/{family}/versions` | GET | List schema versions | +| `/api/families/{family}/schemas/{version}` | GET | Get schema JSON | +| `/api/assemble` | POST | Assemble filename from fields | +| `/api/parse` | POST | Parse a filename into fields | +| `/api/validate` | POST | Validate a schema JSON | +| `/api/cache/clear` | POST | Clear parsEO cache | + +## Usage + +Open `dashboard.html` in a browser. Set the API URL to `http://nucleus:8000` (or the host where the API runs). + +## Files + +- `dashboard.html` — Single-page dashboard app (dark theme) +- `api.py` — FastAPI server (in `~/parseo-api/` on nucleus) +- `Dockerfile` — Docker build (in `~/parseo-api/`) +- `docker-compose.yml` — Run configuration (in `~/parseo-api/`) \ No newline at end of file diff --git a/tools/parseo-dashboard/dashboard.html b/tools/parseo-dashboard/dashboard.html new file mode 100644 index 00000000..74b3f016 --- /dev/null +++ b/tools/parseo-dashboard/dashboard.html @@ -0,0 +1,550 @@ + + + + + +parsEO Dashboard — CLMS Filename Tool + + + + +
+

parsEO Dashboard

+ CLMS Filename Tool +
+
+ + Connecting… + +
+
+
+ +
+ + + + +
+ +
+
+
Filename
+
Parse
+
Schema
+
Validate
+
+ + +
+
+ ASSEMBLED FILENAME + Select a family, fill fields, and assemble. + +
+
+
+ + +
+
+ + +
+ +
+
Parsed fields will appear here.
+
+
+ + +
+
Select a family and version to view the schema.
+
+ + +
+
+ + +
+ +
+
+
+ + + +
+
+ + + + \ No newline at end of file From 3552e8266c6775e3801c998fb991a9b898b8bdd5 Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 29 Jul 2026 12:02:56 +0000 Subject: [PATCH 2/8] fix: use relative API URL, add fetch error handling, debounce, XSS escaping --- tools/parseo-dashboard/dashboard.html | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/tools/parseo-dashboard/dashboard.html b/tools/parseo-dashboard/dashboard.html index 74b3f016..407a67d9 100644 --- a/tools/parseo-dashboard/dashboard.html +++ b/tools/parseo-dashboard/dashboard.html @@ -133,7 +133,7 @@

parsEO Dashboard

API Connection

- +
@@ -216,6 +216,18 @@

Schema Info

let currentFamily = ''; let currentVersion = ''; let currentSchema = null; +let assembleDebounce = null; + +// Safe HTML escaping +function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } + +// Fetch helper that checks response.ok before parsing JSON +function apiJSON(url, opts) { + return fetch(url, opts).then(r => { + if (!r.ok) return r.json().then(d => Promise.reject(d.detail || r.statusText)); + return r.json(); + }); +} function connectAPI() { API_BASE = document.getElementById('api-url').value.replace(/\/+$/, ''); @@ -225,8 +237,7 @@

Schema Info

dot.className = 'conn-dot pending'; text.textContent = 'Connecting…'; - fetch(API_BASE + '/api/health') - .then(r => r.json()) + apiJSON(API_BASE + '/api/health') .then(d => { dot.className = 'conn-dot ok'; text.textContent = 'Connected'; @@ -244,14 +255,13 @@

Schema Info

sel.disabled = true; sel.innerHTML = ''; - fetch(API_BASE + '/api/families') - .then(r => r.json()) + apiJSON(API_BASE + '/api/families') .then(d => { sel.innerHTML = ''; // Sort families const families = (d.families || []).sort(); for (const f of families) { - sel.innerHTML += ``; + sel.innerHTML += ``; } sel.disabled = false; }) @@ -274,8 +284,7 @@

Schema Info

const vlist = document.getElementById('version-list'); vlist.innerHTML = ''; - fetch(API_BASE + '/api/families/' + encodeURIComponent(family) + '/versions') - .then(r => r.json()) + apiJSON(API_BASE + '/api/families/' + encodeURIComponent(family) + '/versions') .then(d => { const versions = d.versions || []; if (versions.length === 0) { @@ -389,6 +398,12 @@

Schema Info

} function assembleFilename() { + // Debounce: wait 300ms after last input before firing + if (assembleDebounce) clearTimeout(assembleDebounce); + assembleDebounce = setTimeout(() => _doAssemble(), 300); +} + +function _doAssemble() { const preview = document.getElementById('filename-preview'); if (!currentSchema) return; @@ -413,7 +428,7 @@

Schema Info

preview.textContent = 'Assembling…'; - fetch(API_BASE + '/api/assemble', { + apiJSON(API_BASE + '/api/assemble', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -422,7 +437,6 @@

Schema Info

fields: fields }) }) - .then(r => r.json()) .then(d => { if (d.filename) { preview.textContent = d.filename; @@ -473,12 +487,11 @@

Schema Info

if (!fn) { result.textContent = 'Enter a filename to parse.'; return; } result.textContent = 'Parsing…'; - fetch(API_BASE + '/api/parse', { + apiJSON(API_BASE + '/api/parse', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: fn }) }) - .then(r => r.json()) .then(d => { if (d.parsed) { result.textContent = JSON.stringify(d.parsed, null, 2); @@ -500,12 +513,11 @@

Schema Info

results.innerHTML = ' Validating…'; - fetch(API_BASE + '/api/validate', { + apiJSON(API_BASE + '/api/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ schema: schema }) }) - .then(r => r.json()) .then(d => { let html = ''; if (d.errors && d.errors.length) { From 30462249f301a4e9f7622a2432d6c46f55cdaf21 Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 29 Jul 2026 12:24:49 +0000 Subject: [PATCH 3/8] fix: empty API base for same-origin (SWAG proxy) --- tools/parseo-dashboard/dashboard.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/parseo-dashboard/dashboard.html b/tools/parseo-dashboard/dashboard.html index 407a67d9..87c746c9 100644 --- a/tools/parseo-dashboard/dashboard.html +++ b/tools/parseo-dashboard/dashboard.html @@ -133,7 +133,7 @@

parsEO Dashboard

API Connection

- +
@@ -554,7 +554,6 @@

Schema Info

// Auto-connect on load document.addEventListener('DOMContentLoaded', () => { - // Try localhost first, then nucleus connectAPI(); }); From 2e2767cb71f05cfdc3183123c2462c95f5847d9e Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 29 Jul 2026 12:29:47 +0000 Subject: [PATCH 4/8] fix: remove API URL field, auto-connect on same-origin --- tools/parseo-dashboard/dashboard.html | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tools/parseo-dashboard/dashboard.html b/tools/parseo-dashboard/dashboard.html index 87c746c9..0eedaa10 100644 --- a/tools/parseo-dashboard/dashboard.html +++ b/tools/parseo-dashboard/dashboard.html @@ -130,14 +130,7 @@

parsEO Dashboard

@@ -210,6 +232,8 @@

Schema Info

let currentVersion = ''; let currentSchema = null; let assembleDebounce = null; +let fieldValues = {}; +let fieldTreeOrder = []; // Safe HTML escaping function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } @@ -322,74 +346,131 @@

Schema Info

} function buildFieldInputs(schema) { - const container = document.getElementById('field-inputs'); + const container = document.getElementById('field-tree'); const fields = schema.fields || {}; container.innerHTML = ''; document.getElementById('schema-fields').style.display = 'block'; + fieldValues = {}; const template = schema.template || ''; - // Extract field order from template const fieldOrder = [...template.matchAll(/\{(\w+)\}/g)].map(m => m[1]); const seen = new Set(); + fieldTreeOrder = []; for (const f of fieldOrder) { if (f === 'extension' || seen.has(f)) continue; seen.add(f); - const def = fields[f]; - if (!def) continue; - addFieldInput(container, f, def); + if (!fields[f]) continue; + fieldTreeOrder.push(f); } - - // Add any remaining fields not in template - for (const [name, def] of Object.entries(fields)) { - if (seen.has(name)) continue; - if (name === 'extension') continue; + for (const name of Object.keys(fields)) { + if (seen.has(name) || name === 'extension') continue; seen.add(name); - addFieldInput(container, name, def); + fieldTreeOrder.push(name); + } + if (fields.extension) fieldTreeOrder.push('extension'); + + // Pre-fill obvious single-enum / default fields so downstream rows unlock immediately + for (const name of fieldTreeOrder) { + const def = fields[name]; + if (def.enum && def.enum.length === 1) fieldValues[name] = def.enum[0]; } + const defaults = { programme: 'CLMS', type: 'R', epsg_code: '03035', variant: 'S2' }; + for (const [name, val] of Object.entries(defaults)) { + if (fields[name] && !fieldValues[name] && !fields[name].enum) fieldValues[name] = val; + } + + renderFieldTree(schema); + assembleFilename(); +} + +function renderFieldTree(schema) { + const container = document.getElementById('field-tree'); + const fields = schema.fields || {}; + container.innerHTML = ''; - // Extension - if (fields.extension) { - addFieldInput(container, 'extension', fields.extension); + for (const name of fieldTreeOrder) { + const def = fields[name]; + if (!def) continue; + container.appendChild(buildFieldRow(name, def)); } } -function addFieldInput(container, name, def) { - const div = document.createElement('div'); - div.className = 'form-row'; - const label = document.createElement('label'); - label.textContent = name.replace(/_/g, ' '); - div.appendChild(label); - - let input; - if (def.enum && def.enum.length <= 20) { - input = document.createElement('select'); - input.innerHTML = ''; +function buildFieldRow(name, def) { + const row = document.createElement('div'); + row.className = 'field-row'; + row.dataset.fieldName = name; + + const label = document.createElement('div'); + label.className = 'field-row-label'; + const labelText = document.createElement('span'); + labelText.textContent = name.replace(/_/g, ' '); + label.appendChild(labelText); + if (fieldValues[name]) { + const valSpan = document.createElement('span'); + valSpan.className = 'val'; + valSpan.textContent = fieldValues[name]; + label.appendChild(valSpan); + } + row.appendChild(label); + + if (def.enum && def.enum.length > 0 && def.enum.length <= 40) { + const chipRow = document.createElement('div'); + chipRow.className = 'chip-row'; for (const v of def.enum) { - input.innerHTML += ``; + const chip = document.createElement('span'); + chip.className = 'chip' + (fieldValues[name] === v ? ' selected' : ''); + chip.textContent = v; + chip.onclick = () => onChipClick(name, v); + chipRow.appendChild(chip); } - } else if (def.enum && def.enum.length > 20) { - input = document.createElement('input'); - input.placeholder = def.enum.slice(0, 5).join(', ') + '…'; - if (def.enum.length === 1) input.value = def.enum[0]; + row.appendChild(chipRow); } else { - input = document.createElement('input'); - if (def.pattern) { - input.placeholder = 'regex: ' + def.pattern.slice(0, 30); + const input = document.createElement('input'); + input.className = 'chip-input'; + input.value = fieldValues[name] || ''; + if (def.enum && def.enum.length > 40) { + input.placeholder = def.enum.slice(0, 5).join(', ') + '…'; + } else if (def.pattern) { + input.placeholder = 'regex: ' + def.pattern.slice(0, 40); } - // Default values for common fields - if (name === 'programme') input.value = 'CLMS'; - if (name === 'type') input.value = 'R'; - if (name === 'epsg_code') input.value = '03035'; - if (name === 'variant') input.value = 'S2'; + input.oninput = () => onChipClick(name, input.value.trim(), true); + row.appendChild(input); } - input.dataset.fieldName = name; - input.onchange = () => assembleFilename(); - input.oninput = () => assembleFilename(); - div.appendChild(input); - container.appendChild(div); + + return row; } +function onChipClick(name, value, isTextInput) { + if (fieldValues[name] === value) { + // toggle off (chip only) + if (!isTextInput) delete fieldValues[name]; + } else if (value === '') { + delete fieldValues[name]; + } else { + fieldValues[name] = value; + } + if (!isTextInput) renderFieldTree(currentSchema); + else { + // just refresh the label val marker without rebuilding the input (keeps focus) + const row = document.querySelector(`.field-row[data-field-name="${CSS.escape(name)}"] .field-row-label`); + if (row) { + row.innerHTML = ''; + const labelText = document.createElement('span'); + labelText.textContent = name.replace(/_/g, ' '); + row.appendChild(labelText); + if (fieldValues[name]) { + const valSpan = document.createElement('span'); + valSpan.className = 'val'; + valSpan.textContent = fieldValues[name]; + row.appendChild(valSpan); + } + } + } + assembleFilename(); +} + + function assembleFilename() { // Debounce: wait 300ms after last input before firing if (assembleDebounce) clearTimeout(assembleDebounce); @@ -400,14 +481,7 @@

Schema Info

const preview = document.getElementById('filename-preview'); if (!currentSchema) return; - const fields = {}; - const inputs = document.querySelectorAll('#field-inputs input, #field-inputs select'); - for (const el of inputs) { - const name = el.dataset.fieldName; - if (!name) continue; - const val = el.value.trim(); - if (val) fields[name] = val; - } + const fields = { ...fieldValues }; // Add programme from enum if not set if (!fields.programme && currentSchema.fields?.programme?.enum?.length === 1) { From 19080f940101ddb6824da713674fb85e4879ec6d Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 29 Jul 2026 13:44:50 +0000 Subject: [PATCH 6/8] feat: add parsEO dashboard to DOCS/tools (Quarto site) --- .github/non_browsable_doc_map.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/non_browsable_doc_map.json b/.github/non_browsable_doc_map.json index ea7c0717..a33e0cf8 100644 --- a/.github/non_browsable_doc_map.json +++ b/.github/non_browsable_doc_map.json @@ -27,4 +27,4 @@ "url": "/a3e44009d6b7375f60a58b11278cbe079d93407249c0c5e1c97f00e12e98c09a.html" } ] -} \ No newline at end of file +}tools/parseo-dashboard From 208ade3b3c3ce04816e0fd0bb72708eb1a6b7d9c Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 29 Jul 2026 13:45:50 +0000 Subject: [PATCH 7/8] feat: add parsEO dashboard to DOCS/tools with index.qmd landing page --- DOCS/tools/parseo-dashboard/dashboard.html | 641 +++++++++++++++++++++ DOCS/tools/parseo-dashboard/index.qmd | 21 + 2 files changed, 662 insertions(+) create mode 100644 DOCS/tools/parseo-dashboard/dashboard.html create mode 100644 DOCS/tools/parseo-dashboard/index.qmd diff --git a/DOCS/tools/parseo-dashboard/dashboard.html b/DOCS/tools/parseo-dashboard/dashboard.html new file mode 100644 index 00000000..c5467e9a --- /dev/null +++ b/DOCS/tools/parseo-dashboard/dashboard.html @@ -0,0 +1,641 @@ + + + + + +parsEO Dashboard — CLMS Filename Tool + + + + +
+

parsEO Dashboard

+ CLMS Filename Tool +
+
+ + Connecting… + +
+
+
+ +
+ + + + +
+ +
+
+
Filename
+
Parse
+
Schema
+
Validate
+
+ + +
+
+ ASSEMBLED FILENAME + Select a family, fill fields, and assemble. + +
+
+
+ + +
+
+ + +
+ +
+
Parsed fields will appear here.
+
+
+ + +
+
Select a family and version to view the schema.
+
+ + +
+
+ + +
+ +
+
+
+ + + +
+
+ + + + \ No newline at end of file diff --git a/DOCS/tools/parseo-dashboard/index.qmd b/DOCS/tools/parseo-dashboard/index.qmd new file mode 100644 index 00000000..24272129 --- /dev/null +++ b/DOCS/tools/parseo-dashboard/index.qmd @@ -0,0 +1,21 @@ +--- +title: "ParsEO Dashboard" +subtitle: "CLMS Filename Builder & Schema Tool" +category: tools +date: '2026-07-29' +description: "Interactive web tool for building, parsing, and validating CLMS filenames using the parsEO API." +--- + +The parsEO dashboard lets you build CLMS filenames by clicking through schema fields, parse existing filenames, and validate schema JSON files — all powered by the parsEO API. + +[Open the Dashboard →](dashboard.html) + +> The dashboard connects to `parseo.mattiuzzi.com` for API calls. Make sure you have network access to it. + +### Features + +- **Clickable chip-tree** — build filenames by selecting values pill-by-pill +- **Live preview** — see the assembled filename update instantly +- **Parse** — paste a filename and get its parsed fields +- **Validate** — paste a schema JSON and run validation checks +- **Schema browser** — explore all parsEO schema families and versions \ No newline at end of file From 5dd9f0847ec922a0c3939e81c9e31dd1aa98d4f2 Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 29 Jul 2026 14:01:01 +0000 Subject: [PATCH 8/8] feat: parsEO dashboard as OJS Quarto dashboard (replaces standalone HTML) --- DOCS/tools/parseo-dashboard/dashboard.html | 641 --------------------- DOCS/tools/parseo-dashboard/index.qmd | 228 +++++++- tools/parseo-dashboard/README.md | 37 -- tools/parseo-dashboard/dashboard.html | 641 --------------------- 4 files changed, 214 insertions(+), 1333 deletions(-) delete mode 100644 DOCS/tools/parseo-dashboard/dashboard.html delete mode 100644 tools/parseo-dashboard/README.md delete mode 100644 tools/parseo-dashboard/dashboard.html diff --git a/DOCS/tools/parseo-dashboard/dashboard.html b/DOCS/tools/parseo-dashboard/dashboard.html deleted file mode 100644 index c5467e9a..00000000 --- a/DOCS/tools/parseo-dashboard/dashboard.html +++ /dev/null @@ -1,641 +0,0 @@ - - - - - -parsEO Dashboard — CLMS Filename Tool - - - - -
-

parsEO Dashboard

- CLMS Filename Tool -
-
- - Connecting… - -
-
-
- -
- - - - -
- -
-
-
Filename
-
Parse
-
Schema
-
Validate
-
- - -
-
- ASSEMBLED FILENAME - Select a family, fill fields, and assemble. - -
-
-
- - -
-
- - -
- -
-
Parsed fields will appear here.
-
-
- - -
-
Select a family and version to view the schema.
-
- - -
-
- - -
- -
-
-
- - - -
-
- - - - \ No newline at end of file diff --git a/DOCS/tools/parseo-dashboard/index.qmd b/DOCS/tools/parseo-dashboard/index.qmd index 24272129..efa06a9d 100644 --- a/DOCS/tools/parseo-dashboard/index.qmd +++ b/DOCS/tools/parseo-dashboard/index.qmd @@ -1,21 +1,221 @@ --- -title: "ParsEO Dashboard" -subtitle: "CLMS Filename Builder & Schema Tool" -category: tools -date: '2026-07-29' -description: "Interactive web tool for building, parsing, and validating CLMS filenames using the parsEO API." +title: "parsEO — CLMS Filename Builder" +subtitle: "Assemble, parse & validate CLMS filenames" +author: "European Environment Agency (EEA)" +date: 2026-07-29 +category: non-browsable +type: dashboard +format: + html: + toc: false + page-layout: full + smooth-scroll: true --- -The parsEO dashboard lets you build CLMS filenames by clicking through schema fields, parse existing filenames, and validate schema JSON files — all powered by the parsEO API. -[Open the Dashboard →](dashboard.html) -> The dashboard connects to `parseo.mattiuzzi.com` for API calls. Make sure you have network access to it. +```{ojs} +// ── parsEO API base ── +const API = "https://parseo.mattiuzzi.com"; -### Features +// ── State ── +const families = view(Inputs.select(new Map(), {label: "Family", value: ""})); +const versions = Mutable([]); +const currentVersion = Mutable(""); +const fieldValues = Mutable({}); +const selectedFamily = Mutable(""); +const familySchemas = Mutable({}); -- **Clickable chip-tree** — build filenames by selecting values pill-by-pill -- **Live preview** — see the assembled filename update instantly -- **Parse** — paste a filename and get its parsed fields -- **Validate** — paste a schema JSON and run validation checks -- **Schema browser** — explore all parsEO schema families and versions \ No newline at end of file +// ── Load families on startup ── +const familiesData = await fetch(API + "/api/families") + .then(r => r.ok ? r.json() : {families: []}) + .catch(() => ({families: []})); + +// Update family selector +families.value = new Map( + familiesData.families.sort().map(f => [f, f]) +); +families.dispatchEvent(new Event("input")); + +// ── When family changes, load versions ── +async function onFamilyChange(family) { + if (!family) return; + selectedFamily.value = family; + const data = await fetch(`${API}/api/families/${family}/versions`) + .then(r => r.ok ? r.json() : {versions: []}) + .catch(() => ({versions: []})); + versions.value = data.versions || []; + currentVersion.value = versions.value.length ? versions.value[versions.value.length - 1] : ""; + + // Load schema for latest version + if (currentVersion.value) { + const schema = await fetch(`${API}/api/families/${family}/schemas/${currentVersion.value}`) + .then(r => r.ok ? r.json() : null) + .catch(() => null); + if (schema) { + familySchemas.value = schema; + // Auto-fill single-enum fields + const fv = {}; + for (const [name, def] of Object.entries(schema.fields || {})) { + if (def.enum && def.enum.length === 1) fv[name] = def.enum[0]; + if (name === "programme") fv[name] = "CLMS"; + if (name === "extension") fv[name] = "tif"; + } + fieldValues.value = fv; + } + } +} + +// ── Version selector ── +const versionPicker = versions.value.length > 0 + ? Inputs.select(versions.value, {label: "Version", value: currentVersion.value}) + : html`No versions`; + +if (versionPicker.tagName === "SELECT") { + versionPicker.addEventListener("input", async () => { + currentVersion.value = versionPicker.value; + const schema = await fetch(`${API}/api/families/${selectedFamily.value}/schemas/${currentVersion.value}`) + .then(r => r.ok ? r.json() : null) + .catch(() => null); + if (schema) familySchemas.value = schema; + }); +} + +// ── Assemble filename ── +async function assemble(fields) { + if (!fields || Object.keys(fields).length === 0) return ""; + const data = await fetch(API + "/api/assemble", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({family: selectedFamily.value, version: currentVersion.value, fields}) + }).then(r => r.ok ? r.json() : null).catch(() => null); + return data?.filename || ""; +} + +// ── Render field chips ── +function renderField(name, def, values, onSelect) { + const label = name.replace(/_/g, " "); + const current = values[name] || ""; + let el; + + if (def.enum && def.enum.length <= 30) { + // Chips mode + el = html`
+
+ ${label}${current ? html` · ${current}` : ""} +
+
+ ${def.enum.map(v => html``)} +
+
`; + } else { + // Input mode + el = html`
+ + onSelect(name, e.target.value)}> +
`; + } + return el; +} + +// ── Parse tab ── +async function doParse(fn) { + if (!fn) return "Enter a filename."; + const data = await fetch(API + "/api/parse", { + method: "POST", headers: {"Content-Type": "application/json"}, + body: JSON.stringify({filename: fn}) + }).then(r => r.ok ? r.json() : null).catch(() => null); + if (!data?.parsed) return "Parse failed."; + return html`
${JSON.stringify(data.parsed, null, 2)}
`; +} + +// ── Validate tab ── +async function doValidate(json) { + let schema; + try { schema = JSON.parse(json); } catch(e) { return html`
Invalid JSON: ${e.message}
`; } + const data = await fetch(API + "/api/validate", { + method: "POST", headers: {"Content-Type": "application/json"}, + body: JSON.stringify({schema}) + }).then(r => r.ok ? r.json() : null).catch(() => null); + if (!data) return "Validation failed."; + return html`
+ ${data.valid + ? html`
\u2713 Valid (${data.errors.length} errors, ${data.warnings.length} warnings)
` + : html`
\u2717 Invalid
`} + ${data.errors.map(e => html`
\u00b7 ${e}
`)} + ${data.warnings.map(w => html`
\u00b7 ${w}
`)} +
`; +} +``` + +## Filename Builder + +
+
+
+ ${families} + ${versionPicker} +
+
+ ${familySchemas.value?.fields ? Object.entries(familySchemas.value.fields).map(([name, def]) => { + if (name === "extension") return null; + return renderField(name, def, fieldValues.value, (n, v) => { + fieldValues.value = {...fieldValues.value, [n]: v}; + assemble(fieldValues.value).then(fn => { + const el = document.getElementById("filename-preview"); + if (el) el.textContent = fn || "\u2014"; + }); + }); + }) : html`Select a family first.`} +
+
+
+
+
+
Assembled Filename
+
\u2014
+ ${familySchemas.value?.examples ? html` +
Examples
+ ${familySchemas.value.examples.slice(0,3).map(ex => html`
${ex}
`)} + ` : ""} +
+ +
+
+
Parse a Filename
+ + +
+
+ +
+
Validate a Schema
+ + +
+
+
+
+
+``` \ No newline at end of file diff --git a/tools/parseo-dashboard/README.md b/tools/parseo-dashboard/README.md deleted file mode 100644 index 193f8d42..00000000 --- a/tools/parseo-dashboard/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# parsEO Dashboard - -Web-based dashboard for the parsEO CLMS Filename API. - -## Setup - -The parsEO API runs as a Docker container on **nucleus** at `http://nucleus:8000`. - -```bash -cd ~/parseo-api -docker compose up -d -``` - -## API Endpoints - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/api/health` | GET | Health check | -| `/api/info` | GET | parsEO version info | -| `/api/families` | GET | List all schema families | -| `/api/families/{family}/versions` | GET | List schema versions | -| `/api/families/{family}/schemas/{version}` | GET | Get schema JSON | -| `/api/assemble` | POST | Assemble filename from fields | -| `/api/parse` | POST | Parse a filename into fields | -| `/api/validate` | POST | Validate a schema JSON | -| `/api/cache/clear` | POST | Clear parsEO cache | - -## Usage - -Open `dashboard.html` in a browser. Set the API URL to `http://nucleus:8000` (or the host where the API runs). - -## Files - -- `dashboard.html` — Single-page dashboard app (dark theme) -- `api.py` — FastAPI server (in `~/parseo-api/` on nucleus) -- `Dockerfile` — Docker build (in `~/parseo-api/`) -- `docker-compose.yml` — Run configuration (in `~/parseo-api/`) \ No newline at end of file diff --git a/tools/parseo-dashboard/dashboard.html b/tools/parseo-dashboard/dashboard.html deleted file mode 100644 index c9ed95cc..00000000 --- a/tools/parseo-dashboard/dashboard.html +++ /dev/null @@ -1,641 +0,0 @@ - - - - - -parsEO Dashboard — CLMS Filename Tool - - - - -
-

parsEO Dashboard

- CLMS Filename Tool -
-
- - Connecting… - -
-
-
- -
- - - - -
- -
-
-
Filename
-
Parse
-
Schema
-
Validate
-
- - -
-
- ASSEMBLED FILENAME - Select a family, fill fields, and assemble. - -
-
-
- - -
-
- - -
- -
-
Parsed fields will appear here.
-
-
- - -
-
Select a family and version to view the schema.
-
- - -
-
- - -
- -
-
-
- - - -
-
- - - - \ No newline at end of file