ZDDC/classifier/template.html
ZDDC ce6efb0201 feat(classifier): CSV path round-trip — export filtered paths, import old→new mapping
Add an AI-friendly classification round-trip alongside the By-tracking grid:

- "⬇ Export paths" (filetree header): downloads the filtered file list as a
  1-column CSV of full (root-relative) paths — the same keys the importer
  matches on. Hand it to an LLM to classify into
  <party>/<direction>/<transmittal>/<file>.ext.
- "Import paths…" (above the target list): loads a 2-column CSV (old path,
  new path). Each new path drives both axes — the trailing filename sets the
  tracking number (rename, via parseFilename → tracking tree) and the leading
  <party>/<direction>/<transmittal> segments route a transmittal (via
  parseFolder → transmittal tree). MERGE semantics: only files named in the
  CSV are touched; others keep their classification.
- Per-row problems (unknown old path, unparseable filename/transmittal, bad
  direction) are collected and offered as a downloadable errors CSV, with a
  summary toast — scales to thousands of rows. Either axis can apply
  independently, so a filename-only new path is a rename with no error.

This replaces the JSON "Export for editing" / "Import edits" pair (the CSV
path form is fully expressive for this model and simpler to round-trip); the
TSV "Export list" clipboard→Excel button is kept. Buttons can grow into a
modal later if more options are needed.

Includes a Playwright test driving the real file-input import (rename+route,
filename-only, merge-preserves-unlisted, CSV-quoted comma in title, error row).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:38:15 -05:00

