Initial commit: RECON codebase baseline

Current state of the pipeline code as of 2026-04-14 (Phase 1 scaffolding complete).
Config has new_pipeline.enabled=false and crawler.sites=[] per refactor plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-04-14 14:57:23 +00:00
commit 563c16bb71
59 changed files with 18327 additions and 0 deletions

39
templates/base.html Normal file
View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>RECON // Aurora Intelligence Pipeline{% if page_title %} — {{ page_title }}{% endif %}</title>
<meta charset="utf-8">
<link rel="stylesheet" href="/static/css/recon.css">
</head>
<body>
<div class="header">
<div class="header-left"><h1><span id="heartbeat" class="heartbeat"></span>RECON</h1><span class="header-subtitle">AURORA INTELLIGENCE PIPELINE</span></div>
<div class="flex gap-16">
<div class="quick-stats">
<span>Docs: <span id="qs-docs"></span></span>
<span>Vectors: <span id="qs-vectors"></span></span>
<span>Pipeline: <span id="qs-pipeline"></span></span>
</div>
</div>
</div>
<div class="nav-domain">
<a href="/"{% if domain == 'knowledge' %} class="active"{% endif %}>Knowledge</a>
<a href="/peertube"{% if domain == 'peertube' %} class="active"{% endif %}>PeerTube</a>
<a href="/search"{% if domain == 'search' %} class="active"{% endif %}>Search</a>
<a href="/settings/keys"{% if domain == 'settings' %} class="active"{% endif %}>Settings</a>
</div>
{% if subnav %}
<div class="nav-sub">
{% for item in subnav %}
<a href="{{ item.href }}"{% if item.href == active_page %} class="active"{% endif %}>{{ item.label }}</a>
{% endfor %}
</div>
{% endif %}
<div class="content" id="main">
{% block content %}{% endblock %}
</div>
<script src="/static/js/common.js"></script>
<script>document.addEventListener('DOMContentLoaded', function() { RECON.loadQuickStats(); });</script>
{% block scripts %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">Document Catalogue</h3>
{% if sources %}
<div class="mb-16">
<a href="/catalogue" class="btn{% if not current_source %} active{% endif %}" style="margin-right:4px;">All</a>
{% for s in sources %}
<a href="/catalogue?source={{ s }}" class="btn{% if current_source == s %} active{% endif %}" style="margin-right:4px;">{{ s }}</a>
{% endfor %}
</div>
{% endif %}
<div class="text-dim text-xs mb-16">
Showing {{ docs|length }}{% if total_count %} of {{ total_count }}{% endif %} documents
{% if current_source %} in <strong>{{ current_source }}</strong>{% endif %}
(page {{ page }} of {{ total_pages }})
</div>
<table>
<tr><th>Filename</th><th>Source</th><th>Status</th><th>Pages</th><th>Concepts</th><th>Vectors</th></tr>
{% for d in docs %}
<tr>
<td>{{ d.filename or '?' }}</td>
<td>{{ d.source or '' }}</td>
<td><span class="status status-{{ d.status or 'unknown' }}">{{ d.status or 'unknown' }}</span></td>
<td>{{ d.pages_extracted or 0 }}</td>
<td>{{ d.concepts_extracted or 0 }}</td>
<td>{{ d.vectors_inserted or 0 }}</td>
</tr>
{% endfor %}
</table>
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="/catalogue?page={{ page - 1 }}{% if current_source %}&source={{ current_source }}{% endif %}&per_page={{ per_page }}">&laquo;</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<span class="current">{{ p }}</span>
{% elif p <= 3 or p > total_pages - 3 or (p >= page - 2 and p <= page + 2) %}
<a href="/catalogue?page={{ p }}{% if current_source %}&source={{ current_source }}{% endif %}&per_page={{ per_page }}">{{ p }}</a>
{% elif p == 4 or p == total_pages - 3 %}
<span class="text-dim">...</span>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="/catalogue?page={{ page + 1 }}{% if current_source %}&source={{ current_source }}{% endif %}&per_page={{ per_page }}">&raquo;</a>
{% endif %}
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,72 @@
{% extends "base.html" %}
{% block content %}
<div id="kb-dashboard">
<div class="stat-grid">
<div class="stat-card"><div class="label">Catalogued</div><div class="value" id="kv-catalogued"></div><div class="sublabel">total known documents</div></div>
<div class="stat-card"><div class="label">In Pipeline</div><div class="value" id="kv-pipeline"></div><div class="sublabel" id="kv-pipeline-sub">processing</div></div>
<div class="stat-card"><div class="label">Complete</div><div class="value" id="kv-complete"></div><div class="sublabel">in Qdrant</div></div>
<div class="stat-card"><div class="label">Failed</div><div class="value" id="kv-failed"></div><div class="sublabel">&nbsp;</div></div>
</div>
<div class="mb-24">
<div class="flex-between mb-16" style="margin-bottom:4px;font-size:11px;color:#888;">
<span id="progress-label">Pipeline Progress</span>
<span id="progress-pct"></span>
</div>
<div id="progress-bar" class="pipeline-bar"></div>
<div id="progress-legend" class="pipeline-legend"></div>
</div>
<div class="stat-grid grid-3">
<div class="stat-card"><div class="label">Concepts</div><div class="value" id="kv-concepts"></div><div class="sublabel">extracted</div></div>
<div class="stat-card"><div class="label">Vectors</div><div class="value" id="kv-vectors"></div><div class="sublabel">in Qdrant</div></div>
<div class="stat-card"><div class="label">Pages</div><div class="value" id="kv-pages"></div><div class="sublabel">processed</div></div>
</div>
<div id="pipeline-activity" class="panel" style="display:none;">
<h3 style="color:#ffa500;font-size:13px;margin-bottom:8px;">Pipeline Activity</h3>
<div id="activity-content" style="font-size:12px;color:#ccc;"></div>
</div>
<div id="qdrant-health" class="panel" style="padding:10px 16px;font-size:12px;color:#888;">
Qdrant: <span id="qdrant-status">checking...</span>
</div>
<div id="kb-chart-container" class="panel" style="display:none;">
<h3 class="section-title" style="margin-bottom:8px;">Pipeline Activity (24h)</h3>
<canvas id="kb-chart" width="800" height="200" style="width:100%;height:200px;"></canvas>
</div>
<h3 class="section-title" id="sources-toggle" style="cursor:pointer;user-select:none;"><span id="sources-arrow">&#9654;</span> Sources</h3>
<table>
<thead id="sources-thead" style="display:none;"><tr><th>Source</th><th>Type</th><th>Catalogued</th><th>Complete</th><th>In Pipeline</th><th>Progress</th><th>Concepts</th><th>Vectors</th></tr></thead>
<tbody id="sources-tbody" style="display:none;"><tr><td colspan="8" class="text-dim">Loading...</td></tr></tbody>
<tfoot id="sources-tfoot"></tfoot>
</table>
<div class="grid-2 mt-24">
<div>
<h3 class="section-title">Domain Distribution</h3>
<div id="domain-bars" class="text-small">Loading...</div>
</div>
<div>
<h3 class="section-title">Knowledge Type</h3>
<div id="knowledge-type-bars" class="text-small">Loading...</div>
<div id="knowledge-type-migration" class="text-small" style="margin-top:6px;color:#666;font-size:11px;"></div>
<h3 class="section-title" style="margin-top:16px;">Complexity</h3>
<div id="complexity-bars" class="text-small">Loading...</div>
<div id="complexity-migration" class="text-small" style="margin-top:6px;color:#666;font-size:11px;"></div>
</div>
</div>
<h3 class="section-title mt-24">Recently Completed</h3>
<table>
<thead><tr><th>Title</th><th>Type</th><th>Concepts</th><th>Vectors</th></tr></thead>
<tbody id="recent-tbody"><tr><td colspan="4" class="text-dim">Loading...</td></tr></tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/charts.js"></script>
<script src="/static/js/dashboard.js"></script>
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block content %}
<h3 style="color:#ff4444;margin-bottom:16px;">Failed Documents</h3>
{% if not failures %}
<p class="text-dim">No failures.</p>
{% else %}
<div style="margin-bottom:16px;">
<button class="btn" id="retry-all-btn" onclick="retryAll()">Retry All ({{ failures|length }})</button>
<span id="retry-all-status" style="margin-left:12px;font-size:12px;"></span>
</div>
<table>
<tr><th>Filename</th><th>Error</th><th>Age</th><th>Retries</th><th>Actions</th></tr>
{% for f in failures %}
<tr>
<td>{{ f.filename or '?' }}</td>
<td style="color:#ff4444;font-size:11px;">{{ (f.error_message or 'unknown')[:100] }}</td>
<td class="text-dim text-xs">{{ f.discovered_at or '' }}</td>
<td>{{ f.retry_count or 0 }}</td>
<td>
<form method="post" action="/api/retry/{{ f.hash }}" style="display:inline;">
<button class="btn" type="submit">Retry</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
async function retryAll() {
var btn = document.getElementById('retry-all-btn');
var status = document.getElementById('retry-all-status');
if (!confirm('Retry all {{ failures|length }} failed documents?')) return;
btn.disabled = true;
status.style.color = '#ffa500';
status.textContent = 'Retrying...';
try {
var resp = await fetch('/api/retry-all', {method: 'POST'});
var data = await resp.json();
if (resp.ok) {
status.style.color = '#00ff41';
status.textContent = 'Retried ' + data.count + ' documents';
setTimeout(function() { location.reload(); }, 2000);
} else {
status.style.color = '#ff4444';
status.textContent = data.error || 'Failed';
}
} catch(e) {
status.style.color = '#ff4444';
status.textContent = 'Error: ' + e.message;
}
btn.disabled = false;
}
</script>
{% endblock %}

