feat: rename PROPAGANDA to UNREDACTED, add nav block-reveal animation and global search

This commit is contained in:
jae 2026-04-15 18:50:19 +00:00
parent e6b36c1ca8
commit 8bb4c6f727
10 changed files with 562 additions and 50 deletions

View file

@ -7,7 +7,7 @@ from pathlib import Path
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from flask import Flask, request, jsonify, abort, send_file from flask import Flask, request, jsonify, abort, send_file, redirect
from flask_cors import CORS from flask_cors import CORS
import jwt import jwt
import requests as req import requests as req
@ -1526,29 +1526,29 @@ def api_govdomains_stats():
}) })
# ─── Propaganda: Declassified Document Archive ──────── # ─── Unredacted: Declassified Document Archive ────────
_propaganda_cache = None _unredacted_cache = None
_propaganda_mtime = 0 _unredacted_mtime = 0
def _load_propaganda(): def _load_unredacted():
global _propaganda_cache, _propaganda_mtime global _unredacted_cache, _unredacted_mtime
p = DATA_DIR / 'propaganda.json' p = DATA_DIR / 'unredacted.json'
if not p.exists(): if not p.exists():
return {'categories': []} return {'categories': []}
mt = p.stat().st_mtime mt = p.stat().st_mtime
if _propaganda_cache is None or mt != _propaganda_mtime: if _unredacted_cache is None or mt != _unredacted_mtime:
with open(p, encoding='utf-8') as f: with open(p, encoding='utf-8') as f:
_propaganda_cache = json.load(f) _unredacted_cache = json.load(f)
_propaganda_mtime = mt _unredacted_mtime = mt
return _propaganda_cache return _unredacted_cache
@app.route('/api/propaganda') @app.route('/api/unredacted')
def get_propaganda(): def get_unredacted():
return jsonify(_load_propaganda()) return jsonify(_load_unredacted())
@app.route('/api/propaganda/categories') @app.route('/api/unredacted/categories')
def get_propaganda_categories(): def get_unredacted_categories():
db = _load_propaganda() db = _load_unredacted()
cats = [] cats = []
for c in db.get('categories', []): for c in db.get('categories', []):
n_countries = len(c.get('countries', [])) n_countries = len(c.get('countries', []))
@ -1565,18 +1565,18 @@ def get_propaganda_categories():
}) })
return jsonify({'categories': cats}) return jsonify({'categories': cats})
@app.route('/api/propaganda/category/<cat_id>') @app.route('/api/unredacted/category/<cat_id>')
def get_propaganda_category(cat_id): def get_unredacted_category(cat_id):
db = _load_propaganda() db = _load_unredacted()
for c in db.get('categories', []): for c in db.get('categories', []):
if c['id'] == cat_id: if c['id'] == cat_id:
return jsonify(c) return jsonify(c)
abort(404, f'Category {cat_id} not found') abort(404, f'Category {cat_id} not found')
@app.route('/api/propaganda/upload', methods=['POST']) @app.route('/api/unredacted/upload', methods=['POST'])
@require_auth @require_auth
def upload_propaganda_doc(): def upload_unredacted_doc():
"""Upload a PDF to the propaganda archive. Future admin use.""" """Upload a PDF to the unredacted archive. Future admin use."""
try: try:
if 'file' not in request.files: if 'file' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400 return jsonify({'error': 'No file uploaded'}), 400
@ -1587,8 +1587,7 @@ def upload_propaganda_doc():
return jsonify({'error': 'category and collection required'}), 400 return jsonify({'error': 'category and collection required'}), 400
if not f.filename.lower().endswith('.pdf'): if not f.filename.lower().endswith('.pdf'):
return jsonify({'error': 'Only PDF files accepted'}), 400 return jsonify({'error': 'Only PDF files accepted'}), 400
# Save file — this saves locally; on VPS would save to /propaganda/docs/ save_dir = Path('/var/www/jaeswift-homepage/unredacted/docs') / cat_id / col_id
save_dir = Path('/var/www/jaeswift-homepage/propaganda/docs') / cat_id / col_id
save_dir.mkdir(parents=True, exist_ok=True) save_dir.mkdir(parents=True, exist_ok=True)
dest = save_dir / f.filename dest = save_dir / f.filename
f.save(str(dest)) f.save(str(dest))
@ -1596,5 +1595,18 @@ def upload_propaganda_doc():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# ─── Legacy redirects: /api/propaganda → /api/unredacted ────────
@app.route('/api/propaganda')
def redirect_propaganda():
return redirect('/api/unredacted', code=301)
@app.route('/api/propaganda/categories')
def redirect_propaganda_categories():
return redirect('/api/unredacted/categories', code=301)
@app.route('/api/propaganda/category/<cat_id>')
def redirect_propaganda_category(cat_id):
return redirect(f'/api/unredacted/category/{cat_id}', code=301)
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False) app.run(host='0.0.0.0', port=5000, debug=False)

