|
76 | 76 | .card { |
77 | 77 | padding: 0.75rem 1rem; background: #161b22; |
78 | 78 | border: 1px solid #30363d; border-radius: 6px; |
79 | | - margin-bottom: 0.5rem; |
| 79 | + margin-bottom: 0.5rem; cursor: pointer; |
| 80 | + transition: border-color 0.15s; |
80 | 81 | } |
| 82 | + .card:hover { border-color: #58a6ff; } |
| 83 | + .card.expanded { border-color: #58a6ff; } |
81 | 84 | .card .type { |
82 | 85 | color: #58a6ff; font-weight: 600; font-size: 0.8rem; |
83 | 86 | text-transform: uppercase; letter-spacing: 0.025em; |
|
91 | 94 | margin-top: 0.375rem; color: #8b949e; font-size: 0.75rem; |
92 | 95 | display: flex; gap: 1rem; flex-wrap: wrap; |
93 | 96 | } |
| 97 | + .card .obs-detail { |
| 98 | + margin-top: 0.75rem; padding-top: 0.75rem; |
| 99 | + border-top: 1px solid #30363d; |
| 100 | + } |
| 101 | + .card .obs-detail .obs-full-text { |
| 102 | + white-space: pre-wrap; word-break: break-word; |
| 103 | + font-size: 0.875rem; line-height: 1.5; margin-bottom: 0.5rem; |
| 104 | + } |
| 105 | + .card .obs-detail .obs-meta-table { |
| 106 | + font-size: 0.8rem; color: #8b949e; |
| 107 | + display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.75rem; |
| 108 | + } |
| 109 | + .card .obs-detail .obs-meta-label { |
| 110 | + color: #8b949e; font-weight: 600; |
| 111 | + } |
| 112 | + .card .obs-detail .obs-meta-value { |
| 113 | + word-break: break-all; |
| 114 | + } |
| 115 | + .card .obs-detail .obs-timeline { |
| 116 | + margin-top: 0.75rem; padding-top: 0.5rem; |
| 117 | + border-top: 1px solid #21262d; |
| 118 | + } |
| 119 | + .card .obs-detail .obs-timeline-header { |
| 120 | + font-size: 0.8rem; color: #8b949e; margin-bottom: 0.5rem; |
| 121 | + } |
| 122 | + .card .obs-detail .timeline-item { |
| 123 | + padding: 0.375rem 0.5rem; font-size: 0.8rem; |
| 124 | + border-left: 2px solid #30363d; margin-bottom: 0.25rem; |
| 125 | + margin-left: 0.25rem; |
| 126 | + } |
| 127 | + .card .obs-detail .timeline-item.current { |
| 128 | + border-left-color: #58a6ff; background: #0d1117; |
| 129 | + border-radius: 0 4px 4px 0; |
| 130 | + } |
| 131 | + .card .obs-detail .timeline-item .tl-type { |
| 132 | + color: #58a6ff; font-weight: 600; text-transform: uppercase; |
| 133 | + font-size: 0.7rem; letter-spacing: 0.025em; |
| 134 | + } |
| 135 | + .card .obs-detail .timeline-item .tl-title { |
| 136 | + color: #c9d1d9; |
| 137 | + } |
| 138 | + .card .obs-detail .timeline-item .tl-time { |
| 139 | + color: #8b949e; font-size: 0.7rem; |
| 140 | + } |
94 | 141 |
|
95 | 142 | /* Placeholder */ |
96 | 143 | .placeholder { |
@@ -460,16 +507,95 @@ <h2 id="section-title">Live Feed</h2> |
460 | 507 | function createEventCard(data, timestamp) { |
461 | 508 | const div = document.createElement('div'); |
462 | 509 | div.className = 'card'; |
| 510 | + const obsId = data.ID || data.id; |
| 511 | + if (obsId) div.dataset.obsId = obsId; |
463 | 512 | div.innerHTML = |
464 | 513 | '<div class="type">' + esc(data.Type || data.type || 'observation') + '</div>' + |
465 | 514 | '<div class="title">' + esc(data.Title || data.title || '') + '</div>' + |
466 | 515 | (data.Text || data.text ? '<div class="text">' + esc(data.Text || data.text) + '</div>' : '') + |
467 | 516 | '<div class="meta"><span>' + esc(timestamp) + '</span>' + |
468 | 517 | (data.Project || data.project ? '<span>' + esc(data.Project || data.project) + '</span>' : '') + |
469 | | - '</div>'; |
| 518 | + '</div>' + |
| 519 | + '<div class="obs-detail" style="display:none"></div>'; |
| 520 | + if (obsId) { |
| 521 | + div.addEventListener('click', function() { |
| 522 | + const detail = this.querySelector('.obs-detail'); |
| 523 | + const isExpanded = this.classList.contains('expanded'); |
| 524 | + if (isExpanded) { |
| 525 | + this.classList.remove('expanded'); |
| 526 | + detail.style.display = 'none'; |
| 527 | + return; |
| 528 | + } |
| 529 | + this.classList.add('expanded'); |
| 530 | + detail.style.display = 'block'; |
| 531 | + if (detail.dataset.loaded === 'true') return; |
| 532 | + loadObservationDetail(obsId, detail); |
| 533 | + }); |
| 534 | + } |
470 | 535 | return div; |
471 | 536 | } |
472 | 537 |
|
| 538 | + async function loadObservationDetail(obsId, container) { |
| 539 | + container.innerHTML = '<div class="obs-timeline-header">Loading...</div>'; |
| 540 | + try { |
| 541 | + const [obsResp, tlResp] = await Promise.all([ |
| 542 | + fetch('/api/observations/' + obsId), |
| 543 | + fetch('/api/observations/timeline/' + obsId + '?before=3&after=3') |
| 544 | + ]); |
| 545 | + if (!obsResp.ok) { |
| 546 | + container.innerHTML = '<div class="obs-timeline-header">Failed to load observation.</div>'; |
| 547 | + container.dataset.loaded = 'true'; |
| 548 | + return; |
| 549 | + } |
| 550 | + const obs = await obsResp.json(); |
| 551 | + let html = ''; |
| 552 | + |
| 553 | + // Full text |
| 554 | + if (obs.Text) { |
| 555 | + html += '<div class="obs-full-text">' + esc(obs.Text) + '</div>'; |
| 556 | + } |
| 557 | + |
| 558 | + // Metadata table |
| 559 | + html += '<div class="obs-meta-table">'; |
| 560 | + html += '<span class="obs-meta-label">Type</span><span class="obs-meta-value">' + esc(obs.Type || '') + '</span>'; |
| 561 | + if (obs.Project) { |
| 562 | + html += '<span class="obs-meta-label">Project</span><span class="obs-meta-value">' + esc(obs.Project) + '</span>'; |
| 563 | + } |
| 564 | + if (obs.SessionID) { |
| 565 | + html += '<span class="obs-meta-label">Session</span><span class="obs-meta-value" style="font-family:monospace;font-size:0.75rem">' + esc(obs.SessionID) + '</span>'; |
| 566 | + } |
| 567 | + html += '<span class="obs-meta-label">Created</span><span class="obs-meta-value">' + esc(fmtTime(obs.CreatedAt)) + '</span>'; |
| 568 | + if (obs.Metadata && obs.Metadata !== '{}' && obs.Metadata !== '') { |
| 569 | + html += '<span class="obs-meta-label">Metadata</span><span class="obs-meta-value" style="font-family:monospace;font-size:0.75rem">' + esc(obs.Metadata) + '</span>'; |
| 570 | + } |
| 571 | + html += '</div>'; |
| 572 | + |
| 573 | + // Timeline |
| 574 | + if (tlResp.ok) { |
| 575 | + const timeline = await tlResp.json(); |
| 576 | + if (timeline && timeline.length > 1) { |
| 577 | + html += '<div class="obs-timeline">'; |
| 578 | + html += '<div class="obs-timeline-header">Timeline</div>'; |
| 579 | + for (const t of timeline) { |
| 580 | + const isCurrent = t.ID === obs.ID; |
| 581 | + html += '<div class="timeline-item' + (isCurrent ? ' current' : '') + '">'; |
| 582 | + html += '<span class="tl-type">' + esc(t.Type || '') + '</span> '; |
| 583 | + html += '<span class="tl-title">' + esc(t.Title || '') + '</span>'; |
| 584 | + html += '<div class="tl-time">' + esc(fmtTime(t.CreatedAt)) + '</div>'; |
| 585 | + html += '</div>'; |
| 586 | + } |
| 587 | + html += '</div>'; |
| 588 | + } |
| 589 | + } |
| 590 | + |
| 591 | + container.innerHTML = html; |
| 592 | + container.dataset.loaded = 'true'; |
| 593 | + } catch (err) { |
| 594 | + container.innerHTML = '<div class="obs-timeline-header">Failed to load details.</div>'; |
| 595 | + console.error('Observation detail error:', err); |
| 596 | + } |
| 597 | + } |
| 598 | + |
473 | 599 | async function loadRecent() { |
474 | 600 | try { |
475 | 601 | const resp = await fetch('/api/observations/recent?limit=50'); |
|
0 commit comments