View file

@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">Upload PDF</h3>
<div class="panel">
<form id="upload-form" enctype="multipart/form-data">
<div class="mb-16">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">PDF File</label>
<input type="file" name="file" accept=".pdf" id="upload-file"
style="background:#0a0a0a;border:1px solid #333;color:#c0c0c0;padding:8px;width:100%;font-family:inherit;">
</div>
<div class="mb-16">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Category</label>
<input type="text" name="category" id="upload-category" list="cat-list" class="search-box"
placeholder="Select or type a category..." style="margin-bottom:0;">
<datalist id="cat-list">{{ options_html|safe }}</datalist>
</div>
<button type="submit" class="btn" id="upload-btn">Upload</button>
<span id="upload-status" style="margin-left:12px;font-size:12px;"></span>
</form>
</div>
<div id="upload-result" style="display:none;" class="panel"></div>
<h3 class="section-title">Recent Documents</h3>
<table>
<tr><th>Filename</th><th>Source</th><th>Status</th></tr>
{% for d in recent %}
<tr>
<td>{{ d.filename or '?' }}</td>
<td>{{ d.source or '' }}</td>
<td><span class="status status-{{ d.status or 'unknown' }}">{{ d.status or 'unknown' }}</span></td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('upload-form').addEventListener('submit', async function(e) {
e.preventDefault();
var btn = document.getElementById('upload-btn');
var status = document.getElementById('upload-status');
var result = document.getElementById('upload-result');
var fileInput = document.getElementById('upload-file');
var category = document.getElementById('upload-category').value;
if (!fileInput.files.length) {
status.style.color = '#ff4444';
status.textContent = 'No file selected';
return;
}
btn.disabled = true;
status.style.color = '#ffa500';
status.textContent = 'Uploading...';
result.style.display = 'none';
var formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('category', category);
try {
var resp = await fetch('/api/upload', { method: 'POST', body: formData });
var data = await resp.json();
if (resp.ok) {
status.style.color = '#00ff41';
status.textContent = 'Upload successful';
result.style.display = 'block';
result.innerHTML = '<span style="color:#00ff41;">Queued for processing</span><br>' +
'<span class="text-dim">Hash: ' + data.hash + '</span><br>' +
'<span class="text-dim">File: ' + data.filename + '</span><br>' +
'<span class="text-dim">Category: ' + data.source + '/' + data.category + '</span>';
fileInput.value = '';
} else {
status.style.color = '#ff4444';
status.textContent = data.error || 'Upload failed';
}
} catch (err) {
status.style.color = '#ff4444';
status.textContent = 'Network error: ' + err.message;
}
btn.disabled = false;
});
</script>
{% endblock %}