View file

@ -86,8 +86,8 @@
"description": "16,000+ free resources catalogued" "description": "16,000+ free resources catalogued"
}, },
{ {
"label": "PROPAGANDA", "label": "UNREDACTED",
"url": "/depot/propaganda", "url": "/depot/unredacted",
"description": "Classified documents & briefings" "description": "Classified documents & briefings"
}, },
{ {

View file

@ -1,5 +1,5 @@
/* /*
AWESOME LISTS // Propaganda Resource Database AWESOME LISTS // Unredacted Resource Database
*/ */
.al-container { .al-container {

View file

@ -1,5 +1,5 @@
/* /*
PROPAGANDA Declassified Document Archive UNREDACTED Declassified Document Archive
*/ */
/* ─── Stats Bar ──────────────────────────────────────── */ /* ─── Stats Bar ──────────────────────────────────────── */
@ -879,4 +879,208 @@
.prop-grid { .prop-grid {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
grid-template-columns: repeat(3, 1fr);
}
}
/*
GLOBAL ARCHIVE SEARCH
*/
.ur-search-container {
margin: 1.5rem 0 2rem;
}
.ur-search-bar {
display: flex;
align-items: center;
background: #0a0a0a;
border: 1px solid #00cc33;
border-radius: 2px;
padding: 0;
position: relative;
box-shadow: 0 0 8px rgba(0, 204, 51, 0.15), inset 0 0 20px rgba(0, 0, 0, 0.5);
transition: border-color 0.2s, box-shadow 0.2s;
}
.ur-search-bar:focus-within {
border-color: #00ff41;
box-shadow: 0 0 15px rgba(0, 204, 51, 0.3), inset 0 0 20px rgba(0, 0, 0, 0.5);
}
.ur-search-icon {
color: #00cc33;
font-size: 1.2rem;
padding: 0.75rem 0 0.75rem 1rem;
opacity: 0.7;
pointer-events: none;
}
.ur-search-input {
flex: 1;
background: transparent;
border: none;
color: #e0e0e0;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
letter-spacing: 0.08em;
padding: 0.75rem 0.75rem;
outline: none;
text-transform: uppercase;
}
.ur-search-input::placeholder {
color: #555;
letter-spacing: 0.1em;
}
.ur-search-clear {
color: #888;
font-size: 1rem;
padding: 0.75rem 1rem;
cursor: pointer;
transition: color 0.2s;
}
.ur-search-clear:hover {
color: #ff4444;
}
.ur-search-status {
margin-top: 0.75rem;
min-height: 1.2rem;
}
.ur-search-count {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.12em;
color: #00cc33;
text-transform: uppercase;
}
.ur-search-none {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.1em;
color: #ff6b35;
text-transform: uppercase;
}
.ur-search-results {
margin-top: 0.5rem;
}
.ur-results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1rem;
}
.ur-result-card {
background: linear-gradient(135deg, rgba(0, 204, 51, 0.04), rgba(0, 0, 0, 0.4));
border: 1px solid rgba(0, 204, 51, 0.15);
border-radius: 2px;
padding: 1rem 1.2rem;
cursor: pointer;
transition: border-color 0.2s, background 0.2s, transform 0.15s;
position: relative;
}
.ur-result-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 3px;
height: 100%;
background: #00cc33;
opacity: 0;
transition: opacity 0.2s;
}
.ur-result-card:hover {
border-color: rgba(0, 204, 51, 0.4);
background: linear-gradient(135deg, rgba(0, 204, 51, 0.08), rgba(0, 0, 0, 0.5));
transform: translateY(-1px);
}
.ur-result-card:hover::before {
opacity: 1;
}
.ur-result-header {
display: flex;
align-items: flex-start;
gap: 0.6rem;
margin-bottom: 0.6rem;
}
.ur-result-icon {
font-size: 1.1rem;
flex-shrink: 0;
margin-top: 0.1rem;
}
.ur-result-title {
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
font-weight: 600;
color: #e0e0e0;
letter-spacing: 0.04em;
line-height: 1.3;
}
.ur-result-meta {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 0.6rem;
}
.ur-result-badge {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
letter-spacing: 0.08em;
padding: 0.2rem 0.5rem;
border-radius: 1px;
text-transform: uppercase;
background: rgba(0, 204, 51, 0.1);
color: #00cc33;
border: 1px solid rgba(0, 204, 51, 0.2);
}
.ur-result-badge-country {
background: rgba(255, 193, 7, 0.08);
color: #ffc107;
border-color: rgba(255, 193, 7, 0.2);
}
.ur-result-badge-col {
background: rgba(100, 149, 237, 0.08);
color: #6495ed;
border-color: rgba(100, 149, 237, 0.2);
}
.ur-result-desc {
font-family: 'JetBrains Mono', monospace;
font-size: 0.72rem;
color: #888;
line-height: 1.5;
letter-spacing: 0.02em;
}
mark.ur-highlight {
background: rgba(0, 204, 51, 0.25);
color: #00ff41;
padding: 0 2px;
border-radius: 1px;
}
@media (max-width: 768px) {
.ur-results-grid {
grid-template-columns: 1fr;
}
.ur-search-input {
font-size: 0.78rem;
}
} }

View file

@ -42,9 +42,9 @@
<div class="section-card-desc">The largest classified index of free resources on the internet — catalogued by type.</div> <div class="section-card-desc">The largest classified index of free resources on the internet — catalogued by type.</div>
<div class="section-card-arrow">ENTER →</div> <div class="section-card-arrow">ENTER →</div>
</a> </a>
<a href="/depot/propaganda" class="section-card"> <a href="/depot/unredacted" class="section-card">
<div class="section-card-icon"></div> <div class="section-card-icon"></div>
<div class="section-card-title">PROPAGANDA</div> <div class="section-card-title">UNREDACTED</div>
<div class="section-card-desc">Recovered publications and documents preserved in digital format. Browse on-site.</div> <div class="section-card-desc">Recovered publications and documents preserved in digital format. Browse on-site.</div>
<div class="section-card-arrow">ENTER →</div> <div class="section-card-arrow">ENTER →</div>
</a> </a>

View file

@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAESWIFT // PROPAGANDA — Declassified Document Archive</title> <title>JAESWIFT // UNREDACTED — Declassified Document Archive</title>
<meta name="description" content="Declassified document archive. Browse categorised intelligence files, military reports, and government documents with built-in PDF viewer."> <meta name="description" content="Declassified document archive. Browse categorised intelligence files, military reports, and government documents with built-in PDF viewer.">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/section.css"> <link rel="stylesheet" href="/css/section.css">
<link rel="stylesheet" href="/css/propaganda.css"> <link rel="stylesheet" href="/css/unredacted.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
</head> </head>
<body> <body>
@ -36,18 +36,18 @@
<span class="separator">/</span> <span class="separator">/</span>
<a href="/depot">DEPOT</a> <a href="/depot">DEPOT</a>
<span class="separator">/</span> <span class="separator">/</span>
<span class="current">PROPAGANDA</span> <span class="current">UNREDACTED</span>
</div> </div>
<section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);"> <section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);">
<div class="section-header-label">DEPOT // DECLASSIFIED ARCHIVE</div> <div class="section-header-label">DEPOT // DECLASSIFIED ARCHIVE</div>
<h1 class="section-header-title">PROPAGANDA</h1> <h1 class="section-header-title">UNREDACTED</h1>
<p class="section-header-sub">&gt; Declassified documents, intelligence briefings, and government files. Browse the archive.</p> <p class="section-header-sub">&gt; Declassified documents, intelligence briefings, and government files. Browse the archive.</p>
</section> </section>
<section class="subpage-content" style="max-width: clamp(1200px, 90vw, 2400px); margin: 0 auto; padding: 0 2rem 3rem;"> <section class="subpage-content" style="max-width: clamp(1200px, 90vw, 2400px); margin: 0 auto; padding: 0 2rem 3rem;">
<div id="propagandaRoot"> <div id="unredactedRoot">
<div class="prop-loading">INITIALISING PROPAGANDA ARCHIVE...</div> <div class="prop-loading">INITIALISING UNREDACTED ARCHIVE...</div>
</div> </div>
</section> </section>
@ -66,6 +66,6 @@
<script src="/js/wallet-connect.js"></script> <script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/propaganda.js"></script> <script src="/js/unredacted.js"></script>
</body> </body>
</html> </html>

