Polish pass after the big refactor in 2d114fc.
== Header elevation slot propagated ==
shared/elevation.{js,css} surface a header checkbox for admins.
30-minute sudo-style cookie window (Max-Age=1800, SameSite=Lax).
Only renders when /.profile/access reports can_elevate=true; quiet
for non-admins. Slot added to all 7 tool templates and concat'd
into all 7 build.sh files; admin in any tool now sees the toggle.
Three text-rename ride-alongs in archive/classifier/transmittal
templates: "Add Local Directory" → "Use Local Directory" (the same
rename that landed in browse earlier in this branch).
== Docs ==
- CLAUDE.md gets an "Admin elevation is sudo-style" paragraph in
the "Things that bite if you forget" section.
- AGENTS.md gets a dedicated "Admin elevation (sudo-style)" section
alongside "Bearer tokens" — same depth as the existing auth docs.
== Helper file splits ==
The retired form editor's shared helpers got bundled into a single
zddc_admin.go in the cleanup; that name is now misleading. Split by
concern:
- admin_helpers.go: hasAnyAdminScope (the only admin-specific helper)
- paths.go: resolvePath, urlPathOf, chainDirs (URL ↔ filesystem path
math — used by several profile / zddc-file handlers)
- profile_assets.go (renamed from zddc_admin_assets.go): custom CSS
pipeline. URL renamed from /.profile/zddc/assets/ → /.profile/assets/
since /.profile/zddc/ no longer hosts an editor.
- treeEntry moves to profilehandler.go (alongside AccessView, its
only consumer).
- writeError moves to profileprojects.go (its only consumer).
== Smell cleanup ==
- zddc.HasAnyAdminGrant(fsRoot, email) — new elevation-independent
primitive that walks the cascade and reports whether email is named
in any admin: list anywhere. Replaces the synthetic-elevated probe
hack in enumerateAccess (`Principal{Email, Elevated: true}` was
"lying" to the elevation gate to ask what it would say). The handler's
hasAnyAdminScope collapses to a 4-line wrapper that gates on
p.Elevated and delegates.
- Access-log middleware records `elevated` per request, so forensics
can distinguish "admin acting as user" from "admin exercising power."
- browse/js/app.js's ?file= deep link walks multi-segment paths. Each
intermediate segment is matched + expanded; the leaf gets
selected/previewed. Auto-shows hidden when any segment starts with
. or _. Silently no-ops on unresolved segments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
374 lines
20 KiB
HTML
374 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ZDDC Archive</title>
|
||
<link rel="icon" type="image/svg+xml" href="{{FAVICON}}">
|
||
<style>
|
||
{{CSS_PLACEHOLDER}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="appContainer">
|
||
<!-- Project access warning banner (shown when URL contains inaccessible projects) -->
|
||
<div id="projectWarningBanner" class="project-warning-banner hidden" role="alert">
|
||
<span class="project-warning-text"></span>
|
||
<button class="project-warning-dismiss" onclick="dismissProjectWarning()" aria-label="Dismiss">×</button>
|
||
</div>
|
||
|
||
<!-- 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 Archive</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 Data">⟳</button>
|
||
</div>
|
||
<div class="header-right">
|
||
<!-- Elevation toggle slot. shared/elevation.js fills it
|
||
when /.profile/access reports the user has admin
|
||
authority; stays empty + hidden for non-admins so
|
||
the chrome is quiet for the common case. -->
|
||
<span id="elevation-toggle" class="elevation-toggle hidden"
|
||
title="Opt into your admin powers for the next 30 minutes (sudo-style)."></span>
|
||
<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 Container -->
|
||
<div class="main-container">
|
||
<!-- Navigation Pane -->
|
||
<nav id="navigationPane" class="nav-pane">
|
||
<!-- Grouping Folders Section -->
|
||
<div class="nav-section" id="groupingSection">
|
||
<div class="nav-section-header">
|
||
<h3>Parties</h3>
|
||
<div class="preset-section" id="presetSection">
|
||
<button id="presetBtn" class="btn btn-secondary btn-sm" title="Party presets">▾ Presets</button>
|
||
<div id="presetDropdown" class="preset-dropdown hidden"></div>
|
||
</div>
|
||
<button id="toggleGroupingBtn" class="btn-icon" title="Collapse/Expand">
|
||
<span id="toggleGroupingIcon">▼</span>
|
||
</button>
|
||
</div>
|
||
<div id="groupingContent" class="nav-section-content">
|
||
<!-- Global folder type toggle bar -->
|
||
<div id="folderTypeBar" class="folder-type-bar">
|
||
<!-- Dynamically populated by renderFolderTypeBar() -->
|
||
</div>
|
||
<div class="filter-select-row">
|
||
<input type="text"
|
||
id="groupingFilter"
|
||
class="filter-input"
|
||
placeholder="Filter parties...">
|
||
<label class="select-all-label select-all-inline" title="Auto-select all visible parties">
|
||
<span>Select<br>All</span>
|
||
<input type="checkbox" id="selectAllGroupingCheckbox" checked>
|
||
</label>
|
||
</div>
|
||
<div id="groupingFoldersList" class="folder-list">
|
||
<!-- Dynamically populated -->
|
||
</div>
|
||
</div>
|
||
<div class="resize-handle-vertical" data-resize="nav-sections"></div>
|
||
</div>
|
||
|
||
<!-- Transmittal Folders Section -->
|
||
<div class="nav-section" id="transmittalSection">
|
||
<div class="nav-section-header">
|
||
<h3>Transmittal Folders</h3>
|
||
<button id="toggleAllDatesBtn" class="btn-icon" title="Expand/Collapse All">
|
||
<span id="toggleAllDatesIcon">▼</span>
|
||
</button>
|
||
</div>
|
||
<div class="filter-select-row">
|
||
<input type="text"
|
||
id="transmittalFilter"
|
||
class="filter-input"
|
||
placeholder="Filter transmittal folders...">
|
||
<label class="select-all-label select-all-inline" title="Auto-select all visible transmittals">
|
||
<span>Select<br>All</span>
|
||
<input type="checkbox" id="selectAllTransmittalsCheckbox" checked>
|
||
</label>
|
||
</div>
|
||
<div id="transmittalFoldersList" class="folder-list">
|
||
<!-- Dynamically populated -->
|
||
</div>
|
||
</div>
|
||
<div class="resize-handle-horizontal" data-resize="nav-pane"></div>
|
||
</nav>
|
||
|
||
<!-- Content Area -->
|
||
<main class="content-area">
|
||
<!-- Content Header -->
|
||
<div class="content-header">
|
||
<!-- Reset Filters -->
|
||
<button id="resetFiltersBtn" class="btn btn-secondary btn-icon-only" title="Reset all column filters">↺</button>
|
||
|
||
<!-- Preview toggle (default on; users can opt out for direct downloads) -->
|
||
<label class="preview-toggle-label" title="Preview PDF, Word, and Excel files in a popup window instead of downloading">
|
||
<input type="checkbox" id="filePreviewToggle" checked>
|
||
<span>Preview</span>
|
||
</label>
|
||
|
||
<!-- Modifier Filter Dropdown -->
|
||
<div class="modifier-filter-container">
|
||
<button id="modifierFilterBtn" class="btn btn-secondary modifier-filter-btn">
|
||
Modifiers ▼
|
||
</button>
|
||
<div id="modifierFilterDropdown" class="modifier-filter-dropdown hidden">
|
||
<div class="modifier-filter-header">
|
||
<label><input type="checkbox" id="modifierSelectAll" checked> Select All</label>
|
||
</div>
|
||
<div id="modifierFilterList" class="modifier-filter-list">
|
||
<!-- Dynamically populated -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toolbar-separator"></div>
|
||
|
||
<div class="content-actions">
|
||
<button id="filterSelectedBtn" class="btn btn-secondary">Filter Selected</button>
|
||
<button id="downloadSelectedBtn" class="btn btn-secondary">Download (ZIP)</button>
|
||
<button id="exportCsvBtn" class="btn btn-secondary">Export (CSV)</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Files Table -->
|
||
<div class="table-container">
|
||
<table id="filesTable" class="files-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="sortable resizable" data-field="trackingNumber">
|
||
<div class="th-content">
|
||
<span>Tracking Number</span>
|
||
<span class="sort-indicator"></span>
|
||
</div>
|
||
<input type="text"
|
||
class="column-filter"
|
||
data-filter-field="trackingNumber"
|
||
placeholder="Filter...">
|
||
<div class="resize-handle"></div>
|
||
</th>
|
||
<th class="sortable resizable" data-field="title">
|
||
<div class="th-content">
|
||
<span>Title</span>
|
||
<span class="sort-indicator"></span>
|
||
</div>
|
||
<input type="text"
|
||
class="column-filter"
|
||
data-filter-field="title"
|
||
placeholder="Filter...">
|
||
<div class="resize-handle"></div>
|
||
</th>
|
||
<th class="resizable" data-field="revisions">
|
||
<div class="th-content th-content--start">
|
||
<input type="checkbox"
|
||
id="selectAllVisibleCheckbox"
|
||
class="select-all-checkbox"
|
||
title="Select/deselect all visible files">
|
||
<span>Revisions</span>
|
||
</div>
|
||
<input type="text"
|
||
class="column-filter"
|
||
data-filter-field="revisions"
|
||
placeholder="Filter...">
|
||
<div class="resize-handle"></div>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="filesTableBody">
|
||
<!-- Dynamically populated -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Status Bar -->
|
||
<div class="status-bar">
|
||
<span id="fileCount">0 files</span>
|
||
<span id="selectedCount">0 selected</span>
|
||
<span id="scanStatus"></span><span id="scanSpinner" class="scan-spinner hidden"></span>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Drop Modal -->
|
||
<div id="dropModal" class="modal hidden">
|
||
<div class="modal-backdrop"></div>
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Create Transmittal</h2>
|
||
<button class="modal-close">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>Transmittal Folder Name:</label>
|
||
<input type="text" id="transmittalName" class="form-input">
|
||
<small class="form-help">Format: YYYY-MM-DD_TRACKINGNUMBER (STATUS) - TITLE</small>
|
||
</div>
|
||
<div class="files-preview">
|
||
<h3>Files to Add:</h3>
|
||
<table class="preview-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Original Name</th>
|
||
<th>New Name</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="filesPreviewBody">
|
||
<!-- Dynamically populated -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary modal-cancel">Cancel</button>
|
||
<button class="btn btn-primary modal-confirm">Create Transmittal</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- No Directory Selected Message -->
|
||
<div id="noDirectoryMessage" class="empty-state empty-state--overlay">
|
||
<div class="empty-state__inner empty-state__inner--centered">
|
||
<h2>Welcome to ZDDC Archive</h2>
|
||
<p>Click <strong>Use Local Directory</strong> to select an archive folder to browse.</p>
|
||
<p>This browser provides a convenient interface for searching and retrieving files from ZDDC-compliant archives.</p>
|
||
<p><strong>How to navigate:</strong></p>
|
||
<ul class="welcome-list">
|
||
<li>Select a party to see their transmittal folders; toggle folder types (Issued, Received, MDL, Incoming) above the list</li>
|
||
<li>Select transmittal folders to see their files</li>
|
||
<li>Use <kbd>Ctrl+Click</kbd> to select multiple folders</li>
|
||
<li>Use <kbd>Shift+Click</kbd> to select a range</li>
|
||
<li><kbd>Ctrl+Click</kbd> chevrons to recursively expand/collapse</li>
|
||
</ul>
|
||
|
||
<details class="windows-tip">
|
||
<summary><strong>⚠️ Windows Path Length Deficiency</strong></summary>
|
||
<div class="windows-tip__body">
|
||
<p>Microsoft Windows has a legacy 260-character path limit that affects most applications. If you see "files skipped" warnings, use Microsoft's own workaround:</p>
|
||
<ol>
|
||
<li>Open Command Prompt as Administrator</li>
|
||
<li>Map your archive to a short drive letter:<br>
|
||
<code class="windows-tip__code">subst Z: "C:\Your\Long\Path\To\Archive"</code>
|
||
</li>
|
||
<li>Use the <strong>Z:</strong> drive in Archive Browser</li>
|
||
<li>To remove later: <code>subst Z: /d</code></li>
|
||
</ol>
|
||
<p class="windows-tip__note">This limitation dates back to Windows 95. The mapping persists until reboot.</p>
|
||
</div>
|
||
</details>
|
||
|
||
<p class="note">Note: This application works entirely in your browser and does not transmit any data.</p>
|
||
</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 Archive</h2>
|
||
<button type="button" class="help-panel__close" id="help-panel-close" aria-label="Close">×</button>
|
||
</div>
|
||
<div class="help-panel__body">
|
||
<h3>What is the Archive Browser?</h3>
|
||
<p>The Archive Browser lets you search and retrieve files from a ZDDC-compliant archive stored on your local file system. Everything runs in your browser — no data is transmitted anywhere.</p>
|
||
|
||
<h3>Getting Started</h3>
|
||
<ol>
|
||
<li>When opened from a web server, the archive loads automatically from that server.</li>
|
||
<li>Click <strong>Use Local Directory</strong> to open a local archive folder — works in both offline and online modes, and local files are merged with any server files already loaded.</li>
|
||
<li>The browser scans for grouping folders and transmittal folders automatically.</li>
|
||
<li>Select folders in the left panel to see their files in the main table.</li>
|
||
</ol>
|
||
|
||
<h3>Navigating Folders</h3>
|
||
<p>The left panel has two sections:</p>
|
||
<dl>
|
||
<dt>Parties</dt>
|
||
<dd>Top-level folders representing other parties. Select one or more to filter which transmittals are shown. Use the folder type buttons above the list to show or hide Issued, Received, MDL, and Incoming folder content.</dd>
|
||
<dt>Transmittal Folders</dt>
|
||
<dd>Grouped by date. Select one or more to filter which files appear in the table.</dd>
|
||
</dl>
|
||
<p><strong>Multi-select:</strong> Hold <kbd>Ctrl</kbd> and click to toggle individual folders. Hold <kbd>Shift</kbd> and click to select a range. <kbd>Ctrl+Click</kbd> a chevron (▶) to recursively expand or collapse all sub-folders.</p>
|
||
|
||
<h3>Searching and Filtering</h3>
|
||
<dl>
|
||
<dt>Column Filters</dt>
|
||
<dd>Type in the filter row under each column header to filter by tracking number, title, or revision/status/extension. Filters support the expression syntax below. Active filters are highlighted in blue; use the ↺ reset button in the toolbar to clear all filters at once.</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>pdf !draft</code></dt>
|
||
<dd>Contains "pdf" and not "draft"</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>
|
||
<dl>
|
||
<dt>Modifiers</dt>
|
||
<dd>Use the Modifiers dropdown to show or hide files by revision modifier type (+B, +C, +N, +Q, or base).</dd>
|
||
</dl>
|
||
|
||
<h3>Downloading Files</h3>
|
||
<dl>
|
||
<dt>Download Selected (ZIP)</dt>
|
||
<dd>Packages all checked files into a ZIP archive for download.</dd>
|
||
<dt>Export Selected (CSV)</dt>
|
||
<dd>Exports the visible file list as a CSV spreadsheet.</dd>
|
||
<dt>File Preview</dt>
|
||
<dd>When enabled, clicking a PDF, Word, or Excel file opens a preview popup instead of downloading it.</dd>
|
||
</dl>
|
||
|
||
<h3>Keyboard Shortcuts</h3>
|
||
<dl>
|
||
<dt><kbd>Ctrl+A</kbd></dt>
|
||
<dd>Select / deselect all visible files in the table.</dd>
|
||
<dt><kbd>F5</kbd></dt>
|
||
<dd>Refresh — rescan the current directory.</dd>
|
||
<dt><kbd>Escape</kbd></dt>
|
||
<dd>Close this help panel (or any open modal).</dd>
|
||
</dl>
|
||
|
||
<h3>Windows Path Length Note</h3>
|
||
<p>Windows limits file paths to 260 characters by default. If files are skipped during scanning, map your archive to a short drive letter using <code>subst Z: "C:\Your\Long\Path"</code> in an Administrator Command Prompt, then open the <strong>Z:</strong> drive in the Archive Browser.</p>
|
||
</div>
|
||
</aside>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
{{JS_PLACEHOLDER}}
|
||
</script>
|
||
</body>
|
||
</html>
|