View file

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">Web Ingest</h3>
<div style="margin-bottom:8px;">
<a href="#single" class="btn active" onclick="showSection('single')" id="tab-single">Single/Batch URL</a>
<a href="#crawl" class="btn" onclick="showSection('crawl')" id="tab-crawl">Site Crawl</a>
</div>
<div id="section-single">
<div class="panel">
<div class="mb-16">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">URL(s) — one per line for batch</label>
<textarea id="wi-urls" class="search-box" rows="4" placeholder="https://example.com/article" style="resize:vertical;margin-bottom:0;"></textarea>
</div>
<div class="mb-16">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Category</label>
<input type="text" id="wi-category" list="wi-cat-list" class="search-box" value="Web"
placeholder="Category..." style="margin-bottom:0;">
<datalist id="wi-cat-list">{{ options_html|safe }}</datalist>
</div>
<button class="btn" id="wi-btn" onclick="doWebIngest()">Ingest</button>
<span id="wi-status" style="margin-left:12px;font-size:12px;"></span>
</div>
<div id="wi-results" style="display:none;" class="panel" style="max-height:300px;overflow-y:auto;"></div>
</div>
<div id="section-crawl" style="display:none;">
<div class="panel">
<div class="mb-16">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Site URL</label>
<input type="text" id="crawl-url" class="search-box" placeholder="https://example.com" style="margin-bottom:0;">
</div>
<div class="grid-2 mb-16">
<div>
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Category</label>
<input type="text" id="crawl-category" list="wi-cat-list" class="search-box" value="Web" style="margin-bottom:0;">
</div>
<div>
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Max Pages</label>
<input type="number" id="crawl-max-pages" class="search-box" value="500" min="1" max="5000" style="margin-bottom:0;">
</div>
</div>
<div class="grid-2 mb-16">
<div>
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Include Paths (comma-separated)</label>
<input type="text" id="crawl-include" class="search-box" placeholder="/docs/, /blog/" style="margin-bottom:0;">
</div>
<div>
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Exclude Paths (comma-separated)</label>
<input type="text" id="crawl-exclude" class="search-box" placeholder="/search, /login" style="margin-bottom:0;">
</div>
</div>
<button class="btn" id="crawl-preview-btn" onclick="doCrawl(true)">Preview</button>
<button class="btn" id="crawl-btn" onclick="doCrawl(false)" style="margin-left:8px;">Crawl &amp; Ingest</button>
<span id="crawl-status" style="margin-left:12px;font-size:12px;"></span>
</div>
<div id="crawl-results" style="display:none;" class="panel" style="max-height:400px;overflow-y:auto;font-size:12px;"></div>
</div>
<h3 class="section-title mt-24">Recent Web Ingestions</h3>
<table>
<tr><th>Title</th><th>Source/Category</th><th>Status</th><th>Pages</th><th>Concepts</th></tr>
{% for d in web_docs %}
<tr>
<td title="{{ d.path or '' }}" style="max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ d.book_title or d.filename or '?' }}</td>
<td>{{ d.source or '' }}/{{ d.category or '' }}</td>
<td><span class="status status-{{ d.status or 'unknown' }}">{{ d.status or 'unknown' }}</span></td>
<td>{{ d.pages_extracted or 0 }}</td>
<td>{{ d.concepts_extracted or 0 }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block scripts %}
<script src="/static/js/web-ingest.js"></script>
{% endblock %}

