diff --git a/api/app.py b/api/app.py index be229c1..5c90db5 100644 --- a/api/app.py +++ b/api/app.py @@ -1525,5 +1525,76 @@ def api_govdomains_stats(): 'types_list': sorted(type_counts.keys()) }) + +# ─── Propaganda: Declassified Document Archive ──────── +_propaganda_cache = None +_propaganda_mtime = 0 + +def _load_propaganda(): + global _propaganda_cache, _propaganda_mtime + p = DATA_DIR / 'propaganda.json' + if not p.exists(): + return {'categories': []} + mt = p.stat().st_mtime + if _propaganda_cache is None or mt != _propaganda_mtime: + with open(p, encoding='utf-8') as f: + _propaganda_cache = json.load(f) + _propaganda_mtime = mt + return _propaganda_cache + +@app.route('/api/propaganda') +def get_propaganda(): + return jsonify(_load_propaganda()) + +@app.route('/api/propaganda/categories') +def get_propaganda_categories(): + db = _load_propaganda() + cats = [] + for c in db.get('categories', []): + n_countries = len(c.get('countries', [])) + n_docs = 0 + n_collections = 0 + for cn in c.get('countries', []): + for col in cn.get('collections', []): + n_collections += 1 + n_docs += len(col.get('documents', [])) + cats.append({ + 'id': c['id'], 'name': c['name'], 'description': c.get('description', ''), + 'icon': c.get('icon', ''), 'countries': n_countries, + 'collections': n_collections, 'documents': n_docs + }) + return jsonify({'categories': cats}) + +@app.route('/api/propaganda/category/') +def get_propaganda_category(cat_id): + db = _load_propaganda() + for c in db.get('categories', []): + if c['id'] == cat_id: + return jsonify(c) + abort(404, f'Category {cat_id} not found') + +@app.route('/api/propaganda/upload', methods=['POST']) +@require_auth +def upload_propaganda_doc(): + """Upload a PDF to the propaganda archive. Future admin use.""" + try: + if 'file' not in request.files: + return jsonify({'error': 'No file uploaded'}), 400 + f = request.files['file'] + cat_id = request.form.get('category', '') + col_id = request.form.get('collection', '') + if not cat_id or not col_id: + return jsonify({'error': 'category and collection required'}), 400 + if not f.filename.lower().endswith('.pdf'): + 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/propaganda/docs') / cat_id / col_id + save_dir.mkdir(parents=True, exist_ok=True) + dest = save_dir / f.filename + f.save(str(dest)) + return jsonify({'ok': True, 'path': str(dest), 'filename': f.filename}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/api/data/propaganda.json b/api/data/propaganda.json new file mode 100644 index 0000000..9d051ac --- /dev/null +++ b/api/data/propaganda.json @@ -0,0 +1,197 @@ +{ + "categories": [ + { + "id": "ufo-uap", + "name": "UFO / UAP", + "description": "Unidentified aerial phenomena declassified files", + "icon": "👽", + "countries": [ + { + "code": "UK", + "name": "United Kingdom", + "flag": "🇬🇧", + "collections": [ + { + "id": "rendlesham-forest", + "name": "Rendlesham Forest Incident", + "year": "1980", + "description": "Britain's most famous UFO incident. USAF personnel reported unexplained lights near RAF Woodbridge.", + "source": "UK National Archives", + "documents": [ + { + "id": "rendlesham-memo", + "title": "Lt Col Halt's Memorandum", + "filename": "rendlesham-halt-memo.pdf", + "pages": 2, + "date_released": "2001", + "description": "Official memo from Deputy Base Commander Charles Halt describing the incident." + } + ] + }, + { + "id": "uk-mod-ufo-files", + "name": "UK MOD UFO Files", + "year": "2009-2013", + "description": "Complete release of Ministry of Defence UFO investigation files.", + "source": "UK National Archives", + "documents": [] + } + ] + }, + { + "code": "USA", + "name": "United States", + "flag": "🇺🇸", + "collections": [ + { + "id": "project-blue-book", + "name": "Project Blue Book", + "year": "1952-1969", + "description": "USAF systematic study of UFOs. 12,618 sightings investigated.", + "source": "National Archives", + "documents": [] + }, + { + "id": "uap-task-force", + "name": "UAP Task Force Reports", + "year": "2021-2024", + "description": "Modern Pentagon UAP investigation reports released to Congress.", + "source": "DoD / ODNI", + "documents": [] + } + ] + }, + { + "code": "RUS", + "name": "Russia", + "flag": "🇷🇺", + "collections": [ + { + "id": "soviet-ufo-files", + "name": "Soviet Military UFO Files", + "year": "1978-1990", + "description": "Declassified Soviet military reports on unidentified aerial phenomena collected during the Cold War era.", + "source": "Russian State Archives", + "documents": [] + } + ] + } + ] + }, + { + "id": "intelligence", + "name": "INTELLIGENCE", + "description": "Declassified intelligence agency operations and programs", + "icon": "🕵️", + "countries": [ + { + "code": "USA", + "name": "United States", + "flag": "🇺🇸", + "collections": [ + { + "id": "mkultra", + "name": "Project MKUltra", + "year": "1953-1973", + "description": "CIA mind control program. Most documents destroyed in 1973, surviving files declassified in 1977.", + "source": "CIA Reading Room", + "documents": [] + }, + { + "id": "cointelpro", + "name": "COINTELPRO", + "year": "1956-1971", + "description": "FBI counterintelligence program targeting domestic political organisations.", + "source": "FBI Vault", + "documents": [] + } + ] + }, + { + "code": "UK", + "name": "United Kingdom", + "flag": "🇬🇧", + "collections": [ + { + "id": "mi5-cold-war", + "name": "MI5 Cold War Files", + "year": "1945-1991", + "description": "Declassified Security Service files from the Cold War period including surveillance and counter-espionage operations.", + "source": "UK National Archives", + "documents": [] + } + ] + } + ] + }, + { + "id": "military-ops", + "name": "MILITARY OPERATIONS", + "description": "Declassified military operations and strategic planning", + "icon": "⚔️", + "countries": [ + { + "code": "USA", + "name": "United States", + "flag": "🇺🇸", + "collections": [ + { + "id": "operation-northwoods", + "name": "Operation Northwoods", + "year": "1962", + "description": "Proposed false flag operations against American citizens to justify military intervention in Cuba. Rejected by JFK.", + "source": "National Security Archive", + "documents": [] + } + ] + } + ] + }, + { + "id": "government", + "name": "GOVERNMENT", + "description": "Released government documents, investigations, and correspondence", + "icon": "🏛️", + "countries": [ + { + "code": "USA", + "name": "United States", + "flag": "🇺🇸", + "collections": [ + { + "id": "jfk-files", + "name": "JFK Assassination Files", + "year": "1963-2023", + "description": "Released files related to the assassination of President John F. Kennedy.", + "source": "National Archives", + "documents": [] + } + ] + } + ] + }, + { + "id": "science-tech", + "name": "SCIENCE & TECHNOLOGY", + "description": "Declassified research programs and technical studies", + "icon": "🔬", + "countries": [ + { + "code": "USA", + "name": "United States", + "flag": "🇺🇸", + "collections": [ + { + "id": "project-stargate", + "name": "Project Stargate", + "year": "1978-1995", + "description": "CIA/DIA remote viewing program investigating psychic phenomena for intelligence applications.", + "source": "CIA Reading Room", + "documents": [] + } + ] + } + ] + } + ] +} diff --git a/css/propaganda.css b/css/propaganda.css new file mode 100644 index 0000000..9cbfd9c --- /dev/null +++ b/css/propaganda.css @@ -0,0 +1,726 @@ +/* ═══════════════════════════════════════════════════════ + PROPAGANDA — Declassified Document Archive + ═══════════════════════════════════════════════════════ */ + +/* ─── Stats Bar ──────────────────────────────────────── */ +.prop-stats-bar { + display: flex; + gap: 2rem; + padding: 1rem 1.5rem; + background: rgba(16, 16, 16, 0.8); + border: 1px solid var(--border); + border-left: 3px solid var(--mil-red); + margin-bottom: 1.5rem; + font-family: var(--font-mono); + font-size: 0.85rem; + letter-spacing: 1px; + flex-wrap: wrap; +} + +.prop-stat { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.prop-stat-label { + color: var(--text-muted); + font-size: 0.75rem; + text-transform: uppercase; +} + +.prop-stat-value { + color: var(--status-green); + font-weight: 700; + font-size: 1rem; +} + +.prop-stat-value.amber { color: var(--status-amber); } +.prop-stat-value.red { color: var(--mil-red); } + +/* ─── Breadcrumb Trail ───────────────────────────────── */ +.prop-breadcrumb { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: rgba(16, 16, 16, 0.6); + border: 1px solid var(--border); + border-left: 3px solid var(--status-green); + margin-bottom: 1.5rem; + font-family: var(--font-mono); + font-size: 0.8rem; + letter-spacing: 1px; + flex-wrap: wrap; +} + +.prop-breadcrumb a { + color: var(--status-green); + text-decoration: none; + cursor: pointer; + transition: color 0.2s; +} + +.prop-breadcrumb a:hover { + color: #fff; + text-decoration: underline; +} + +.prop-breadcrumb .sep { + color: var(--text-muted); + opacity: 0.5; +} + +.prop-breadcrumb .current { + color: var(--text-primary); + font-weight: 700; +} + +/* ─── Category Grid ──────────────────────────────────── */ +.prop-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.prop-card { + background: rgba(16, 16, 16, 0.6); + border: 1px solid var(--border); + padding: 1.25rem; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.prop-card::before { + content: ''; + position: absolute; + top: 0; left: 0; + width: 3px; height: 100%; + background: var(--mil-red); + opacity: 0; + transition: opacity 0.3s ease; +} + +.prop-card:hover { + border-color: var(--text-muted); + background: rgba(20, 20, 20, 0.9); + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); +} + +.prop-card:hover::before { opacity: 1; } + +.prop-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.6rem; +} + +.prop-card-icon { + font-size: 2rem; + opacity: 0.6; + line-height: 1; +} + +.prop-card-name { + font-family: var(--font-display); + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: 1px; +} + +.prop-card-desc { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.5; + margin-bottom: 0.75rem; +} + +.prop-card-meta { + display: flex; + justify-content: space-between; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-muted); + letter-spacing: 1px; + border-top: 1px solid var(--border); + padding-top: 0.5rem; +} + +.prop-card-count { + color: var(--status-green); +} + +/* ─── Country Selector ───────────────────────────────── */ +.prop-country-grid { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 1.5rem; +} + +.prop-country-btn { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.65rem 1.25rem; + background: rgba(16, 16, 16, 0.8); + border: 1px solid var(--border); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 0.85rem; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + position: relative; +} + +.prop-country-btn::before { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--mil-red); + transform: scaleX(0); + transition: transform 0.3s ease; +} + +.prop-country-btn:hover { + border-color: var(--text-muted); + color: #fff; + background: rgba(20, 20, 20, 0.9); +} + +.prop-country-btn:hover::before { + transform: scaleX(1); +} + +.prop-country-btn.active { + border-color: var(--mil-red); + color: #fff; + background: rgba(139, 0, 0, 0.15); + box-shadow: 0 0 15px rgba(139, 0, 0, 0.1); +} + +.prop-country-btn.active::before { + transform: scaleX(1); +} + +.prop-country-flag { + font-size: 1.2rem; + line-height: 1; +} + +.prop-country-label { + font-weight: 700; +} + +.prop-country-count { + color: var(--text-muted); + font-size: 0.7rem; + margin-left: 0.25rem; +} + +/* ─── Collection Cards ───────────────────────────────── */ +.prop-collection-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.prop-collection-card { + background: rgba(16, 16, 16, 0.6); + border: 1px solid var(--border); + padding: 1.25rem; + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.prop-collection-card::before { + content: ''; + position: absolute; + top: 0; left: 0; + width: 100%; height: 2px; + background: linear-gradient(90deg, var(--mil-red), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.prop-collection-card:hover { + border-color: var(--text-muted); + background: rgba(20, 20, 20, 0.9); + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); +} + +.prop-collection-card:hover::before { opacity: 1; } + +.prop-collection-name { + font-family: var(--font-display); + font-size: 0.95rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: 1px; + margin-bottom: 0.25rem; +} + +.prop-collection-year { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--mil-red); + letter-spacing: 2px; + margin-bottom: 0.5rem; +} + +.prop-collection-desc { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.5; + margin-bottom: 0.75rem; +} + +.prop-collection-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--text-muted); + letter-spacing: 1px; + border-top: 1px solid var(--border); + padding-top: 0.5rem; +} + +.prop-collection-source { + opacity: 0.7; +} + +.prop-collection-docs { + color: var(--status-green); + font-weight: 700; +} + +/* ─── Collection Detail Header ───────────────────────── */ +.prop-detail-header { + background: rgba(16, 16, 16, 0.8); + border: 1px solid var(--border); + border-left: 3px solid var(--mil-red); + padding: 1.25rem 1.5rem; + margin-bottom: 1.5rem; +} + +.prop-detail-title { + font-family: var(--font-display); + font-size: 1.2rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: 2px; + margin-bottom: 0.25rem; +} + +.prop-detail-year { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--mil-red); + letter-spacing: 2px; + margin-bottom: 0.5rem; +} + +.prop-detail-desc { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 0.5rem; +} + +.prop-detail-source { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--text-muted); + letter-spacing: 1px; +} + +.prop-detail-source span { + color: var(--status-green); +} + +/* ─── Document List ──────────────────────────────────── */ +.prop-doc-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 2rem; +} + +.prop-doc-item { + display: flex; + align-items: center; + gap: 1rem; + background: rgba(16, 16, 16, 0.6); + border: 1px solid var(--border); + padding: 1rem 1.25rem; + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.prop-doc-item::before { + content: ''; + position: absolute; + left: 0; top: 0; + width: 3px; height: 100%; + background: var(--status-green); + opacity: 0; + transition: opacity 0.3s ease; +} + +.prop-doc-item:hover { + border-color: var(--status-green); + background: rgba(20, 20, 20, 0.9); + transform: translateX(4px); +} + +.prop-doc-item:hover::before { opacity: 1; } + +.prop-doc-icon { + font-size: 1.8rem; + opacity: 0.5; + flex-shrink: 0; +} + +.prop-doc-info { + flex: 1; + min-width: 0; +} + +.prop-doc-title { + font-family: var(--font-mono); + font-size: 0.85rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: 1px; + margin-bottom: 0.25rem; +} + +.prop-doc-desc { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--text-muted); + line-height: 1.4; +} + +.prop-doc-meta { + display: flex; + gap: 1rem; + font-family: var(--font-mono); + font-size: 0.65rem; + color: var(--text-muted); + letter-spacing: 1px; + margin-top: 0.35rem; +} + +.prop-doc-meta span { + opacity: 0.7; +} + +.prop-doc-badge { + flex-shrink: 0; + padding: 0.3rem 0.6rem; + background: rgba(139, 0, 0, 0.2); + border: 1px solid rgba(139, 0, 0, 0.4); + font-family: var(--font-mono); + font-size: 0.65rem; + color: var(--mil-red); + letter-spacing: 2px; + text-transform: uppercase; +} + +/* ─── Empty State ────────────────────────────────────── */ +.prop-empty { + text-align: center; + padding: 3rem 2rem; + background: rgba(16, 16, 16, 0.4); + border: 1px dashed var(--border); +} + +.prop-empty-icon { + font-size: 2.5rem; + opacity: 0.3; + margin-bottom: 1rem; +} + +.prop-empty-title { + font-family: var(--font-display); + font-size: 0.9rem; + color: var(--text-muted); + letter-spacing: 2px; + margin-bottom: 0.5rem; +} + +.prop-empty-text { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-muted); + opacity: 0.6; +} + +/* ─── PDF Viewer ─────────────────────────────────────── */ +.prop-viewer-container { + background: rgba(10, 10, 10, 0.95); + border: 1px solid var(--border); + margin-bottom: 2rem; + position: relative; +} + +.prop-viewer-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1rem; + background: rgba(16, 16, 16, 0.95); + border-bottom: 1px solid var(--border); + font-family: var(--font-mono); + font-size: 0.75rem; + flex-wrap: wrap; + gap: 0.5rem; +} + +.prop-viewer-title { + color: var(--text-primary); + letter-spacing: 1px; + font-weight: 700; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.prop-viewer-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.prop-viewer-btn { + padding: 0.35rem 0.65rem; + background: rgba(30, 30, 30, 0.8); + border: 1px solid var(--border); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; + letter-spacing: 1px; +} + +.prop-viewer-btn:hover { + border-color: var(--status-green); + color: #fff; + background: rgba(0, 204, 51, 0.1); +} + +.prop-viewer-btn.active { + border-color: var(--status-green); + color: var(--status-green); +} + +.prop-viewer-page-info { + color: var(--text-muted); + letter-spacing: 1px; + min-width: 80px; + text-align: center; +} + +.prop-viewer-canvas-wrap { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 1rem; + min-height: 500px; + max-height: 80vh; + overflow: auto; + background: rgba(5, 5, 5, 0.9); +} + +.prop-viewer-canvas-wrap canvas { + max-width: 100%; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.6); +} + +.prop-viewer-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + font-family: var(--font-mono); + font-size: 0.85rem; + color: var(--text-muted); + letter-spacing: 2px; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 1; } +} + +.prop-viewer-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + gap: 1rem; + padding: 2rem; + text-align: center; +} + +.prop-viewer-error-icon { + font-size: 2rem; + opacity: 0.4; +} + +.prop-viewer-error-text { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--mil-red); + letter-spacing: 1px; +} + +.prop-viewer-download { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(139, 0, 0, 0.2); + border: 1px solid rgba(139, 0, 0, 0.4); + color: var(--mil-red); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 1px; + text-decoration: none; + cursor: pointer; + transition: all 0.2s; +} + +.prop-viewer-download:hover { + background: rgba(139, 0, 0, 0.35); + color: #fff; +} + +/* ─── Classification Banner ──────────────────────────── */ +.prop-classification { + text-align: center; + padding: 0.4rem; + font-family: var(--font-mono); + font-size: 0.65rem; + letter-spacing: 3px; + color: var(--mil-red); + border: 1px solid rgba(139, 0, 0, 0.3); + background: rgba(139, 0, 0, 0.05); + margin-bottom: 1.5rem; +} + +/* ─── Back Button ────────────────────────────────────── */ +.prop-back-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(16, 16, 16, 0.8); + border: 1px solid var(--border); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.2s; + margin-bottom: 1rem; + text-decoration: none; +} + +.prop-back-btn:hover { + border-color: var(--status-green); + color: #fff; +} + +/* ─── Loading ────────────────────────────────────────── */ +.prop-loading { + text-align: center; + padding: 3rem; + font-family: var(--font-mono); + font-size: 0.85rem; + color: var(--text-muted); + letter-spacing: 2px; + animation: pulse 1.5s ease-in-out infinite; +} + +/* ─── Section Label ──────────────────────────────────── */ +.prop-section-label { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--text-muted); + letter-spacing: 3px; + text-transform: uppercase; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} + +/* ─── Responsive ─────────────────────────────────────── */ +@media (max-width: 768px) { + .prop-grid { + grid-template-columns: 1fr; + } + .prop-collection-grid { + grid-template-columns: 1fr; + } + .prop-stats-bar { + gap: 1rem; + } + .prop-country-grid { + gap: 0.5rem; + } + .prop-country-btn { + padding: 0.5rem 0.85rem; + font-size: 0.8rem; + } + .prop-doc-item { + flex-direction: column; + align-items: flex-start; + } + .prop-doc-badge { + align-self: flex-start; + } + .prop-viewer-toolbar { + flex-direction: column; + align-items: flex-start; + } + .prop-viewer-canvas-wrap { + min-height: 300px; + max-height: 60vh; + } + .prop-breadcrumb { + font-size: 0.7rem; + } +} + +@media (min-width: 1400px) { + .prop-grid { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/depot/propaganda.html b/depot/propaganda.html index b066b50..4217638 100644 --- a/depot/propaganda.html +++ b/depot/propaganda.html @@ -3,49 +3,69 @@ - JAESWIFT // PROPAGANDA + JAESWIFT // PROPAGANDA — Declassified Document Archive + + +
+ + +
-
DEPOT // INFORMATION WARFARE
+
DEPOT // DECLASSIFIED ARCHIVE