View file

@ -87,6 +87,8 @@
initMobileDropdowns(); initMobileDropdowns();
// Inject shared wrapper for SOL price + wallet, then inject both // Inject shared wrapper for SOL price + wallet, then inject both
// Start UNREDACTED block-reveal animation
initUnredactedAnimation();
injectSolWalletGroup(); injectSolWalletGroup();
} catch (err) { } catch (err) {
console.warn('Nav load failed, keeping existing:', err); console.warn('Nav load failed, keeping existing:', err);
@ -449,6 +451,97 @@
if (dd) dd.classList.add('hidden'); if (dd) dd.classList.add('hidden');
} }
// ─── UNREDACTED Block-Reveal Animation ──────────────────────
function initUnredactedAnimation() {
// Find the UNREDACTED dropdown link
const allLinks = document.querySelectorAll('.dropdown-link');
let targetLink = null;
for (const link of allLinks) {
// Check the first text node or childNode text for UNREDACTED
const txt = link.childNodes[0];
if (txt && txt.nodeType === 3 && txt.textContent.trim() === 'UNREDACTED') {
targetLink = link;
break;
}
}
if (!targetLink) return;
const WORD = 'UNREDACTED';
const LEN = WORD.length;
const FRAME_MS = 90;
const HOLD_REVEALED = 2000;
const HOLD_REDACTED = 1000;
const BLOCK = '\u2588'; // █
// Wrap the text node in a span so we can animate just the label text
const originalText = targetLink.childNodes[0];
const animSpan = document.createElement('span');
animSpan.className = 'unredacted-anim';
animSpan.textContent = WORD;
targetLink.replaceChild(animSpan, originalText);
// Add minimal CSS for the animation
const style = document.createElement('style');
style.textContent = `
.unredacted-anim { font-family: inherit; }
.unredacted-anim .ur-block { color: #00cc33; opacity: 0.35; }
`;
document.head.appendChild(style);
function renderFrame(revealCount) {
// revealCount = number of letters revealed from the left
let html = '';
for (let i = 0; i < LEN; i++) {
if (i < revealCount) {
html += WORD[i];
} else {
html += '<span class="ur-block">' + BLOCK + '</span>';
}
}
animSpan.innerHTML = html;
}
let frame = 0;
let direction = 1; // 1 = revealing, -1 = redacting
let holdTimer = null;
function tick() {
if (direction === 1) {
// Forward reveal
renderFrame(frame);
frame++;
if (frame > LEN) {
// Fully revealed — hold
animSpan.textContent = WORD;
direction = 0;
holdTimer = setTimeout(() => {
direction = -1;
frame = LEN;
holdTimer = null;
}, HOLD_REVEALED);
}
} else if (direction === -1) {
// Reverse redact
frame--;
renderFrame(frame);
if (frame <= 0) {
// Fully redacted — hold
direction = 0;
holdTimer = setTimeout(() => {
direction = 1;
frame = 0;
holdTimer = null;
}, HOLD_REDACTED);
}
}
// direction === 0 means we're holding, do nothing
}
// Start fully redacted
renderFrame(0);
setInterval(tick, FRAME_MS);
}
// Run on DOM ready // Run on DOM ready
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {

View file

@ -1,23 +1,23 @@
/* /*
PROPAGANDA Declassified Document Archive UNREDACTED Declassified Document Archive
SPA Engine with hash routing & PDF.js viewer SPA Engine with hash routing & PDF.js viewer
*/ */
(function () { (function () {
'use strict'; 'use strict';
const ROOT = document.getElementById('propagandaRoot'); const ROOT = document.getElementById('unredactedRoot');
const PDF_BASE = '/propaganda/docs'; const PDF_BASE = '/unredacted/docs';
let DATA = null; let DATA = null;
// ─── Data Loading ──────────────────────────────────── // ─── Data Loading ────────────────────────────────────
async function loadData() { async function loadData() {
try { try {
const r = await fetch('/api/propaganda'); const r = await fetch('/api/unredacted');
if (!r.ok) throw new Error(`HTTP ${r.status}`); if (!r.ok) throw new Error(`HTTP ${r.status}`);
DATA = await r.json(); DATA = await r.json();
} catch (e) { } catch (e) {
console.error('PROPAGANDA: data load failed', e); console.error('UNREDACTED: data load failed', e);
ROOT.innerHTML = `<div class="prop-empty"> ROOT.innerHTML = `<div class="prop-empty">
<div class="prop-empty-icon"></div> <div class="prop-empty-icon"></div>
<div class="prop-empty-title">DATA FEED OFFLINE</div> <div class="prop-empty-title">DATA FEED OFFLINE</div>
@ -121,10 +121,21 @@
<div class="prop-stat"><span class="prop-stat-label">DOCUMENTS</span><span class="prop-stat-value red">${stats.docs}</span></div> <div class="prop-stat"><span class="prop-stat-label">DOCUMENTS</span><span class="prop-stat-value red">${stats.docs}</span></div>
</div>`; </div>`;
html += breadcrumb([{ label: 'PROPAGANDA' }]); html += breadcrumb([{ label: 'UNREDACTED' }]);
// ─── Global Search Bar ────────────────────────────────
html += `<div class="ur-search-container">
<div class="ur-search-bar">
<span class="ur-search-icon"></span>
<input type="text" id="urSearchInput" class="ur-search-input" placeholder="SEARCH CLASSIFIED ARCHIVES..." autocomplete="off" spellcheck="false">
<span class="ur-search-clear" id="urSearchClear" style="display:none;"></span>
</div>
<div class="ur-search-status" id="urSearchStatus"></div>
<div class="ur-search-results" id="urSearchResults"></div>
</div>`;
html += '<div id="urCategorySection">';
html += '<div class="prop-section-label">SELECT CATEGORY</div>'; html += '<div class="prop-section-label">SELECT CATEGORY</div>';
html += '<div class="prop-grid">';
DATA.categories.forEach(cat => { DATA.categories.forEach(cat => {
const nCountries = countCountries(cat); const nCountries = countCountries(cat);
@ -144,7 +155,11 @@
}); });
html += '</div>'; html += '</div>';
html += '</div>'; // close #urCategorySection
ROOT.innerHTML = html; ROOT.innerHTML = html;
// ─── Bind search events ────────────────────────────────
initSearchListeners();
} }
// ─── View: Category (Country Selector + Collections) ── // ─── View: Category (Country Selector + Collections) ──
@ -156,7 +171,7 @@
html += `<div class="prop-classification">DECLASSIFIED // ${esc(cat.name)} // ACCESS GRANTED</div>`; html += `<div class="prop-classification">DECLASSIFIED // ${esc(cat.name)} // ACCESS GRANTED</div>`;
html += breadcrumb([ html += breadcrumb([
{ label: 'PROPAGANDA', hash: '' }, { label: 'UNREDACTED', hash: '' },
{ label: cat.name } { label: cat.name }
]); ]);
@ -251,7 +266,7 @@
html += `<div class="prop-classification">DECLASSIFIED // ${esc(col.name).toUpperCase()} // ACCESS GRANTED</div>`; html += `<div class="prop-classification">DECLASSIFIED // ${esc(col.name).toUpperCase()} // ACCESS GRANTED</div>`;
html += breadcrumb([ html += breadcrumb([
{ label: 'PROPAGANDA', hash: '' }, { label: 'UNREDACTED', hash: '' },
{ label: cat.name, hash: `category/${catId}` }, { label: cat.name, hash: `category/${catId}` },
{ label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` }, { label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` },
{ label: col.name } { label: col.name }
@ -309,7 +324,7 @@
html += `<div class="prop-classification">DECLASSIFIED // ${esc(doc.title).toUpperCase()}</div>`; html += `<div class="prop-classification">DECLASSIFIED // ${esc(doc.title).toUpperCase()}</div>`;
html += breadcrumb([ html += breadcrumb([
{ label: 'PROPAGANDA', hash: '' }, { label: 'UNREDACTED', hash: '' },
{ label: cat.name, hash: `category/${catId}` }, { label: cat.name, hash: `category/${catId}` },
{ label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` }, { label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` },
{ label: col.name, hash: `collection/${catId}/${countryCode}/${colId}` }, { label: col.name, hash: `collection/${catId}/${countryCode}/${colId}` },
@ -582,7 +597,7 @@
}); });
pdfAllText.push({ pageNum: i, text: fullText, items: tc.items, itemOffsets: itemOffsets }); pdfAllText.push({ pageNum: i, text: fullText, items: tc.items, itemOffsets: itemOffsets });
} }
console.log('PROPAGANDA: Extracted text from ' + pdfAllText.length + ' pages'); console.log('UNREDACTED: Extracted text from ' + pdfAllText.length + ' pages');
} }
// ─── Search Controls Init ──────────────────────────── // ─── Search Controls Init ────────────────────────────
@ -781,6 +796,157 @@
} }
// ─── Global Archive Search ─────────────────────────────
let searchDebounceTimer = null;
function initSearchListeners() {
const input = document.getElementById('urSearchInput');
const clearBtn = document.getElementById('urSearchClear');
if (!input) return;
input.addEventListener('input', () => {
clearTimeout(searchDebounceTimer);
const val = input.value.trim();
clearBtn.style.display = val.length > 0 ? 'inline-block' : 'none';
searchDebounceTimer = setTimeout(() => {
globalArchiveSearch(val);
}, 300);
});
clearBtn.addEventListener('click', () => {
input.value = '';
clearBtn.style.display = 'none';
globalArchiveSearch('');
input.focus();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
input.value = '';
clearBtn.style.display = 'none';
globalArchiveSearch('');
}
});
}
function globalArchiveSearch(query) {
const resultsEl = document.getElementById('urSearchResults');
const statusEl = document.getElementById('urSearchStatus');
const catSection = document.getElementById('urCategorySection');
if (!resultsEl || !statusEl) return;
if (!query || query.length === 0) {
resultsEl.innerHTML = '';
statusEl.innerHTML = '';
if (catSection) catSection.style.display = '';
return;
}
const q = query.toLowerCase();
const results = [];
if (!DATA || !DATA.categories) return;
DATA.categories.forEach(cat => {
(cat.countries || []).forEach(cn => {
(cn.collections || []).forEach(col => {
(col.documents || []).forEach(doc => {
const fields = [
doc.title || '',
doc.description || '',
col.name || '',
cat.name || '',
cn.name || '',
doc.year ? String(doc.year) : '',
doc.classification || ''
];
const combined = fields.join(' ').toLowerCase();
if (combined.includes(q)) {
results.push({
doc,
catName: cat.name,
catId: cat.id,
catIcon: cat.icon || '📁',
countryName: cn.name,
countryCode: cn.code,
countryFlag: cn.flag || '',
colName: col.name,
colId: col.id
});
}
});
// Also match collection-level (even if no docs match)
const colFields = [col.name || '', col.description || '', cat.name || '', cn.name || ''].join(' ').toLowerCase();
if (colFields.includes(q) && !results.find(r => r.colId === col.id && r.catId === cat.id && r.countryCode === cn.code && !r.doc.id)) {
// Check if we already added docs from this collection
const hasDocResults = results.some(r => r.colId === col.id && r.catId === cat.id && r.countryCode === cn.code);
if (!hasDocResults) {
results.push({
doc: { title: col.name, description: col.description || 'Collection in ' + cat.name, id: '__col__' },
catName: cat.name,
catId: cat.id,
catIcon: cat.icon || '📁',
countryName: cn.name,
countryCode: cn.code,
countryFlag: cn.flag || '',
colName: col.name,
colId: col.id,
isCollection: true
});
}
}
});
});
});
// Hide categories when searching
if (catSection) catSection.style.display = 'none';
if (results.length === 0) {
statusEl.innerHTML = '<span class="ur-search-none">NO MATCHING DOCUMENTS // REFINE SEARCH PARAMETERS</span>';
resultsEl.innerHTML = '';
return;
}
statusEl.innerHTML = `<span class="ur-search-count">FOUND ${results.length} MATCHING DOCUMENT${results.length === 1 ? '' : 'S'}</span>`;
let html = '<div class="ur-results-grid">';
results.forEach(r => {
const title = highlightMatch(esc(r.doc.title || 'Untitled'), q);
const desc = r.doc.description ? highlightMatch(esc(truncateText(r.doc.description, 160)), q) : '';
const hash = r.isCollection
? `country/${r.catId}/${r.countryCode}`
: `doc/${r.catId}/${r.countryCode}/${r.colId}/${r.doc.id}`;
html += `<div class="ur-result-card" onclick="location.hash='${hash}'">
<div class="ur-result-header">
<span class="ur-result-icon">${r.catIcon}</span>
<span class="ur-result-title">${title}</span>
</div>
<div class="ur-result-meta">
<span class="ur-result-badge">${esc(r.catName)}</span>
<span class="ur-result-badge ur-result-badge-country">${r.countryFlag} ${esc(r.countryName)}</span>
<span class="ur-result-badge ur-result-badge-col">${esc(r.colName)}</span>
</div>
${desc ? `<div class="ur-result-desc">${desc}</div>` : ''}
</div>`;
});
html += '</div>';
resultsEl.innerHTML = html;
}
function highlightMatch(text, query) {
if (!query) return text;
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return text.replace(regex, '<mark class="ur-highlight">$1</mark>');
}
function truncateText(text, maxLen) {
if (text.length <= maxLen) return text;
return text.substring(0, maxLen).replace(/\s+\S*$/, '') + '...';
}
// ─── Not Found / Empty ──────────────────────────────── // ─── Not Found / Empty ────────────────────────────────
function renderNotFound(msg) { function renderNotFound(msg) {
ROOT.innerHTML = `<div class="prop-empty"> ROOT.innerHTML = `<div class="prop-empty">
@ -840,7 +1006,7 @@
// ─── Init ───────────────────────────────────────────── // ─── Init ─────────────────────────────────────────────
async function init() { async function init() {
ROOT.innerHTML = '<div class="prop-loading">INITIALISING PROPAGANDA ARCHIVE...</div>'; ROOT.innerHTML = '<div class="prop-loading">INITIALISING UNREDACTED ARCHIVE...</div>';
await loadData(); await loadData();
if (DATA) { if (DATA) {
window.addEventListener('hashchange', route); window.addEventListener('hashchange', route);

37
redownload_missing.sh Normal file
View file

@ -0,0 +1,37 @@
#!/bin/bash
# Re-download missing UK MOD UFO PDFs
MASTER_DIR="/var/www/jaeswift-homepage/propaganda/docs/ufo-uap/uk-mod-complete"
BASE_DIR="/var/www/jaeswift-homepage/propaganda/docs/ufo-uap"
echo "[$(date)] Re-downloading 53 missing files"
wget --tries=5 \
--wait=1 \
--random-wait \
--timeout=60 \
--directory-prefix="$MASTER_DIR" \
--no-check-certificate \
--user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
"https://documents.theblackvault.com/documents/ufos/UK/aug-2009-highlights-guide.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1948.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1958-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1959.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1960.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1961.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1962.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1963.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1964.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1965.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1970.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1972.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1974.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1975.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1976.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1984-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1985-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1986-1-2.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1987-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1997-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-1999-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2005-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2006-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2018-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2019-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2020-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2021-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2022-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2023-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2024-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2025-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2026-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2027-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2028-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2030-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2031-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2032-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2033-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2034-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2035-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2036-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2037-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2038-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2039-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2040-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2041-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2043-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2044-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2046-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2047-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2048-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2049-1-1.pdf" "https://documents.theblackvault.com/documents/ufos/UK/defe-24-2050-1-1.pdf"
echo "[$(date)] Re-download complete"
# Recreate ALL symlinks
for f in "$MASTER_DIR"/defe-24-*.pdf; do
[ -f "$f" ] && ln -sf "$f" "$BASE_DIR/uk-mod-ufo-desk/$(basename $f)"
done
for f in "$MASTER_DIR"/defe-31-*.pdf; do
[ -f "$f" ] && ln -sf "$f" "$BASE_DIR/di55-ufo-archive/$(basename $f)"
done
[ -f "$MASTER_DIR/defe-24-1948.pdf" ] && ln -sf "$MASTER_DIR/defe-24-1948.pdf" "$BASE_DIR/rendlesham-forest/defe-24-1948.pdf"
for f in aug-2009-highlights-guide.pdf highlights-guide-09-08-11.pdf mar-2011-highlights-guide.pdf uk2009.pdf; do
[ -f "$MASTER_DIR/$f" ] && ln -sf "$MASTER_DIR/$f" "$BASE_DIR/highlights-and-guides/$f"
done
echo "[$(date)] Symlinks recreated"
DOWN=$(ls -1 "$MASTER_DIR"/*.pdf 2>/dev/null | wc -l)
SIZE=$(du -sh "$MASTER_DIR" | cut -f1)
echo "[$(date)] Total: ${DOWN}/112 files (${SIZE})"