View file

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">PeerTube Channels</h3>
<div class="stat-grid" id="pt-stats" style="margin-bottom:24px;">
<div class="stat-card"><div class="value" id="pt-total-ch"></div><div class="label">Channels</div></div>
<div class="stat-card"><div class="value" id="pt-total-vid"></div><div class="label">Videos</div></div>
<div class="stat-card"><div class="value" id="pt-dl-status"></div><div class="label">Downloader</div></div>
</div>
<div class="panel">
<div class="flex gap-8" style="flex-wrap:wrap;align-items:flex-end;margin-bottom:12px;">
<div style="flex:1;min-width:250px;">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">YouTube URL</label>
<input type="text" id="pt-yt-url" class="search-box" placeholder="https://www.youtube.com/@ChannelName" style="margin-bottom:0;width:100%;">
</div>
<div style="min-width:150px;">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Category</label>
<input type="text" id="pt-category" list="pt-cat-list" class="search-box" placeholder="e.g. OPSEC/Privacy" style="margin-bottom:0;width:100%;">
<datalist id="pt-cat-list"></datalist>
</div>
<div style="min-width:60px;">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Priority</label>
<select id="pt-priority" style="background:#0a0a0a;border:1px solid #333;color:#c0c0c0;padding:6px 10px;font-family:inherit;font-size:12px;width:100%;">
<option value="M">M</option>
<option value="H">H</option>
<option value="L">L</option>
</select>
</div>
<button class="btn" id="pt-add-btn" onclick="addChannel()">Add Channel</button>
</div>
<div id="pt-feedback" style="font-size:12px;min-height:18px;"></div>
</div>
<div style="background:#111;border:1px solid #222;overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:12px;" id="pt-channel-table">
<thead>
<tr style="border-bottom:1px solid #222;">
<th style="text-align:left;padding:10px;">Channel</th>
<th style="text-align:center;padding:10px;">Videos</th>
<th style="text-align:left;padding:10px;">Category</th>
<th style="text-align:center;padding:10px;">Pri</th>
<th style="text-align:center;padding:10px;">Status</th>
<th style="text-align:center;padding:10px;width:60px;"></th>
</tr>
</thead>
<tbody id="pt-channel-tbody"><tr><td colspan="6" style="text-align:center;padding:20px;color:#555;">Loading...</td></tr></tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/channels.js"></script>
{% endblock %}

