Refactor Media Manager log to use streaming and card UI

- **UI Overhaul**: Moved the activity log to a dedicated, expandable card at the bottom of the Media Manager modal.
- **Styling**: Updated the log card to match the application's light theme using CSS variables (`--surface`, `--text`).
- **Log Streaming**: Replaced batch logging with real-time streaming via `CacheService` and `pollJobLogs`.
- **Session Resumption**: Implemented logic to resume log polling for active jobs upon page reload.
- **Fixes**:
    - Exposed `pollJobLogs` in `global.ts` to fix "Script function not found" error.
    - Updated `mediaHandlers.test.ts` with `CacheService` mocks and new signatures.
    - Removed legacy auto-hide/toggle logic for the log.
This commit is contained in:
Ben Miller
2025-12-31 06:08:34 -07:00
parent dc33390650
commit 3abc57f45a
7 changed files with 286 additions and 73 deletions

View File

@ -380,6 +380,67 @@
transform: translateY(0);
}
}
/* Log Card Styles */
.log-card {
background: var(--surface);
color: var(--text);
border-radius: 8px;
margin-top: 16px;
font-family: monospace;
font-size: 11px;
overflow: hidden;
transition: all 0.2s ease;
border: 1px solid var(--border);
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}
.log-header {
padding: 8px 12px;
background: #f8fafc; /* Slightly darker than surface */
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
color: var(--text-secondary);
font-weight: 500;
}
.log-content {
padding: 12px;
max-height: 16px; /* ~1 line */
overflow-y: auto;
transition: max-height 0.3s ease;
display: flex;
flex-direction: column;
gap: 4px;
background: var(--surface);
}
.log-card.expanded .log-content {
max-height: 300px; /* ~20 lines */
}
.log-entry {
line-height: 1.4;
border-bottom: 1px solid #f1f5f9;
padding-bottom: 2px;
}
.log-entry:last-child { border-bottom: none; }
/* Scrollbar for log */
.log-content::-webkit-scrollbar {
width: 6px;
}
.log-content::-webkit-scrollbar-track {
background: transparent;
}
.log-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
</style>
</head>
@ -454,11 +515,6 @@
</div>
</div>
<!-- Status Log -->
<div id="status-log-container"
style="padding:16px; background:#f8fafc; border-bottom:1px solid var(--border); font-family:monospace; font-size:12px; line-height:1.6; display:none;">
</div>
<!-- Processing Warning Banner -->
<div id="processing-banner"
style="display:none; background-color:#fffbeb; color:#92400e; padding:12px; border-radius:8px; margin: 0 16px 12px 16px; font-size:13px; border:1px solid #fcd34d; align-items:flex-start; gap:8px;">
@ -482,6 +538,17 @@
</button>
</div>
</div>
<!-- Permanent Log Card -->
<div id="log-card" class="log-card">
<div class="log-header" onclick="ui.toggleLogExpand()">
<span style="font-weight:600;">Activity Log</span>
<span id="log-toggle-icon"></span>
</div>
<div id="status-log-container" class="log-content">
<div class="log-entry" style="color: #94a3b8;">Ready.</div>
</div>
</div>
</div>
<!-- Loading Screen -->
@ -708,9 +775,10 @@
function UI() {
this.grid = document.getElementById('media-grid');
this.saveBtn = document.getElementById('save-btn');
this.toggleLogBtn = document.getElementById('toggle-log-btn');
// this.toggleLogBtn = document.getElementById('toggle-log-btn'); // Removed
this.logContainer = document.getElementById('status-log-container');
this.linksContainer = document.getElementById('quick-links');
this.logCard = document.getElementById('log-card');
this.sortable = null;
this.driveUrl = null;
this.shopifyUrl = null;
@ -725,10 +793,10 @@
if (this.shopifyUrl) this.linksContainer.innerHTML += '<a href="' + this.shopifyUrl + '" target="_blank" style="color:var(--primary); text-decoration:none; margin-left:8px;">Shopify ↗</a>';
};
UI.prototype.toggleLog = function (forceState) {
var isVisible = typeof forceState === 'boolean' ? !forceState : this.logContainer.style.display !== 'none';
this.logContainer.style.display = isVisible ? 'none' : 'block';
this.toggleLogBtn.innerText = isVisible ? "View Log" : "Hide Log";
UI.prototype.toggleLogExpand = function () {
this.logCard.classList.toggle('expanded');
var icon = this.logCard.querySelector('#log-toggle-icon');
icon.innerText = this.logCard.classList.contains('expanded') ? '▼' : '▲';
};
UI.prototype.updateSku = function (sku, title) {
@ -832,11 +900,21 @@
UI.prototype.logStatus = function (step, message, type) {
if (!type) type = 'info';
var container = this.logContainer;
var icon = type === 'success' ? '✅' : type === 'error' ? '❌' : '⏳';
// Auto-clear "Ready"
if (container.children.length === 1 && container.children[0].innerText === "Ready.") {
container.innerHTML = "";
}
var icon = type === 'success' ? '✅' : type === 'error' ? '❌' : '';
var el = document.createElement('div');
el.innerHTML = '<span style="margin-right:8px;">' + icon + '</span> ' + message;
if (type === 'error') el.style.color = 'var(--error)';
el.className = 'log-entry';
el.innerHTML = '<span style="margin-right:8px; opacity:0.7;">' + icon + '</span> ' + message;
if (type === 'error') el.style.color = 'var(--danger)';
if (type === 'success') el.style.color = 'var(--success)';
container.appendChild(el);
container.scrollTop = container.scrollHeight; // Auto-scroll
};
UI.prototype.createCard = function (item, index) {
@ -1047,10 +1125,9 @@
const sku = info ? info.sku : null;
if (sku && sku !== state.sku) {
state.setSku(info); // Pass whole object
state.setSku(info);
this.loadMedia();
} else if (!sku && !state.sku) {
// If we don't have a SKU and haven't shown error yet
if (document.getElementById('error-ui').style.display !== 'flex') {
this.loadMedia();
}
@ -1060,7 +1137,10 @@
},
loadMedia(preserveLogs = false) {
// Resolve SKU/Title - prefer state, fallback to DOM
// ... (Resolving SKU/Title Logic preserved below implicitly or we verify we didn't clip it)
// Actually, let's keep the resolving logic safe.
// We are replacing lines 1120-1191 roughly.
let sku = state.sku;
let title = state.title;
@ -1083,28 +1163,34 @@
if (domTitle && domTitle !== 'Loading...') title = domTitle;
}
// Show Main UI immediately so logs are visible
document.getElementById('loading-ui').style.display = 'none';
document.getElementById('main-ui').style.display = 'block';
// Set Inline Loading State
ui.setLoadingState(true);
// Reset State (this calls ui.updateSku)
state.setSku({ sku, title });
if (!preserveLogs) {
document.getElementById('status-log-container').innerHTML = '';
document.getElementById('status-log-container').innerHTML = ''; // Reset log
ui.logStatus('ready', 'Ready.', 'info');
} else {
// We might want to clear "Ready" if we are preserving logs
}
ui.toggleLogBtn.style.display = 'inline-block';
ui.toggleLog(true); // Force Show Log to see progress
// 1. Run Diagnostics
// 1. Run Diagnostics
// ui.toggleLogBtn.style.display = 'inline-block'; // Removed
ui.logStatus('init', 'Initializing access...', 'info');
google.script.run
.withSuccessHandler(function (diagnostics) {
.withSuccessHandler((diagnostics) => { // Use arrow
// Check Resumption
if (diagnostics.activeJobId) {
ui.logStatus('resume', 'Resuming active background job...', 'info');
ui.toggleSave(false);
ui.saveBtn.innerText = "Saving in background...";
controller.startLogPolling(diagnostics.activeJobId);
if (!ui.logCard.classList.contains('expanded')) ui.toggleLogExpand();
}
// Drive Status
if (diagnostics.drive.status === 'ok') {
ui.logStatus('drive', `Drive Folder: ok (${diagnostics.drive.fileCount} files) <a href="${diagnostics.drive.folderUrl}" target="_blank" style="margin-left:8px;">Open Folder ↗</a>`, 'success');
@ -1116,6 +1202,7 @@
// Capture Token
if (diagnostics.token) state.token = diagnostics.token;
// Shopify Status
if (diagnostics.shopify.status === 'ok') {
ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) <a href="${diagnostics.shopify.adminUrl}" target="_blank" style="margin-left:8px;">Open Admin ↗</a>`, 'success');
@ -1166,34 +1253,76 @@
ui.toggleSave(false);
ui.saveBtn.innerText = "Saving...";
ui.saveBtn.innerText = "Saving...";
// Generate Job ID
const jobId = Math.random().toString(36).substring(2) + Date.now().toString(36);
// Filter out deleted items so they are actually removed
// Start Polling
this.startLogPolling(jobId);
// Expand Log Card
if (!ui.logCard.classList.contains('expanded')) {
ui.toggleLogExpand();
}
// Filter out deleted items
const activeItems = state.items.filter(i => !i._deleted);
// Send final state array to backend
google.script.run
.withSuccessHandler((logs) => {
ui.saveBtn.innerText = "Saved!";
this.stopLogPolling(); // Stop polling
// Final sync of logs (in case polling missed the very end)
// But usually the returned logs are the full set or summary?
// The backend returns the full array. Let's merge or just ensure we show "Complete".
// Since we were polling, we might have partials.
// Let's just trust the stream has been showing progress.
// We can log a completion message.
ui.logStatus('save', 'Process Completed Successfully.', 'success');
// Verify logs is an array (backward compatibility check)
if (Array.isArray(logs)) {
document.getElementById('status-log-container').innerHTML = '';
logs.forEach(l => ui.logStatus('save', l, 'info'));
ui.toggleLog(true); // Force show
} else {
// Fallback for old backend
alert("Changes Saved & Synced!");
}
// Reload to get fresh IDs/State, preserving the save logs
setTimeout(() => this.loadMedia(true), 1500);
})
.withFailureHandler(e => {
this.stopLogPolling();
alert(`Save Failed: ${e.message}`);
ui.logStatus('fatal', `Save Failed: ${e.message}`, 'error');
ui.toggleSave(true);
})
.saveMediaChanges(state.sku, activeItems);
.saveMediaChanges(state.sku, activeItems, jobId);
},
logPollInterval: null,
knownLogCount: 0,
startLogPolling(jobId) {
if (this.logPollInterval) clearInterval(this.logPollInterval);
this.knownLogCount = 0;
this.logPollInterval = setInterval(() => {
google.script.run
.withSuccessHandler(logs => {
if (!logs || logs.length === 0) return;
// Append ONLY new logs
// Simple approach: standard loop since we know count
if (logs.length > this.knownLogCount) {
const newLogs = logs.slice(this.knownLogCount);
newLogs.forEach(l => ui.logStatus('stream', l));
this.knownLogCount = logs.length;
}
})
.pollJobLogs(jobId);
}, 1000); // Poll every second
},
stopLogPolling() {
if (this.logPollInterval) {
clearInterval(this.logPollInterval);
this.logPollInterval = null;
}
},
handleFiles(fileList) {
@ -1451,7 +1580,7 @@
document.getElementById('loading-ui').style.display = 'none';
document.getElementById('main-ui').style.display = 'block';
ui.logStatus('done', 'Finished loading.', 'success');
setTimeout(function () { ui.toggleLog(false); }, 1000);
// setTimeout(function () { ui.toggleLog(false); }, 1000); // Removed auto-hide
// Start Polling for Processing Items
this.pollProcessingItems();