@eldrex/dashboard
Advanced tools
Sorry, the diff of this file is too big to display
+37
| # @eldrex/dashboard | ||
| ## Visual, offline-first dashboard for DevDiff changelog management | ||
| [](https://npmjs.com/package/@eldrex/dashboard) | ||
| [](https://socket.dev/npm/package/@eldrex/dashboard) | ||
| [](LICENSE) | ||
| ## Overview | ||
| `@eldrex/dashboard` is the web client interface that allows developers and team leads to view, search, curate, and publish generated changelogs. It acts as the frontend companion for the DevDiff local server gateway. | ||
| ## Installation | ||
| ```bash | ||
| npm install @eldrex/dashboard | ||
| ``` | ||
| ## Running the Dashboard | ||
| To serve the dashboard locally, use the CLI: | ||
| ```bash | ||
| npx devdiff report --port 4200 | ||
| ``` | ||
| ## 🔒 Security & Privacy | ||
| The dashboard operates entirely inside your local network. | ||
| - **Zero Cloud Tracking**: We do not use trackers, analytics tools, or third-party tracking scripts. | ||
| - **Local Storage**: All data, preferences, and custom views are saved in your local workspace or browser localStorage. | ||
| - **Access Control**: When connected to `@eldrex/gateway`, requests are authenticated via secure local tokens. | ||
| ## License | ||
| MIT © DevDiff Contributors |
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>DevDiff Playground — Connected to Your Workspace</title> | ||
| <meta name="description" content="DevDiff local playground — analyze your code changes with AI, switch personas, and explore formats. All data stays on your machine." /> | ||
| <!-- Google Fonts --> | ||
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" /> | ||
| <!-- marked.js for markdown rendering --> | ||
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | ||
| <!-- mermaid.js for diagram rendering --> | ||
| <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> | ||
| <style> | ||
| /* ── Design Tokens ─────────────────────────────────────────────────────── */ | ||
| :root { | ||
| --bg: #0a0f1e; | ||
| --bg2: #0f172a; | ||
| --surface: #1e293b; | ||
| --surface2: #263248; | ||
| --border: #2d3f55; | ||
| --text: #f1f5f9; | ||
| --text-dim: #94a3b8; | ||
| --text-muted:#64748b; | ||
| --accent: #6366f1; | ||
| --accent-h: #818cf8; | ||
| --cyan: #22d3ee; | ||
| --green: #22c55e; | ||
| --red: #ef4444; | ||
| --yellow: #f59e0b; | ||
| --radius: 10px; | ||
| --shadow: 0 4px 24px rgba(0,0,0,0.4); | ||
| } | ||
| * { margin: 0; padding: 0; box-sizing: border-box; } | ||
| body { | ||
| font-family: 'Inter', system-ui, sans-serif; | ||
| background: var(--bg); | ||
| color: var(--text); | ||
| min-height: 100vh; | ||
| font-size: 14px; | ||
| line-height: 1.6; | ||
| } | ||
| /* ── Layout ─────────────────────────────────────────────────────────────── */ | ||
| .app { display: flex; height: 100vh; overflow: hidden; } | ||
| /* ── Sidebar ─────────────────────────────────────────────────────────────── */ | ||
| .sidebar { | ||
| width: 264px; | ||
| flex-shrink: 0; | ||
| background: var(--bg2); | ||
| border-right: 1px solid var(--border); | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow-y: auto; | ||
| } | ||
| .sidebar-header { | ||
| padding: 20px 20px 16px; | ||
| border-bottom: 1px solid var(--border); | ||
| } | ||
| .sidebar-logo { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 10px; | ||
| margin-bottom: 4px; | ||
| } | ||
| .sidebar-logo .logo-icon { | ||
| width: 32px; | ||
| height: 32px; | ||
| background: linear-gradient(135deg, var(--accent), var(--cyan)); | ||
| border-radius: 8px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| font-size: 16px; | ||
| flex-shrink: 0; | ||
| } | ||
| .sidebar-logo h1 { | ||
| font-size: 15px; | ||
| font-weight: 700; | ||
| letter-spacing: -0.3px; | ||
| } | ||
| .sidebar-subtitle { | ||
| font-size: 11px; | ||
| color: var(--text-muted); | ||
| margin-left: 42px; | ||
| } | ||
| .sidebar-body { padding: 16px; flex: 1; } | ||
| .info-block { | ||
| background: var(--surface); | ||
| border: 1px solid var(--border); | ||
| border-radius: var(--radius); | ||
| padding: 12px; | ||
| margin-bottom: 10px; | ||
| } | ||
| .info-label { | ||
| font-size: 10px; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.8px; | ||
| color: var(--text-muted); | ||
| margin-bottom: 4px; | ||
| font-weight: 600; | ||
| } | ||
| .info-value { | ||
| font-size: 13px; | ||
| font-weight: 500; | ||
| color: var(--text); | ||
| word-break: break-all; | ||
| } | ||
| .info-value.mono { | ||
| font-family: 'JetBrains Mono', monospace; | ||
| font-size: 12px; | ||
| } | ||
| /* AI status badge */ | ||
| .ai-badge { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 8px; | ||
| padding: 8px 12px; | ||
| border-radius: 8px; | ||
| margin-bottom: 10px; | ||
| font-size: 12px; | ||
| font-weight: 500; | ||
| border: 1px solid transparent; | ||
| transition: all 0.3s; | ||
| } | ||
| .ai-badge.connected { background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.3); color: var(--green); } | ||
| .ai-badge.disconnected { background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.25); color: var(--red); } | ||
| .ai-badge.checking { background: rgba(245,158,11,.1); border-color: rgba(245,158,11,.25); color: var(--yellow); } | ||
| .ai-badge .pulse { | ||
| width: 7px; height: 7px; | ||
| border-radius: 50%; | ||
| background: currentColor; | ||
| animation: pulse 2s infinite; | ||
| } | ||
| @keyframes pulse { | ||
| 0%, 100% { opacity: 1; transform: scale(1); } | ||
| 50% { opacity: 0.5; transform: scale(0.85); } | ||
| } | ||
| /* Range selector */ | ||
| .range-select { | ||
| width: 100%; | ||
| background: var(--surface); | ||
| border: 1px solid var(--border); | ||
| color: var(--text); | ||
| border-radius: 8px; | ||
| padding: 8px 10px; | ||
| font-size: 12px; | ||
| font-family: 'JetBrains Mono', monospace; | ||
| margin-bottom: 10px; | ||
| cursor: pointer; | ||
| appearance: none; | ||
| -webkit-appearance: none; | ||
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); | ||
| background-repeat: no-repeat; | ||
| background-position: right 8px center; | ||
| background-size: 14px; | ||
| } | ||
| .range-select:focus { outline: none; border-color: var(--accent); } | ||
| /* Refresh button */ | ||
| .btn-refresh { | ||
| width: 100%; | ||
| padding: 11px 16px; | ||
| background: linear-gradient(135deg, var(--accent), #4f46e5); | ||
| color: #fff; | ||
| border: none; | ||
| border-radius: 9px; | ||
| cursor: pointer; | ||
| font-size: 13px; | ||
| font-weight: 600; | ||
| font-family: inherit; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| gap: 8px; | ||
| transition: all 0.2s; | ||
| margin-bottom: 16px; | ||
| box-shadow: 0 2px 12px rgba(99,102,241,.35); | ||
| } | ||
| .btn-refresh:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(99,102,241,.45); } | ||
| .btn-refresh:active { transform: translateY(0); } | ||
| .btn-refresh.loading { opacity: 0.7; pointer-events: none; } | ||
| .privacy-notice { | ||
| font-size: 11px; | ||
| color: var(--text-muted); | ||
| line-height: 1.5; | ||
| padding: 8px 10px; | ||
| background: rgba(255,255,255,.03); | ||
| border-radius: 8px; | ||
| border: 1px solid var(--border); | ||
| } | ||
| .privacy-notice span { color: var(--green); } | ||
| /* Session timer */ | ||
| .session-timer { | ||
| font-family: 'JetBrains Mono', monospace; | ||
| font-size: 18px; | ||
| font-weight: 500; | ||
| color: var(--cyan); | ||
| margin-bottom: 2px; | ||
| } | ||
| /* ── Main ───────────────────────────────────────────────────────────────── */ | ||
| .main { | ||
| flex: 1; | ||
| display: flex; | ||
| flex-direction: column; | ||
| overflow: hidden; | ||
| } | ||
| /* Top toolbar */ | ||
| .toolbar { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 12px; | ||
| padding: 12px 20px; | ||
| background: var(--bg2); | ||
| border-bottom: 1px solid var(--border); | ||
| flex-wrap: wrap; | ||
| } | ||
| .toolbar-section { display: flex; align-items: center; gap: 6px; } | ||
| .toolbar-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.7px; margin-right: 2px; } | ||
| .toolbar-divider { width: 1px; height: 24px; background: var(--border); margin: 0 4px; } | ||
| /* Persona/format pill buttons */ | ||
| .pill-btn { | ||
| padding: 5px 13px; | ||
| border-radius: 20px; | ||
| border: 1px solid var(--border); | ||
| background: transparent; | ||
| color: var(--text-dim); | ||
| cursor: pointer; | ||
| font-size: 12px; | ||
| font-family: inherit; | ||
| font-weight: 500; | ||
| transition: all 0.18s; | ||
| white-space: nowrap; | ||
| } | ||
| .pill-btn:hover { border-color: var(--accent); color: var(--text); } | ||
| .pill-btn.active { | ||
| background: var(--accent); | ||
| border-color: var(--accent); | ||
| color: #fff; | ||
| box-shadow: 0 0 12px rgba(99,102,241,.3); | ||
| } | ||
| /* ── Stats bar ──────────────────────────────────────────────────────────── */ | ||
| .stats-bar { | ||
| display: grid; | ||
| grid-template-columns: repeat(4, 1fr); | ||
| gap: 1px; | ||
| background: var(--border); | ||
| border-bottom: 1px solid var(--border); | ||
| } | ||
| .stat-card { | ||
| background: var(--bg2); | ||
| padding: 12px 20px; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 12px; | ||
| } | ||
| .stat-card .stat-icon { | ||
| font-size: 20px; | ||
| opacity: 0.8; | ||
| } | ||
| .stat-card .stat-body {} | ||
| .stat-card .stat-num { | ||
| font-size: 22px; | ||
| font-weight: 700; | ||
| font-family: 'JetBrains Mono', monospace; | ||
| color: var(--cyan); | ||
| line-height: 1; | ||
| } | ||
| .stat-card .stat-label { | ||
| font-size: 11px; | ||
| color: var(--text-muted); | ||
| margin-top: 2px; | ||
| } | ||
| /* ── Output area ────────────────────────────────────────────────────────── */ | ||
| .output-area { | ||
| flex: 1; | ||
| overflow-y: auto; | ||
| padding: 24px; | ||
| } | ||
| .output-panel { | ||
| background: var(--bg2); | ||
| border: 1px solid var(--border); | ||
| border-radius: 12px; | ||
| min-height: 400px; | ||
| padding: 28px; | ||
| } | ||
| /* Loading state */ | ||
| .loading-state { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| height: 320px; | ||
| gap: 16px; | ||
| color: var(--text-muted); | ||
| } | ||
| .spinner { | ||
| width: 36px; height: 36px; | ||
| border: 3px solid var(--border); | ||
| border-top-color: var(--accent); | ||
| border-radius: 50%; | ||
| animation: spin 0.7s linear infinite; | ||
| } | ||
| @keyframes spin { to { transform: rotate(360deg); } } | ||
| .loading-state p { font-size: 13px; animation: blink 1.5s ease-in-out infinite; } | ||
| @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } | ||
| /* Markdown styles */ | ||
| .md-output h1 { font-size: 22px; font-weight: 700; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--border); } | ||
| .md-output h2 { font-size: 17px; font-weight: 600; color: var(--cyan); margin: 20px 0 10px; } | ||
| .md-output h3 { font-size: 14px; font-weight: 600; margin: 14px 0 6px; } | ||
| .md-output p { color: #cbd5e1; margin-bottom: 10px; } | ||
| .md-output ul, .md-output ol { margin: 8px 0 12px 20px; color: #cbd5e1; } | ||
| .md-output li { margin-bottom: 4px; } | ||
| .md-output strong { color: var(--text); font-weight: 600; } | ||
| .md-output em { color: var(--text-dim); font-style: italic; } | ||
| .md-output code { | ||
| background: var(--surface2); | ||
| padding: 2px 6px; | ||
| border-radius: 4px; | ||
| font-family: 'JetBrains Mono', monospace; | ||
| font-size: 12px; | ||
| color: var(--cyan); | ||
| } | ||
| .md-output pre { | ||
| background: var(--surface2); | ||
| padding: 16px; | ||
| border-radius: 8px; | ||
| overflow-x: auto; | ||
| margin: 12px 0; | ||
| border: 1px solid var(--border); | ||
| } | ||
| .md-output pre code { background: none; padding: 0; color: var(--text); } | ||
| .md-output blockquote { | ||
| border-left: 3px solid var(--accent); | ||
| padding-left: 16px; | ||
| color: var(--text-dim); | ||
| margin: 12px 0; | ||
| } | ||
| .md-output hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; } | ||
| .md-output table { width: 100%; border-collapse: collapse; margin: 12px 0; } | ||
| .md-output th { background: var(--surface2); padding: 8px 12px; text-align: left; font-size: 12px; text-transform: uppercase; color: var(--text-dim); } | ||
| .md-output td { padding: 8px 12px; border-bottom: 1px solid var(--border); color: #cbd5e1; } | ||
| /* JSON output */ | ||
| .json-output { | ||
| font-family: 'JetBrains Mono', monospace; | ||
| font-size: 12px; | ||
| line-height: 1.7; | ||
| color: #94a3b8; | ||
| white-space: pre-wrap; | ||
| word-break: break-all; | ||
| } | ||
| .json-output .k { color: #818cf8; } /* keys */ | ||
| .json-output .s { color: #6ee7b7; } /* strings */ | ||
| .json-output .n { color: #fb923c; } /* numbers */ | ||
| .json-output .b { color: #f472b6; } /* booleans */ | ||
| /* Mermaid container */ | ||
| .mermaid-wrap { | ||
| background: #fff; | ||
| border-radius: 10px; | ||
| padding: 24px; | ||
| margin: 12px 0; | ||
| text-align: center; | ||
| overflow: auto; | ||
| } | ||
| /* Error state */ | ||
| .error-state { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| height: 320px; | ||
| gap: 12px; | ||
| text-align: center; | ||
| } | ||
| .error-state .error-icon { font-size: 40px; } | ||
| .error-state h3 { font-size: 16px; color: var(--red); } | ||
| .error-state p { font-size: 13px; color: var(--text-muted); max-width: 380px; } | ||
| .error-state code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; font-size: 12px; } | ||
| /* Toast notification */ | ||
| .toast { | ||
| position: fixed; | ||
| top: 16px; | ||
| right: 16px; | ||
| background: var(--surface); | ||
| border: 1px solid var(--border); | ||
| border-radius: 10px; | ||
| padding: 10px 16px; | ||
| font-size: 12px; | ||
| color: var(--text); | ||
| z-index: 9999; | ||
| animation: slideIn 0.3s ease, fadeOut 0.4s 2.6s ease forwards; | ||
| box-shadow: var(--shadow); | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 8px; | ||
| } | ||
| @keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } } | ||
| @keyframes fadeOut { to { opacity: 0; transform: translateY(-10px); } } | ||
| /* Responsive */ | ||
| @media (max-width: 900px) { | ||
| .app { flex-direction: column; } | ||
| .sidebar { width: 100%; height: auto; } | ||
| .stats-bar { grid-template-columns: repeat(2, 1fr); } | ||
| } | ||
| /* Scrollbar */ | ||
| ::-webkit-scrollbar { width: 6px; } | ||
| ::-webkit-scrollbar-track { background: var(--bg2); } | ||
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } | ||
| ::-webkit-scrollbar-thumb:hover { background: #3d5168; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="app"> | ||
| <!-- ── Sidebar ──────────────────────────────────────────────────────── --> | ||
| <aside class="sidebar"> | ||
| <div class="sidebar-header"> | ||
| <div class="sidebar-logo"> | ||
| <div class="logo-icon">🎮</div> | ||
| <h1>DevDiff</h1> | ||
| </div> | ||
| <p class="sidebar-subtitle">Local Playground</p> | ||
| </div> | ||
| <div class="sidebar-body"> | ||
| <!-- Workspace info --> | ||
| <div class="info-block"> | ||
| <div class="info-label">Workspace</div> | ||
| <div class="info-value" id="ws-name">Loading…</div> | ||
| </div> | ||
| <div class="info-block"> | ||
| <div class="info-label">Git</div> | ||
| <div class="info-value" id="ws-git">Loading…</div> | ||
| </div> | ||
| <!-- AI status --> | ||
| <div class="ai-badge checking" id="ai-badge"> | ||
| <span class="pulse"></span> | ||
| <span id="ai-badge-text">Checking AI…</span> | ||
| </div> | ||
| <div class="info-block"> | ||
| <div class="info-label">AI Provider</div> | ||
| <div class="info-value mono" id="ws-provider">—</div> | ||
| </div> | ||
| <!-- Range selector --> | ||
| <div class="info-label" style="margin-bottom:5px;padding-left:1px;">Commit Range</div> | ||
| <select class="range-select" id="range-select"> | ||
| <option value="HEAD~1..HEAD">Last 1 commit</option> | ||
| <option value="HEAD~5..HEAD" selected>Last 5 commits</option> | ||
| <option value="HEAD~10..HEAD">Last 10 commits</option> | ||
| <option value="HEAD~20..HEAD">Last 20 commits</option> | ||
| <option value="HEAD~50..HEAD">Last 50 commits</option> | ||
| </select> | ||
| <!-- Refresh --> | ||
| <button class="btn-refresh" id="btn-refresh" onclick="refreshChangelog()"> | ||
| <span id="refresh-icon">🔄</span> | ||
| <span id="refresh-label">Analyze Changes</span> | ||
| </button> | ||
| <!-- Session timer --> | ||
| <div class="info-block"> | ||
| <div class="info-label">Session Time</div> | ||
| <div class="session-timer" id="session-timer">00:00</div> | ||
| </div> | ||
| <!-- Privacy notice --> | ||
| <div class="privacy-notice"> | ||
| <span>🔒</span> All analysis runs locally.<br> | ||
| Nothing is sent to the cloud. | ||
| </div> | ||
| </div> | ||
| </aside> | ||
| <!-- ── Main ────────────────────────────────────────────────────────── --> | ||
| <div class="main"> | ||
| <!-- Toolbar --> | ||
| <div class="toolbar"> | ||
| <div class="toolbar-section"> | ||
| <span class="toolbar-label">Persona</span> | ||
| <div id="persona-btns"> | ||
| <button class="pill-btn active" data-persona="developer">👨💻 Developer</button> | ||
| <button class="pill-btn" data-persona="ceo">📊 CEO</button> | ||
| <button class="pill-btn" data-persona="educator">📚 Educator</button> | ||
| <button class="pill-btn" data-persona="robot">🤖 Robot</button> | ||
| <button class="pill-btn" data-persona="data-analyst">📈 Analyst</button> | ||
| <button class="pill-btn" data-persona="journalist">📰 Journalist</button> | ||
| <button class="pill-btn" data-persona="pm">📋 PM</button> | ||
| <button class="pill-btn" data-persona="compliance">🔒 Compliance</button> | ||
| </div> | ||
| </div> | ||
| <div class="toolbar-divider"></div> | ||
| <div class="toolbar-section"> | ||
| <span class="toolbar-label">Format</span> | ||
| <div id="format-btns"> | ||
| <button class="pill-btn active" data-format="markdown">📝 Markdown</button> | ||
| <button class="pill-btn" data-format="mermaid">📊 Mermaid</button> | ||
| <button class="pill-btn" data-format="json">{ } JSON</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <!-- Stats bar --> | ||
| <div class="stats-bar"> | ||
| <div class="stat-card"> | ||
| <span class="stat-icon">📁</span> | ||
| <div class="stat-body"> | ||
| <div class="stat-num" id="stat-files">—</div> | ||
| <div class="stat-label">Files Changed</div> | ||
| </div> | ||
| </div> | ||
| <div class="stat-card"> | ||
| <span class="stat-icon">✅</span> | ||
| <div class="stat-body"> | ||
| <div class="stat-num" id="stat-add">—</div> | ||
| <div class="stat-label">Lines Added</div> | ||
| </div> | ||
| </div> | ||
| <div class="stat-card"> | ||
| <span class="stat-icon">🗑️</span> | ||
| <div class="stat-body"> | ||
| <div class="stat-num" id="stat-del">—</div> | ||
| <div class="stat-label">Lines Removed</div> | ||
| </div> | ||
| </div> | ||
| <div class="stat-card"> | ||
| <span class="stat-icon">🤖</span> | ||
| <div class="stat-body"> | ||
| <div class="stat-num" id="stat-tokens">—</div> | ||
| <div class="stat-label">Tokens Used</div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <!-- Changelog output --> | ||
| <div class="output-area"> | ||
| <div class="output-panel" id="output-panel"> | ||
| <div class="loading-state"> | ||
| <div class="spinner"></div> | ||
| <p>Connecting to workspace…</p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <script> | ||
| // ── State ──────────────────────────────────────────────────────────────── | ||
| let persona = 'developer'; | ||
| let format = 'markdown'; | ||
| let wsConn = null; | ||
| let sessionStart = Date.now(); | ||
| // ── Init ───────────────────────────────────────────────────────────────── | ||
| async function init() { | ||
| mermaid.initialize({ startOnLoad: false, theme: 'default' }); | ||
| await loadWorkspaceInfo(); | ||
| await loadStats(); | ||
| connectWebSocket(); | ||
| startSessionTimer(); | ||
| // Persona buttons | ||
| document.querySelectorAll('#persona-btns .pill-btn').forEach(btn => { | ||
| btn.addEventListener('click', () => { | ||
| document.querySelectorAll('#persona-btns .pill-btn').forEach(b => b.classList.remove('active')); | ||
| btn.classList.add('active'); | ||
| persona = btn.dataset.persona; | ||
| refreshChangelog(); | ||
| }); | ||
| }); | ||
| // Format buttons | ||
| document.querySelectorAll('#format-btns .pill-btn').forEach(btn => { | ||
| btn.addEventListener('click', () => { | ||
| document.querySelectorAll('#format-btns .pill-btn').forEach(b => b.classList.remove('active')); | ||
| btn.classList.add('active'); | ||
| format = btn.dataset.format; | ||
| refreshChangelog(); | ||
| }); | ||
| }); | ||
| // Range selector | ||
| document.getElementById('range-select').addEventListener('change', () => { | ||
| loadStats(); | ||
| refreshChangelog(); | ||
| }); | ||
| // Initial changelog load | ||
| await refreshChangelog(); | ||
| } | ||
| // ── Workspace info ─────────────────────────────────────────────────────── | ||
| async function loadWorkspaceInfo() { | ||
| try { | ||
| const data = await fetchJSON('/api/workspace'); | ||
| document.getElementById('ws-name').textContent = data.name || '(unknown)'; | ||
| const git = data.git || {}; | ||
| document.getElementById('ws-git').textContent = | ||
| `${git.commits ?? '?'} commits · ${git.currentBranch ?? '?'}`; | ||
| document.getElementById('ws-provider').textContent = data.ai?.provider || 'ollama'; | ||
| const badge = document.getElementById('ai-badge'); | ||
| const text = document.getElementById('ai-badge-text'); | ||
| if (data.ai?.status === 'connected') { | ||
| badge.className = 'ai-badge connected'; | ||
| text.textContent = `AI Connected`; | ||
| if (data.ai.modelsAvailable?.length) { | ||
| text.textContent += ` · ${data.ai.modelsAvailable[0]}`; | ||
| } | ||
| } else { | ||
| badge.className = 'ai-badge disconnected'; | ||
| text.textContent = 'AI Disconnected'; | ||
| } | ||
| } catch { | ||
| document.getElementById('ws-name').textContent = '(could not load)'; | ||
| } | ||
| } | ||
| // ── Stats ──────────────────────────────────────────────────────────────── | ||
| async function loadStats() { | ||
| const range = document.getElementById('range-select').value; | ||
| try { | ||
| const data = await fetchJSON(`/api/stats?since=${encodeURIComponent(range)}`); | ||
| document.getElementById('stat-files').textContent = data.filesChanged ?? '0'; | ||
| document.getElementById('stat-add').textContent = data.linesAdded ?? '0'; | ||
| document.getElementById('stat-del').textContent = data.linesRemoved ?? '0'; | ||
| document.getElementById('stat-tokens').textContent = data.tokensUsed ?? '0'; | ||
| } catch {} | ||
| } | ||
| // ── Changelog refresh ──────────────────────────────────────────────────── | ||
| async function refreshChangelog() { | ||
| const panel = document.getElementById('output-panel'); | ||
| const btn = document.getElementById('btn-refresh'); | ||
| const icon = document.getElementById('refresh-icon'); | ||
| const label = document.getElementById('refresh-label'); | ||
| const range = document.getElementById('range-select').value; | ||
| // Loading state | ||
| panel.innerHTML = `<div class="loading-state"><div class="spinner"></div><p>Generating ${persona} changelog…</p></div>`; | ||
| btn.classList.add('loading'); | ||
| icon.textContent = '⏳'; | ||
| label.textContent = 'Analyzing…'; | ||
| try { | ||
| const data = await fetchJSON( | ||
| `/api/changelog?persona=${persona}&format=${format}&since=${encodeURIComponent(range)}` | ||
| ); | ||
| if (!data.success) { | ||
| panel.innerHTML = ` | ||
| <div class="error-state"> | ||
| <span class="error-icon">⚠️</span> | ||
| <h3>${escapeHtml(data.error || 'Analysis failed')}</h3> | ||
| <p>Try changing the commit range, or make sure your AI provider is running.</p> | ||
| <p>Start Ollama: <code>ollama serve</code></p> | ||
| </div>`; | ||
| } else if (format === 'json') { | ||
| panel.innerHTML = `<div class="json-output">${syntaxHighlightJSON( | ||
| JSON.stringify(data.changelog, null, 2) | ||
| )}</div>`; | ||
| } else if (format === 'mermaid') { | ||
| const id = 'mermaid-' + Date.now(); | ||
| panel.innerHTML = `<div class="mermaid-wrap"><div class="mermaid" id="${id}">${escapeHtml(data.changelog)}</div></div>`; | ||
| try { await mermaid.run({ nodes: [document.getElementById(id)] }); } catch {} | ||
| } else { | ||
| panel.innerHTML = `<div class="md-output">${marked.parse(data.changelog || '')}</div>`; | ||
| } | ||
| await loadStats(); | ||
| } catch (err) { | ||
| panel.innerHTML = ` | ||
| <div class="error-state"> | ||
| <span class="error-icon">🔌</span> | ||
| <h3>Cannot reach DevDiff server</h3> | ||
| <p>Make sure the playground is running: <code>devdiff playground</code></p> | ||
| </div>`; | ||
| } finally { | ||
| btn.classList.remove('loading'); | ||
| icon.textContent = '🔄'; | ||
| label.textContent = 'Analyze Changes'; | ||
| } | ||
| } | ||
| // ── WebSocket ──────────────────────────────────────────────────────────── | ||
| function connectWebSocket() { | ||
| const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; | ||
| wsConn = new WebSocket(`${proto}//${location.host}`); | ||
| let refreshTimeout = null; | ||
| wsConn.onmessage = (e) => { | ||
| try { | ||
| const msg = JSON.parse(e.data); | ||
| if (msg.type === 'workspace-changed') { | ||
| toast('📝 Changes detected — refreshing…'); | ||
| clearTimeout(refreshTimeout); | ||
| refreshTimeout = setTimeout(() => { | ||
| loadStats(); | ||
| refreshChangelog(); | ||
| }, 1500); | ||
| } | ||
| } catch {} | ||
| }; | ||
| wsConn.onclose = () => setTimeout(connectWebSocket, 3000); | ||
| } | ||
| // ── Session timer ───────────────────────────────────────────────────────── | ||
| function startSessionTimer() { | ||
| setInterval(() => { | ||
| const elapsed = Math.floor((Date.now() - sessionStart) / 1000); | ||
| const m = String(Math.floor(elapsed / 60)).padStart(2, '0'); | ||
| const s = String(elapsed % 60).padStart(2, '0'); | ||
| document.getElementById('session-timer').textContent = `${m}:${s}`; | ||
| }, 1000); | ||
| } | ||
| // ── Helpers ─────────────────────────────────────────────────────────────── | ||
| async function fetchJSON(url) { | ||
| const res = await fetch(url); | ||
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | ||
| return res.json(); | ||
| } | ||
| function escapeHtml(s = '') { | ||
| return String(s).replace(/[&<>"']/g, c => | ||
| ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]) | ||
| ); | ||
| } | ||
| function syntaxHighlightJSON(json) { | ||
| return escapeHtml(json).replace( | ||
| /("[\w\s]+")\s*:/g, '<span class="k">$1</span>:' | ||
| ).replace( | ||
| /:\s*(".*?")/g, ': <span class="s">$1</span>' | ||
| ).replace( | ||
| /:\s*(\d+\.?\d*)/g, ': <span class="n">$1</span>' | ||
| ).replace( | ||
| /:\s*(true|false|null)/g, ': <span class="b">$1</span>' | ||
| ); | ||
| } | ||
| function toast(msg) { | ||
| const el = document.createElement('div'); | ||
| el.className = 'toast'; | ||
| el.textContent = msg; | ||
| document.body.appendChild(el); | ||
| setTimeout(() => el.remove(), 3200); | ||
| } | ||
| // ── Start ───────────────────────────────────────────────────────────────── | ||
| init(); | ||
| </script> | ||
| </body> | ||
| </html> |
@@ -5,5 +5,5 @@ | ||
| [36mvite v5.4.21 [32mbuilding for production...[36m[39m | ||
| [36mvite v6.4.3 [32mbuilding for production...[36m[39m | ||
| transforming... | ||
| [32m✓[39m 31 modules transformed. | ||
| [32m✓[39m 27 modules transformed. | ||
| rendering chunks... | ||
@@ -13,3 +13,3 @@ computing gzip size... | ||
| [2mdist/[22m[35massets/index-3hymSP_O.css [39m[1m[2m 3.80 kB[22m[1m[22m[2m │ gzip: 1.41 kB[22m | ||
| [2mdist/[22m[36massets/index-DAFVNwFY.js [39m[1m[2m152.12 kB[22m[1m[22m[2m │ gzip: 48.62 kB[22m | ||
| [32m✓ built in 1.11s[39m | ||
| [2mdist/[22m[36massets/index-Dk4e-dWH.js [39m[1m[2m153.29 kB[22m[1m[22m[2m │ gzip: 49.00 kB[22m | ||
| [32m✓ built in 934ms[39m |
+1
-1
@@ -12,3 +12,3 @@ <!doctype html> | ||
| /> | ||
| <script type="module" crossorigin src="/demo/assets/index-DAFVNwFY.js"></script> | ||
| <script type="module" crossorigin src="/demo/assets/index-Dk4e-dWH.js"></script> | ||
| <link rel="stylesheet" crossorigin href="/demo/assets/index-3hymSP_O.css"> | ||
@@ -15,0 +15,0 @@ </head> |
+11
-8
| { | ||
| "name": "@eldrex/dashboard", | ||
| "version": "1.0.2", | ||
| "version": "1.0.3", | ||
| "publishConfig": { | ||
| "registry": "https://registry.npmjs.org" | ||
| }, | ||
| "description": "Web dashboard for DevDiff — visualize changes and changelogs", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "tsc && vite build", | ||
| "preview": "vite preview" | ||
| }, | ||
| "dependencies": { | ||
@@ -23,8 +31,3 @@ "react": "^18.3.1", | ||
| "author": "Eldrex Delos Reyes Bula <eldrexdelosreyesbula@gmail.com>", | ||
| "license": "MIT", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "tsc && vite build", | ||
| "preview": "vite preview" | ||
| } | ||
| } | ||
| "license": "MIT" | ||
| } |
Sorry, the diff of this file is too big to display
-21
| MIT License | ||
| Copyright (c) 2026 Eldrex Delos Reyes Bula | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
212030
17.23%13
8.33%1356
0.44%0
-100%38
Infinity%7
-12.5%