View file

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block content %}
<div id="pt-dashboard">
<div class="stat-grid" style="grid-template-columns:repeat(6, 1fr);">
<div class="stat-card"><div class="label">Published</div><div class="value" id="pt-published"></div></div>
<div class="stat-card"><div class="label">In Pipeline</div><div class="value" id="pt-in-pipeline"></div></div>
<div class="stat-card"><div class="label">Failed</div><div class="value" id="pt-failed"></div></div>
<div class="stat-card"><div class="label">Import Rate</div><div class="value" id="pt-import-rate"></div><div class="sublabel">/hour</div></div>
<div class="stat-card"><div class="label">GPU Util</div><div class="value" id="pt-gpu-util"></div><div class="sublabel">%</div></div>
<div class="stat-card"><div class="label">GPU Temp</div><div class="value" id="pt-gpu-temp"></div><div class="sublabel">&deg;C</div></div>
</div>
<div class="mb-24">
<div class="flex-between" style="margin-bottom:4px;font-size:11px;color:#888;">
<span>Pipeline Flow</span>
<span id="pt-pipeline-summary"></span>
</div>
<div id="pt-pipeline-bar" class="pipeline-bar"></div>
<div id="pt-pipeline-legend" class="pipeline-legend"></div>
</div>
<div class="svc-row">
<div class="svc-item"><span class="svc-dot unknown" id="svc-downloader"></span>Downloader</div>
<div class="svc-item"><span class="svc-dot unknown" id="svc-importer"></span>Importer</div>
<div class="svc-item"><span class="svc-dot unknown" id="svc-transcoder"></span>Transcoder</div>
<div class="svc-item"><span class="svc-dot unknown" id="svc-runner"></span>Runner</div>
</div>
<div id="pt-gpu-panel" class="panel" style="display:none;">
<h3 class="section-title" style="margin-bottom:8px;">GPU Status</h3>
<div id="pt-gpu-detail" class="text-small text-muted"></div>
</div>
<div id="pt-chart-container" class="panel" style="display:none;">
<h3 class="section-title" style="margin-bottom:8px;">Pipeline Activity (24h)</h3>
<canvas id="pt-chart" width="800" height="200" style="width:100%;height:200px;"></canvas>
</div>
<div id="pt-storage" class="panel">
<h3 class="section-title" style="margin-bottom:12px;">Pipeline Storage</h3>
<div id="pt-storage-content" class="text-small text-muted">Loading...</div>
</div>
<details id="pt-errors-panel" class="errors-panel panel">
<summary>Recent Errors (<span id="pt-error-count">0</span>)</summary>
<div id="pt-errors-content" style="margin-top:8px;"></div>
</details>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/charts.js"></script>
<script src="/static/js/peertube.js"></script>
{% endblock %}

