- .claude/agents/test-runner.md: New Claude Code agent definition for running cargo test suites and analyzing results, configured with haiku model for fast execution. - skills/agent-swarm-launcher/: New skill for bootstrapping coordinated multi-agent workflows with AGENTS.md reconnaissance, Agent Mail coordination, and beads task tracking. - api-review.html, phase-a-review.html: Self-contained HTML review artifacts for API audit and Phase A search pipeline review. - .beads/issues.jsonl, .beads/last-touched: Updated issue tracker state reflecting current project work items. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1261 lines
62 KiB
HTML
1261 lines
62 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Phase A: Complete API Field Capture — Review</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--surface2: #1c2129;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-dim: #8b949e;
|
|
--accent: #58a6ff;
|
|
--green: #3fb950;
|
|
--green-bg: #0d2818;
|
|
--yellow: #d29922;
|
|
--yellow-bg: #2d2000;
|
|
--red: #f85149;
|
|
--red-bg: #3d1418;
|
|
--purple: #bc8cff;
|
|
--purple-bg: #1e1534;
|
|
--blue-bg: #0c2d6b;
|
|
--orange: #db6d28;
|
|
--cyan: #39d2c0;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 20px 32px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
.header h1 { font-size: 20px; font-weight: 600; }
|
|
.header .subtitle { color: var(--text-dim); font-size: 13px; margin-top: 2px; }
|
|
.header .badges { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
|
|
.badge {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500;
|
|
}
|
|
.badge-green { background: var(--green-bg); color: var(--green); border: 1px solid #1a4d2e; }
|
|
.badge-yellow { background: var(--yellow-bg); color: var(--yellow); border: 1px solid #4d3800; }
|
|
.badge-purple { background: var(--purple-bg); color: var(--purple); border: 1px solid #3b2a5e; }
|
|
.badge-blue { background: var(--blue-bg); color: var(--accent); border: 1px solid #1a4480; }
|
|
|
|
/* Tabs */
|
|
.tab-bar {
|
|
display: flex;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 32px;
|
|
overflow-x: auto;
|
|
position: sticky;
|
|
top: 72px;
|
|
z-index: 99;
|
|
}
|
|
.tab {
|
|
padding: 10px 16px;
|
|
font-size: 13px;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
white-space: nowrap;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
user-select: none;
|
|
}
|
|
.tab:hover { color: var(--text); }
|
|
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
.tab .count {
|
|
background: var(--surface2);
|
|
color: var(--text-dim);
|
|
font-size: 11px;
|
|
padding: 1px 6px;
|
|
border-radius: 8px;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
/* Content */
|
|
.content { max-width: 1100px; margin: 0 auto; padding: 24px 32px 80px; }
|
|
.tab-content { display: none; }
|
|
.tab-content.active { display: block; }
|
|
|
|
/* Cards */
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
margin-bottom: 16px;
|
|
overflow: hidden;
|
|
}
|
|
.card-header {
|
|
padding: 14px 18px;
|
|
border-bottom: 1px solid var(--border);
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.card-body { padding: 18px; }
|
|
|
|
/* Principle banner */
|
|
.principle {
|
|
background: var(--blue-bg);
|
|
border: 1px solid #1a4480;
|
|
border-radius: 8px;
|
|
padding: 16px 20px;
|
|
margin-bottom: 20px;
|
|
font-size: 14px;
|
|
}
|
|
.principle strong { color: var(--accent); }
|
|
|
|
/* Scope cards */
|
|
.scope-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
.scope-item {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 14px;
|
|
}
|
|
.scope-item .num { font-size: 28px; font-weight: 700; color: var(--accent); }
|
|
.scope-item .label { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
|
|
|
|
/* Exclusion cards */
|
|
.exclusion {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 14px 18px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.exclusion .field-name { font-weight: 600; color: var(--red); font-size: 14px; }
|
|
.exclusion .rationale { color: var(--text-dim); font-size: 13px; margin-top: 4px; }
|
|
.exclusion .note { color: var(--yellow); font-size: 12px; margin-top: 6px; font-style: italic; }
|
|
|
|
/* Tables */
|
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
th {
|
|
text-align: left;
|
|
padding: 10px 14px;
|
|
background: var(--surface2);
|
|
color: var(--text-dim);
|
|
font-weight: 600;
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 1px solid var(--border);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1;
|
|
}
|
|
td {
|
|
padding: 8px 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
vertical-align: top;
|
|
}
|
|
tr:hover td { background: rgba(88,166,255,0.04); }
|
|
code {
|
|
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
font-size: 12px;
|
|
background: var(--surface2);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
color: var(--cyan);
|
|
}
|
|
td code { background: transparent; padding: 0; }
|
|
|
|
/* Status dots */
|
|
.dot {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
margin-right: 6px;
|
|
position: relative;
|
|
top: -1px;
|
|
}
|
|
.dot-green { background: var(--green); }
|
|
.dot-yellow { background: var(--yellow); }
|
|
.dot-purple { background: var(--purple); }
|
|
.dot-red { background: var(--red); }
|
|
|
|
/* Category badges in tables */
|
|
.cat {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
.cat-meta { background: #1a2740; color: #79b8ff; }
|
|
.cat-engagement { background: #2a1e00; color: #f0b832; }
|
|
.cat-time { background: #1e2d1e; color: #7ee787; }
|
|
.cat-task { background: #2d1e3a; color: #d2a8ff; }
|
|
.cat-ref { background: #1a3040; color: #56d4dd; }
|
|
.cat-tracking { background: #2d1a1a; color: #ff9492; }
|
|
.cat-premium { background: #2a1a3d; color: #bc8cff; }
|
|
.cat-merge { background: #0d2818; color: #3fb950; }
|
|
.cat-fork { background: #1e2d1e; color: #7ee787; }
|
|
.cat-branch { background: #2d2700; color: #e3b341; }
|
|
.cat-schedule { background: #1a2740; color: #79b8ff; }
|
|
.cat-import { background: #2d1a1a; color: #ff9492; }
|
|
.cat-author { background: #1a3040; color: #56d4dd; }
|
|
|
|
/* Filter bar */
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 12px 0;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
.filter-btn {
|
|
padding: 4px 12px;
|
|
border-radius: 14px;
|
|
font-size: 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
user-select: none;
|
|
}
|
|
.filter-btn:hover { border-color: var(--accent); color: var(--text); }
|
|
.filter-btn.active { background: var(--blue-bg); border-color: var(--accent); color: var(--accent); }
|
|
.search-input {
|
|
flex: 1;
|
|
min-width: 180px;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface2);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
.search-input:focus { border-color: var(--accent); }
|
|
.search-input::placeholder { color: var(--text-dim); }
|
|
|
|
/* SQL block */
|
|
.sql-block {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 18px;
|
|
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
font-size: 12.5px;
|
|
line-height: 1.7;
|
|
overflow-x: auto;
|
|
white-space: pre;
|
|
color: var(--text);
|
|
max-height: 600px;
|
|
overflow-y: auto;
|
|
}
|
|
.sql-comment { color: var(--text-dim); }
|
|
.sql-keyword { color: var(--accent); font-weight: 600; }
|
|
.sql-type { color: var(--green); }
|
|
.sql-default { color: var(--yellow); }
|
|
.sql-divider { color: var(--text-dim); }
|
|
.sql-line { display: block; padding: 0 4px; }
|
|
.sql-line:hover { background: rgba(88,166,255,0.06); }
|
|
|
|
/* Struct block */
|
|
.struct-block {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 18px;
|
|
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
font-size: 12.5px;
|
|
line-height: 1.8;
|
|
overflow-x: auto;
|
|
}
|
|
.struct-field { color: var(--text); }
|
|
.struct-type { color: var(--green); }
|
|
.struct-comment { color: var(--text-dim); }
|
|
.struct-line { display: block; padding: 0 4px; }
|
|
.struct-line:hover { background: rgba(88,166,255,0.06); }
|
|
|
|
/* File list */
|
|
.file-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.file-item:last-child { border-bottom: none; }
|
|
.file-icon {
|
|
width: 32px; height: 32px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
.file-icon-new { background: var(--green-bg); color: var(--green); }
|
|
.file-icon-mod { background: var(--yellow-bg); color: var(--yellow); }
|
|
.file-path { font-family: 'SF Mono', monospace; font-size: 13px; color: var(--accent); }
|
|
.file-desc { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
|
|
|
|
/* Decision table */
|
|
.decision-row {
|
|
display: flex;
|
|
gap: 16px;
|
|
padding: 14px 18px;
|
|
border-bottom: 1px solid var(--border);
|
|
align-items: flex-start;
|
|
}
|
|
.decision-row:last-child { border-bottom: none; }
|
|
.decision-field {
|
|
font-weight: 600;
|
|
min-width: 100px;
|
|
font-family: 'SF Mono', monospace;
|
|
font-size: 13px;
|
|
}
|
|
.decision-verdict {
|
|
font-weight: 600;
|
|
min-width: 140px;
|
|
}
|
|
.verdict-excluded { color: var(--red); }
|
|
.verdict-flatten { color: var(--green); }
|
|
.decision-rationale { color: var(--text-dim); font-size: 13px; flex: 1; }
|
|
|
|
/* Transform rules */
|
|
.transform-rule {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 10px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
align-items: baseline;
|
|
}
|
|
.transform-rule:last-child { border-bottom: none; }
|
|
.transform-from {
|
|
font-family: 'SF Mono', monospace;
|
|
font-size: 12px;
|
|
color: var(--yellow);
|
|
min-width: 200px;
|
|
}
|
|
.transform-arrow { color: var(--text-dim); min-width: 24px; text-align: center; }
|
|
.transform-to { font-size: 13px; color: var(--text); flex: 1; }
|
|
|
|
/* Not-included list */
|
|
.not-included {
|
|
list-style: none;
|
|
padding: 0;
|
|
}
|
|
.not-included li {
|
|
padding: 8px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 13px;
|
|
color: var(--text-dim);
|
|
}
|
|
.not-included li:last-child { border-bottom: none; }
|
|
.not-included li::before {
|
|
content: "\2717";
|
|
color: var(--red);
|
|
margin-right: 10px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Column count summary */
|
|
.col-summary {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.col-summary-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 18px;
|
|
}
|
|
.col-summary-card h3 { font-size: 14px; margin-bottom: 10px; }
|
|
.col-bar {
|
|
display: flex;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin: 8px 0;
|
|
background: var(--surface2);
|
|
}
|
|
.col-bar-existing { background: var(--text-dim); }
|
|
.col-bar-new { background: var(--green); }
|
|
.col-legend {
|
|
display: flex;
|
|
gap: 16px;
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
}
|
|
.col-legend span { display: flex; align-items: center; gap: 4px; }
|
|
.legend-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
|
|
|
|
/* Toggles for SQL sections */
|
|
.toggle-header {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.toggle-header::after {
|
|
content: " \25BC";
|
|
font-size: 10px;
|
|
color: var(--text-dim);
|
|
}
|
|
.toggle-header.collapsed::after {
|
|
content: " \25B6";
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.header { padding: 14px 16px; }
|
|
.tab-bar { padding: 0 16px; }
|
|
.content { padding: 16px; }
|
|
.col-summary { grid-template-columns: 1fr; }
|
|
.scope-grid { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1>Phase A: Complete API Field Capture</h1>
|
|
<div class="subtitle">Migration 007 — Mirror all GitLab API response data into the local DB</div>
|
|
<div class="badges">
|
|
<span class="badge badge-green">No new API calls</span>
|
|
<span class="badge badge-green">No new tables</span>
|
|
<span class="badge badge-yellow">1 migration</span>
|
|
<span class="badge badge-purple">6 files touched</span>
|
|
<span class="badge badge-blue">Independent of CP3</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-bar" id="tabBar">
|
|
<div class="tab active" data-tab="overview">Overview</div>
|
|
<div class="tab" data-tab="issues">Issues <span class="count" id="issueCount"></span></div>
|
|
<div class="tab" data-tab="mrs">Merge Requests <span class="count" id="mrCount"></span></div>
|
|
<div class="tab" data-tab="migration">Migration SQL</div>
|
|
<div class="tab" data-tab="structs">Serde Structs</div>
|
|
<div class="tab" data-tab="transforms">Transformers</div>
|
|
<div class="tab" data-tab="files">Files Touched</div>
|
|
<div class="tab" data-tab="decisions">Decisions</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<!-- OVERVIEW -->
|
|
<div class="tab-content active" id="tab-overview">
|
|
<div class="principle">
|
|
<strong>Guiding Principle:</strong> Mirror everything GitLab gives us. The local DB should be a complete representation of all data returned by the API. This ensures maximum context for processing and analysis in later steps.
|
|
</div>
|
|
|
|
<div class="scope-grid">
|
|
<div class="scope-item">
|
|
<div class="num" id="totalNewCols">0</div>
|
|
<div class="label">New columns total</div>
|
|
</div>
|
|
<div class="scope-item">
|
|
<div class="num" id="issueNewCols">0</div>
|
|
<div class="label">New issue columns</div>
|
|
</div>
|
|
<div class="scope-item">
|
|
<div class="num" id="mrNewCols">0</div>
|
|
<div class="label">New MR columns</div>
|
|
</div>
|
|
<div class="scope-item">
|
|
<div class="num">6</div>
|
|
<div class="label">New serde helper types</div>
|
|
</div>
|
|
<div class="scope-item">
|
|
<div class="num">2</div>
|
|
<div class="label">Fields excluded</div>
|
|
</div>
|
|
<div class="scope-item">
|
|
<div class="num">6</div>
|
|
<div class="label">Files touched</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-summary">
|
|
<div class="col-summary-card">
|
|
<h3>Issues table</h3>
|
|
<div class="col-bar">
|
|
<div class="col-bar-existing" id="issueBarExisting"></div>
|
|
<div class="col-bar-new" id="issueBarNew"></div>
|
|
</div>
|
|
<div class="col-legend">
|
|
<span><span class="legend-dot" style="background:var(--text-dim)"></span> <span id="issueExistingNum">18</span> existing</span>
|
|
<span><span class="legend-dot" style="background:var(--green)"></span> <span id="issueNewNum">0</span> new</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-summary-card">
|
|
<h3>Merge Requests table</h3>
|
|
<div class="col-bar">
|
|
<div class="col-bar-existing" id="mrBarExisting"></div>
|
|
<div class="col-bar-new" id="mrBarNew"></div>
|
|
</div>
|
|
<div class="col-legend">
|
|
<span><span class="legend-dot" style="background:var(--text-dim)"></span> <span id="mrExistingNum">29</span> existing</span>
|
|
<span><span class="legend-dot" style="background:var(--green)"></span> <span id="mrNewNum">0</span> new</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">Scope</div>
|
|
<div class="card-body" style="font-size:14px;">
|
|
<p style="margin-bottom:10px;">One migration. Three categories of work:</p>
|
|
<ol style="padding-left:20px; color:var(--text-dim); line-height:2;">
|
|
<li><strong style="color:var(--text)">New columns</strong> on <code>issues</code> and <code>merge_requests</code> for fields currently dropped by serde or during transform</li>
|
|
<li><strong style="color:var(--text)">New serde fields</strong> on <code>GitLabIssue</code> and <code>GitLabMergeRequest</code> to deserialize currently-silently-dropped JSON fields</li>
|
|
<li><strong style="color:var(--text)">Transformer + insert updates</strong> to pass the new fields through to the DB</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">What this does NOT include</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<ul class="not-included">
|
|
<li>No new API endpoints called</li>
|
|
<li>No new tables (except reusing existing <code>milestones</code> for MRs)</li>
|
|
<li>No CLI changes (new fields stored but not surfaced in <code>lore issues</code> / <code>lore mrs</code>)</li>
|
|
<li>No changes to discussion/note ingestion (Phase A is issues + MRs only)</li>
|
|
<li>No observability instrumentation (that's Phase B)</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ISSUES TAB -->
|
|
<div class="tab-content" id="tab-issues">
|
|
<h2 style="font-size:16px; margin-bottom:16px;">Issues: Field Gap Inventory</h2>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">Currently Stored</div>
|
|
<div class="card-body" style="font-size:13px; color:var(--text-dim); line-height:2;">
|
|
<span style="color:var(--text)">id</span>, <span style="color:var(--text)">iid</span>, <span style="color:var(--text)">project_id</span>, <span style="color:var(--text)">title</span>, <span style="color:var(--text)">description</span>, <span style="color:var(--text)">state</span>, <span style="color:var(--text)">author_username</span>, <span style="color:var(--text)">created_at</span>, <span style="color:var(--text)">updated_at</span>, <span style="color:var(--text)">web_url</span>, <span style="color:var(--text)">due_date</span>, <span style="color:var(--text)">milestone_id</span>, <span style="color:var(--text)">milestone_title</span>, <span style="color:var(--text)">raw_payload_id</span>, <span style="color:var(--text)">last_seen_at</span>, <span style="color:var(--text)">discussions_synced_for_updated_at</span>, <span style="color:var(--text)">labels</span> (junction), <span style="color:var(--text)">assignees</span> (junction)
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter-bar">
|
|
<input type="text" class="search-input" id="issueSearch" placeholder="Filter fields...">
|
|
<button class="filter-btn active" data-filter="all" data-table="issues">All</button>
|
|
<button class="filter-btn" data-filter="wired" data-table="issues">Deserialized, not stored</button>
|
|
<button class="filter-btn" data-filter="new" data-table="issues">New from API</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body" style="padding:0; overflow-x:auto;">
|
|
<table id="issueTable">
|
|
<thead>
|
|
<tr>
|
|
<th>API Field</th>
|
|
<th>Type</th>
|
|
<th>DB Column</th>
|
|
<th>Category</th>
|
|
<th>Status</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MRs TAB -->
|
|
<div class="tab-content" id="tab-mrs">
|
|
<h2 style="font-size:16px; margin-bottom:16px;">Merge Requests: Field Gap Inventory</h2>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">Currently Stored</div>
|
|
<div class="card-body" style="font-size:13px; color:var(--text-dim); line-height:2;">
|
|
<span style="color:var(--text)">id</span>, <span style="color:var(--text)">iid</span>, <span style="color:var(--text)">project_id</span>, <span style="color:var(--text)">title</span>, <span style="color:var(--text)">description</span>, <span style="color:var(--text)">state</span>, <span style="color:var(--text)">draft</span>, <span style="color:var(--text)">author_username</span>, <span style="color:var(--text)">source_branch</span>, <span style="color:var(--text)">target_branch</span>, <span style="color:var(--text)">head_sha</span>, <span style="color:var(--text)">references_short</span>, <span style="color:var(--text)">references_full</span>, <span style="color:var(--text)">detailed_merge_status</span>, <span style="color:var(--text)">merge_user_username</span>, <span style="color:var(--text)">created_at</span>, <span style="color:var(--text)">updated_at</span>, <span style="color:var(--text)">merged_at</span>, <span style="color:var(--text)">closed_at</span>, <span style="color:var(--text)">last_seen_at</span>, <span style="color:var(--text)">web_url</span>, <span style="color:var(--text)">raw_payload_id</span>, <span style="color:var(--text)">discussions_synced_for_updated_at</span>, <span style="color:var(--text)">labels</span> (junction), <span style="color:var(--text)">assignees</span> (junction), <span style="color:var(--text)">reviewers</span> (junction)
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter-bar">
|
|
<input type="text" class="search-input" id="mrSearch" placeholder="Filter fields...">
|
|
<button class="filter-btn active" data-filter="all" data-table="mrs">All</button>
|
|
<button class="filter-btn" data-filter="wired" data-table="mrs">Deserialized, not stored</button>
|
|
<button class="filter-btn" data-filter="new" data-table="mrs">New from API</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body" style="padding:0; overflow-x:auto;">
|
|
<table id="mrTable">
|
|
<thead>
|
|
<tr>
|
|
<th>API Field</th>
|
|
<th>Type</th>
|
|
<th>DB Column</th>
|
|
<th>Category</th>
|
|
<th>Status</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MIGRATION SQL TAB -->
|
|
<div class="tab-content" id="tab-migration">
|
|
<h2 style="font-size:16px; margin-bottom:16px;">Migration 007: complete_field_capture.sql</h2>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">
|
|
<span>Issues columns</span>
|
|
<span class="badge badge-green" id="issueSqlCount"></span>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<div class="sql-block" id="issueSql"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span>Merge Requests columns</span>
|
|
<span class="badge badge-green" id="mrSqlCount"></span>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<div class="sql-block" id="mrSql"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SERDE STRUCTS TAB -->
|
|
<div class="tab-content" id="tab-structs">
|
|
<h2 style="font-size:16px; margin-bottom:16px;">Serde Struct Changes</h2>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">New Helper Types</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<div class="struct-block" id="helperTypes"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">
|
|
<span>GitLabIssue: new fields</span>
|
|
<span class="badge badge-yellow" id="issueFieldCount"></span>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<div class="struct-block" id="issueStruct"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span>GitLabMergeRequest: new fields</span>
|
|
<span class="badge badge-yellow" id="mrFieldCount"></span>
|
|
</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<div class="struct-block" id="mrStruct"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TRANSFORMERS TAB -->
|
|
<div class="tab-content" id="tab-transforms">
|
|
<h2 style="font-size:16px; margin-bottom:16px;">Transformer Changes</h2>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">IssueRow: transform rules</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<p style="padding:12px 16px; font-size:13px; color:var(--text-dim); border-bottom:1px solid var(--border);">
|
|
All new fields map 1:1 from the serde struct <strong style="color:var(--text)">except</strong> these special cases:
|
|
</p>
|
|
<div id="issueTransforms"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">NormalizedMergeRequest: transform rules</div>
|
|
<div class="card-body" style="padding:0;">
|
|
<p style="padding:12px 16px; font-size:13px; color:var(--text-dim); border-bottom:1px solid var(--border);">
|
|
Same patterns as issues, plus:
|
|
</p>
|
|
<div id="mrTransforms"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">Insert statement changes</div>
|
|
<div class="card-body" style="font-size:13px; color:var(--text-dim);">
|
|
Both <code>process_issue_in_transaction</code> and <code>process_mr_in_transaction</code> need their
|
|
<strong style="color:var(--text)">INSERT</strong> and <strong style="color:var(--text)">ON CONFLICT DO UPDATE</strong>
|
|
statements extended with all new columns. The ON CONFLICT clause should update all new fields on re-sync.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FILES TOUCHED TAB -->
|
|
<div class="tab-content" id="tab-files">
|
|
<h2 style="font-size:16px; margin-bottom:16px;">Files Touched</h2>
|
|
<div class="card">
|
|
<div class="card-body" style="padding:0;" id="filesList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DECISIONS TAB -->
|
|
<div class="tab-content" id="tab-decisions">
|
|
<h2 style="font-size:16px; margin-bottom:16px;">Resolved Decisions</h2>
|
|
|
|
<div class="card" style="margin-bottom:20px;">
|
|
<div class="card-header">Exclusions (2 fields)</div>
|
|
<div class="card-body">
|
|
<div class="exclusion">
|
|
<div class="field-name"><code>subscribed</code> — Excluded</div>
|
|
<div class="rationale">User-relative field. Reflects the token holder's subscription state, not a property of the entity itself. Changes meaning if the token is rotated to a different user. Not entity data.</div>
|
|
</div>
|
|
<div class="exclusion">
|
|
<div class="field-name"><code>_links</code> — Excluded</div>
|
|
<div class="rationale">HATEOAS API navigation metadata, not entity data. Every URL is deterministically constructable from <code>project_id</code> + <code>iid</code> + GitLab base URL.</div>
|
|
<div class="note">Note: <code>closed_as_duplicate_of</code> inside <code>_links</code> contains a real entity reference. Extracting that is deferred to a future phase.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">Included with special handling</div>
|
|
<div class="card-body">
|
|
<div class="exclusion" style="margin-bottom:0;">
|
|
<div class="field-name" style="color:var(--green);"><code>epic</code> / <code>iteration</code> — Flatten to columns</div>
|
|
<div class="rationale">Same denormalization pattern as milestones. Epic gets 5 columns (<code>epic_id</code>, <code>epic_iid</code>, <code>epic_title</code>, <code>epic_url</code>, <code>epic_group_id</code>). Iteration gets 6 columns (<code>iteration_id</code>, <code>iteration_iid</code>, <code>iteration_title</code>, <code>iteration_state</code>, <code>iteration_start_date</code>, <code>iteration_due_date</code>). Both nullable (null on Free tier).</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// ============================================================
|
|
// DATA
|
|
// ============================================================
|
|
|
|
const issueFields = [
|
|
// Deserialized but not stored
|
|
{ api: 'closed_at', type: 'String', col: 'closed_at', cat: 'tracking', catLabel: 'Tracking', status: 'wired', notes: 'Deserialized in serde struct, DB column exists (migration 002), but transformer never populates it' },
|
|
{ api: 'author.id', type: 'i64', col: 'author_id', cat: 'author', catLabel: 'Author', status: 'wired', notes: 'Already deserialized, just not passed to DB' },
|
|
{ api: 'author.name', type: 'String', col: 'author_name', cat: 'author', catLabel: 'Author', status: 'wired', notes: 'Already deserialized, just not passed to DB' },
|
|
// New from API
|
|
{ api: 'type', type: 'String', col: 'issue_type', cat: 'meta', catLabel: 'Metadata', status: 'new', notes: '"ISSUE", "INCIDENT", "TEST_CASE"' },
|
|
{ api: 'upvotes', type: 'i64', col: 'upvotes', cat: 'engagement', catLabel: 'Engagement', status: 'new', notes: '' },
|
|
{ api: 'downvotes', type: 'i64', col: 'downvotes', cat: 'engagement', catLabel: 'Engagement', status: 'new', notes: '' },
|
|
{ api: 'user_notes_count', type: 'i64', col: 'user_notes_count', cat: 'engagement', catLabel: 'Engagement', status: 'new', notes: 'Useful for discussion sync optimization' },
|
|
{ api: 'merge_requests_count', type: 'i64', col: 'merge_requests_count', cat: 'engagement', catLabel: 'Engagement', status: 'new', notes: 'Count of linked MRs' },
|
|
{ api: 'confidential', type: 'bool', col: 'confidential', cat: 'meta', catLabel: 'Metadata', status: 'new', notes: '0/1' },
|
|
{ api: 'discussion_locked', type: 'bool', col: 'discussion_locked', cat: 'meta', catLabel: 'Metadata', status: 'new', notes: '0/1' },
|
|
{ api: 'weight', type: 'Option<i64>', col: 'weight', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'Premium/Ultimate, null on Free' },
|
|
{ api: 'time_stats.time_estimate', type: 'i64', col: 'time_estimate', cat: 'time', catLabel: 'Time', status: 'new', notes: 'Seconds' },
|
|
{ api: 'time_stats.total_time_spent', type: 'i64', col: 'time_spent', cat: 'time', catLabel: 'Time', status: 'new', notes: 'Seconds' },
|
|
{ api: 'time_stats.human_time_estimate', type: 'Option<String>', col: 'human_time_estimate', cat: 'time', catLabel: 'Time', status: 'new', notes: 'e.g. "3h 30m"' },
|
|
{ api: 'time_stats.human_total_time_spent', type: 'Option<String>', col: 'human_time_spent', cat: 'time', catLabel: 'Time', status: 'new', notes: 'e.g. "1h 15m"' },
|
|
{ api: 'task_completion_status.count', type: 'i64', col: 'task_count', cat: 'task', catLabel: 'Tasks', status: 'new', notes: 'Checkbox total' },
|
|
{ api: 'task_completion_status.completed_count', type: 'i64', col: 'task_completed_count', cat: 'task', catLabel: 'Tasks', status: 'new', notes: 'Checkboxes checked' },
|
|
{ api: 'has_tasks', type: 'bool', col: 'has_tasks', cat: 'task', catLabel: 'Tasks', status: 'new', notes: '0/1' },
|
|
{ api: 'severity', type: 'Option<String>', col: 'severity', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'Incident severity' },
|
|
{ api: 'closed_by', type: 'Option<object>', col: 'closed_by_username', cat: 'tracking', catLabel: 'Tracking', status: 'new', notes: 'Username only (consistent with author pattern)' },
|
|
{ api: 'imported', type: 'bool', col: 'imported', cat: 'import', catLabel: 'Import', status: 'new', notes: '0/1' },
|
|
{ api: 'imported_from', type: 'Option<String>', col: 'imported_from', cat: 'import', catLabel: 'Import', status: 'new', notes: 'Import source' },
|
|
{ api: 'moved_to_id', type: 'Option<i64>', col: 'moved_to_id', cat: 'tracking', catLabel: 'Tracking', status: 'new', notes: 'Target issue if moved' },
|
|
{ api: 'references.short', type: 'String', col: 'references_short', cat: 'ref', catLabel: 'References', status: 'new', notes: 'e.g. "#42"' },
|
|
{ api: 'references.relative', type: 'String', col: 'references_relative', cat: 'ref', catLabel: 'References', status: 'new', notes: 'e.g. "#42" or "group/proj#42"' },
|
|
{ api: 'references.full', type: 'String', col: 'references_full', cat: 'ref', catLabel: 'References', status: 'new', notes: 'e.g. "group/project#42"' },
|
|
{ api: 'health_status', type: 'Option<String>', col: 'health_status', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'Ultimate only' },
|
|
{ api: 'issue_type', type: 'Option<String>', col: '(same as type)', cat: 'meta', catLabel: 'Metadata', status: 'new', notes: 'Alias, use whichever GitLab returns' },
|
|
{ api: 'epic.id', type: 'Option<i64>', col: 'epic_id', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'Premium/Ultimate, null on Free' },
|
|
{ api: 'epic.iid', type: 'Option<i64>', col: 'epic_iid', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'epic.title', type: 'Option<String>', col: 'epic_title', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'epic.url', type: 'Option<String>', col: 'epic_url', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'epic.group_id', type: 'Option<i64>', col: 'epic_group_id', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'iteration.id', type: 'Option<i64>', col: 'iteration_id', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'Premium/Ultimate, null on Free' },
|
|
{ api: 'iteration.iid', type: 'Option<i64>', col: 'iteration_iid', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'iteration.title', type: 'Option<String>', col: 'iteration_title', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'iteration.state', type: 'Option<i64>', col: 'iteration_state', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '1=upcoming, 2=current, 3=closed' },
|
|
{ api: 'iteration.start_date', type: 'Option<String>', col: 'iteration_start_date', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'ISO date' },
|
|
{ api: 'iteration.due_date', type: 'Option<String>', col: 'iteration_due_date', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'ISO date' },
|
|
];
|
|
|
|
const mrFields = [
|
|
// Deserialized but not stored
|
|
{ api: 'author.id', type: 'i64', col: 'author_id', cat: 'author', catLabel: 'Author', status: 'wired', notes: 'Already deserialized, just not passed to DB' },
|
|
{ api: 'author.name', type: 'String', col: 'author_name', cat: 'author', catLabel: 'Author', status: 'wired', notes: 'Already deserialized, just not passed to DB' },
|
|
// New from API
|
|
{ api: 'upvotes', type: 'i64', col: 'upvotes', cat: 'engagement', catLabel: 'Engagement', status: 'new', notes: '' },
|
|
{ api: 'downvotes', type: 'i64', col: 'downvotes', cat: 'engagement', catLabel: 'Engagement', status: 'new', notes: '' },
|
|
{ api: 'user_notes_count', type: 'i64', col: 'user_notes_count', cat: 'engagement', catLabel: 'Engagement', status: 'new', notes: '' },
|
|
{ api: 'source_project_id', type: 'i64', col: 'source_project_id', cat: 'fork', catLabel: 'Fork', status: 'new', notes: 'Fork source' },
|
|
{ api: 'target_project_id', type: 'i64', col: 'target_project_id', cat: 'fork', catLabel: 'Fork', status: 'new', notes: 'Fork target' },
|
|
{ api: 'milestone', type: 'Option<object>', col: 'milestone_id, milestone_title', cat: 'meta', catLabel: 'Metadata', status: 'new', notes: 'Reuse issue milestone pattern' },
|
|
{ api: 'merge_when_pipeline_succeeds', type: 'bool', col: 'merge_when_pipeline_succeeds', cat: 'merge', catLabel: 'Merge', status: 'new', notes: '0/1, auto-merge flag' },
|
|
{ api: 'merge_commit_sha', type: 'Option<String>', col: 'merge_commit_sha', cat: 'merge', catLabel: 'Merge', status: 'new', notes: 'Commit ref after merge' },
|
|
{ api: 'squash_commit_sha', type: 'Option<String>', col: 'squash_commit_sha', cat: 'merge', catLabel: 'Merge', status: 'new', notes: 'Commit ref after squash' },
|
|
{ api: 'discussion_locked', type: 'bool', col: 'discussion_locked', cat: 'meta', catLabel: 'Metadata', status: 'new', notes: '0/1' },
|
|
{ api: 'should_remove_source_branch', type: 'Option<bool>', col: 'should_remove_source_branch', cat: 'branch', catLabel: 'Branch', status: 'new', notes: '0/1' },
|
|
{ api: 'force_remove_source_branch', type: 'Option<bool>', col: 'force_remove_source_branch', cat: 'branch', catLabel: 'Branch', status: 'new', notes: '0/1' },
|
|
{ api: 'squash', type: 'bool', col: 'squash', cat: 'merge', catLabel: 'Merge', status: 'new', notes: '0/1' },
|
|
{ api: 'squash_on_merge', type: 'bool', col: 'squash_on_merge', cat: 'merge', catLabel: 'Merge', status: 'new', notes: '0/1' },
|
|
{ api: 'has_conflicts', type: 'bool', col: 'has_conflicts', cat: 'merge', catLabel: 'Merge', status: 'new', notes: '0/1' },
|
|
{ api: 'blocking_discussions_resolved', type: 'bool', col: 'blocking_discussions_resolved', cat: 'merge', catLabel: 'Merge', status: 'new', notes: '0/1' },
|
|
{ api: 'time_stats.time_estimate', type: 'i64', col: 'time_estimate', cat: 'time', catLabel: 'Time', status: 'new', notes: 'Seconds' },
|
|
{ api: 'time_stats.total_time_spent', type: 'i64', col: 'time_spent', cat: 'time', catLabel: 'Time', status: 'new', notes: 'Seconds' },
|
|
{ api: 'time_stats.human_time_estimate', type: 'Option<String>', col: 'human_time_estimate', cat: 'time', catLabel: 'Time', status: 'new', notes: '' },
|
|
{ api: 'time_stats.human_total_time_spent', type: 'Option<String>', col: 'human_time_spent', cat: 'time', catLabel: 'Time', status: 'new', notes: '' },
|
|
{ api: 'task_completion_status.count', type: 'i64', col: 'task_count', cat: 'task', catLabel: 'Tasks', status: 'new', notes: '' },
|
|
{ api: 'task_completion_status.completed_count', type: 'i64', col: 'task_completed_count', cat: 'task', catLabel: 'Tasks', status: 'new', notes: '' },
|
|
{ api: 'closed_by', type: 'Option<object>', col: 'closed_by_username', cat: 'tracking', catLabel: 'Tracking', status: 'new', notes: '' },
|
|
{ api: 'prepared_at', type: 'Option<String>', col: 'prepared_at', cat: 'schedule', catLabel: 'Schedule', status: 'new', notes: 'ms epoch, nullable' },
|
|
{ api: 'merge_after', type: 'Option<String>', col: 'merge_after', cat: 'schedule', catLabel: 'Schedule', status: 'new', notes: 'ms epoch, nullable (scheduled merge)' },
|
|
{ api: 'imported', type: 'bool', col: 'imported', cat: 'import', catLabel: 'Import', status: 'new', notes: '0/1' },
|
|
{ api: 'imported_from', type: 'Option<String>', col: 'imported_from', cat: 'import', catLabel: 'Import', status: 'new', notes: '' },
|
|
{ api: 'approvals_before_merge', type: 'Option<i64>', col: 'approvals_before_merge', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'Deprecated but still returned' },
|
|
{ api: 'references.relative', type: 'String', col: 'references_relative', cat: 'ref', catLabel: 'References', status: 'new', notes: 'Currently only short + full stored' },
|
|
{ api: 'confidential', type: 'bool', col: 'confidential', cat: 'meta', catLabel: 'Metadata', status: 'new', notes: '0/1 (MRs can be confidential too)' },
|
|
{ api: 'iteration.id', type: 'Option<i64>', col: 'iteration_id', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'Premium/Ultimate, null on Free' },
|
|
{ api: 'iteration.iid', type: 'Option<i64>', col: 'iteration_iid', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'iteration.title', type: 'Option<String>', col: 'iteration_title', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'iteration.state', type: 'Option<i64>', col: 'iteration_state', cat: 'premium', catLabel: 'Premium', status: 'new', notes: '' },
|
|
{ api: 'iteration.start_date', type: 'Option<String>', col: 'iteration_start_date', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'ISO date' },
|
|
{ api: 'iteration.due_date', type: 'Option<String>', col: 'iteration_due_date', cat: 'premium', catLabel: 'Premium', status: 'new', notes: 'ISO date' },
|
|
];
|
|
|
|
const issueSqlLines = [
|
|
{ type: 'comment', text: '-- Migration 007: Capture all remaining GitLab API response fields.' },
|
|
{ type: 'comment', text: '-- Principle: mirror everything GitLab returns. No field left behind.' },
|
|
{ type: 'blank' },
|
|
{ type: 'divider', text: '-- ============================================================' },
|
|
{ type: 'divider', text: '-- ISSUES: new columns' },
|
|
{ type: 'divider', text: '-- ============================================================' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Fields currently deserialized but not stored' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN closed_at INTEGER;', note: 'ms epoch' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN author_id INTEGER;', note: 'GitLab user ID' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN author_name TEXT;', note: 'Display name' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Issue metadata' },
|
|
{ type: 'sql', text: "ALTER TABLE issues ADD COLUMN issue_type TEXT;", note: "'issue' | 'incident' | 'test_case'" },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN confidential INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN discussion_locked INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Engagement' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN upvotes INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN downvotes INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN user_notes_count INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN merge_requests_count INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Time tracking' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN time_estimate INTEGER NOT NULL DEFAULT 0;', note: 'seconds' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN time_spent INTEGER NOT NULL DEFAULT 0;', note: 'seconds' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN human_time_estimate TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN human_time_spent TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Task lists' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN task_count INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN task_completed_count INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN has_tasks INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- References' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN references_short TEXT;', note: 'e.g. "#42"' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN references_relative TEXT;', note: 'context-dependent' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN references_full TEXT;', note: 'e.g. "group/project#42"' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Close/move tracking' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN closed_by_username TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Premium/Ultimate fields (nullable, null on Free tier)' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN weight INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN severity TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN health_status TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Import tracking' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN imported INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN imported_from TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN moved_to_id INTEGER;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Epic (Premium/Ultimate, null on Free)' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN epic_id INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN epic_iid INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN epic_title TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN epic_url TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN epic_group_id INTEGER;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Iteration (Premium/Ultimate, null on Free)' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN iteration_id INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN iteration_iid INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN iteration_title TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN iteration_state INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN iteration_start_date TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE issues ADD COLUMN iteration_due_date TEXT;' },
|
|
];
|
|
|
|
const mrSqlLines = [
|
|
{ type: 'divider', text: '-- ============================================================' },
|
|
{ type: 'divider', text: '-- MERGE REQUESTS: new columns' },
|
|
{ type: 'divider', text: '-- ============================================================' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Author enrichment' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN author_id INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN author_name TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Engagement' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN upvotes INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN downvotes INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN user_notes_count INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Fork tracking' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN source_project_id INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN target_project_id INTEGER;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Milestone (parity with issues)' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN milestone_id INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN milestone_title TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Merge behavior' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN merge_when_pipeline_succeeds INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN merge_commit_sha TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN squash_commit_sha TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN squash INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN squash_on_merge INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Merge readiness' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN has_conflicts INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN blocking_discussions_resolved INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Branch cleanup' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN should_remove_source_branch INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN force_remove_source_branch INTEGER;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Discussion lock' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN discussion_locked INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Time tracking' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN time_estimate INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN time_spent INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN human_time_estimate TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN human_time_spent TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Task lists' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN task_count INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN task_completed_count INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Close tracking' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN closed_by_username TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Scheduling' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN prepared_at INTEGER;', note: 'ms epoch' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN merge_after INTEGER;', note: 'ms epoch' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- References (add relative, short + full already exist)' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN references_relative TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Import tracking' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN imported INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN imported_from TEXT;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Premium/Ultimate' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN approvals_before_merge INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN confidential INTEGER NOT NULL DEFAULT 0;' },
|
|
{ type: 'blank' },
|
|
{ type: 'comment', text: '-- Iteration (Premium/Ultimate, null on Free)' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN iteration_id INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN iteration_iid INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN iteration_title TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN iteration_state INTEGER;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN iteration_start_date TEXT;' },
|
|
{ type: 'sql', text: 'ALTER TABLE merge_requests ADD COLUMN iteration_due_date TEXT;' },
|
|
];
|
|
|
|
const issueTransformRules = [
|
|
{ from: 'closed_at', to: 'iso_to_ms() conversion (already in serde struct, just not passed through)' },
|
|
{ from: 'time_stats', to: 'Flatten to 4 individual fields: time_estimate, time_spent, human_time_estimate, human_time_spent' },
|
|
{ from: 'task_completion_status', to: 'Flatten to 2 individual fields: task_count, task_completed_count' },
|
|
{ from: 'references', to: 'Flatten to 3 individual fields: references_short, references_relative, references_full' },
|
|
{ from: 'closed_by', to: 'Extract username only (consistent with author pattern)' },
|
|
{ from: 'author', to: 'Additionally extract id and name (currently only username)' },
|
|
{ from: 'type / issue_type', to: 'Prefer issue_type, fall back to type field' },
|
|
{ from: 'epic', to: 'Flatten to 5 fields: epic_id, epic_iid, epic_title, epic_url, epic_group_id' },
|
|
{ from: 'iteration', to: 'Flatten to 6 fields: iteration_id, iteration_iid, iteration_title, iteration_state, iteration_start_date, iteration_due_date' },
|
|
];
|
|
|
|
const mrTransformRules = [
|
|
{ from: 'milestone', to: 'Reuse upsert_milestone_tx from issue pipeline, add milestone_id + milestone_title' },
|
|
{ from: 'prepared_at, merge_after', to: 'iso_to_ms() conversion' },
|
|
{ from: 'source_project_id, target_project_id', to: 'Direct pass-through' },
|
|
{ from: 'iteration', to: 'Flatten to 6 fields (same as issues)' },
|
|
];
|
|
|
|
const filesTouched = [
|
|
{ path: 'migrations/007_complete_field_capture.sql', change: 'New file', isNew: true },
|
|
{ path: 'src/gitlab/types.rs', change: 'Add fields to GitLabIssue, GitLabMergeRequest; add GitLabTimeStats, GitLabTaskCompletionStatus, GitLabEpic, GitLabIteration', isNew: false },
|
|
{ path: 'src/gitlab/transformers/issue.rs', change: 'Extend IssueRow, IssueWithMetadata, transform_issue()', isNew: false },
|
|
{ path: 'src/gitlab/transformers/merge_request.rs', change: 'Extend NormalizedMergeRequest, MergeRequestWithMetadata, transform_merge_request()', isNew: false },
|
|
{ path: 'src/ingestion/issues.rs', change: 'Extend INSERT/UPSERT SQL, add milestone for completeness', isNew: false },
|
|
{ path: 'src/ingestion/merge_requests.rs', change: 'Extend INSERT/UPSERT SQL, add milestone upsert', isNew: false },
|
|
];
|
|
|
|
const helperTypes = [
|
|
{ name: 'GitLabTimeStats', fields: 'time_estimate, total_time_spent, human_time_estimate, human_total_time_spent' },
|
|
{ name: 'GitLabTaskCompletionStatus', fields: 'count, completed_count' },
|
|
{ name: 'GitLabReferences', fields: 'short, relative, full (already exists for MRs, reuse for issues)' },
|
|
{ name: 'GitLabClosedBy', fields: 'id, username, name (reuse GitLabAuthor shape)' },
|
|
{ name: 'GitLabEpic', fields: 'id, iid, title, url, group_id' },
|
|
{ name: 'GitLabIteration', fields: 'id, iid, title, state, start_date, due_date' },
|
|
];
|
|
|
|
const issueStructFields = [
|
|
{ field: 'r#type', type: 'Option<String>', comment: '#[serde(rename = "type")]' },
|
|
{ field: 'upvotes', type: 'i64', comment: '#[serde(default)]' },
|
|
{ field: 'downvotes', type: 'i64', comment: '#[serde(default)]' },
|
|
{ field: 'user_notes_count', type: 'i64', comment: '#[serde(default)]' },
|
|
{ field: 'merge_requests_count', type: 'i64', comment: '#[serde(default)]' },
|
|
{ field: 'confidential', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'discussion_locked', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'weight', type: 'Option<i64>', comment: '' },
|
|
{ field: 'time_stats', type: 'Option<GitLabTimeStats>', comment: '' },
|
|
{ field: 'task_completion_status', type: 'Option<GitLabTaskCompletionStatus>', comment: '' },
|
|
{ field: 'has_tasks', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'references', type: 'Option<GitLabReferences>', comment: '' },
|
|
{ field: 'closed_by', type: 'Option<GitLabAuthor>', comment: '' },
|
|
{ field: 'severity', type: 'Option<String>', comment: '' },
|
|
{ field: 'health_status', type: 'Option<String>', comment: '' },
|
|
{ field: 'imported', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'imported_from', type: 'Option<String>', comment: '' },
|
|
{ field: 'moved_to_id', type: 'Option<i64>', comment: '' },
|
|
{ field: 'issue_type', type: 'Option<String>', comment: 'alias for type' },
|
|
{ field: 'epic', type: 'Option<GitLabEpic>', comment: '' },
|
|
{ field: 'iteration', type: 'Option<GitLabIteration>', comment: '' },
|
|
];
|
|
|
|
const mrStructFields = [
|
|
{ field: 'upvotes', type: 'i64', comment: '#[serde(default)]' },
|
|
{ field: 'downvotes', type: 'i64', comment: '#[serde(default)]' },
|
|
{ field: 'user_notes_count', type: 'i64', comment: '#[serde(default)]' },
|
|
{ field: 'source_project_id', type: 'Option<i64>', comment: '' },
|
|
{ field: 'target_project_id', type: 'Option<i64>', comment: '' },
|
|
{ field: 'milestone', type: 'Option<GitLabMilestone>', comment: 'reuse existing type' },
|
|
{ field: 'merge_when_pipeline_succeeds', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'merge_commit_sha', type: 'Option<String>', comment: '' },
|
|
{ field: 'squash_commit_sha', type: 'Option<String>', comment: '' },
|
|
{ field: 'squash', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'squash_on_merge', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'has_conflicts', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'blocking_discussions_resolved', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'should_remove_source_branch', type: 'Option<bool>', comment: '' },
|
|
{ field: 'force_remove_source_branch', type: 'Option<bool>', comment: '' },
|
|
{ field: 'discussion_locked', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'time_stats', type: 'Option<GitLabTimeStats>', comment: '' },
|
|
{ field: 'task_completion_status', type: 'Option<GitLabTaskCompletionStatus>', comment: '' },
|
|
{ field: 'closed_by', type: 'Option<GitLabAuthor>', comment: '' },
|
|
{ field: 'prepared_at', type: 'Option<String>', comment: '' },
|
|
{ field: 'merge_after', type: 'Option<String>', comment: '' },
|
|
{ field: 'imported', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'imported_from', type: 'Option<String>', comment: '' },
|
|
{ field: 'approvals_before_merge', type: 'Option<i64>', comment: '' },
|
|
{ field: 'confidential', type: 'bool', comment: '#[serde(default)]' },
|
|
{ field: 'iteration', type: 'Option<GitLabIteration>', comment: '' },
|
|
];
|
|
|
|
// ============================================================
|
|
// RENDER
|
|
// ============================================================
|
|
|
|
function esc(s) {
|
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
function renderFieldTable(fields, tbodyId) {
|
|
const tbody = document.querySelector(`#${tbodyId} tbody`);
|
|
fields.forEach(f => {
|
|
const tr = document.createElement('tr');
|
|
tr.dataset.status = f.status;
|
|
tr.dataset.search = `${f.api} ${f.col} ${f.cat} ${f.notes}`.toLowerCase();
|
|
const dotClass = f.status === 'wired' ? 'dot-yellow' : 'dot-green';
|
|
const statusText = f.status === 'wired' ? 'Wire up' : 'New capture';
|
|
tr.innerHTML = `
|
|
<td><code>${esc(f.api)}</code></td>
|
|
<td style="color:var(--text-dim); font-size:12px; font-family:monospace;">${esc(f.type)}</td>
|
|
<td><code>${esc(f.col)}</code></td>
|
|
<td><span class="cat cat-${f.cat}">${esc(f.catLabel)}</span></td>
|
|
<td><span class="dot ${dotClass}"></span>${statusText}</td>
|
|
<td style="color:var(--text-dim); font-size:12px;">${esc(f.notes)}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function renderSqlBlock(lines, containerId) {
|
|
const container = document.getElementById(containerId);
|
|
let count = 0;
|
|
const html = lines.map(l => {
|
|
if (l.type === 'blank') return '<span class="sql-line"> </span>';
|
|
if (l.type === 'comment') return `<span class="sql-line"><span class="sql-comment">${esc(l.text)}</span></span>`;
|
|
if (l.type === 'divider') return `<span class="sql-line"><span class="sql-divider">${esc(l.text)}</span></span>`;
|
|
count++;
|
|
let formatted = l.text
|
|
.replace(/^(ALTER TABLE)/, '<span class="sql-keyword">$1</span>')
|
|
.replace(/(ADD COLUMN)/, '<span class="sql-keyword">$1</span>')
|
|
.replace(/(INTEGER|TEXT)/, '<span class="sql-type">$1</span>')
|
|
.replace(/(NOT NULL DEFAULT \d+)/, '<span class="sql-default">$1</span>')
|
|
.replace(/(NOT NULL DEFAULT 0)/, '<span class="sql-default">$1</span>');
|
|
const note = l.note ? ` <span class="sql-comment">-- ${esc(l.note)}</span>` : '';
|
|
return `<span class="sql-line">${formatted}${note}</span>`;
|
|
}).join('\n');
|
|
container.innerHTML = html;
|
|
return count;
|
|
}
|
|
|
|
function renderStructBlock(fields, containerId) {
|
|
const container = document.getElementById(containerId);
|
|
const html = fields.map(f => {
|
|
const comment = f.comment ? ` <span class="struct-comment">// ${esc(f.comment)}</span>` : '';
|
|
return `<span class="struct-line"><span class="struct-field">${esc(f.field)}</span>: <span class="struct-type">${esc(f.type)}</span>,${comment}</span>`;
|
|
}).join('\n');
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderHelperTypes() {
|
|
const container = document.getElementById('helperTypes');
|
|
const html = helperTypes.map(t => {
|
|
return `<span class="struct-line"><span class="struct-type">${esc(t.name)}</span> <span class="struct-comment">{ ${esc(t.fields)} }</span></span>`;
|
|
}).join('\n');
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderTransforms(rules, containerId) {
|
|
const container = document.getElementById(containerId);
|
|
rules.forEach(r => {
|
|
const div = document.createElement('div');
|
|
div.className = 'transform-rule';
|
|
div.innerHTML = `
|
|
<span class="transform-from"><code>${esc(r.from)}</code></span>
|
|
<span class="transform-arrow">→</span>
|
|
<span class="transform-to">${esc(r.to)}</span>
|
|
`;
|
|
container.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function renderFiles() {
|
|
const container = document.getElementById('filesList');
|
|
filesTouched.forEach(f => {
|
|
const div = document.createElement('div');
|
|
div.className = 'file-item';
|
|
const iconClass = f.isNew ? 'file-icon-new' : 'file-icon-mod';
|
|
const icon = f.isNew ? '+' : '~';
|
|
div.innerHTML = `
|
|
<div class="file-icon ${iconClass}">${icon}</div>
|
|
<div>
|
|
<div class="file-path">${esc(f.path)}</div>
|
|
<div class="file-desc">${esc(f.change)}</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(div);
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// TABS
|
|
// ============================================================
|
|
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// FILTERS
|
|
// ============================================================
|
|
|
|
function setupFilters(tableId, searchId, filterTable) {
|
|
const search = document.getElementById(searchId);
|
|
const rows = document.querySelectorAll(`#${tableId} tbody tr`);
|
|
|
|
function applyFilters() {
|
|
const query = search.value.toLowerCase();
|
|
const activeFilter = document.querySelector(`.filter-btn.active[data-table="${filterTable}"]`);
|
|
const filter = activeFilter ? activeFilter.dataset.filter : 'all';
|
|
rows.forEach(row => {
|
|
const matchesSearch = !query || row.dataset.search.includes(query);
|
|
const matchesFilter = filter === 'all' || row.dataset.status === filter;
|
|
row.style.display = (matchesSearch && matchesFilter) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
search.addEventListener('input', applyFilters);
|
|
document.querySelectorAll(`.filter-btn[data-table="${filterTable}"]`).forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll(`.filter-btn[data-table="${filterTable}"]`).forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
applyFilters();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// INIT
|
|
// ============================================================
|
|
|
|
renderFieldTable(issueFields, 'issueTable');
|
|
renderFieldTable(mrFields, 'mrTable');
|
|
const issueSqlCount = renderSqlBlock(issueSqlLines, 'issueSql');
|
|
const mrSqlCount = renderSqlBlock(mrSqlLines, 'mrSql');
|
|
renderStructBlock(issueStructFields, 'issueStruct');
|
|
renderStructBlock(mrStructFields, 'mrStruct');
|
|
renderHelperTypes();
|
|
renderTransforms(issueTransformRules, 'issueTransforms');
|
|
renderTransforms(mrTransformRules, 'mrTransforms');
|
|
renderFiles();
|
|
|
|
setupFilters('issueTable', 'issueSearch', 'issues');
|
|
setupFilters('mrTable', 'mrSearch', 'mrs');
|
|
|
|
// Counts
|
|
document.getElementById('issueCount').textContent = issueFields.length;
|
|
document.getElementById('mrCount').textContent = mrFields.length;
|
|
document.getElementById('issueSqlCount').textContent = issueSqlCount + ' ALTER statements';
|
|
document.getElementById('mrSqlCount').textContent = mrSqlCount + ' ALTER statements';
|
|
document.getElementById('issueFieldCount').textContent = issueStructFields.length + ' fields';
|
|
document.getElementById('mrFieldCount').textContent = mrStructFields.length + ' fields';
|
|
|
|
document.getElementById('issueNewCols').textContent = issueSqlCount;
|
|
document.getElementById('mrNewCols').textContent = mrSqlCount;
|
|
document.getElementById('totalNewCols').textContent = issueSqlCount + mrSqlCount;
|
|
document.getElementById('issueNewNum').textContent = issueSqlCount;
|
|
document.getElementById('mrNewNum').textContent = mrSqlCount;
|
|
|
|
// Bars
|
|
const issueExisting = 18;
|
|
const mrExisting = 29;
|
|
const issueTotal = issueExisting + issueSqlCount;
|
|
const mrTotal = mrExisting + mrSqlCount;
|
|
document.getElementById('issueBarExisting').style.width = (issueExisting / issueTotal * 100) + '%';
|
|
document.getElementById('issueBarNew').style.width = (issueSqlCount / issueTotal * 100) + '%';
|
|
document.getElementById('mrBarExisting').style.width = (mrExisting / mrTotal * 100) + '%';
|
|
document.getElementById('mrBarNew').style.width = (mrSqlCount / mrTotal * 100) + '%';
|
|
</script>
|
|
</body>
|
|
</html>
|