feat: move revision tracking to ExportRecord, add perceptual hash comparison, cut list modal, and auto-start API

- Move Revision from Drawing to ExportRecord so each export captures its own revision snapshot
- Add Hamming distance comparison for perceptual hashes (tolerance of 10 bits) to avoid false revision bumps
- Replace CoenM.ImageHash with inline DifferenceHash impl (compatible with ImageSharp 3.x)
- Increase PDF render DPI from 72 to 150 for better hash fidelity
- Add download-dxfs-by-drawing endpoint for cross-export DXF zip downloads
- Prefix DXF filenames with equipment number when no drawing number is present
- Pass original filename to storage service for standalone part exports
- Auto-start FabWorks.Api from ExportDXF client if not already running
- Add cut list modal with copy-to-clipboard in the web UI
- Update PDF hash on existing export records after upload
- Bump static asset cache versions to v3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 15:48:28 -05:00
parent f6cd91f1b5
commit b9e84de7c0
20 changed files with 803 additions and 50 deletions

View File

@@ -735,6 +735,85 @@ tbody tr:last-child td { border-bottom: none; }
.equip-group.collapsed .equip-body { display: none; }
.equip-group.collapsed .equip-header { border-radius: 6px; }
/* ─── Modal ─── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 640px;
max-height: 80vh;
display: flex;
flex-direction: column;
animation: fadeSlideIn 0.2s ease forwards;
}
.modal-header {
padding: 14px 18px;
border-bottom: 1px solid var(--border-subtle);
font-family: var(--font-display);
font-weight: 600;
font-size: 14px;
letter-spacing: 0.02em;
display: flex;
align-items: center;
gap: 10px;
text-transform: uppercase;
color: var(--text-secondary);
}
.modal-header svg { width: 16px; height: 16px; }
.modal-body {
overflow-y: auto;
flex: 1;
}
.btn-green {
background: var(--green-dim);
color: var(--green);
border-color: rgba(6, 118, 71, 0.25);
}
.btn-green:hover {
background: rgba(6, 118, 71, 0.15);
border-color: rgba(6, 118, 71, 0.4);
color: var(--green);
}
/* ─── Toast ─── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--text);
color: #fff;
padding: 8px 20px;
border-radius: 4px;
font-family: var(--font-mono);
font-size: 13px;
z-index: 300;
animation: fadeSlideIn 0.2s ease, fadeOut 0.3s ease 2s forwards;
}
@keyframes fadeOut { to { opacity: 0; } }
/* ─── Responsive ─── */
@media (max-width: 768px) {
.sidebar { display: none; }