283 lines
21 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="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZDDC Classifier</title>
<link rel="icon" type="image/svg+xml" href="{{FAVICON}}">
<style>
{{CSS_PLACEHOLDER}}
</style>
</head>
<body>
<div id="app">
<!-- Main Application -->
<div id="mainApp" class="main-app">
<!-- Header -->
<header class="app-header">
<div class="header-left">
<svg class="app-header__logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true">
<rect width="64" height="64" rx="12" fill="#1e3a5f"/>
<g fill="#fff">
<rect x="14" y="18" width="36" height="7"/>
<polygon points="43,25 50,25 21,43 14,43"/>
<rect x="14" y="43" width="36" height="7"/>
</g>
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp">{{BUILD_LABEL}}</span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>
<button id="workspacesBtn" class="btn btn-secondary btn-sm" title="Workspaces — open or create a classification project">≡ Workspaces</button>
<button id="connectDirBtn" class="btn btn-primary btn-sm" title="Connect this workspace's source directory to preview, copy, or finish scanning" hidden>⮷ Connect directory</button>
</div>
<div class="header-right">
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)"></button>
<button id="help-btn" class="btn btn-secondary" title="Help">?</button>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<!-- Folder Tree -->
<aside class="folder-tree-pane" id="folderTreePane">
<div class="pane-header">
<div class="pane-header-title">
<button class="btn btn-sm collapse-tree-btn" id="collapseTreeBtn" title="Collapse folder tree"></button>
<h3>Folder Tree</h3>
</div>
<div class="pane-header-controls">
<label class="checkbox-label" title="Auto-scroll folder tree when hovering files">
<input type="checkbox" id="autoScrollCheckbox" checked>
Auto-scroll
</label>
<label class="checkbox-label" id="hideCompliantLabel">
<input type="checkbox" id="hideCompliantCheckbox">
Hide Compliant
</label>
</div>
</div>
<div id="classifyFilters" class="classify-filters tree-toolbar" hidden>
<span class="tree-toolbar__label">Show</span>
<label class="checkbox-label" title="Not assigned on either axis">
<input type="checkbox" id="showUnassignedCheckbox" checked>
Unassigned <span class="filter-count" id="showUnassignedCount"></span>
</label>
<label class="checkbox-label" title="Assigned in the OTHER tab but not this one — the to-do for this tab">
<input type="checkbox" id="showPartialCheckbox" checked>
Partial <span class="filter-count" id="showPartialCount"></span>
</label>
<label class="checkbox-label" title="Already assigned in the active tab">
<input type="checkbox" id="showAssignedCheckbox" checked>
Assigned <span class="filter-count" id="showAssignedCount"></span>
</label>
<label class="checkbox-label" title="Excluded files">
<input type="checkbox" id="showExcludedCheckbox" checked>
Excluded <span class="filter-count" id="showExcludedCount"></span>
</label>
<label class="checkbox-label" title="Folders that contain no files">
<input type="checkbox" id="showEmptyCheckbox" checked>
Empty
</label>
<button class="btn btn-sm export-list-btn" id="exportListBtn"
title="Copy the filtered file list (path + file columns, no folders) as TSV — paste into Excel, edit, then paste back via “Paste rows”. Paste a full path into the Current name column to bind that exact file.">⬆ Export list</button>
<button class="btn btn-sm export-list-btn" id="exportPathsBtn"
title="Download the filtered file list as a 1-column CSV of full paths. Feed it to an AI to classify into <party>/<direction>/<transmittal>/<file>.ext, then bring the 2-column result back via “Import paths” above the target list.">⬇ Export paths</button>
</div>
<input type="search" id="treeFilterInput" class="tree-filter" spellcheck="false"
placeholder="Filter files… (e.g. master deliverables list)" aria-label="Filter files">
<div id="folderTree" class="folder-tree">
<!-- Dynamically populated -->
</div>
<div class="resize-handle" id="treeResizeHandle"></div>
</aside>
<!-- The classify surface: one editable By-tracking grid + a
transmittal tree. Tracking number alone ⇒ a Rename (in place);
add a transmittal folder ⇒ a Copy into the archive. -->
<main class="target-pane" id="targetPane">
<div class="pane-header pane-header--target">
<p class="target-goal">Give each file a <strong>tracking number</strong> (revision + status + title) under <em>By tracking number</em> — that alone is a <strong>Rename</strong> in place. Route it into a <strong>transmittal folder</strong> under <em>By transmittal</em> to <strong>Copy</strong> it into the archive.</p>
<div class="target-tabs" role="tablist">
<button class="target-tab active" id="trackingTab" role="tab">By tracking number</button>
<button class="target-tab" id="transmittalTab" role="tab">By transmittal</button>
</div>
<div class="pane-header-right">
<span id="classifyStats" class="file-stats"></span>
<span class="header-divider">|</span>
<button id="importPathsBtn" class="btn btn-secondary btn-sm" title="Import a 2-column CSV (old path, new path). Each new path “<party>/<direction>/<transmittal>/<file>.ext” sets that files tracking number (rename) and routes it into a transmittal. Only files named in the CSV are touched — others keep their current classification. Export the source list first via “Export paths” on the left.">Import paths…</button>
<input type="file" id="importPathsInput" accept=".csv,text/csv,text/plain" hidden>
<button id="resetDatasetBtn" class="btn btn-sm btn-danger" title="Discard all classifications and start over from the raw scanned input (does not touch your files)">Reset</button>
</div>
</div>
<div class="target-body">
<section id="trackingPanel" class="target-panel">
<div class="target-panel__toolbar">
<button id="loadWorklistBtn" class="btn btn-sm btn-secondary" title="Add tracking numbers from the project archive/MDL (pick directories to scan) as rows to fill.">⊞ Load…</button>
<button id="pasteRowsBtn" class="btn btn-sm btn-secondary" title="Paste rows from Excel: Tracking · Rev (Status) · Title · Current name.">⎘ Paste rows…</button>
<button id="matchNamesBtn" class="btn btn-sm btn-secondary" title="Auto-match unassigned files to list rows by name.">⚡ Match names</button>
<button id="addFilteredBtn" class="btn btn-sm btn-secondary" title="Add every file the left tree currently shows (filters applied) to the grid.">⊕ Add filtered files</button>
<button id="clearListBtn" class="btn btn-sm btn-secondary" title="Remove the list (placeholder) rows. Every assignment is kept.">Clear list</button>
<button id="trackingColsBtn" class="btn btn-sm btn-secondary" title="Show or hide columns">Columns ▾</button>
<span class="target-hint">Drag files in (ctrl-shift-click the left tree to multi-select; drop on a row to fill a contiguous block), or “Add filtered files”. Type the tracking number, revision (“A (IFR)”) and title — already-ZDDC-named files fill in. “Load…” / “Paste rows…” add rows to drop files onto.</span>
<button id="renameBtn" class="btn btn-sm btn-danger" disabled title="Rename the name-complete files IN PLACE on disk. Destructive — no backup.">Rename…</button>
</div>
<input type="search" id="trackingFilterInput" class="tree-filter target-filter" spellcheck="false"
placeholder="Filter the grid…" aria-label="Filter the tracking grid">
<div id="trackingTree" class="target-tree"></div>
</section>
<section id="transmittalPanel" class="target-panel" hidden>
<div class="target-panel__toolbar">
<button id="addPartyBtn" class="btn btn-sm btn-secondary">+ Party</button>
<span class="target-hint">&lt;party&gt;/{received,issued}/&lt;transmittal&gt;. Drag files (or a whole folder) into a transmittal, then Copy into the archive.</span>
<button id="checkDuplicatesBtn" class="btn btn-secondary btn-sm" title="Check for files with the same tracking number + revision but different content (flagged ≠ in red)">Check</button>
<button id="copyOutputBtn" class="btn btn-primary btn-sm" disabled title="Copy fully-classified files (+ their transmittal folders) into the archive — server or a local folder. Source untouched, resumable, verified.">Copy…</button>
</div>
<input type="search" id="transmittalFilterInput" class="tree-filter target-filter" spellcheck="false"
placeholder="Filter the transmittal tree…" aria-label="Filter transmittal tree">
<div id="transmittalTree" class="target-tree"></div>
</section>
</div>
</main>
</div>
<!-- Page footer — scan status lives here -->
<footer class="app-footer">
<span id="scanStatus" class="scan-status" aria-live="polite"></span>
</footer>
<!-- Empty State — shown until a directory is selected -->
<div id="welcomeScreen" class="empty-state empty-state--overlay">
<div class="empty-state__inner empty-state__inner--centered welcome">
<h1 class="welcome__title">ZDDC Classifier</h1>
<p class="welcome__lede">Turn a messy folder of project files into clean, correctly-named ZDDC documents — organized by tracking number and transmittal — <strong>without ever changing your originals</strong>.</p>
<!-- Workspaces (Classify & Copy) -->
<section id="workspacesSection" class="workspaces">
<div class="ws-head">
<h2>Your workspaces</h2>
<div class="ws-head__actions">
<button id="importWorkspaceBtn" class="btn btn-secondary" title="Import a whole workspace (.zddc-workspace.json) exported from another browser — restores the scanned snapshot so you don't re-scan">⭱ Import workspace</button>
<button id="newWorkspaceBtn" class="btn btn-primary">+ New workspace</button>
</div>
<input type="file" id="importWorkspaceInput" accept="application/json,.json" hidden>
</div>
<div id="workspaceList" class="ws-list"><!-- rendered --></div>
</section>
<!-- One flow, two endings: rename in place or copy to the archive -->
<div class="welcome__methods">
<section class="method method--primary">
<h3 class="method__title">Classify, then Rename or Copy</h3>
<p class="method__what">Give each file a ZDDC name in the <strong>By tracking number</strong> grid. A tracking number alone <em>is</em> a rename; add a <strong>transmittal folder</strong> and it can be copied into the archive.</p>
<ol class="method__steps">
<li><strong>New workspace</strong> → pick a folder. It scans <em>once</em> and saves to this browser, so you can close the tab and pick up later.</li>
<li><strong>Add files to the grid</strong> — drag them from the left tree (ctrl-shift-click to multi-select and fill a block of rows), use <strong>⊕ Add filtered files</strong>, or <strong>⊞ Load…</strong> / <strong>⎘ Paste rows…</strong> a list of expected tracking numbers and drop the matching files on. Already-ZDDC-named files fill in automatically.</li>
<li>Type each file's <strong>tracking number</strong>, <strong>revision</strong> (e.g. <code>A (IFR)</code>) and <strong>title</strong>.</li>
<li><strong>Rename…</strong> (By tracking number) renames the named files <em>in place</em> on disk — <span class="method__tag method__tag--warn">destructive, no backup</span>. Or place them into a transmittal under <strong>By transmittal</strong> and <strong>Copy…</strong> them into the archive (source untouched, resumable, verified).</li>
</ol>
</section>
</div>
<!-- Browser Compatibility Warning -->
<div id="browserWarning" class="browser-warning hidden">
<h3>⚠️ Browser Not Supported</h3>
<p>This application requires the File System Access API, available only in Chromium-based browsers (Chrome, Edge, Brave, Opera).</p>
</div>
<p class="welcome__note">Everything runs in your browser — no files are uploaded. Tip: if your folder lives on OneDrive/SharePoint, set it to <em>“Always keep on this device”</em> first for a much faster scan.</p>
</div>
</div>
</div>
<!-- Help Panel -->
<aside id="help-panel" class="help-panel" hidden aria-labelledby="help-panel-title">
<div class="help-panel__header">
<h2 id="help-panel-title" class="help-panel__title">Help — ZDDC Classifier</h2>
<button type="button" class="help-panel__close" id="help-panel-close" aria-label="Close">&times;</button>
</div>
<div class="help-panel__body">
<h3>What is the Classifier?</h3>
<p>The Classifier is a spreadsheet-based tool for renaming files to ZDDC naming conventions. It reads a folder of files and presents them in an editable grid where you can set tracking number, revision, status, and title — then saves the renamed files back to disk.</p>
<h3>Getting Started</h3>
<ol>
<li>Click <strong>Use Local Directory</strong> to open a folder containing files to rename.</li>
<li>The folder tree on the left shows all sub-folders. Click a folder to load its files.</li>
<li>Edit cells in the spreadsheet to set the new filename components.</li>
<li>Click <strong>Save All</strong> (or save individual rows) to rename the files on disk.</li>
</ol>
<h3>Folder Tree</h3>
<dl>
<dt>Multi-select</dt>
<dd>Hold <kbd>Ctrl</kbd> and click to select multiple folders. Hold <kbd>Shift</kbd> to select a range. Files from all selected folders are shown together.</dd>
<dt>Hide Compliant</dt>
<dd>Hides folders where all files already have valid ZDDC names, letting you focus on work remaining.</dd>
<dt>Auto-scroll</dt>
<dd>When enabled, the folder tree scrolls to highlight the folder containing the row you are editing.</dd>
</dl>
<h3>Spreadsheet Editing</h3>
<dl>
<dt>Direct cell editing</dt>
<dd>Click any cell in the New Filename, Tracking, Rev, Status, or Title columns to edit it. Press <kbd>Enter</kbd> to confirm, <kbd>Escape</kbd> to cancel.</dd>
<dt>RC References</dt>
<dd>Type a formula like <code>=R[-1]C</code> to copy the value from the cell one row above in the same column — similar to Excel relative references.</dd>
<dt>Regex capture groups</dt>
<dd>Type a formula like <code>=RE(RC[-3], "(\w+)-(\d+)", "$1")</code> to extract a pattern from another cell using a regular expression.</dd>
<dt>Validation</dt>
<dd>Cells are validated automatically. Invalid values are highlighted in red. The New Filename column shows the composed result.</dd>
<dt>Column Filters</dt>
<dd>Each column header has a filter input. Supported syntax:</dd>
</dl>
<dl>
<dt><code>term</code></dt>
<dd>Contains "term" (case-insensitive)</dd>
<dt><code>!term</code></dt>
<dd>Does not contain "term"</dd>
<dt><code>^term</code></dt>
<dd>Starts with "term"</dd>
<dt><code>term$</code></dt>
<dd>Ends with "term"</dd>
<dt><code>a b</code></dt>
<dd>Matches both (AND)</dd>
<dt><code>a | b</code></dt>
<dd>Matches either (OR)</dd>
<dt><code>^IFA | ^IFB</code></dt>
<dd>Starts with IFA or IFB</dd>
<dt><code>!^~</code></dt>
<dd>Does not start with ~ (excludes drafts)</dd>
<dt><code>el.*spc</code></dt>
<dd>Regex: contains "el" followed by "spc" (use <code>.</code> for any char, <code>.*</code> for any sequence)</dd>
<dt><code>[ei]fa</code></dt>
<dd>Regex character class: matches "efa" or "ifa"</dd>
</dl>
<h3>Saving Files</h3>
<dl>
<dt>Save All</dt>
<dd>Renames all modified files in one operation. Confirms before proceeding.</dd>
<dt>Cancel All</dt>
<dd>Reverts all unsaved edits back to the original filenames.</dd>
<dt>SHA256</dt>
<dd>Enable to compute a cryptographic hash of each file. Use <strong>Export Hashes</strong> to save a <code>sha256sum</code>-compatible file.</dd>
</dl>
<h3>ZDDC Filename Format</h3>
<p>The required format is:</p>
<p><code>TRACKINGNUMBER_REVISION (STATUS) - Title.ext</code></p>
<p>Example: <code>123456-EL-SPC-2623_A (IFR) - Electrical Specification.pdf</code></p>
<p>Valid statuses: IFA, IFB, IFC, IFD, IFI, IFP, IFR, IFU, REC, RSA, RSB, RSC, RSD, RSI, ---</p>
</div>
</aside>
</div>
<script>
{{JS_PLACEHOLDER}}
</script>
</body>
</html>