41
templates/search.html Normal file
View file

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">Semantic Search</h3>
<form method="get" action="/search">
<input type="text" name="q" class="search-box" placeholder="Search the knowledge base..." value="{{ query or '' }}" autofocus>
</form>
{% if not query %}
<p class="text-dim text-small" style="margin-top:8px;">Enter a query to search across all embedded concepts.</p>
{% elif results is defined %}
<p class="text-dim text-small mb-16">{{ results|length }} results for: <strong class="text-green">{{ query }}</strong></p>
{% for r in results %}
<div class="result">
<span class="score">{{ '%.4f'|format(r.score) }}</span>
<div class="title">{{ r.title }}</div>
<div class="meta">
{{ r.citation }}
{% if r.download_url %}
{% if r.source_type == 'web' or (r.download_url.startswith('http') and 'files.echo6.co' not in r.download_url) %}
| <a href="{{ r.download_url }}" target="_blank" style="color:#00bfff;text-decoration:none;">Web</a>
{% else %}
| <a href="{{ r.download_url }}" style="color:#00bfff;text-decoration:none;">PDF</a>
{% endif %}
{% endif %}
{% if r.knowledge_type %}| {{ r.knowledge_type }}{% endif %}
{% if r.complexity %}/ {{ r.complexity }}{% endif %}
</div>
<div class="content-text">{{ r.summary }}</div>
<div style="margin-top:6px;">
{% for d in r.domains %}
<span class="domain-tag">{{ d }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
{% elif error %}
<p style="color:#ff4444;">Search error: {{ error }}</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,94 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">YouTube Cookies</h3>
<div class="panel">
<div id="cookie-status" style="margin-bottom:16px;font-size:12px;color:#666;">Loading cookie status...</div>
<div class="mb-16">
<label class="text-dim text-xs" style="text-transform:uppercase;display:block;margin-bottom:4px;">Cookies.txt File (Netscape format)</label>
<input type="file" id="cookie-file" accept=".txt"
style="background:#0a0a0a;border:1px solid #333;color:#c0c0c0;padding:8px;width:100%;font-family:inherit;">
</div>
<button class="btn" id="cookie-btn" onclick="uploadCookies()">Upload Cookies</button>
<span id="cookie-upload-status" style="margin-left:12px;font-size:12px;"></span>
<div id="cookie-result" style="display:none;background:#0a0a0a;border:1px solid #222;padding:12px;margin-top:16px;font-size:11px;white-space:pre-wrap;color:#888;max-height:200px;overflow-y:auto;"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadCookieStatus() {
try {
var resp = await fetch('/api/cookies/status');
var data = await resp.json();
if (resp.ok) {
var age = data.age_hours;
var ageStr, ageColor;
if (age < 24) {
ageStr = Math.round(age) + ' hours ago';
ageColor = '#00ff41';
} else {
var days = Math.round(age / 24);
ageStr = days + ' days ago';
ageColor = days > 14 ? '#ff4444' : days > 7 ? '#ffa500' : '#00ff41';
}
var html = '<span style="color:' + ageColor + ';">Last updated: ' + ageStr + '</span>';
if (data.is_stale) {
html += ' <span style="color:#ff4444;font-weight:bold;">[STALE - cookies likely expired]</span>';
}
if (data.recent_rate_limits > 0) {
html += '<br><span style="color:#ffa500;">YouTube rate limits in last 30min: ' + data.recent_rate_limits + '</span>';
}
html += '<br><span class="text-faint">Downloader: ' + (data.downloader_active ? 'active' : 'stopped') + '</span>';
document.getElementById('cookie-status').innerHTML = html;
} else {
document.getElementById('cookie-status').innerHTML = '<span class="text-red">Could not check cookie status</span>';
}
} catch(e) {
document.getElementById('cookie-status').innerHTML = '<span class="text-red">Error: ' + e.message + '</span>';
}
}
async function uploadCookies() {
var fileInput = document.getElementById('cookie-file');
var btn = document.getElementById('cookie-btn');
var status = document.getElementById('cookie-upload-status');
var result = document.getElementById('cookie-result');
if (!fileInput.files.length) {
status.style.color = '#ff4444';
status.textContent = 'No file selected';
return;
}
btn.disabled = true;
status.style.color = '#ffa500';
status.textContent = 'Uploading and testing cookies...';
result.style.display = 'none';
var formData = new FormData();
formData.append('file', fileInput.files[0]);
try {
var resp = await fetch('/api/cookies/upload', { method: 'POST', body: formData });
var data = await resp.json();
if (data.ok) {
status.style.color = '#00ff41';
status.textContent = 'Cookies updated and verified';
result.style.display = 'block';
result.style.borderColor = '#00ff41';
result.innerHTML = '<span style="color:#00ff41;">SUCCESS</span><br>' + (data.test_output || '') + '<br>Data lines: ' + data.data_lines;
loadCookieStatus();
} else {
status.style.color = data.error ? '#ff4444' : '#ffa500';
status.textContent = data.error || data.message || 'Upload issue';
if (data.test_output) {
result.style.display = 'block';
result.style.borderColor = '#ff4444';
result.textContent = data.test_output;
}
}
} catch(e) {
status.style.color = '#ff4444';
status.textContent = 'Network error: ' + e.message;
}
btn.disabled = false;
}
loadCookieStatus();
</script>
{% endblock %}

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">Service Health</h3>
<div id="health-grid" class="stat-grid" style="grid-template-columns:repeat(auto-fit, minmax(250px, 1fr));">
<div class="stat-card">
<div class="label">Qdrant</div>
<div class="value text-small" id="h-qdrant"><span class="svc-dot unknown"></span>Checking...</div>
</div>
<div class="stat-card">
<div class="label">TEI Embeddings</div>
<div class="value text-small" id="h-tei"><span class="svc-dot unknown"></span>Checking...</div>
</div>
<div class="stat-card">
<div class="label">NFS Mount</div>
<div class="value text-small" id="h-nfs"><span class="svc-dot unknown"></span>Checking...</div>
</div>
<div class="stat-card">
<div class="label">Gemini API</div>
<div class="value text-small" id="h-gemini"><span class="svc-dot unknown"></span>Checking...</div>
</div>
</div>
<h3 class="section-title mt-24">Pipeline Status</h3>
<div id="h-pipeline" class="panel text-small text-dim">Loading...</div>
{% endblock %}
{% block scripts %}
<script>
async function loadHealth() {
try {
var resp = await fetch('/api/health');
var data = await resp.json();
var c = data.components || {};
function dot(status) {
var cls = status === 'up' ? 'active' : (status === 'configured' ? 'active' : 'inactive');
return '<span class="svc-dot ' + cls + '"></span>';
}
var q = c.qdrant || {};
document.getElementById('h-qdrant').innerHTML = dot(q.status) + (q.status === 'up' ? 'Online — ' + RECON.fmt(q.vectors) + ' vectors' : 'Offline' + (q.error ? ' — ' + q.error : ''));
var t = c.tei || {};
document.getElementById('h-tei').innerHTML = dot(t.status) + (t.status === 'up' ? 'Online' : 'Offline' + (t.error ? ' — ' + t.error : ''));
var n = c.nfs || {};
document.getElementById('h-nfs').innerHTML = dot(n.status) + (n.status === 'up' ? 'Mounted' : 'Not mounted');
var g = c.gemini || {};
document.getElementById('h-gemini').innerHTML = dot(g.status === 'configured' ? 'up' : 'down') + (g.status === 'configured' ? g.keys + ' keys configured' : 'No keys');
// Pipeline
var p = data.pipeline || {};
var html = '';
Object.keys(p).forEach(function(k) {
html += '<div style="margin:4px 0;"><span class="status status-' + k + '">' + k + '</span>: ' + p[k] + '</div>';
});
document.getElementById('h-pipeline').innerHTML = html || '<span class="text-dim">No pipeline data</span>';
} catch(e) {
document.getElementById('h-qdrant').innerHTML = '<span class="svc-dot inactive"></span>Error: ' + e.message;
}
}
document.addEventListener('DOMContentLoaded', function() {
RECON.startRefresh(loadHealth, 30000);
});
</script>
{% endblock %}

View file

@ -0,0 +1,137 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">API Keys</h3>
<div style="margin-bottom:20px;">
<button class="btn" onclick="validateAll()" id="btn-validate">Validate All</button>
<button class="btn" onclick="reloadKeys()" style="margin-left:8px;">Reload from .env</button>
<button class="btn btn-warn" onclick="restartService()" style="margin-left:8px;">Restart Service</button>
<span id="validate-status" style="margin-left:12px;color:#666;font-size:12px;"></span>
</div>
<table id="keys-table">
<tr><th>#</th><th>Key</th><th>Status</th><th>Calls</th><th>Errors</th><th>Last Used</th><th>Actions</th></tr>
{% for k in keys_data %}
<tr id="key-row-{{ k.index }}">
<td>{{ k.index + 1 }}</td>
<td class="mono text-small">{{ k.masked }}</td>
<td>
{% if k.valid is true %}
<span class="text-green">Valid</span>
{% elif k.valid is false %}
<span class="text-red">Invalid</span>
{% else %}
<span class="text-dim">&mdash;</span>
{% endif %}
</td>
<td>{{ k.calls }}</td>
<td class="{% if k.errors %}text-red{% else %}text-muted{% endif %}">{{ k.errors }}</td>
<td class="text-dim text-xs">{{ k.last_used or '&mdash;' }}</td>
<td>
<button class="btn text-xs" onclick="validateKey({{ k.index }})">Test</button>
<button class="btn btn-danger text-xs" onclick="removeKey({{ k.index }})">Remove</button>
</td>
</tr>
{% endfor %}
</table>
<div style="margin-top:24px;border-top:1px solid #222;padding-top:16px;">
<h4 class="text-muted" style="margin-bottom:12px;">Add Key</h4>
<div class="flex gap-8" style="align-items:center;">
<input type="text" id="new-key" placeholder="Paste Gemini API key..."
style="flex:1;background:#1a1a1a;border:1px solid #333;color:#ccc;padding:8px 12px;border-radius:4px;font-family:monospace;font-size:13px;">
<button class="btn" onclick="addKey()">Add</button>
</div>
<div id="add-result" style="margin-top:8px;font-size:12px;"></div>
</div>
<div style="margin-top:24px;border-top:1px solid #222;padding-top:16px;">
<h4 class="text-muted" style="margin-bottom:12px;">Replace Key</h4>
<div class="flex gap-8" style="align-items:center;">
<input type="number" id="replace-index" placeholder="#" min="0" max="9"
style="width:50px;background:#1a1a1a;border:1px solid #333;color:#ccc;padding:8px;border-radius:4px;text-align:center;">
<input type="text" id="replace-key" placeholder="New Gemini API key..."
style="flex:1;background:#1a1a1a;border:1px solid #333;color:#ccc;padding:8px 12px;border-radius:4px;font-family:monospace;font-size:13px;">
<button class="btn" onclick="replaceKey()">Replace</button>
</div>
<div id="replace-result" style="margin-top:8px;font-size:12px;"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function validateAll() {
document.getElementById('btn-validate').disabled = true;
document.getElementById('validate-status').textContent = 'Validating...';
try {
var r = await fetch('/api/keys/validate', {method:'POST'});
var data = await r.json();
document.getElementById('validate-status').textContent = 'Done — ' + data.results.filter(function(r){return r.valid;}).length + '/' + data.results.length + ' valid';
setTimeout(function() { location.reload(); }, 1000);
} catch(e) {
document.getElementById('validate-status').textContent = 'Error: ' + e;
}
document.getElementById('btn-validate').disabled = false;
}
async function validateKey(idx) {
try {
var r = await fetch('/api/keys/' + idx + '/validate', {method:'POST'});
var data = await r.json();
alert('Key ' + (idx+1) + ': ' + data.message);
location.reload();
} catch(e) { alert('Error: ' + e); }
}
async function removeKey(idx) {
if (!confirm('Remove key ' + (idx+1) + '? Pipeline needs at least 1 key.')) return;
try {
var r = await fetch('/api/keys/' + idx, {method:'DELETE'});
var data = await r.json();
if (data.error) { alert(data.error); return; }
location.reload();
} catch(e) { alert('Error: ' + e); }
}
async function addKey() {
var key = document.getElementById('new-key').value.trim();
if (!key) return;
try {
var r = await fetch('/api/keys', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({key:key})});
var data = await r.json();
if (data.error) { document.getElementById('add-result').innerHTML = '<span class="text-red">' + data.error + '</span>'; return; }
document.getElementById('add-result').innerHTML = '<span class="text-green">Added at position ' + (data.index+1) + '</span>';
setTimeout(function() { location.reload(); }, 1000);
} catch(e) { document.getElementById('add-result').innerHTML = '<span class="text-red">' + e + '</span>'; }
}
async function replaceKey() {
var idx = parseInt(document.getElementById('replace-index').value) - 1;
var key = document.getElementById('replace-key').value.trim();
if (isNaN(idx) || !key) return;
try {
var r = await fetch('/api/keys/' + idx, {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({key:key})});
var data = await r.json();
if (data.error) { document.getElementById('replace-result').innerHTML = '<span class="text-red">' + data.error + '</span>'; return; }
document.getElementById('replace-result').innerHTML = '<span class="text-green">Replaced key ' + (idx+1) + '</span>';
setTimeout(function() { location.reload(); }, 1000);
} catch(e) { document.getElementById('replace-result').innerHTML = '<span class="text-red">' + e + '</span>'; }
}
async function restartService() {
if (!confirm('Restart RECON service? Pipeline will pause for ~10 seconds.')) return;
document.getElementById('validate-status').textContent = 'Restarting...';
try {
await fetch('/api/service/restart', {method:'POST'});
} catch(e) {}
document.getElementById('validate-status').innerHTML = '<span style="color:#ff8800;">Restarting... page will reload in 10s</span>';
setTimeout(function() { location.reload(); }, 30000);
}
async function reloadKeys() {
try {
var r = await fetch('/api/keys/reload', {method:'POST'});
var data = await r.json();
alert('Reloaded ' + data.count + ' key(s) from .env');
location.reload();
} catch(e) { alert('Error: ' + e); }
}
</script>
{% endblock %}

