COD-39 attachment history drawer

Stacks on COD-38: accumulates a per-session attachment history and exposes it
through a slide-in drawer with an unread badge, so attachments stay reachable
after their cards are dismissed.

Backend:
- session-attachment-history: history state — dedupe by source path / relative
  path, newest-first, 100-item cap, and externalPath sanitization (the absolute
  host path is server-private and never leaves toState()).
- session.ts: _attachmentHistory + getter (sanitized) / upsert / restore /
  getAttachmentHistoryForPersist; restored from saved state in the constructor.
- file-routes: GET /attachments (list — resolves each entry to live metadata +
  routes; external entries are re-registered) and GET /attachments/:id
  (metadata poll). The by-id route guards via the registry's TOCTOU-safe
  resolveServableAttachmentPath.
- server.ts: detected/registered attachments upsert into history and persist;
  the private (externalPath-bearing) history rides on disk under
  __attachmentHistory, separate from the sanitized public copy, and is restored
  on mux-session recovery.
- types/session.ts: SessionAttachmentHistoryItem + SessionState.attachmentHistory.

Frontend:
- panels-ui: the drawer (lazy-built), unread badge, list render with per-item
  preview/download/open/"Card" (reshow) actions, and live refresh of the open
  drawer on new detections.
- app.js: history state + per-session badge/cleanup wiring.
- index.html / styles.css / mobile.css: header button + badge and the drawer.

Verified: tsc / eslint / prettier / frontend-syntax / public-assets clean; new
history-module unit tests pass; full test:ci green (2866 passed); badge, drawer
open/render/reshow/close verified in-browser.
This commit is contained in:
Aamer Akhter
2026-06-14 09:05:17 +02:00
committed by Claude (Codeman maintainer)
parent 5eacb1cf03
commit 577b6d7384
11 changed files with 1025 additions and 6 deletions
+210
View File
@@ -9401,6 +9401,216 @@ body.touch-device.cjk-input-visible .main {
background: #fff;
}
.attachment-history-badge {
position: absolute;
top: 2px;
right: 1px;
min-width: 16px;
height: 16px;
padding: 0 4px;
background: var(--accent);
color: #fff;
font-size: 0.6rem;
font-weight: 700;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
@keyframes notif-badge-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
/* Attachment History Drawer */
.attachment-history-drawer {
position: fixed;
top: var(--header-height);
right: 0;
width: 390px;
max-width: calc(100vw - 24px);
height: calc(100vh - var(--header-height) - var(--toolbar-height));
height: calc(100dvh - var(--header-height) - var(--toolbar-height));
background: rgba(19, 19, 22, 0.98);
border-left: 1px solid var(--border);
z-index: 10000;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.18s ease;
box-shadow: -10px 0 28px rgba(0, 0, 0, 0.36);
}
.attachment-history-drawer.open {
transform: translateX(0);
}
.attachment-history-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.attachment-history-title {
color: var(--text);
font-size: 0.9rem;
font-weight: 650;
}
.attachment-history-subtitle {
margin-top: 2px;
color: var(--text-dim);
font-size: 0.68rem;
}
.attachment-history-header-actions {
display: flex;
align-items: center;
gap: 6px;
}
.attachment-history-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.attachment-history-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 160px;
padding: 24px 16px;
color: var(--text-muted);
font-size: 0.78rem;
text-align: center;
}
.attachment-history-empty-title {
color: var(--text-primary);
font-size: 0.9rem;
font-weight: 600;
}
.attachment-history-empty code {
max-width: 100%;
overflow-wrap: anywhere;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 5px 7px;
color: var(--text-primary);
background: var(--bg-tertiary);
font-size: 0.76rem;
}
.attachment-history-item {
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
gap: 10px;
align-items: center;
padding: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.attachment-history-item:last-child {
border-bottom: none;
}
.attachment-history-item.missing {
opacity: 0.58;
}
.attachment-history-thumb {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 112px;
aspect-ratio: 16 / 9;
overflow: hidden;
border: 1px solid var(--border-light);
border-radius: 6px;
background: #f8f8fb;
}
.attachment-history-thumb-img {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
object-position: center;
}
.attachment-history-thumb-fallback {
display: none;
align-items: center;
justify-content: center;
position: absolute;
inset: 0;
background: #20202a;
color: var(--text);
font-size: 0.72rem;
font-weight: 700;
}
.attachment-history-thumb-fallback.visible {
display: flex;
}
.attachment-history-item-main {
min-width: 0;
}
.attachment-history-file-name {
overflow: hidden;
color: var(--text);
font-size: 0.8rem;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.attachment-history-meta {
margin-top: 2px;
color: var(--text-dim);
font-size: 0.67rem;
}
.attachment-history-actions {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 7px;
}
.attachment-history-actions button {
padding: 3px 7px;
border: 1px solid var(--border-light);
border-radius: 5px;
background: var(--bg-input);
color: var(--text);
font-size: 0.67rem;
cursor: pointer;
}
.attachment-history-actions button:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent-hover);
}
.attachment-history-actions button:disabled {
opacity: 0.45;
cursor: default;
}
/* ============================================================
=== v1.0 Carbon Aurora · Daylight overrides ===
Single appended block — trivially removable. Retones the hardcoded