|
439 | 439 | padding-right: 14px; |
440 | 440 | } |
441 | 441 |
|
| 442 | + /* Paste modal */ |
| 443 | + .paste-overlay { |
| 444 | + position: fixed; |
| 445 | + inset: 0; |
| 446 | + background: rgba(0, 0, 0, 0.6); |
| 447 | + display: flex; |
| 448 | + align-items: center; |
| 449 | + justify-content: center; |
| 450 | + z-index: 100; |
| 451 | + } |
| 452 | + |
| 453 | + .paste-modal { |
| 454 | + background: var(--surface); |
| 455 | + border: 1px solid var(--border); |
| 456 | + border-radius: 10px; |
| 457 | + padding: 20px; |
| 458 | + width: 640px; |
| 459 | + max-width: 90vw; |
| 460 | + max-height: 80vh; |
| 461 | + display: flex; |
| 462 | + flex-direction: column; |
| 463 | + gap: 12px; |
| 464 | + } |
| 465 | + |
| 466 | + .paste-modal h3 { |
| 467 | + font-size: 15px; |
| 468 | + font-weight: 600; |
| 469 | + } |
| 470 | + |
| 471 | + .paste-modal textarea { |
| 472 | + background: var(--bg); |
| 473 | + border: 1px solid var(--border); |
| 474 | + color: var(--text); |
| 475 | + border-radius: 6px; |
| 476 | + padding: 10px; |
| 477 | + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; |
| 478 | + font-size: 12px; |
| 479 | + resize: vertical; |
| 480 | + min-height: 200px; |
| 481 | + flex: 1; |
| 482 | + outline: none; |
| 483 | + } |
| 484 | + |
| 485 | + .paste-modal textarea:focus { |
| 486 | + border-color: var(--accent); |
| 487 | + } |
| 488 | + |
| 489 | + .paste-modal-buttons { |
| 490 | + display: flex; |
| 491 | + gap: 8px; |
| 492 | + justify-content: flex-end; |
| 493 | + } |
| 494 | + |
| 495 | + .paste-modal-buttons button { |
| 496 | + padding: 6px 16px; |
| 497 | + border-radius: 6px; |
| 498 | + font-size: 13px; |
| 499 | + font-weight: 600; |
| 500 | + cursor: pointer; |
| 501 | + border: 1px solid var(--border); |
| 502 | + } |
| 503 | + |
| 504 | + .btn-cancel { |
| 505 | + background: var(--surface-hover); |
| 506 | + color: var(--text); |
| 507 | + } |
| 508 | + |
| 509 | + .btn-load { |
| 510 | + background: var(--accent); |
| 511 | + color: #0d1117; |
| 512 | + border-color: var(--accent); |
| 513 | + } |
| 514 | + |
| 515 | + .btn-load:hover { |
| 516 | + opacity: 0.85; |
| 517 | + } |
| 518 | + |
| 519 | + .drop-zone-separator { |
| 520 | + margin-top: 8px; |
| 521 | + color: var(--text-muted); |
| 522 | + font-size: 13px; |
| 523 | + } |
| 524 | + |
| 525 | + .paste-btn { |
| 526 | + background: var(--accent); |
| 527 | + color: #0d1117; |
| 528 | + border: none; |
| 529 | + border-radius: 6px; |
| 530 | + padding: 8px 18px; |
| 531 | + font-size: 13px; |
| 532 | + font-weight: 600; |
| 533 | + cursor: pointer; |
| 534 | + } |
| 535 | + |
442 | 536 | #minimap { |
443 | 537 | position: absolute; |
444 | 538 | right: 8px; |
|
478 | 572 | checked></label> |
479 | 573 | </div> |
480 | 574 |
|
481 | | - <input type="text" id="commit-input" placeholder="Commit SHA (enables source links)" disabled> |
| 575 | + <input type="text" id="commit-input" placeholder="Commit SHA or tag (enables source links)" disabled> |
482 | 576 |
|
483 | 577 | <span class="stats" id="stats"></span> |
484 | 578 | </div> |
485 | 579 |
|
486 | 580 | <div class="drop-zone" id="drop-zone"> |
487 | 581 | <div class="drop-zone-icon">📄</div> |
488 | 582 | <div>Drop a <strong>.jsonl</strong> log file here or click <strong>Open Log File</strong></div> |
| 583 | + <div class="drop-zone-separator">or</div> |
| 584 | + <button id="paste-btn" class="paste-btn">📋 |
| 585 | + Paste from Clipboard</button> |
| 586 | + </div> |
| 587 | + |
| 588 | + <div class="paste-overlay" id="paste-overlay" style="display:none"> |
| 589 | + <div class="paste-modal" role="dialog" aria-modal="true" aria-labelledby="paste-modal-title"> |
| 590 | + <h3 id="paste-modal-title">Paste Log Lines</h3> |
| 591 | + <textarea id="paste-textarea" placeholder="Paste JSONL log lines here…"></textarea> |
| 592 | + <div class="paste-modal-buttons"> |
| 593 | + <button class="btn-cancel" id="paste-cancel">Cancel</button> |
| 594 | + <button class="btn-load" id="paste-load">Load</button> |
| 595 | + </div> |
| 596 | + </div> |
489 | 597 | </div> |
490 | 598 |
|
491 | 599 | <div class="log-wrapper" id="log-wrapper"> |
|
513 | 621 | const commitInput = document.getElementById("commit-input"); |
514 | 622 | const statsEl = document.getElementById("stats"); |
515 | 623 | const levelFilters = document.getElementById("level-filters"); |
| 624 | + const pasteOverlay = document.getElementById("paste-overlay"); |
| 625 | + const pasteTextarea = document.getElementById("paste-textarea"); |
| 626 | + |
| 627 | + // --- Paste modal --- |
| 628 | + |
| 629 | + const pasteBtn = document.getElementById("paste-btn"); |
| 630 | + |
| 631 | + function openPasteModal() { |
| 632 | + pasteOverlay.style.display = "flex"; |
| 633 | + pasteTextarea.value = ""; |
| 634 | + pasteTextarea.focus(); |
| 635 | + } |
| 636 | + |
| 637 | + function closePasteModal() { |
| 638 | + pasteOverlay.style.display = "none"; |
| 639 | + pasteBtn.focus(); |
| 640 | + } |
| 641 | + |
| 642 | + pasteBtn.addEventListener("click", openPasteModal); |
| 643 | + |
| 644 | + document.getElementById("paste-cancel").addEventListener("click", closePasteModal); |
| 645 | + |
| 646 | + pasteOverlay.addEventListener("click", (e) => { |
| 647 | + if (e.target === pasteOverlay) closePasteModal(); |
| 648 | + }); |
| 649 | + |
| 650 | + pasteOverlay.addEventListener("keydown", (e) => { |
| 651 | + if (e.key === "Escape") closePasteModal(); |
| 652 | + }); |
| 653 | + |
| 654 | + document.getElementById("paste-load").addEventListener("click", () => { |
| 655 | + const text = pasteTextarea.value.trim(); |
| 656 | + if (text) { |
| 657 | + closePasteModal(); |
| 658 | + parseLog(text); |
| 659 | + } |
| 660 | + }); |
516 | 661 |
|
517 | 662 | // --- File loading --- |
518 | 663 |
|
|
549 | 694 | function parseLog(text) { |
550 | 695 | const lines = text.split("\n"); |
551 | 696 | allEntries = []; |
| 697 | + bookmarkedSet.clear(); |
552 | 698 | let parseErrors = 0; |
553 | 699 |
|
554 | | - for (const line of lines) { |
555 | | - const trimmed = line.trim(); |
| 700 | + for (let i = 0; i < lines.length; i++) { |
| 701 | + const trimmed = lines[i].trim(); |
556 | 702 | if (!trimmed) continue; |
557 | 703 | try { |
558 | 704 | const entry = JSON.parse(trimmed); |
| 705 | + // Validate expected fields |
| 706 | + if (!("level" in entry) && !("message" in entry) && !("timestamp" in entry)) { |
| 707 | + parseErrors++; |
| 708 | + console.warn(`Log parser: line ${i + 1} has no expected fields (level, message, timestamp):`, entry); |
| 709 | + continue; |
| 710 | + } |
559 | 711 | // Normalize level to lowercase |
560 | 712 | if (entry.level) entry.level = entry.level.toLowerCase(); |
561 | 713 | entry._idx = allEntries.length; |
|
565 | 717 | } |
566 | 718 | } |
567 | 719 |
|
568 | | - // Auto-detect commit hash from version message |
| 720 | + if (parseErrors > 0) { |
| 721 | + console.warn(`Log parser: ${parseErrors} line(s) could not be parsed as JSON`); |
| 722 | + } |
| 723 | + |
| 724 | + // Auto-detect commit hash or tag from version message |
569 | 725 | if (!commitInput.value.trim() && allEntries.length > 0) { |
570 | 726 | const firstMsg = allEntries[0].message || ""; |
571 | | - const versionMatch = firstMsg.match(/Trident version:.*-v([0-9a-fA-F]+)/); |
572 | | - if (versionMatch) { |
573 | | - commitInput.value = versionMatch[1]; |
| 727 | + const commitMatch = firstMsg.match(/Trident version:.*[.\-]v([0-9a-fA-F]+)/); |
| 728 | + if (commitMatch) { |
| 729 | + commitInput.value = commitMatch[1]; |
| 730 | + } else { |
| 731 | + // Prod builds: MAJOR.MINOR.PATCH-RELEASE.DISTRO → use tag MAJOR.MINOR.PATCH |
| 732 | + const tagMatch = firstMsg.match(/Trident version:\s*(\d+\.\d+\.\d+)-\d+\.\w+/); |
| 733 | + if (tagMatch) { |
| 734 | + commitInput.value = tagMatch[1]; |
| 735 | + } |
574 | 736 | } |
575 | 737 | } |
576 | 738 |
|
|
644 | 806 | // --- Rendering --- |
645 | 807 |
|
646 | 808 | function escapeHtml(str) { |
647 | | - const div = document.createElement("div"); |
648 | | - div.textContent = str; |
649 | | - return div.innerHTML; |
| 809 | + return String(str) |
| 810 | + .replace(/&/g, "&") |
| 811 | + .replace(/</g, "<") |
| 812 | + .replace(/>/g, ">") |
| 813 | + .replace(/"/g, """) |
| 814 | + .replace(/'/g, "'"); |
650 | 815 | } |
651 | 816 |
|
652 | 817 | function highlightText(escaped, query) { |
|
658 | 823 |
|
659 | 824 | function formatTimestamp(ts) { |
660 | 825 | if (!ts) return ""; |
| 826 | + |
661 | 827 | try { |
662 | 828 | const d = new Date(ts); |
663 | 829 | return d.toISOString().replace("T", " ").replace("Z", " UTC"); |
|
673 | 839 | } |
674 | 840 |
|
675 | 841 | function makeFileLink(file, line) { |
676 | | - const commit = commitInput.value.trim(); |
| 842 | + const ref = commitInput.value.trim(); |
677 | 843 | const text = escapeHtml(file + ":" + line); |
678 | | - if (commit) { |
679 | | - const url = `https://github.com/microsoft/trident/blob/${encodeURIComponent(commit)}/${encodeURIComponent(file)}#L${line}`; |
| 844 | + if (ref) { |
| 845 | + // If ref looks like a semver tag (e.g. "0.21.0"), prefix with "v" |
| 846 | + const gitRef = /^\d+\.\d+\.\d+$/.test(ref) ? "v" + ref : ref; |
| 847 | + const encodedFile = file.split("/").map(encodeURIComponent).join("/"); |
| 848 | + const url = `https://github.com/microsoft/trident/blob/${encodeURIComponent(gitRef)}/${encodedFile}#L${line}`; |
680 | 849 | return `<a href="${url}" target="_blank" rel="noopener">${text}</a>`; |
681 | 850 | } |
682 | 851 | return text; |
|
0 commit comments