- Add tui-driver MCP server to .mcp.json for TUI app testing - Add mcp__tui-driver__* wildcard permission to settings.json - Add notify-subagent-done.sh hook - Update plugin configs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
616 lines
48 KiB
HTML
616 lines
48 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Major Domo Memory Graph</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { background: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; height: 100vh; overflow: hidden; }
|
|
|
|
.layout { display: grid; grid-template-columns: 280px 1fr 320px; height: 100vh; }
|
|
|
|
/* Left panel */
|
|
.controls { background: #161b22; border-right: 1px solid #30363d; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
|
|
.controls h1 { font-size: 15px; color: #58a6ff; font-weight: 600; letter-spacing: 0.5px; }
|
|
.controls h2 { font-size: 11px; text-transform: uppercase; color: #8b949e; letter-spacing: 1px; margin-bottom: 6px; }
|
|
|
|
.search-box { width: 100%; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 8px 10px; color: #c9d1d9; font-size: 13px; outline: none; }
|
|
.search-box:focus { border-color: #58a6ff; }
|
|
.search-box::placeholder { color: #484f58; }
|
|
|
|
.type-filters { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
.type-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 12px; font-size: 11px; cursor: pointer; border: 1px solid transparent; transition: all 0.15s; user-select: none; }
|
|
.type-chip.active { border-color: currentColor; }
|
|
.type-chip.inactive { opacity: 0.35; }
|
|
.type-chip .dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
.type-chip .count { color: #8b949e; font-size: 10px; }
|
|
|
|
.tag-list { display: flex; flex-wrap: wrap; gap: 3px; max-height: 200px; overflow-y: auto; }
|
|
.tag-chip { padding: 2px 7px; border-radius: 10px; font-size: 10px; background: #21262d; color: #8b949e; cursor: pointer; transition: all 0.15s; user-select: none; border: 1px solid transparent; }
|
|
.tag-chip:hover { background: #30363d; color: #c9d1d9; }
|
|
.tag-chip.active { background: #1f3a5f; color: #58a6ff; border-color: #58a6ff; }
|
|
|
|
.stat-row { display: flex; justify-content: space-between; font-size: 12px; color: #8b949e; }
|
|
.stat-row .val { color: #c9d1d9; font-weight: 500; }
|
|
|
|
.legend { font-size: 11px; color: #8b949e; line-height: 1.6; }
|
|
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
.legend-icon { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
|
|
|
.decay-bar { display: flex; gap: 2px; align-items: center; margin-top: 4px; }
|
|
.decay-segment { height: 6px; border-radius: 3px; flex: 1; }
|
|
|
|
.btn-row { display: flex; gap: 6px; }
|
|
.btn { padding: 5px 10px; border-radius: 6px; border: 1px solid #30363d; background: #21262d; color: #c9d1d9; font-size: 11px; cursor: pointer; }
|
|
.btn:hover { background: #30363d; }
|
|
|
|
/* Canvas */
|
|
.canvas-wrap { position: relative; overflow: hidden; }
|
|
canvas { display: block; width: 100%; height: 100%; }
|
|
.canvas-hud { position: absolute; bottom: 12px; left: 12px; font-size: 11px; color: #484f58; pointer-events: none; }
|
|
.zoom-controls { position: absolute; bottom: 12px; right: 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
.zoom-btn { width: 32px; height: 32px; border-radius: 6px; background: #161b22cc; border: 1px solid #30363d; color: #c9d1d9; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
.zoom-btn:hover { background: #21262d; }
|
|
|
|
/* Right panel - detail */
|
|
.detail-panel { background: #161b22; border-left: 1px solid #30363d; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
|
|
.detail-panel h2 { font-size: 11px; text-transform: uppercase; color: #8b949e; letter-spacing: 1px; }
|
|
.detail-title { font-size: 15px; color: #f0f6fc; font-weight: 600; line-height: 1.4; }
|
|
.detail-meta { font-size: 12px; color: #8b949e; }
|
|
.detail-tags { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
.detail-tag { padding: 2px 8px; border-radius: 10px; font-size: 11px; background: #21262d; color: #8b949e; }
|
|
.detail-edges { font-size: 12px; }
|
|
.detail-edge { padding: 6px 0; border-bottom: 1px solid #21262d; }
|
|
.detail-edge-type { color: #f97583; font-size: 10px; font-weight: 600; text-transform: uppercase; }
|
|
.detail-edge-target { color: #c9d1d9; }
|
|
|
|
.decay-meter { height: 8px; border-radius: 4px; background: #21262d; overflow: hidden; margin-top: 4px; }
|
|
.decay-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
|
|
|
|
.empty-state { color: #484f58; font-size: 13px; text-align: center; margin-top: 60px; line-height: 1.6; }
|
|
|
|
.view-toggle { display: flex; background: #0d1117; border-radius: 6px; overflow: hidden; border: 1px solid #30363d; }
|
|
.view-opt { flex: 1; padding: 6px; text-align: center; font-size: 11px; cursor: pointer; color: #8b949e; transition: all 0.15s; }
|
|
.view-opt.active { background: #1f3a5f; color: #58a6ff; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="layout">
|
|
<!-- Left Controls -->
|
|
<div class="controls">
|
|
<h1>Major Domo Memory Graph</h1>
|
|
|
|
<div>
|
|
<h2>Search</h2>
|
|
<input class="search-box" type="text" id="search" placeholder="Filter by title...">
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Layout</h2>
|
|
<div class="view-toggle" id="viewToggle">
|
|
<div class="view-opt active" data-view="force">Force</div>
|
|
<div class="view-opt" data-view="cluster">Cluster</div>
|
|
<div class="view-opt" data-view="radial">Radial</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Types</h2>
|
|
<div class="type-filters" id="typeFilters"></div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Tags <span id="tagCount" style="color:#484f58"></span></h2>
|
|
<div class="tag-list" id="tagList"></div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Decay</h2>
|
|
<div class="decay-bar">
|
|
<div class="decay-segment" style="background:#f85149" title="Dormant (<0.2)"></div>
|
|
<div class="decay-segment" style="background:#d29922" title="Fading (0.2-0.5)"></div>
|
|
<div class="decay-segment" style="background:#3fb950" title="Active (>0.5)"></div>
|
|
</div>
|
|
<div style="display:flex; justify-content:space-between; font-size:10px; color:#484f58; margin-top:2px;">
|
|
<span>Dormant</span><span>Fading</span><span>Active</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Stats</h2>
|
|
<div class="stat-row"><span>Memories</span><span class="val" id="statTotal">0</span></div>
|
|
<div class="stat-row"><span>Visible</span><span class="val" id="statVisible">0</span></div>
|
|
<div class="stat-row"><span>Edges</span><span class="val" id="statEdges">0</span></div>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn" id="btnReset">Reset Filters</button>
|
|
<button class="btn" id="btnRecenter">Recenter</button>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<h2>Size & Opacity</h2>
|
|
<div style="color:#484f58; font-size:11px; margin-top:4px;">
|
|
Node size = importance<br>
|
|
Node brightness = decay score<br>
|
|
Brighter = more active
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Canvas -->
|
|
<div class="canvas-wrap">
|
|
<canvas id="graph"></canvas>
|
|
<div class="canvas-hud" id="hud">Click a node to inspect</div>
|
|
<div class="zoom-controls">
|
|
<button class="zoom-btn" id="zoomIn">+</button>
|
|
<button class="zoom-btn" id="zoomOut">−</button>
|
|
<button class="zoom-btn" id="zoomFit" title="Fit all">❐</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail Panel -->
|
|
<div class="detail-panel" id="detailPanel">
|
|
<h2>Memory Detail</h2>
|
|
<div class="empty-state" id="emptyDetail">
|
|
Click a node in the graph<br>to view its details
|
|
</div>
|
|
<div id="detailContent" style="display:none">
|
|
<div class="detail-title" id="detailTitle"></div>
|
|
<div style="display:flex; gap:8px; align-items:center; margin-top:4px;">
|
|
<span class="type-chip" id="detailType" style="font-size:12px;"></span>
|
|
<span class="detail-meta" id="detailId"></span>
|
|
</div>
|
|
<div>
|
|
<h2>Importance</h2>
|
|
<div class="decay-meter"><div class="decay-fill" id="detailImportance" style="background:#58a6ff;"></div></div>
|
|
<div class="detail-meta" id="detailImportanceVal"></div>
|
|
</div>
|
|
<div>
|
|
<h2>Decay Score</h2>
|
|
<div class="decay-meter"><div class="decay-fill" id="detailDecay"></div></div>
|
|
<div class="detail-meta" id="detailDecayVal"></div>
|
|
</div>
|
|
<div>
|
|
<h2>Tags</h2>
|
|
<div class="detail-tags" id="detailTags"></div>
|
|
</div>
|
|
<div id="detailEdgesSection" style="display:none">
|
|
<h2>Edges</h2>
|
|
<div class="detail-edges" id="detailEdges"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ── Data ──
|
|
const DATA = {"nodes":[{"id":"0dd72eed","title":"Fix: Roster validation missed org affiliate transactions (PORMIL, PORIL)","type":"fix","tags":["major-domo","major-domo-v2","roster-validation","transaction-builder","trades","mil","affiliates","fix","discord-bot"],"importance":0.8,"decay":0.4},{"id":"fe6307d0","title":"Pattern: Database API transaction endpoint uses exact team_abbrev matching","type":"insight","tags":["major-domo","major-domo-v2","database-api","transactions","team-abbrev","query-params","affiliates","mil","insight","code-pattern"],"importance":0.7,"decay":0.44},{"id":"c29d2fb4","title":"Fix SOAK and DraftList test failures","type":"solution","tags":["major-domo","python","test-fix","pydantic"],"importance":0.5,"decay":0.05},{"id":"be968601","title":"Draft services API compliance and test suite","type":"solution","tags":["major-domo","python","draft","api","testing"],"importance":0.6,"decay":0.06},{"id":"74d19d12","title":"Discord Bot Test Fix Patterns - Guild Mock and Decorator Patches","type":"solution","tags":["major-domo","discord-bot","testing","python","fix","mock","decorator"],"importance":0.9,"decay":0.09},{"id":"18f79428","title":"Fix test suite failures in Major Domo Discord Bot","type":"solution","tags":["major-domo","python","testing","pytest","fix"],"importance":0.7,"decay":0.07},{"id":"4f3d2923","title":"PostgreSQL migration workflow for Major Domo","type":"solution","tags":["major-domo","postgresql","migrations","docker","workflow"],"importance":0.7,"decay":0.07},{"id":"9e67e15c","title":"Added salary_cap field to Team model","type":"solution","tags":["major-domo","python","model","schema-sync"],"importance":0.4,"decay":0.04},{"id":"e5bc69ae","title":"Salary cap helper functions added","type":"solution","tags":["major-domo","python","refactor","salary-cap","helpers"],"importance":0.6,"decay":0.06},{"id":"65a610d6","title":"Dynamic salary cap refactor complete","type":"solution","tags":["major-domo","python","refactor","salary-cap","transactions","draft"],"importance":0.7,"decay":0.07},{"id":"a72e4aa7","title":"Added salary_cap column to Major Domo Team model","type":"solution","tags":["major-domo","postgresql","docker","release"],"importance":0.5,"decay":0.05},{"id":"f8d1d735","title":"CACHE_ENABLED env var for Major Domo API","type":"solution","tags":["major-domo","redis","caching","docker","config"],"importance":0.6,"decay":0.06},{"id":"e5c0460c","title":"Draft pick API parsing fix","type":"solution","tags":["major-domo","python","fix","api","discord-bot"],"importance":0.6,"decay":0.06},{"id":"6401b7ce","title":"Double emoji fix in Discord embeds","type":"solution","tags":["major-domo","python","fix","discord","ui"],"importance":0.5,"decay":0.05},{"id":"8b130d45","title":"Draft list command UX improvements","type":"solution","tags":["major-domo","discord","ux","fix"],"importance":0.5,"decay":0.05},{"id":"e4f0bee9","title":"Draft monitor auto-start and on-clock embed","type":"solution","tags":["major-domo","discord","draft","python","fix"],"importance":0.7,"decay":0.08},{"id":"a55e8eb5","title":"Draft skipped pick support","type":"solution","tags":["major-domo","discord-bot","draft","python","fix"],"importance":0.6,"decay":0.07},{"id":"41a4294a","title":"Google Sheets draft pick tracking integration","type":"solution","tags":["major-domo","python","google-sheets","draft","feature"],"importance":0.6,"decay":0.07},{"id":"7c146559","title":"Fix auto-draft nested API parsing","type":"solution","tags":["major-domo","python","fix","draft","pydantic"],"importance":0.7,"decay":0.08},{"id":"d8f456f0","title":"Auto-draft posts to result channel","type":"solution","tags":["major-domo","python","draft","discord"],"importance":0.5,"decay":0.06},{"id":"27a581bc","title":"Draft pause API support","type":"solution","tags":["major-domo","database","fastapi","draft-system","feature"],"importance":0.6,"decay":0.07},{"id":"73c88b54","title":"Draft pause/resume feature","type":"solution","tags":["major-domo","discord-bot","draft-system","feature"],"importance":0.6,"decay":0.07},{"id":"147abb39","title":"Draft embed sheet links","type":"solution","tags":["major-domo","discord-bot","draft-system","ui"],"importance":0.5,"decay":0.06},{"id":"2850ff71","title":"Discord bot v2.24.0 release","type":"solution","tags":["major-domo","discord-bot","release","docker"],"importance":0.7,"decay":0.08},{"id":"827908bf","title":"Draft sheet batch write optimization","type":"solution","tags":["major-domo","python","google-sheets","optimization","fix"],"importance":0.6,"decay":0.07},{"id":"f51cae70","title":"sWAR precision fix and draft team role pings","type":"solution","tags":["major-domo","discord-app-v2","swar","draft","commit"],"importance":0.6,"decay":0.07},{"id":"becb5ba4","title":"Draft on-clock announcement after picks","type":"solution","tags":["major-domo","python","discord","draft","feature"],"importance":0.5,"decay":0.06},{"id":"90f27d04","title":"Fix draft cap validation max_zeroes logic","type":"solution","tags":["major-domo","python","fix","draft","cap-space"],"importance":0.7,"decay":0.08},{"id":"15a8cda0","title":"Fix custom command creator POST validation","type":"solution","tags":["major-domo","database","python","fastapi","pydantic","fix"],"importance":0.6,"decay":0.07},{"id":"dba267e7","title":"Injury log posting + view auth fix","type":"solution","tags":["major-domo","discord","python","fix","feature"],"importance":0.7,"decay":0.1},{"id":"226792fc","title":"Pending transaction validation for /dropadd","type":"solution","tags":["major-domo","python","discord","fix","transaction"],"importance":0.6,"decay":0.09},{"id":"d1c857db","title":"Fix week rollover 60x spam bug","type":"solution","tags":["major-domo","python","fix","transaction-freeze","discord-bot"],"importance":0.8,"decay":0.12},{"id":"7773f9ff","title":"Fix injury commands player search endpoint","type":"solution","tags":["major-domo","python","fix","discord-bot","api"],"importance":0.6,"decay":0.11},{"id":"4207f988","title":"sWAR cap validation for transaction builder","type":"solution","tags":["major-domo","python","fix","transaction-builder","swar"],"importance":0.7,"decay":0.17},{"id":"2b013e1b","title":"Fixed standings wildcard calculation showing 99 for all teams","type":"solution","tags":["major-domo","standings","wildcard","fix","data-fix"],"importance":0.8,"decay":0.19},{"id":"51d21e5e","title":"Loaded dice dev command for testing","type":"solution","tags":["major-domo","discord","python","feature"],"importance":0.5,"decay":0.13},{"id":"d2e4a41a","title":"Fixed weekly freeze/thaw automation - use_query_params=True","type":"solution","tags":["major-domo","discord-bot","fix","freeze-thaw","transaction","api","python"],"importance":0.9,"decay":0.27},{"id":"5be0ef6b","title":"Fixed frozen flag bug and added thaw report","type":"solution","tags":["major-domo","python","discord","fix","feature"],"importance":0.6,"decay":0.22},{"id":"a4efacad","title":"Shell aliases not available in Claude Code Bash tool","type":"solution","tags":["claude-code","bash","aliases","major-domo","cli"],"importance":0.7,"decay":0.28},{"id":"4728ce74","title":"Discord followup.send() doesn't trigger mention notifications","type":"solution","tags":["major-domo","discord.py","fix","mentions"],"importance":0.7,"decay":0.33},{"id":"8c7c7977","title":"Custom command delete permission check fix","type":"solution","tags":["major-domo","discord-bot","fix","custom-commands"],"importance":0.6,"decay":0.28},{"id":"4ede959f","title":"Clear confirmation content on edit_original_response","type":"solution","tags":["major-domo","discord-bot","fix","ui"],"importance":0.5,"decay":0.23},{"id":"69415fc4","title":"Implemented dem_week parameter for player team updates","type":"solution","tags":["major-domo","python","discord-bot","fix","feature","database","api"],"importance":0.7,"decay":0.37},{"id":"8bd768ce","title":"Discord bot dynamic configuration with live reload","type":"solution","tags":["discord-bot","python","discord.py","live-reload","configuration","pattern","claude-coordinator"],"importance":0.7,"decay":0.53},{"id":"de738797","title":"Major Domo CLI modular refactor with 6 new command modules","type":"solution","tags":["major-domo","cli","architecture","refactoring"],"importance":0.8,"decay":0.66},{"id":"b51ca30a","title":"Draft pick service API parameter fix","type":"fix","tags":["major-domo","python","fix","api","draft"],"importance":0.5,"decay":0.04},{"id":"4e7066fc","title":"Standardized sWAR display to 2 decimals","type":"fix","tags":["major-domo","python","formatting","swar","fix"],"importance":0.5,"decay":0.04},{"id":"7888f693","title":"Fix Player model validation in draft pick test","type":"fix","tags":["major-domo","python","test-fix","pydantic"],"importance":0.4,"decay":0.04},{"id":"83dbebd0","title":"DraftList nested Player.team_id extraction bug","type":"fix","tags":["major-domo","python","fix","draft","pydantic","nested-objects"],"importance":0.7,"decay":0.06},{"id":"ed668fcc","title":"sWAR formatting changed from 1 to 2 decimal places","type":"fix","tags":["major-domo","discord-app-v2","swar","formatting","fix"],"importance":0.5,"decay":0.05},{"id":"9fa127bc","title":"Draft monitor now pings team role instead of GM","type":"fix","tags":["major-domo","discord-app-v2","draft","ping","role","fix"],"importance":0.5,"decay":0.05},{"id":"f7824582","title":"Draft monitor missing guild variable","type":"fix","tags":["major-domo","python","fix","discord","draft"],"importance":0.6,"decay":0.06},{"id":"d32832d7","title":"Discord autocomplete sends display text not value","type":"fix","tags":["major-domo","python","discord","autocomplete","fix"],"importance":0.6,"decay":0.06},{"id":"bf9b2bc3","title":"Draft results post to result_channel fix","type":"fix","tags":["major-domo","discord-bot","draft","fix"],"importance":0.6,"decay":0.06},{"id":"dccfe52e","title":"Draft recent picks off-by-one error","type":"fix","tags":["major-domo","discord-bot","draft","fix","off-by-one"],"importance":0.6,"decay":0.06},{"id":"baefb777","title":"Transaction freeze bypass bug fix","type":"fix","tags":["major-domo","python","discord","fix","transactions"],"importance":0.6,"decay":0.06},{"id":"1aa4308c","title":"IL moves bypass transaction freeze for log posting","type":"fix","tags":["major-domo","python","fix","transactions","freeze"],"importance":0.6,"decay":0.07},{"id":"adaf8114","title":"Restrict injury logging to team GMs","type":"fix","tags":["major-domo","discord","python","security","fix"],"importance":0.6,"decay":0.07},{"id":"bde32b12","title":"Transaction API week_start parameter","type":"fix","tags":["major-domo","python","api","fix","transaction"],"importance":0.6,"decay":0.08},{"id":"ef15149d","title":"Custom command delete UI showed success but didn't delete","type":"fix","tags":["major-domo","discord.py","fix","bug"],"importance":0.6,"decay":0.23},{"id":"3ec7209e","title":"API delete endpoint used wrong dict key","type":"fix","tags":["major-domo","database","api","fix"],"importance":0.5,"decay":0.2},{"id":"4af66093","title":"Fixed thaw report channel lookup","type":"fix","tags":["major-domo","discord-bot","transaction-freeze","bugfix","config"],"importance":0.6,"decay":0.39},{"id":"ca0a8939","title":"[major-domo] Session: commit, config, debugging, deployment","type":"fix","tags":["major-domo","commit","configuration","debugging","deployment","fix","tooling","session-log"],"importance":0.7,"decay":0.47},{"id":"c1e9d0ea","title":"[major-domo] Session: commit, creation, debugging, deployment","type":"fix","tags":["major-domo","commit","creation","debugging","deployment","fix","testing","tooling","session-log"],"importance":0.7,"decay":0.47},{"id":"eb9818c3","title":"[major-domo] CLI modular refactor + 6 new command modules","type":"fix","tags":["major-domo","automation","commit","creation","debugging","feature","fix","refactoring","tooling","session-log"],"importance":0.6,"decay":0.41},{"id":"af8b63f8","title":"[major-domo] Session: creation, debugging, deployment, feature, fix","type":"fix","tags":["major-domo","creation","debugging","deployment","feature","fix","session-log"],"importance":0.7,"decay":0.5},{"id":"ce2a22a6","title":"[major-domo] fix: Gitea API for tag creation","type":"fix","tags":["major-domo","commit","configuration","creation","debugging","deployment","fix","session-log"],"importance":0.7,"decay":0.5},{"id":"425a60f6","title":"[major-domo] fix: Gitea API tag creation instead of git push","type":"fix","tags":["major-domo","commit","creation","debugging","fix","session-log"],"importance":0.6,"decay":0.43},{"id":"b1eeb082","title":"[major-domo] Session: automation, config, debugging, deployment","type":"fix","tags":["major-domo","automation","configuration","debugging","deployment","feature","fix","tooling","session-log"],"importance":0.7,"decay":0.52},{"id":"ed9e1e96","title":"Salary cap refactor plan created","type":"decision","tags":["major-domo","python","refactor","salary-cap"],"importance":0.5,"decay":0.06},{"id":"9ba78799","title":"CLAUDE.md documentation maintenance","type":"decision","tags":["major-domo","documentation","claude-md","maintenance"],"importance":0.4,"decay":0.05},{"id":"0c487d17","title":"Major Domo Production Bot Location","type":"configuration","tags":["major-domo","production","akamai","docker","discord-bot"],"importance":0.9,"decay":0.09},{"id":"8c26aa9a","title":"Major Domo production deployment info","type":"configuration","tags":["major-domo","docker","deployment","akamai"],"importance":0.7,"decay":0.24},{"id":"d0e2e9ab","title":"Discord bot systemd service with environment variables","type":"configuration","tags":["discord-bot","systemd","deployment","claude-cli","configuration","claude-coordinator"],"importance":0.7,"decay":0.49},{"id":"bfee3e27","title":"major-domo-v2 main branch is protected on Gitea","type":"configuration","tags":["major-domo","gitea","config","git"],"importance":0.6,"decay":0.42},{"id":"9e9497ba","title":"SBA Database API - Server & Deployment Details","type":"configuration","tags":["major-domo","deployment","infrastructure"],"importance":0.9,"decay":0.7},{"id":"a49eac33","title":"Subdirectory CLAUDE.md files for major-domo-database","type":"configuration","tags":["major-domo","claude-config","documentation","optimization"],"importance":0.5,"decay":0.39},{"id":"82a1387d","title":"Major Domo Draft Module Review - S13 Prep","type":"workflow","tags":["major-domo","draft","discord-bot","reference"],"importance":0.7,"decay":0.06},{"id":"a8820359","title":"Deploy Major Domo with feature branch workflow","type":"workflow","tags":["major-domo","deployment","workflow","docker","git","production"],"importance":0.6,"decay":0.27},{"id":"81d3eb1f","title":"Comprehensive unit test coverage pattern","type":"code_pattern","tags":["major-domo","python","testing","patterns"],"importance":0.6,"decay":0.06},{"id":"ffdfef93","title":"Production crash: Missing Optional import in type hint","type":"error","tags":["major-domo","python","production","error","deployment","type-hints","imports"],"importance":0.8,"decay":0.28},{"id":"0d5b864a","title":"Critical: Wrong API parameter name - dem_week vs demotion_week","type":"error","tags":["major-domo","python","api","parameter-naming","production","critical"],"importance":0.9,"decay":0.32},{"id":"ec25c1ae","title":"[major-domo] fix: ContextualLogger exc_info=True crash","type":"fix","tags":["major-domo","automation","commit","creation","debugging","deployment","fix","testing","tooling","session-log"],"importance":0.7,"decay":0.53},{"id":"5786f964","title":"Deploy script for Discord Bot v2 production deploys","type":"solution","tags":["major-domo","deployment","bash","automation","discord-bot","akamai"],"importance":0.7,"decay":0.64},{"id":"1eb7759d","title":"Fix: tea CLI requires explicit --repo flag","type":"fix","tags":["major-domo","gitea","tea-cli","fix"],"importance":0.4,"decay":0.3},{"id":"148f15d3","title":"next-release branch pattern for batching changes","type":"workflow","tags":["major-domo","git","workflow","deployment","ci-cd","branching"],"importance":0.5,"decay":0.38},{"id":"12d0b655","title":"[major-domo] feat: add local deploy script","type":"fix","tags":["major-domo","automation","commit","creation","debugging","deployment","fix","testing","tooling","session-log"],"importance":0.7,"decay":0.54},{"id":"04acc924","title":"Fix: 7 security issues in major-domo-v2","type":"fix","tags":["major-domo","security","discord-bot","fix","python","gitea"],"importance":0.8,"decay":0.62},{"id":"29c1f2c9","title":"Gitea issue management: comment before closing","type":"procedure","tags":["major-domo","gitea","workflow","git","issues"],"importance":0.6,"decay":0.65},{"id":"455da58f","title":"next-release branch accumulates changes before main merge","type":"decision","tags":["major-domo","git","workflow","decision","ci-cd","release"],"importance":0.65,"decay":0.65},{"id":"1a1784e4","title":"[major-domo] fix: address 7 security issues","type":"fix","tags":["major-domo","automation","commit","configuration","creation","debugging","deployment","feature","fix","tooling","session-log"],"importance":0.7,"decay":0.54},{"id":"ed8f5008","title":"[major-domo] fix: batch quick-wins (closes #37, #27, #25, #38)","type":"fix","tags":["major-domo","commit","configuration","creation","debugging","deployment","feature","fix","refactoring","testing","tooling","session-log"],"importance":0.7,"decay":0.54},{"id":"fbc3a7f4","title":"Fix: ScorecardTracker stale data - load_data() on every read","type":"fix","tags":["major-domo","scorebug","scorecard-tracker","fix","stale-data","discord-bot"],"importance":0.7,"decay":0.54},{"id":"afbc470d","title":"Fix: Win percentage parsing robustness","type":"fix","tags":["major-domo","scorebug","win-probability","fix","parsing","discord-bot"],"importance":0.65,"decay":0.51},{"id":"4c2c50d7","title":"Fix: Win percentage orientation bug","type":"fix","tags":["major-domo","scorebug","win-probability","fix","orientation","google-sheets","discord-bot"],"importance":0.75,"decay":0.58},{"id":"543706bd","title":"Fix: publish-scorecard error embed cleanup","type":"fix","tags":["major-domo","scorebug","ux","fix","embed-template","discord-bot"],"importance":0.55,"decay":0.43},{"id":"0960205f","title":"Fix: Scorebug tracker read-failure tolerance","type":"fix","tags":["major-domo","scorebug","live-scorebug-tracker","resilience","google-sheets","discord","fix"],"importance":0.7,"decay":0.54},{"id":"8ce81bc9","title":"Fix: Scorebug win probability orientation","type":"fix","tags":["major-domo","scorebug","win-probability","google-sheets","fix","data-orientation"],"importance":0.7,"decay":0.54},{"id":"fb33558f","title":"Voice channel cleanup auto-unpublishes scorecards","type":"insight","tags":["major-domo","scorebug","voice-cleanup","scorecard-tracker","discord","insight"],"importance":0.55,"decay":0.53},{"id":"4a8de106","title":"Gitea auto-close only on direct merge to default branch","type":"decision","tags":["gitea","ci-cd","release-workflow","issues","next-release","major-domo","decision"],"importance":0.65,"decay":0.66},{"id":"c9577d44","title":"[major-domo] fix: scorebug stale data, win probability","type":"fix","tags":["major-domo","automation","commit","configuration","creation","debugging","deployment","feature","fix","testing","tooling","session-log"],"importance":0.7,"decay":0.55},{"id":"7ab7f240","title":"[major-domo] Session: fix, tooling","type":"fix","tags":["major-domo","fix","tooling","session-log"],"importance":0.4,"decay":0.32},{"id":"125cef15","title":"[major-domo] Session: config, creation, deployment, feature","type":"fix","tags":["major-domo","configuration","creation","deployment","feature","fix","tooling","session-log"],"importance":0.7,"decay":0.58},{"id":"4579c929","title":"Fix: UTC/CST timezone ambiguity in freeze/thaw scheduling","type":"fix","tags":["major-domo","timezone","transaction-freeze","production-fix","discord-bot","python","fix"],"importance":0.75,"decay":0.62},{"id":"c0239088","title":"Rebase feature branch onto next-release before PR","type":"procedure","tags":["major-domo","git-workflow","next-release","rebase","discord-bot","procedure"],"importance":0.6,"decay":0.7},{"id":"5adcad70","title":"Deploy script for discord-app-v2 usage and pre-flight","type":"configuration","tags":["major-domo","deployment","discord-bot","akamai","docker","configuration"],"importance":0.6,"decay":0.55},{"id":"6d00a9cb","title":"[major-domo] fix: explicit America/Chicago timezone","type":"fix","tags":["major-domo","automation","commit","creation","debugging","deployment","feature","fix","testing","tooling","session-log"],"importance":0.7,"decay":0.59},{"id":"22c77d0d","title":"[major-domo] Session: config, creation, debugging, deployment","type":"fix","tags":["major-domo","configuration","creation","debugging","deployment","feature","fix","tooling","session-log"],"importance":0.7,"decay":0.59},{"id":"47156f29","title":"[major-domo] Fix skill description budget overflow","type":"fix","tags":["major-domo","automation","commit","configuration","debugging","deployment","fix","tooling","session-log"],"importance":0.7,"decay":0.35},{"id":"7eea6748","title":"[major-domo] fix: roster validation org affiliate transactions","type":"fix","tags":["major-domo","automation","commit","creation","deployment","feature","fix","testing","session-log"],"importance":0.7,"decay":0.35},{"id":"48e397e9","title":"Athletics team name alias fix","type":"solution","tags":["paper-dynasty","python","fix","discord-bot"],"importance":0.5,"decay":0.11},{"id":"5d8e1ff5","title":"Fix for play lock never released on exception","type":"solution","tags":["paper-dynasty","python","discord-bot","sqlalchemy","fix","play-lock","concurrency","critical"],"importance":0.95,"decay":0.54},{"id":"1c795804","title":"Circular import fix: move shared utilities to standalone module","type":"solution","tags":["python","circular-import","architecture","fix","paper-dynasty","discord-bot"],"importance":0.85,"decay":0.49},{"id":"40da57ca","title":"Position validation missing in lineup sheet loading","type":"solution","tags":["paper-dynasty","python","fix","discord-bot","position-validation"],"importance":0.7,"decay":0.44},{"id":"b9f0edd4","title":"Fix pack type grouping in packs display","type":"fix","tags":["paper-dynasty","python","discord-bot","fix"],"importance":0.5,"decay":0.11},{"id":"638ac861","title":"Production deployment checklist for Paper Dynasty bot","type":"decision","tags":["paper-dynasty","discord-bot","deployment","production","checklist","devops"],"importance":0.8,"decay":0.5},{"id":"18d507ca","title":"CalVer versioning for all Major Domo and Paper Dynasty services","type":"decision","tags":["major-domo","paper-dynasty","ci-cd","gitea","docker","calver"],"importance":0.8,"decay":0.74},{"id":"711ea568","title":"Paper Dynasty bot freeze from duplicate X-Check submissions","type":"problem","tags":["paper-dynasty","discord-bot","sqlalchemy","race-condition","x-check","concurrency","database","deadlock"],"importance":0.9,"decay":0.37},{"id":"9b70e3d5","title":"Play lock never released on exception - permanent user lockout","type":"problem","tags":["paper-dynasty","python","discord-bot","sqlalchemy","concurrency","critical-bug","play-lock","database"],"importance":0.95,"decay":0.41},{"id":"c253c9de","title":"CRITICAL: Git commit requires explicit user approval","type":"workflow","tags":["paper-dynasty","major-domo","git","commit","workflow","approval-required","critical"],"importance":0.9,"decay":0.43},{"id":"88bbf5f1","title":"Optional locking parameter pattern for read vs write commands","type":"code_pattern","tags":["paper-dynasty","python","discord-bot","architecture","locking","concurrency","pattern"],"importance":0.75,"decay":0.39},{"id":"d3f80a8b","title":"Bulk codebase audit and Gitea issue creation with parallel agents","type":"procedure","tags":["gitea","tea-cli","code-review","automation","major-domo","paper-dynasty","claude-code","agents"],"importance":0.7,"decay":0.75},{"id":"803b3f29","title":"Cross-cutting code quality anti-patterns in repos","type":"insight","tags":["major-domo","paper-dynasty","code-quality","security","patterns","homelab"],"importance":0.6,"decay":0.57},{"id":"dfa75d94","title":"Scope guards added to major-domo, paper-dynasty, proxmox skills","type":"configuration","tags":["claude-code","skills","scope-guards","configuration","claude-code-config","major-domo","paper-dynasty","proxmox"],"importance":0.6,"decay":0.66}],"edges":[{"source":"fe6307d0","target":"0dd72eed","type":"RELATED_TO","strength":0.8},{"source":"fe6307d0","target":"0dd72eed","type":"CAUSES","strength":0.9},{"source":"83dbebd0","target":"7c146559","type":"BUILDS_ON","strength":0.85},{"source":"ed9e1e96","target":"e5bc69ae","type":"BUILDS_ON","strength":0.9},{"source":"f7824582","target":"e4f0bee9","type":"BUILDS_ON","strength":0.85},{"source":"7888f693","target":"c29d2fb4","type":"BUILDS_ON","strength":0.85}]};
|
|
|
|
// ── Color scheme ──
|
|
const TYPE_COLORS = {
|
|
fix: '#f85149', solution: '#58a6ff', decision: '#d2a8ff', configuration: '#3fb950',
|
|
workflow: '#f0883e', code_pattern: '#79c0ff', error: '#ff7b72', insight: '#d29922',
|
|
problem: '#f97583', procedure: '#a5d6ff', general: '#8b949e', unknown: '#484f58',
|
|
};
|
|
|
|
// ── State ──
|
|
const state = {
|
|
activeTypes: new Set(Object.keys(TYPE_COLORS)),
|
|
activeTag: null,
|
|
searchQuery: '',
|
|
view: 'force',
|
|
selected: null,
|
|
zoom: 1, panX: 0, panY: 0,
|
|
};
|
|
|
|
// ── Canvas setup ──
|
|
const canvas = document.getElementById('graph');
|
|
const ctx = canvas.getContext('2d');
|
|
let W, H, dpr;
|
|
function resize() {
|
|
const r = canvas.parentElement.getBoundingClientRect();
|
|
dpr = window.devicePixelRatio || 1;
|
|
W = r.width; H = r.height;
|
|
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
}
|
|
window.addEventListener('resize', () => { resize(); render(); });
|
|
resize();
|
|
|
|
// ── Build node positions ──
|
|
const nodes = DATA.nodes.map((n, i) => ({
|
|
...n,
|
|
x: W/2 + (Math.random() - 0.5) * W * 0.7,
|
|
y: H/2 + (Math.random() - 0.5) * H * 0.7,
|
|
vx: 0, vy: 0,
|
|
radius: 4 + n.importance * 12,
|
|
visible: true,
|
|
}));
|
|
const nodeMap = Object.fromEntries(nodes.map(n => [n.id, n]));
|
|
const edges = DATA.edges.filter(e => nodeMap[e.source] && nodeMap[e.target]);
|
|
|
|
// ── Tag counting ──
|
|
const tagCounts = {};
|
|
nodes.forEach(n => n.tags.forEach(t => { tagCounts[t] = (tagCounts[t] || 0) + 1; }));
|
|
const sortedTags = Object.entries(tagCounts)
|
|
.filter(([t]) => t !== 'major-domo' && t !== 'fix' && t !== 'session-log')
|
|
.sort((a, b) => b[1] - a[1]);
|
|
|
|
// ── Type counting ──
|
|
const typeCounts = {};
|
|
nodes.forEach(n => { typeCounts[n.type] = (typeCounts[n.type] || 0) + 1; });
|
|
|
|
// ── Build UI ──
|
|
function buildTypeFilters() {
|
|
const el = document.getElementById('typeFilters');
|
|
el.innerHTML = '';
|
|
Object.entries(typeCounts).sort((a,b) => b[1]-a[1]).forEach(([type, count]) => {
|
|
const chip = document.createElement('div');
|
|
chip.className = 'type-chip active';
|
|
chip.innerHTML = `<span class="dot" style="background:${TYPE_COLORS[type] || '#484f58'}"></span>${type}<span class="count">${count}</span>`;
|
|
chip.addEventListener('click', () => {
|
|
if (state.activeTypes.has(type)) { state.activeTypes.delete(type); chip.classList.remove('active'); chip.classList.add('inactive'); }
|
|
else { state.activeTypes.add(type); chip.classList.add('active'); chip.classList.remove('inactive'); }
|
|
updateVisibility(); render();
|
|
});
|
|
el.appendChild(chip);
|
|
});
|
|
}
|
|
|
|
function buildTagList() {
|
|
const el = document.getElementById('tagList');
|
|
document.getElementById('tagCount').textContent = `(${sortedTags.length})`;
|
|
el.innerHTML = '';
|
|
sortedTags.slice(0, 60).forEach(([tag, count]) => {
|
|
const chip = document.createElement('div');
|
|
chip.className = 'tag-chip';
|
|
chip.textContent = `${tag} (${count})`;
|
|
chip.addEventListener('click', () => {
|
|
if (state.activeTag === tag) { state.activeTag = null; chip.classList.remove('active'); }
|
|
else {
|
|
el.querySelectorAll('.tag-chip').forEach(c => c.classList.remove('active'));
|
|
state.activeTag = tag; chip.classList.add('active');
|
|
}
|
|
updateVisibility(); render();
|
|
});
|
|
el.appendChild(chip);
|
|
});
|
|
}
|
|
|
|
buildTypeFilters();
|
|
buildTagList();
|
|
|
|
// ── Visibility ──
|
|
function updateVisibility() {
|
|
const q = state.searchQuery.toLowerCase();
|
|
let vis = 0;
|
|
nodes.forEach(n => {
|
|
n.visible = state.activeTypes.has(n.type);
|
|
if (n.visible && state.activeTag) n.visible = n.tags.includes(state.activeTag);
|
|
if (n.visible && q) n.visible = n.title.toLowerCase().includes(q);
|
|
if (n.visible) vis++;
|
|
});
|
|
document.getElementById('statVisible').textContent = vis;
|
|
}
|
|
|
|
document.getElementById('search').addEventListener('input', e => {
|
|
state.searchQuery = e.target.value;
|
|
updateVisibility(); render();
|
|
});
|
|
|
|
// ── Stats ──
|
|
document.getElementById('statTotal').textContent = nodes.length;
|
|
document.getElementById('statVisible').textContent = nodes.length;
|
|
document.getElementById('statEdges').textContent = edges.length;
|
|
|
|
// ── Layout engines ──
|
|
function layoutForce() {
|
|
for (let iter = 0; iter < 200; iter++) {
|
|
// Repulsion
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
for (let j = i + 1; j < nodes.length; j++) {
|
|
let dx = nodes[j].x - nodes[i].x;
|
|
let dy = nodes[j].y - nodes[i].y;
|
|
let d = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
let force = 800 / (d * d);
|
|
nodes[i].vx -= dx/d * force;
|
|
nodes[i].vy -= dy/d * force;
|
|
nodes[j].vx += dx/d * force;
|
|
nodes[j].vy += dy/d * force;
|
|
}
|
|
}
|
|
// Edge attraction
|
|
edges.forEach(e => {
|
|
const s = nodeMap[e.source], t = nodeMap[e.target];
|
|
if (!s || !t) return;
|
|
let dx = t.x - s.x, dy = t.y - s.y;
|
|
let d = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
let force = (d - 80) * 0.01;
|
|
s.vx += dx/d * force; s.vy += dy/d * force;
|
|
t.vx -= dx/d * force; t.vy -= dy/d * force;
|
|
});
|
|
// Center gravity
|
|
nodes.forEach(n => {
|
|
n.vx += (W/2 - n.x) * 0.001;
|
|
n.vy += (H/2 - n.y) * 0.001;
|
|
n.x += n.vx * 0.3; n.y += n.vy * 0.3;
|
|
n.vx *= 0.85; n.vy *= 0.85;
|
|
});
|
|
}
|
|
}
|
|
|
|
function layoutCluster() {
|
|
const types = [...new Set(nodes.map(n => n.type))];
|
|
const cx = W / 2, cy = H / 2;
|
|
const clusterR = Math.min(W, H) * 0.32;
|
|
types.forEach((type, ti) => {
|
|
const angle = (ti / types.length) * Math.PI * 2 - Math.PI/2;
|
|
const tcx = cx + Math.cos(angle) * clusterR;
|
|
const tcy = cy + Math.sin(angle) * clusterR;
|
|
const members = nodes.filter(n => n.type === type);
|
|
const cols = Math.ceil(Math.sqrt(members.length));
|
|
members.forEach((n, i) => {
|
|
const row = Math.floor(i / cols), col = i % cols;
|
|
n.x = tcx + (col - cols/2) * 28;
|
|
n.y = tcy + (row - cols/2) * 28;
|
|
});
|
|
});
|
|
}
|
|
|
|
function layoutRadial() {
|
|
const cx = W / 2, cy = H / 2;
|
|
const types = [...new Set(nodes.map(n => n.type))];
|
|
let idx = 0;
|
|
types.forEach(type => {
|
|
const members = nodes.filter(n => n.type === type);
|
|
members.sort((a, b) => b.decay - a.decay);
|
|
members.forEach((n, i) => {
|
|
const angle = (idx / nodes.length) * Math.PI * 2 - Math.PI/2;
|
|
const r = 60 + (1 - n.decay) * Math.min(W, H) * 0.38;
|
|
n.x = cx + Math.cos(angle) * r;
|
|
n.y = cy + Math.sin(angle) * r;
|
|
idx++;
|
|
});
|
|
});
|
|
}
|
|
|
|
function applyLayout() {
|
|
if (state.view === 'force') layoutForce();
|
|
else if (state.view === 'cluster') layoutCluster();
|
|
else layoutRadial();
|
|
}
|
|
|
|
// ── View toggle ──
|
|
document.getElementById('viewToggle').addEventListener('click', e => {
|
|
const opt = e.target.closest('.view-opt');
|
|
if (!opt) return;
|
|
document.querySelectorAll('.view-opt').forEach(o => o.classList.remove('active'));
|
|
opt.classList.add('active');
|
|
state.view = opt.dataset.view;
|
|
// Re-randomize for force
|
|
if (state.view === 'force') {
|
|
nodes.forEach(n => { n.x = W/2 + (Math.random()-0.5)*W*0.7; n.y = H/2 + (Math.random()-0.5)*H*0.7; n.vx=0; n.vy=0; });
|
|
}
|
|
applyLayout();
|
|
fitView();
|
|
render();
|
|
});
|
|
|
|
// ── Rendering ──
|
|
function render() {
|
|
ctx.clearRect(0, 0, W, H);
|
|
ctx.save();
|
|
ctx.translate(state.panX, state.panY);
|
|
ctx.scale(state.zoom, state.zoom);
|
|
|
|
// Edges
|
|
edges.forEach(e => {
|
|
const s = nodeMap[e.source], t = nodeMap[e.target];
|
|
if (!s || !t || !s.visible || !t.visible) return;
|
|
ctx.beginPath();
|
|
ctx.moveTo(s.x, s.y);
|
|
ctx.lineTo(t.x, t.y);
|
|
ctx.strokeStyle = e.type === 'BUILDS_ON' ? '#3fb95088' : e.type === 'CAUSES' ? '#f8514988' : '#58a6ff55';
|
|
ctx.lineWidth = e.strength * 2.5;
|
|
ctx.stroke();
|
|
// Edge label
|
|
const mx = (s.x + t.x)/2, my = (s.y + t.y)/2;
|
|
ctx.font = '9px -apple-system, sans-serif';
|
|
ctx.fillStyle = '#8b949e';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(e.type, mx, my - 4);
|
|
});
|
|
|
|
// Nodes
|
|
nodes.forEach(n => {
|
|
if (!n.visible) return;
|
|
const alpha = 0.15 + n.decay * 0.85;
|
|
const color = TYPE_COLORS[n.type] || '#484f58';
|
|
const isSelected = state.selected === n.id;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
|
|
ctx.fillStyle = color + Math.round(alpha * 255).toString(16).padStart(2, '0');
|
|
ctx.fill();
|
|
|
|
if (isSelected) {
|
|
ctx.strokeStyle = '#f0f6fc';
|
|
ctx.lineWidth = 2.5;
|
|
ctx.stroke();
|
|
// Title label
|
|
ctx.font = 'bold 11px -apple-system, sans-serif';
|
|
ctx.fillStyle = '#f0f6fc';
|
|
ctx.textAlign = 'center';
|
|
const label = n.title.length > 50 ? n.title.slice(0, 47) + '...' : n.title;
|
|
ctx.fillText(label, n.x, n.y - n.radius - 8);
|
|
}
|
|
});
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// ── Interaction ──
|
|
let dragging = null, dragStart = null, isPanning = false;
|
|
|
|
function screenToWorld(sx, sy) {
|
|
return { x: (sx - state.panX) / state.zoom, y: (sy - state.panY) / state.zoom };
|
|
}
|
|
|
|
function hitTest(sx, sy) {
|
|
const {x, y} = screenToWorld(sx, sy);
|
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
const n = nodes[i];
|
|
if (!n.visible) continue;
|
|
const dx = n.x - x, dy = n.y - y;
|
|
if (dx*dx + dy*dy < (n.radius + 3) * (n.radius + 3)) return n;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
canvas.addEventListener('mousedown', e => {
|
|
const r = canvas.getBoundingClientRect();
|
|
const sx = e.clientX - r.left, sy = e.clientY - r.top;
|
|
const hit = hitTest(sx, sy);
|
|
if (hit) { dragging = hit; dragStart = {x: sx, y: sy, ox: hit.x, oy: hit.y}; }
|
|
else { isPanning = true; dragStart = {x: sx, y: sy, ox: state.panX, oy: state.panY}; }
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', e => {
|
|
const r = canvas.getBoundingClientRect();
|
|
const sx = e.clientX - r.left, sy = e.clientY - r.top;
|
|
if (dragging && dragStart) {
|
|
dragging.x = dragStart.ox + (sx - dragStart.x) / state.zoom;
|
|
dragging.y = dragStart.oy + (sy - dragStart.y) / state.zoom;
|
|
render();
|
|
} else if (isPanning && dragStart) {
|
|
state.panX = dragStart.ox + (sx - dragStart.x);
|
|
state.panY = dragStart.oy + (sy - dragStart.y);
|
|
render();
|
|
} else {
|
|
const hit = hitTest(sx, sy);
|
|
canvas.style.cursor = hit ? 'pointer' : 'grab';
|
|
if (hit) {
|
|
document.getElementById('hud').textContent = hit.title;
|
|
} else {
|
|
document.getElementById('hud').textContent = 'Click a node to inspect';
|
|
}
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', e => {
|
|
if (dragging && dragStart) {
|
|
const r = canvas.getBoundingClientRect();
|
|
const sx = e.clientX - r.left, sy = e.clientY - r.top;
|
|
const dist = Math.hypot(sx - dragStart.x, sy - dragStart.y);
|
|
if (dist < 5) selectNode(dragging);
|
|
} else if (!isPanning || (dragStart && Math.hypot(e.clientX - canvas.getBoundingClientRect().left - dragStart.x, e.clientY - canvas.getBoundingClientRect().top - dragStart.y) < 5)) {
|
|
selectNode(null);
|
|
}
|
|
dragging = null; isPanning = false; dragStart = null;
|
|
});
|
|
|
|
canvas.addEventListener('wheel', e => {
|
|
e.preventDefault();
|
|
const r = canvas.getBoundingClientRect();
|
|
const mx = e.clientX - r.left, my = e.clientY - r.top;
|
|
const oldZoom = state.zoom;
|
|
state.zoom *= e.deltaY > 0 ? 0.92 : 1.08;
|
|
state.zoom = Math.max(0.15, Math.min(5, state.zoom));
|
|
state.panX = mx - (mx - state.panX) * (state.zoom / oldZoom);
|
|
state.panY = my - (my - state.panY) * (state.zoom / oldZoom);
|
|
render();
|
|
}, { passive: false });
|
|
|
|
// ── Selection ──
|
|
function selectNode(n) {
|
|
state.selected = n ? n.id : null;
|
|
const panel = document.getElementById('detailContent');
|
|
const empty = document.getElementById('emptyDetail');
|
|
if (!n) { panel.style.display = 'none'; empty.style.display = 'block'; render(); return; }
|
|
panel.style.display = 'block'; empty.style.display = 'none';
|
|
document.getElementById('detailTitle').textContent = n.title;
|
|
const typeEl = document.getElementById('detailType');
|
|
typeEl.textContent = n.type;
|
|
typeEl.style.color = TYPE_COLORS[n.type];
|
|
document.getElementById('detailId').textContent = n.id;
|
|
document.getElementById('detailImportance').style.width = (n.importance * 100) + '%';
|
|
document.getElementById('detailImportanceVal').textContent = n.importance.toFixed(2);
|
|
const decayFill = document.getElementById('detailDecay');
|
|
decayFill.style.width = (n.decay * 100) + '%';
|
|
decayFill.style.background = n.decay > 0.5 ? '#3fb950' : n.decay > 0.2 ? '#d29922' : '#f85149';
|
|
document.getElementById('detailDecayVal').textContent = n.decay.toFixed(2) + (n.decay > 0.5 ? ' (active)' : n.decay > 0.2 ? ' (fading)' : ' (dormant)');
|
|
|
|
const tagsEl = document.getElementById('detailTags');
|
|
tagsEl.innerHTML = '';
|
|
n.tags.forEach(t => {
|
|
const tag = document.createElement('span');
|
|
tag.className = 'detail-tag';
|
|
tag.textContent = t;
|
|
tagsEl.appendChild(tag);
|
|
});
|
|
|
|
// Edges
|
|
const nodeEdges = edges.filter(e => e.source === n.id || e.target === n.id);
|
|
const edgesSection = document.getElementById('detailEdgesSection');
|
|
const edgesEl = document.getElementById('detailEdges');
|
|
if (nodeEdges.length) {
|
|
edgesSection.style.display = 'block';
|
|
edgesEl.innerHTML = '';
|
|
nodeEdges.forEach(e => {
|
|
const otherId = e.source === n.id ? e.target : e.source;
|
|
const other = nodeMap[otherId];
|
|
const div = document.createElement('div');
|
|
div.className = 'detail-edge';
|
|
const dir = e.source === n.id ? '\u2192' : '\u2190';
|
|
div.innerHTML = `<span class="detail-edge-type">${e.type}</span> ${dir} <span class="detail-edge-target">${other ? other.title : otherId}</span>`;
|
|
edgesEl.appendChild(div);
|
|
});
|
|
} else {
|
|
edgesSection.style.display = 'none';
|
|
}
|
|
render();
|
|
}
|
|
|
|
// ── Zoom buttons ──
|
|
document.getElementById('zoomIn').addEventListener('click', () => { state.zoom = Math.min(5, state.zoom * 1.3); render(); });
|
|
document.getElementById('zoomOut').addEventListener('click', () => { state.zoom = Math.max(0.15, state.zoom * 0.7); render(); });
|
|
|
|
function fitView() {
|
|
const visible = nodes.filter(n => n.visible);
|
|
if (!visible.length) return;
|
|
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
visible.forEach(n => { minX = Math.min(minX, n.x - n.radius); maxX = Math.max(maxX, n.x + n.radius); minY = Math.min(minY, n.y - n.radius); maxY = Math.max(maxY, n.y + n.radius); });
|
|
const gw = maxX - minX || 1, gh = maxY - minY || 1;
|
|
const pad = 40;
|
|
state.zoom = Math.min((W - pad*2) / gw, (H - pad*2) / gh, 2);
|
|
state.panX = W/2 - (minX + gw/2) * state.zoom;
|
|
state.panY = H/2 - (minY + gh/2) * state.zoom;
|
|
}
|
|
|
|
document.getElementById('zoomFit').addEventListener('click', () => { fitView(); render(); });
|
|
|
|
// ── Reset ──
|
|
document.getElementById('btnReset').addEventListener('click', () => {
|
|
state.activeTypes = new Set(Object.keys(TYPE_COLORS));
|
|
state.activeTag = null;
|
|
state.searchQuery = '';
|
|
state.selected = null;
|
|
document.getElementById('search').value = '';
|
|
document.querySelectorAll('.type-chip').forEach(c => { c.classList.add('active'); c.classList.remove('inactive'); });
|
|
document.querySelectorAll('.tag-chip').forEach(c => c.classList.remove('active'));
|
|
updateVisibility(); render();
|
|
});
|
|
|
|
document.getElementById('btnRecenter').addEventListener('click', () => { fitView(); render(); });
|
|
|
|
// ── Init ──
|
|
applyLayout();
|
|
fitView();
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|