PROPAGANDA

-

> Documents, briefings, and intelligence reports. Information is power.

+

> Declassified documents, intelligence briefings, and government files. Browse the archive.

-
-
-
-
UNDER CONSTRUCTION
-
This section is being prepared. Content deployment imminent.
-
CLASSIFICATION: PENDING // STATUS: STANDBY
+ +
+
+
INITIALISING PROPAGANDA ARCHIVE...
+
+ + - \ No newline at end of file + diff --git a/js/propaganda.js b/js/propaganda.js new file mode 100644 index 0000000..1eee7aa --- /dev/null +++ b/js/propaganda.js @@ -0,0 +1,530 @@ +/* ═══════════════════════════════════════════════════════ + PROPAGANDA — Declassified Document Archive + SPA Engine with hash routing & PDF.js viewer + ═══════════════════════════════════════════════════════ */ + +(function () { + 'use strict'; + + const ROOT = document.getElementById('propagandaRoot'); + const PDF_BASE = '/propaganda/docs'; + let DATA = null; + + // ─── Data Loading ──────────────────────────────────── + async function loadData() { + try { + const r = await fetch('/api/propaganda'); + if (!r.ok) throw new Error(`HTTP ${r.status}`); + DATA = await r.json(); + } catch (e) { + console.error('PROPAGANDA: data load failed', e); + ROOT.innerHTML = `
+
⚠️
+
DATA FEED OFFLINE
+
Failed to load archive index. ${e.message}
+
`; + } + } + + // ─── Helpers ────────────────────────────────────────── + function countDocuments(obj) { + if (!obj) return 0; + if (Array.isArray(obj.documents)) return obj.documents.length; + if (Array.isArray(obj.collections)) { + return obj.collections.reduce((s, c) => s + (c.documents ? c.documents.length : 0), 0); + } + if (Array.isArray(obj.countries)) { + return obj.countries.reduce((s, cn) => s + countDocuments(cn), 0); + } + return 0; + } + + function countCollections(obj) { + if (!obj) return 0; + if (Array.isArray(obj.collections)) return obj.collections.length; + if (Array.isArray(obj.countries)) { + return obj.countries.reduce((s, cn) => s + (cn.collections ? cn.collections.length : 0), 0); + } + return 0; + } + + function countCountries(cat) { + return cat.countries ? cat.countries.length : 0; + } + + function totalStats() { + if (!DATA || !DATA.categories) return { cats: 0, countries: 0, collections: 0, docs: 0 }; + let countries = new Set(); + let collections = 0; + let docs = 0; + DATA.categories.forEach(cat => { + (cat.countries || []).forEach(cn => { + countries.add(cn.code); + (cn.collections || []).forEach(col => { + collections++; + docs += (col.documents || []).length; + }); + }); + }); + return { cats: DATA.categories.length, countries: countries.size, collections, docs }; + } + + function findCategory(id) { + return DATA.categories.find(c => c.id === id); + } + + function findCountry(cat, code) { + return (cat.countries || []).find(c => c.code === code); + } + + function findCollection(country, colId) { + return (country.collections || []).find(c => c.id === colId); + } + + function findDocument(collection, docId) { + return (collection.documents || []).find(d => d.id === docId); + } + + function esc(s) { + const d = document.createElement('div'); + d.textContent = s || ''; + return d.innerHTML; + } + + // ─── Breadcrumb Builder ─────────────────────────────── + function breadcrumb(parts) { + // parts: [{label, hash?}, ...] + let html = '
'; + parts.forEach((p, i) => { + if (i > 0) html += '/'; + if (p.hash) { + html += `${esc(p.label)}`; + } else { + html += `${esc(p.label)}`; + } + }); + html += '
'; + return html; + } + + // ─── View: Categories (Home) ────────────────────────── + function renderCategories() { + const stats = totalStats(); + let html = ''; + + html += `
DECLASSIFIED // DOCUMENT ARCHIVE // ACCESS GRANTED
`; + + html += `
+
CATEGORIES${stats.cats}
+
COUNTRIES${stats.countries}
+
COLLECTIONS${stats.collections}
+
DOCUMENTS${stats.docs}
+
`; + + html += breadcrumb([{ label: 'PROPAGANDA' }]); + + html += ''; + html += '
'; + + DATA.categories.forEach(cat => { + const nCountries = countCountries(cat); + const nDocs = countDocuments(cat); + const nCol = countCollections(cat); + html += `
+
+
${esc(cat.name)}
+
${cat.icon || '📁'}
+
+
${esc(cat.description)}
+
+ ${nCountries} COUNTR${nCountries === 1 ? 'Y' : 'IES'} · ${nCol} COLLECTION${nCol === 1 ? '' : 'S'} + ${nDocs} DOC${nDocs === 1 ? '' : 'S'} +
+
`; + }); + + html += '
'; + ROOT.innerHTML = html; + } + + // ─── View: Category (Country Selector + Collections) ── + function renderCategory(catId, activeCountry) { + const cat = findCategory(catId); + if (!cat) return renderNotFound('Category not found'); + + let html = ''; + html += `
DECLASSIFIED // ${esc(cat.name)} // ACCESS GRANTED
`; + + html += breadcrumb([ + { label: 'PROPAGANDA', hash: '' }, + { label: cat.name } + ]); + + // Country selector + if (cat.countries && cat.countries.length > 0) { + html += ''; + html += '
'; + cat.countries.forEach(cn => { + const nCol = cn.collections ? cn.collections.length : 0; + const isActive = activeCountry === cn.code; + html += `
+ ${cn.flag || ''} + ${esc(cn.name)} + [${nCol}] +
`; + }); + html += '
'; + + // If a country is selected, show its collections + if (activeCountry) { + const country = findCountry(cat, activeCountry); + if (country) { + html += renderCountryCollections(cat, country); + } + } else { + // Show all collections from all countries + html += ''; + let hasCols = false; + cat.countries.forEach(cn => { + if (cn.collections && cn.collections.length > 0) { + hasCols = true; + } + }); + if (hasCols) { + html += '
'; + cat.countries.forEach(cn => { + (cn.collections || []).forEach(col => { + html += renderCollectionCard(catId, cn.code, col, cn.flag); + }); + }); + html += '
'; + } else { + html += renderEmpty('NO COLLECTIONS YET', 'Collections will appear here once documents are added to this category.'); + } + } + } else { + html += renderEmpty('NO COUNTRIES REGISTERED', 'No country data available for this category yet.'); + } + + ROOT.innerHTML = html; + } + + function renderCountryCollections(cat, country) { + let html = ''; + html += ``; + + if (country.collections && country.collections.length > 0) { + html += '
'; + country.collections.forEach(col => { + html += renderCollectionCard(cat.id, country.code, col); + }); + html += '
'; + } else { + html += renderEmpty('NO COLLECTIONS', 'No document collections available for this country yet.'); + } + return html; + } + + function renderCollectionCard(catId, countryCode, col, flag) { + const nDocs = col.documents ? col.documents.length : 0; + return `
+
${flag ? flag + ' ' : ''}${esc(col.name)}
+
${esc(col.year)}
+
${esc(col.description)}
+ +
`; + } + + // ─── View: Collection (Documents) ───────────────────── + function renderCollection(catId, countryCode, colId) { + const cat = findCategory(catId); + if (!cat) return renderNotFound('Category not found'); + const country = findCountry(cat, countryCode); + if (!country) return renderNotFound('Country not found'); + const col = findCollection(country, colId); + if (!col) return renderNotFound('Collection not found'); + + let html = ''; + html += `
DECLASSIFIED // ${esc(col.name).toUpperCase()} // ACCESS GRANTED
`; + + html += breadcrumb([ + { label: 'PROPAGANDA', hash: '' }, + { label: cat.name, hash: `category/${catId}` }, + { label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` }, + { label: col.name } + ]); + + // Detail header + html += `
+
${esc(col.name)}
+
${esc(col.year)}
+
${esc(col.description)}
+
SOURCE: ${esc(col.source)}
+
`; + + // Documents + if (col.documents && col.documents.length > 0) { + html += ``; + html += '
'; + col.documents.forEach(doc => { + html += `
+
📄
+
+
${esc(doc.title)}
+
${esc(doc.description)}
+
+ PAGES: ${doc.pages || '?'} + RELEASED: ${esc(doc.date_released || 'UNKNOWN')} + ${esc(doc.filename)} +
+
+
VIEW
+
`; + }); + html += '
'; + } else { + html += renderEmpty('NO DOCUMENTS UPLOADED YET', 'This collection has been catalogued but documents have not yet been uploaded to the archive.'); + } + + ROOT.innerHTML = html; + } + + // ─── View: Document (PDF Viewer) ────────────────────── + function renderDocument(catId, countryCode, colId, docId) { + const cat = findCategory(catId); + if (!cat) return renderNotFound('Category not found'); + const country = findCountry(cat, countryCode); + if (!country) return renderNotFound('Country not found'); + const col = findCollection(country, colId); + if (!col) return renderNotFound('Collection not found'); + const doc = findDocument(col, docId); + if (!doc) return renderNotFound('Document not found'); + + const pdfUrl = `${PDF_BASE}/${catId}/${colId}/${doc.filename}`; + + let html = ''; + html += `
DECLASSIFIED // ${esc(doc.title).toUpperCase()}
`; + + html += breadcrumb([ + { label: 'PROPAGANDA', hash: '' }, + { label: cat.name, hash: `category/${catId}` }, + { label: `${country.flag || ''} ${country.name}`, hash: `country/${catId}/${countryCode}` }, + { label: col.name, hash: `collection/${catId}/${countryCode}/${colId}` }, + { label: doc.title } + ]); + + // Viewer + html += `
+
+
${esc(doc.title)}
+
+ + — / — + + + + + ⬇ DOWNLOAD +
+
+
+
DECRYPTING DOCUMENT...
+
+
`; + + // Doc info + html += `
+
${esc(doc.title)}
+
${esc(doc.description)}
+
+ PAGES: ${doc.pages || '?'} + RELEASED: ${esc(doc.date_released || 'UNKNOWN')} + FILE: ${esc(doc.filename)} +
+
`; + + ROOT.innerHTML = html; + + // Init PDF viewer + initPdfViewer(pdfUrl); + } + + // ─── PDF.js Viewer ─────────────────────────────────── + let pdfDoc = null; + let pdfPage = 1; + let pdfScale = 1.5; + let pdfRendering = false; + let pdfPending = null; + + async function initPdfViewer(url) { + const canvasWrap = document.getElementById('pdfCanvasWrap'); + const loadingEl = document.getElementById('pdfLoading'); + const pageInfo = document.getElementById('pdfPageInfo'); + + // Check if pdf.js is loaded + if (typeof pdfjsLib === 'undefined') { + canvasWrap.innerHTML = `
+
⚠️
+
PDF.js library not loaded
+ ⬇ DOWNLOAD PDF DIRECTLY +
`; + return; + } + + try { + pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + const loadingTask = pdfjsLib.getDocument(url); + pdfDoc = await loadingTask.promise; + pdfPage = 1; + + loadingEl.remove(); + + // Create canvas + const canvas = document.createElement('canvas'); + canvas.id = 'pdfCanvas'; + canvasWrap.appendChild(canvas); + + updatePageInfo(); + renderPdfPage(); + + // Controls + document.getElementById('pdfPrev').addEventListener('click', () => { + if (pdfPage <= 1) return; + pdfPage--; + queueRender(); + }); + document.getElementById('pdfNext').addEventListener('click', () => { + if (pdfPage >= pdfDoc.numPages) return; + pdfPage++; + queueRender(); + }); + document.getElementById('pdfZoomIn').addEventListener('click', () => { + pdfScale = Math.min(pdfScale + 0.25, 4); + queueRender(); + }); + document.getElementById('pdfZoomOut').addEventListener('click', () => { + pdfScale = Math.max(pdfScale - 0.25, 0.5); + queueRender(); + }); + document.getElementById('pdfFit').addEventListener('click', () => { + pdfScale = 1.5; + queueRender(); + }); + } catch (e) { + console.error('PDF load failed:', e); + canvasWrap.innerHTML = `
+
⚠️
+
DOCUMENT LOAD FAILED — ${esc(e.message || 'File may not exist yet')}
+ ⬇ ATTEMPT DIRECT DOWNLOAD +
`; + } + } + + function updatePageInfo() { + const el = document.getElementById('pdfPageInfo'); + if (el && pdfDoc) { + el.textContent = `${pdfPage} / ${pdfDoc.numPages}`; + } + } + + async function renderPdfPage() { + if (!pdfDoc) return; + pdfRendering = true; + + const page = await pdfDoc.getPage(pdfPage); + const viewport = page.getViewport({ scale: pdfScale }); + const canvas = document.getElementById('pdfCanvas'); + if (!canvas) { pdfRendering = false; return; } + const ctx = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + await page.render({ canvasContext: ctx, viewport: viewport }).promise; + pdfRendering = false; + updatePageInfo(); + + if (pdfPending !== null) { + renderPdfPage(); + pdfPending = null; + } + } + + function queueRender() { + if (pdfRendering) { + pdfPending = pdfPage; + } else { + renderPdfPage(); + } + } + + // ─── Not Found / Empty ──────────────────────────────── + function renderNotFound(msg) { + ROOT.innerHTML = `
+
⚠️
+
${esc(msg)}
+
The requested resource could not be located in the archive.
+
+
+ ◂ RETURN TO ARCHIVE INDEX +
`; + } + + function renderEmpty(title, text) { + return `
+
+
${esc(title)}
+
${esc(text)}
+
`; + } + + // ─── Router ─────────────────────────────────────────── + function route() { + if (!DATA) return; + + // Reset PDF state + pdfDoc = null; + pdfPage = 1; + pdfRendering = false; + pdfPending = null; + + const hash = (location.hash || '').replace(/^#\/?/, ''); + const parts = hash.split('/').filter(Boolean); + + if (parts.length === 0 || hash === '') { + renderCategories(); + } else if (parts[0] === 'category' && parts[1]) { + renderCategory(parts[1], null); + } else if (parts[0] === 'country' && parts[1] && parts[2]) { + renderCategory(parts[1], parts[2]); + } else if (parts[0] === 'collection' && parts.length >= 4) { + renderCollection(parts[1], parts[2], parts[3]); + } else if (parts[0] === 'doc' && parts.length >= 5) { + renderDocument(parts[1], parts[2], parts[3], parts[4]); + } else { + renderNotFound('INVALID ROUTE'); + } + + // Scroll to top + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + + // ─── Init ───────────────────────────────────────────── + async function init() { + ROOT.innerHTML = '
INITIALISING PROPAGANDA ARCHIVE...
'; + await loadData(); + if (DATA) { + window.addEventListener('hashchange', route); + route(); + } + } + + // Wait for DOM + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})();