View file

@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block content %}
<h3 class="section-title mb-16">NordVPN</h3>
<div class="panel">
<div id="vpn-status" style="margin-bottom:16px;font-size:12px;color:#666;">Loading VPN status...</div>
<div class="flex gap-8" style="flex-wrap:wrap;margin-bottom:12px;">
<button class="btn" onclick="vpnRotate()" id="vpn-rotate-btn">Rotate</button>
<button class="btn" onclick="vpnDisconnect()" id="vpn-disconnect-btn">Disconnect</button>
<select id="vpn-country" style="background:#0a0a0a;border:1px solid #333;color:#c0c0c0;padding:6px;font-family:inherit;font-size:12px;">
<option value="United_States">United States</option>
<option value="Canada">Canada</option>
<option value="United_Kingdom">United Kingdom</option>
<option value="Germany">Germany</option>
<option value="Netherlands">Netherlands</option>
<option value="Sweden">Sweden</option>
</select>
<button class="btn" onclick="vpnConnect()" id="vpn-connect-btn">Connect</button>
</div>
<span id="vpn-action-status" style="font-size:12px;"></span>
<details style="margin-top:16px;">
<summary class="text-faint" style="cursor:pointer;font-size:11px;">Setup (one-time)</summary>
<div style="margin-top:8px;">
<input type="password" id="vpn-token" placeholder="NordVPN token"
style="background:#0a0a0a;border:1px solid #333;color:#c0c0c0;padding:6px;width:300px;font-family:inherit;font-size:12px;">
<button class="btn" onclick="vpnLogin()">Login</button>
<span id="vpn-login-status" style="font-size:11px;margin-left:8px;"></span>
</div>
</details>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadVpnStatus() {
try {
var resp = await fetch('/api/vpn/status');
var data = await resp.json();
if (resp.ok) {
var dot = data.connected ? '<span style="color:#00ff41;">&#9679;</span>' : '<span style="color:#ff4444;">&#9679;</span>';
var html = dot + ' ' + (data.connected ? 'Connected' : 'Disconnected');
if (data.connected) {
html += ' &mdash; <span style="color:#00ff41;">' + data.country + '</span>';
html += ' <span class="text-faint">(' + data.ip + ')</span>';
}
if (data.rotations_today > 0) {
html += '<br><span class="text-faint">Rotations today: ' + data.rotations_today + '</span>';
}
document.getElementById('vpn-status').innerHTML = html;
}
} catch(e) {
document.getElementById('vpn-status').innerHTML = '<span class="text-red">Error: ' + e.message + '</span>';
}
}
async function vpnAction(url, opts, statusEl) {
var el = document.getElementById(statusEl || 'vpn-action-status');
el.style.color = '#ffa500';
el.textContent = 'Working...';
try {
var resp = await fetch(url, opts);
var data = await resp.json();
if (data.ok) {
el.style.color = '#00ff41';
el.textContent = data.country ? (data.country + ' (' + data.ip + ')') : (data.message || 'Done');
} else {
el.style.color = '#ff4444';
el.textContent = data.error || data.message || 'Failed';
}
loadVpnStatus();
} catch(e) {
el.style.color = '#ff4444';
el.textContent = 'Error: ' + e.message;
}
}
function vpnRotate() { vpnAction('/api/vpn/rotate', {method:'POST'}); }
function vpnDisconnect() { vpnAction('/api/vpn/disconnect', {method:'POST'}); }
function vpnConnect() {
var country = document.getElementById('vpn-country').value;
vpnAction('/api/vpn/connect', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({country: country})
});
}
function vpnLogin() {
var token = document.getElementById('vpn-token').value;
if (!token) return;
vpnAction('/api/vpn/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({token: token})
}, 'vpn-login-status');
}
loadVpnStatus();
</script>
{% endblock %}