diff --git a/api/app.py b/api/app.py index 876ad70..6459121 100644 --- a/api/app.py +++ b/api/app.py @@ -1086,5 +1086,88 @@ def api_radar_refresh(): RADAR_CACHE['last_fetch'] = time.time() return jsonify({'status': 'ok', 'total': len(RADAR_CACHE['items'])}) +# ─── SITREP: Daily AI Briefing ──────────────────────── +SITREP_DIR = DATA_DIR / 'sitreps' + +@app.route('/api/sitrep') +def api_sitrep(): + """Return today's SITREP, or most recent if not yet generated. ?date=YYYY-MM-DD for specific date.""" + date_param = request.args.get('date', '').strip() + sitrep_dir = SITREP_DIR + if not sitrep_dir.exists(): + return jsonify({'error': 'No SITREPs available yet', 'sitrep': None}), 404 + + if date_param: + target = sitrep_dir / f'{date_param}.json' + if target.exists(): + return jsonify(json.loads(target.read_text())) + return jsonify({'error': f'No SITREP for {date_param}', 'sitrep': None}), 404 + + # Try today first, then most recent + today = datetime.datetime.utcnow().strftime('%Y-%m-%d') + today_file = sitrep_dir / f'{today}.json' + if today_file.exists(): + data = json.loads(today_file.read_text()) + data['is_today'] = True + return jsonify(data) + + # Find most recent + files = sorted(sitrep_dir.glob('*.json'), reverse=True) + if files: + data = json.loads(files[0].read_text()) + data['is_today'] = False + data['notice'] = f"Today's SITREP not yet generated. Showing most recent: {data.get('date', 'unknown')}" + return jsonify(data) + + return jsonify({'error': 'No SITREPs available yet', 'sitrep': None}), 404 + +@app.route('/api/sitrep/list') +def api_sitrep_list(): + """Return list of all available SITREP dates for archive browsing.""" + sitrep_dir = SITREP_DIR + if not sitrep_dir.exists(): + return jsonify({'dates': [], 'total': 0}) + + entries = [] + for f in sorted(sitrep_dir.glob('*.json'), reverse=True): + try: + data = json.loads(f.read_text()) + entries.append({ + 'date': data.get('date', f.stem), + 'headline': data.get('headline', ''), + 'model': data.get('model', 'unknown'), + 'sources_used': data.get('sources_used', 0) + }) + except Exception: + entries.append({'date': f.stem, 'headline': '', 'model': 'unknown', 'sources_used': 0}) + + return jsonify({'dates': entries, 'total': len(entries)}) + +@app.route('/api/sitrep/generate', methods=['POST']) +def api_sitrep_generate(): + """Manually trigger SITREP generation.""" + try: + import subprocess as sp + script = Path(__file__).parent / 'sitrep_generator.py' + if not script.exists(): + return jsonify({'error': 'sitrep_generator.py not found'}), 500 + result = sp.run( + ['python3', str(script)], + capture_output=True, text=True, timeout=120, + cwd=str(script.parent) + ) + if result.returncode == 0: + today = datetime.datetime.utcnow().strftime('%Y-%m-%d') + sitrep_file = SITREP_DIR / f'{today}.json' + if sitrep_file.exists(): + return jsonify({'status': 'ok', 'message': 'SITREP generated', 'date': today, 'log': result.stdout[-500:] if result.stdout else ''}) + return jsonify({'status': 'ok', 'message': 'Generator ran but no file produced', 'log': result.stdout[-500:] if result.stdout else ''}) + else: + return jsonify({'status': 'error', 'message': 'Generator failed', 'stderr': result.stderr[-500:] if result.stderr else '', 'stdout': result.stdout[-500:] if result.stdout else ''}), 500 + except sp.TimeoutExpired: + return jsonify({'status': 'error', 'message': 'Generation timed out (120s)'}), 504 + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/api/sitrep_generator.py b/api/sitrep_generator.py new file mode 100644 index 0000000..00479d8 --- /dev/null +++ b/api/sitrep_generator.py @@ -0,0 +1,472 @@ +#!/usr/bin/env python3 +"""SITREP Generator — Automated Daily AI Briefing for jaeswift.xyz + +Fetches tech news from RSS/JSON feeds and crypto data from Binance, +sends to Venice AI for military-style summarisation, saves as JSON. + +Usage: + python3 sitrep_generator.py # Generate today's SITREP + python3 sitrep_generator.py 2026-04-06 # Generate for specific date +""" +import json +import os +import sys +import time +import logging +from datetime import datetime, timezone +from pathlib import Path + +import feedparser +import requests + +# ─── Configuration ──────────────────────────────────── +BASE_DIR = Path(__file__).parent +DATA_DIR = BASE_DIR / 'data' +SITREP_DIR = DATA_DIR / 'sitreps' +KEYS_FILE = DATA_DIR / 'apikeys.json' + +SITREP_DIR.mkdir(parents=True, exist_ok=True) + +# Logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [SITREP] %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) +log = logging.getLogger('sitrep') + +# Request headers +HEADERS = { + 'User-Agent': 'JAESWIFT-SITREP/1.0 (https://jaeswift.xyz)' +} + +REDDIT_HEADERS = { + 'User-Agent': 'JAESWIFT-SITREP/1.0 by jaeswift' +} + +# ─── Feed Sources ───────────────────────────────────── +RSS_FEEDS = { + 'hackernews': { + 'url': 'https://hnrss.org/frontpage?count=50', + 'type': 'rss', + 'label': 'Hacker News' + } +} + +REDDIT_FEEDS = { + 'r_technology': { + 'url': 'https://www.reddit.com/r/technology/hot.json?limit=30', + 'label': 'r/technology' + }, + 'r_programming': { + 'url': 'https://www.reddit.com/r/programming/hot.json?limit=30', + 'label': 'r/programming' + }, + 'r_netsec': { + 'url': 'https://www.reddit.com/r/netsec/hot.json?limit=20', + 'label': 'r/netsec' + } +} + +LOBSTERS_URL = 'https://lobste.rs/hottest.json' + +CRYPTO_SYMBOLS = ['SOLUSDT', 'BTCUSDT', 'ETHUSDT'] +BINANCE_TICKER_URL = 'https://api.binance.com/api/v3/ticker/24hr' + + +# ─── Fetch Functions ────────────────────────────────── +def fetch_hn_stories(): + """Fetch Hacker News top stories via RSS.""" + stories = [] + try: + feed = feedparser.parse(RSS_FEEDS['hackernews']['url']) + for entry in feed.entries: + pub = '' + if hasattr(entry, 'published_parsed') and entry.published_parsed: + pub = time.strftime('%Y-%m-%dT%H:%M:%SZ', entry.published_parsed) + stories.append({ + 'title': entry.get('title', 'Untitled'), + 'url': entry.get('link', ''), + 'source': 'Hacker News', + 'published': pub, + 'summary': (entry.get('summary', '') or '')[:300] + }) + log.info(f'Fetched {len(stories)} stories from Hacker News') + except Exception as e: + log.error(f'HN fetch error: {e}') + return stories + + +def fetch_reddit_posts(sub_key, sub_config): + """Fetch Reddit posts via .json endpoint.""" + posts = [] + try: + resp = requests.get(sub_config['url'], headers=REDDIT_HEADERS, timeout=15) + resp.raise_for_status() + data = resp.json() + children = data.get('data', {}).get('children', []) + for child in children: + d = child.get('data', {}) + if d.get('stickied'): + continue + created = d.get('created_utc', 0) + pub = datetime.fromtimestamp(created, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') if created else '' + posts.append({ + 'title': d.get('title', 'Untitled'), + 'url': d.get('url', ''), + 'source': sub_config['label'], + 'published': pub, + 'summary': (d.get('selftext', '') or '')[:300], + 'score': d.get('score', 0), + 'num_comments': d.get('num_comments', 0), + 'permalink': f"https://reddit.com{d.get('permalink', '')}" + }) + log.info(f"Fetched {len(posts)} posts from {sub_config['label']}") + except Exception as e: + log.error(f"Reddit fetch error ({sub_config['label']}): {e}") + return posts + + +def fetch_lobsters(): + """Fetch Lobste.rs hottest stories via JSON.""" + stories = [] + try: + resp = requests.get(LOBSTERS_URL, headers=HEADERS, timeout=15) + resp.raise_for_status() + data = resp.json() + for item in data[:30]: + stories.append({ + 'title': item.get('title', 'Untitled'), + 'url': item.get('url', '') or item.get('comments_url', ''), + 'source': 'Lobste.rs', + 'published': item.get('created_at', ''), + 'summary': (item.get('description', '') or '')[:300], + 'score': item.get('score', 0), + 'tags': item.get('tags', []) + }) + log.info(f'Fetched {len(stories)} stories from Lobste.rs') + except Exception as e: + log.error(f'Lobsters fetch error: {e}') + return stories + + +def fetch_crypto_data(): + """Fetch crypto market data from Binance.""" + crypto = {} + for symbol in CRYPTO_SYMBOLS: + try: + resp = requests.get( + BINANCE_TICKER_URL, + params={'symbol': symbol}, + headers=HEADERS, + timeout=10 + ) + resp.raise_for_status() + data = resp.json() + ticker = symbol.replace('USDT', '') + crypto[ticker] = { + 'price': round(float(data.get('lastPrice', 0)), 2), + 'change': round(float(data.get('priceChangePercent', 0)), 2), + 'high_24h': round(float(data.get('highPrice', 0)), 2), + 'low_24h': round(float(data.get('lowPrice', 0)), 2), + 'volume': round(float(data.get('volume', 0)), 2) + } + log.info(f"{ticker}: ${crypto[ticker]['price']} ({crypto[ticker]['change']}%)") + except Exception as e: + log.error(f'Binance fetch error ({symbol}): {e}') + ticker = symbol.replace('USDT', '') + crypto[ticker] = {'price': 0, 'change': 0, 'high_24h': 0, 'low_24h': 0, 'volume': 0} + return crypto + + +# ─── AI Generation ──────────────────────────────────── +def build_ai_prompt(all_stories, crypto): + """Build the prompt for Venice AI with all fetched data.""" + lines = [] + lines.append('=== RAW INTELLIGENCE DATA FOR SITREP GENERATION ===') + lines.append(f'Date: {datetime.now(timezone.utc).strftime("%d %B %Y")}') + lines.append(f'Time: {datetime.now(timezone.utc).strftime("%H%M")} HRS UTC') + lines.append('') + + # Group stories by source + sources = {} + for s in all_stories: + src = s.get('source', 'Unknown') + if src not in sources: + sources[src] = [] + sources[src].append(s) + + lines.append('=== TECHNOLOGY & PROGRAMMING FEEDS ===') + for src_name in ['Hacker News', 'r/technology', 'r/programming', 'Lobste.rs']: + if src_name in sources: + lines.append(f'\n--- {src_name.upper()} ---') + for i, s in enumerate(sources[src_name][:25], 1): + score_str = f" [score:{s.get('score', '?')}]" if s.get('score') else '' + lines.append(f"{i}. {s['title']}{score_str}") + if s.get('summary'): + lines.append(f" Summary: {s['summary'][:150]}") + + lines.append('\n=== CYBERSECURITY FEEDS ===') + if 'r/netsec' in sources: + lines.append('--- R/NETSEC ---') + for i, s in enumerate(sources['r/netsec'][:15], 1): + lines.append(f"{i}. {s['title']}") + if s.get('summary'): + lines.append(f" Summary: {s['summary'][:150]}") + + # Also extract security-related stories from other feeds + sec_keywords = ['security', 'vulnerability', 'exploit', 'hack', 'breach', + 'malware', 'ransomware', 'CVE', 'zero-day', 'phishing', + 'encryption', 'privacy', 'backdoor', 'patch', 'attack'] + sec_stories = [] + for s in all_stories: + if s.get('source') == 'r/netsec': + continue + title_lower = s.get('title', '').lower() + if any(kw in title_lower for kw in sec_keywords): + sec_stories.append(s) + if sec_stories: + lines.append('\n--- SECURITY-RELATED FROM OTHER FEEDS ---') + for i, s in enumerate(sec_stories[:10], 1): + lines.append(f"{i}. [{s['source']}] {s['title']}") + + lines.append('\n=== CRYPTO MARKET DATA ===') + for ticker, data in crypto.items(): + direction = '▲' if data['change'] >= 0 else '▼' + lines.append( + f"{ticker}: ${data['price']:,.2f} {direction} {data['change']:+.2f}% " + f"| 24h High: ${data['high_24h']:,.2f} | 24h Low: ${data['low_24h']:,.2f} " + f"| Volume: {data['volume']:,.0f}" + ) + + return '\n'.join(lines) + + +SITREP_SYSTEM_PROMPT = """You are a military intelligence analyst preparing a classified daily situation report (SITREP) for a special operations technology unit. Your callsign is JAE-SIGINT. + +Write in terse, professional military briefing style. Use abbreviations common to military communications. Be direct, analytical, and occasionally darkly witty. + +Format the SITREP in markdown with the following EXACT structure: + +# DAILY SITREP — [DATE] +**CLASSIFICATION: OPEN SOURCE // JAESWIFT SIGINT** +**DTG:** [Date-Time Group in military format, e.g., 060700ZAPR2026] +**PREPARED BY:** JAE-SIGINT / AUTOMATED COLLECTION + +--- + +## SECTOR ALPHA — TECHNOLOGY + +Summarise the top 8-10 most significant technology stories. Group loosely by theme (AI/ML, infrastructure, programming languages, open source, industry moves). Each item should be 1-2 sentences max. Use bullet points. Prioritise stories by significance and novelty. + +--- + +## SECTOR BRAVO — CYBERSECURITY + +Summarise any cybersecurity-related stories. Include CVEs, breaches, new tools, threat intel. If few dedicated security stories, note the relatively quiet SIGINT environment. 4-8 items. + +--- + +## SECTOR CHARLIE — CRYPTO MARKETS + +Report crypto prices with 24h movement. Note any significant moves (>5% change). Include any crypto-related news from the feeds. Brief market sentiment. + +--- + +## ASSESSMENT + +2-3 sentences providing overall analysis. What trends are emerging? What should the operator be watching? Include one forward-looking statement. + +--- + +**// END TRANSMISSION //** +**NEXT SCHEDULED SITREP: [TOMORROW'S DATE] 0700Z** + +Rules: +- Keep total length under 1500 words +- Do NOT invent stories or data — only summarise what's provided +- If data is sparse for a sector, acknowledge it briefly +- Use markdown formatting (headers, bold, bullets, horizontal rules) +- Include the exact crypto prices provided — do not round them differently +- For each tech story, mention the source in brackets like [HN] [Reddit] [Lobsters]""" + + +def generate_with_venice(user_prompt): + """Call Venice AI to generate the SITREP.""" + try: + keys = json.loads(KEYS_FILE.read_text()) + venice_key = keys.get('venice', {}).get('api_key', '') + venice_model = keys.get('venice', {}).get('model', 'llama-3.3-70b') + if not venice_key: + log.error('Venice API key not found in apikeys.json') + return None, None + + log.info(f'Calling Venice AI (model: {venice_model})...') + resp = requests.post( + 'https://api.venice.ai/api/v1/chat/completions', + headers={ + 'Authorization': f'Bearer {venice_key}', + 'Content-Type': 'application/json' + }, + json={ + 'model': venice_model, + 'messages': [ + {'role': 'system', 'content': SITREP_SYSTEM_PROMPT}, + {'role': 'user', 'content': user_prompt} + ], + 'max_tokens': 2048, + 'temperature': 0.7 + }, + timeout=60 + ) + resp.raise_for_status() + result = resp.json() + content = result['choices'][0]['message']['content'] + log.info(f'Venice AI response received ({len(content)} chars)') + return content, venice_model + except Exception as e: + log.error(f'Venice AI error: {e}') + return None, None + + +def generate_headline(content): + """Extract or generate a one-line headline from the SITREP content.""" + # Try to pull the first significant bullet point + lines = content.split('\n') + for line in lines: + stripped = line.strip() + if stripped.startswith('- ') or stripped.startswith('* '): + headline = stripped.lstrip('-* ').strip() + if len(headline) > 20: + # Trim to reasonable length + if len(headline) > 120: + headline = headline[:117] + '...' + return headline + return 'Daily intelligence briefing — technology, cybersecurity, and crypto markets' + + +def build_fallback_sitrep(all_stories, crypto): + """Build a raw fallback SITREP when Venice AI is unavailable.""" + now = datetime.now(timezone.utc) + lines = [] + lines.append(f'# DAILY SITREP — {now.strftime("%d %B %Y").upper()}') + lines.append('**CLASSIFICATION: OPEN SOURCE // JAESWIFT SIGINT**') + lines.append(f'**DTG:** {now.strftime("%d%H%MZ%b%Y").upper()}') + lines.append('**PREPARED BY:** JAE-SIGINT / RAW FEED (AI UNAVAILABLE)') + lines.append('') + lines.append('---') + lines.append('') + lines.append('> ⚠️ **NOTICE:** AI summarisation unavailable. Raw intelligence feed follows.') + lines.append('') + lines.append('## SECTOR ALPHA — TECHNOLOGY') + lines.append('') + + tech_stories = [s for s in all_stories if s.get('source') != 'r/netsec'] + for s in tech_stories[:15]: + lines.append(f"- **[{s['source']}]** {s['title']}") + + lines.append('') + lines.append('---') + lines.append('') + lines.append('## SECTOR BRAVO — CYBERSECURITY') + lines.append('') + + sec_stories = [s for s in all_stories if s.get('source') == 'r/netsec'] + sec_keywords = ['security', 'vulnerability', 'exploit', 'hack', 'breach', + 'malware', 'ransomware', 'CVE', 'zero-day', 'phishing'] + for s in all_stories: + if s.get('source') != 'r/netsec' and any(kw in s.get('title', '').lower() for kw in sec_keywords): + sec_stories.append(s) + for s in sec_stories[:10]: + lines.append(f"- **[{s['source']}]** {s['title']}") + if not sec_stories: + lines.append('- No cybersecurity stories intercepted this cycle.') + + lines.append('') + lines.append('---') + lines.append('') + lines.append('## SECTOR CHARLIE — CRYPTO MARKETS') + lines.append('') + + for ticker, data in crypto.items(): + direction = '🟢' if data['change'] >= 0 else '🔴' + lines.append( + f"- **{ticker}**: ${data['price']:,.2f} {direction} {data['change']:+.2f}%" + ) + + lines.append('') + lines.append('---') + lines.append('') + lines.append('## ASSESSMENT') + lines.append('') + lines.append('AI analysis unavailable. Operator should review raw feed data above for emerging patterns.') + lines.append('') + lines.append('---') + lines.append('') + lines.append('**// END TRANSMISSION //**') + + return '\n'.join(lines) + + +# ─── Main Generation Pipeline ──────────────────────── +def generate_sitrep(target_date=None): + """Main pipeline: fetch data → AI summarise → save JSON.""" + now = datetime.now(timezone.utc) + date_str = target_date or now.strftime('%Y-%m-%d') + output_path = SITREP_DIR / f'{date_str}.json' + + log.info(f'=== SITREP GENERATION STARTED for {date_str} ===') + + # 1. Fetch all data + log.info('Phase 1: Fetching intelligence data...') + hn_stories = fetch_hn_stories() + reddit_stories = [] + for key, config in REDDIT_FEEDS.items(): + reddit_stories.extend(fetch_reddit_posts(key, config)) + lobster_stories = fetch_lobsters() + crypto = fetch_crypto_data() + + all_stories = hn_stories + reddit_stories + lobster_stories + total_sources = len(all_stories) + log.info(f'Total stories collected: {total_sources}') + + if total_sources == 0: + log.error('No stories fetched from any source. Aborting.') + return False + + # 2. Build AI prompt and generate + log.info('Phase 2: Generating AI briefing...') + user_prompt = build_ai_prompt(all_stories, crypto) + ai_content, model_used = generate_with_venice(user_prompt) + + # 3. Build SITREP content (AI or fallback) + if ai_content: + content = ai_content + headline = generate_headline(content) + else: + log.warning('Venice AI failed — using fallback raw SITREP') + content = build_fallback_sitrep(all_stories, crypto) + headline = 'Raw intelligence feed — AI summarisation unavailable' + model_used = 'fallback' + + # 4. Save JSON + sitrep_data = { + 'date': date_str, + 'generated_at': now.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'headline': headline, + 'content': content, + 'crypto': crypto, + 'sources_used': total_sources, + 'model': model_used or 'unknown' + } + + output_path.write_text(json.dumps(sitrep_data, indent=2)) + log.info(f'SITREP saved to {output_path}') + log.info(f'=== SITREP GENERATION COMPLETE for {date_str} ===') + return True + + +if __name__ == '__main__': + target = sys.argv[1] if len(sys.argv) > 1 else None + success = generate_sitrep(target) + sys.exit(0 if success else 1) diff --git a/css/sitrep.css b/css/sitrep.css new file mode 100644 index 0000000..06948a3 --- /dev/null +++ b/css/sitrep.css @@ -0,0 +1,465 @@ +/* ─── SITREP: Daily AI Briefing ─────────────────── */ + +.sitrep-container { + max-width: 960px; + margin: 0 auto; + padding: 0 2rem 3rem; +} + +/* ─── Classification Banner ────────────────────── */ +.sitrep-classification { + text-align: center; + padding: 0.5rem 1rem; + background: rgba(201, 162, 39, 0.08); + border: 1px solid rgba(201, 162, 39, 0.25); + font-family: 'JetBrains Mono', monospace; + font-size: 0.7rem; + letter-spacing: 3px; + color: #c9a227; + margin-bottom: 1.5rem; +} + +/* ─── Date Header ──────────────────────────────── */ +.sitrep-date-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; +} + +.sitrep-date-title { + font-family: 'Orbitron', monospace; + font-size: 1.1rem; + font-weight: 700; + color: #00cc33; + letter-spacing: 2px; + text-transform: uppercase; +} + +.sitrep-date-nav { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.sitrep-nav-btn { + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; + padding: 0.35rem 0.7rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.sitrep-nav-btn:hover { + background: rgba(0, 204, 51, 0.06); + border-color: rgba(0, 204, 51, 0.3); + color: #00cc33; +} + +.sitrep-nav-btn.disabled { + opacity: 0.3; + cursor: default; + pointer-events: none; +} + +/* ─── Notice Bar ───────────────────────────────── */ +.sitrep-notice { + padding: 0.6rem 1rem; + background: rgba(201, 162, 39, 0.06); + border-left: 3px solid #c9a227; + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + color: rgba(201, 162, 39, 0.8); + margin-bottom: 1.5rem; + display: none; +} + +.sitrep-notice.visible { + display: block; +} + +/* ─── Crypto Ticker Bar ────────────────────────── */ +.sitrep-crypto-bar { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + padding: 0.8rem 1rem; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.06); + flex-wrap: wrap; + justify-content: center; +} + +.crypto-ticker { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; +} + +.crypto-symbol { + color: rgba(255, 255, 255, 0.5); + font-weight: 700; + letter-spacing: 1px; +} + +.crypto-price { + color: rgba(255, 255, 255, 0.85); + font-weight: 500; +} + +.crypto-change { + font-size: 0.75rem; + font-weight: 600; + padding: 0.15rem 0.4rem; + border-radius: 2px; +} + +.crypto-change.positive { + color: #00cc33; + background: rgba(0, 204, 51, 0.1); +} + +.crypto-change.negative { + color: #ff3333; + background: rgba(255, 51, 51, 0.1); +} + +.crypto-divider { + width: 1px; + height: 1.2rem; + background: rgba(255, 255, 255, 0.08); +} + +/* ─── Main Briefing Panel ──────────────────────── */ +.sitrep-briefing { + background: #161616; + border: 1px solid rgba(255, 255, 255, 0.06); + padding: 2rem 2.5rem; + position: relative; + margin-bottom: 2rem; +} + +.sitrep-briefing::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, #00cc33, transparent); +} + +/* ─── Briefing Content Markdown Rendering ──────── */ +.sitrep-content { + font-family: 'JetBrains Mono', monospace; + font-size: 0.82rem; + line-height: 1.75; + color: rgba(255, 255, 255, 0.8); +} + +.sitrep-content h1 { + font-family: 'Orbitron', monospace; + font-size: 1.2rem; + font-weight: 900; + color: #00cc33; + letter-spacing: 3px; + text-transform: uppercase; + margin: 0 0 0.5rem 0; + padding-bottom: 0.5rem; +} + +.sitrep-content h2 { + font-family: 'Orbitron', monospace; + font-size: 0.95rem; + font-weight: 700; + color: #00cc33; + letter-spacing: 2px; + text-transform: uppercase; + margin: 1.5rem 0 1rem 0; + padding: 0.6rem 0; + border-top: 1px solid rgba(0, 204, 51, 0.15); + border-bottom: 1px solid rgba(0, 204, 51, 0.08); +} + +.sitrep-content h3 { + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; + font-weight: 700; + color: #c9a227; + letter-spacing: 1.5px; + text-transform: uppercase; + margin: 1.2rem 0 0.6rem 0; +} + +.sitrep-content p { + margin: 0.6rem 0; +} + +.sitrep-content strong { + color: rgba(255, 255, 255, 0.95); + font-weight: 700; +} + +.sitrep-content em { + color: rgba(201, 162, 39, 0.8); + font-style: italic; +} + +.sitrep-content ul, +.sitrep-content ol { + margin: 0.5rem 0; + padding-left: 1.5rem; +} + +.sitrep-content li { + margin: 0.4rem 0; + padding-left: 0.3rem; +} + +.sitrep-content li::marker { + color: rgba(0, 204, 51, 0.5); +} + +.sitrep-content hr { + border: none; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(0, 204, 51, 0.2), transparent); + margin: 1.5rem 0; +} + +.sitrep-content a { + color: #00cc33; + text-decoration: none; + border-bottom: 1px solid rgba(0, 204, 51, 0.3); + transition: border-color 0.2s; +} + +.sitrep-content a:hover { + border-color: #00cc33; +} + +.sitrep-content code { + background: rgba(0, 204, 51, 0.08); + padding: 0.15rem 0.4rem; + font-size: 0.78rem; + color: #00cc33; + border-radius: 2px; +} + +.sitrep-content blockquote { + border-left: 3px solid #c9a227; + padding: 0.5rem 1rem; + margin: 0.8rem 0; + background: rgba(201, 162, 39, 0.04); + color: rgba(201, 162, 39, 0.85); +} + +/* ─── Meta Footer ──────────────────────────────── */ +.sitrep-meta { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; + padding-top: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.05); + font-family: 'JetBrains Mono', monospace; + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.25); + letter-spacing: 1px; +} + +.sitrep-meta-item { + display: flex; + align-items: center; + gap: 0.4rem; +} + +.sitrep-meta-label { + color: rgba(255, 255, 255, 0.15); + text-transform: uppercase; +} + +/* ─── Archive Panel ────────────────────────────── */ +.sitrep-archive { + margin-top: 2rem; +} + +.sitrep-archive-header { + font-family: 'Orbitron', monospace; + font-size: 0.8rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.4); + letter-spacing: 2px; + text-transform: uppercase; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.sitrep-archive-list { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.sitrep-archive-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.6rem 1rem; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.04); + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.sitrep-archive-item:hover { + background: rgba(0, 204, 51, 0.04); + border-color: rgba(0, 204, 51, 0.15); +} + +.sitrep-archive-item.active { + background: rgba(0, 204, 51, 0.06); + border-color: rgba(0, 204, 51, 0.25); +} + +.archive-date { + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + color: #c9a227; + letter-spacing: 1px; + min-width: 100px; + font-weight: 600; +} + +.archive-headline { + font-family: 'JetBrains Mono', monospace; + font-size: 0.72rem; + color: rgba(255, 255, 255, 0.45); + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.archive-sources { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + color: rgba(255, 255, 255, 0.2); + letter-spacing: 1px; +} + +/* ─── Loading State ────────────────────────────── */ +.sitrep-loading { + text-align: center; + padding: 3rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; + color: rgba(0, 204, 51, 0.5); + letter-spacing: 2px; + animation: sitrep-pulse 1.5s ease-in-out infinite; +} + +@keyframes sitrep-pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 1; } +} + +/* ─── Error State ──────────────────────────────── */ +.sitrep-error { + text-align: center; + padding: 2rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; + color: rgba(255, 51, 51, 0.7); + background: rgba(255, 51, 51, 0.04); + border: 1px solid rgba(255, 51, 51, 0.15); +} + +/* ─── Generate Button ──────────────────────────── */ +.sitrep-generate-btn { + font-family: 'JetBrains Mono', monospace; + font-size: 0.7rem; + letter-spacing: 1.5px; + padding: 0.4rem 0.8rem; + background: rgba(0, 204, 51, 0.06); + border: 1px solid rgba(0, 204, 51, 0.2); + color: rgba(0, 204, 51, 0.7); + cursor: pointer; + transition: all 0.2s; + text-transform: uppercase; +} + +.sitrep-generate-btn:hover { + background: rgba(0, 204, 51, 0.12); + border-color: rgba(0, 204, 51, 0.5); + color: #00cc33; +} + +.sitrep-generate-btn:disabled { + opacity: 0.3; + cursor: default; +} + +/* ─── Responsive ───────────────────────────────── */ +@media (max-width: 768px) { + .sitrep-container { + padding: 0 1rem 2rem; + } + + .sitrep-briefing { + padding: 1.5rem 1.2rem; + } + + .sitrep-date-header { + flex-direction: column; + align-items: flex-start; + } + + .sitrep-crypto-bar { + flex-direction: column; + gap: 0.5rem; + align-items: flex-start; + } + + .crypto-divider { + display: none; + } + + .sitrep-meta { + flex-direction: column; + align-items: flex-start; + } + + .sitrep-content h1 { + font-size: 1rem; + } + + .sitrep-content h2 { + font-size: 0.85rem; + } + + .archive-headline { + display: none; + } +} + +@media (max-width: 480px) { + .sitrep-content { + font-size: 0.75rem; + } + + .sitrep-date-title { + font-size: 0.9rem; + } +} diff --git a/js/sitrep.js b/js/sitrep.js new file mode 100644 index 0000000..fcb8cf2 --- /dev/null +++ b/js/sitrep.js @@ -0,0 +1,365 @@ +/* ─── SITREP: Daily AI Briefing Frontend ─────────── */ +(function() { + 'use strict'; + + const API_BASE = '/api/sitrep'; + let archiveDates = []; + let currentDate = null; + + // ─── DOM Elements ──────────────────────────────── + const els = { + dateTitle: document.getElementById('sitrepDateTitle'), + notice: document.getElementById('sitrepNotice'), + noticeText: document.getElementById('sitrepNoticeText'), + cryptoBar: document.getElementById('sitrepCryptoBar'), + briefing: document.getElementById('sitrepBriefing'), + content: document.getElementById('sitrepContent'), + metaSources: document.getElementById('metaSources'), + metaModel: document.getElementById('metaModel'), + metaGenerated: document.getElementById('metaGenerated'), + archiveList: document.getElementById('sitrepArchiveList'), + btnPrev: document.getElementById('sitrepPrev'), + btnNext: document.getElementById('sitrepNext'), + btnGenerate: document.getElementById('sitrepGenerate') + }; + + // ─── Markdown to HTML ──────────────────────────── + function mdToHtml(md) { + if (!md) return '
No content available.
'; + + let html = md; + + // Escape HTML entities (but preserve markdown) + html = html.replace(/&/g, '&').replace(//g, '>'); + + // Restore markdown-safe chars + html = html.replace(/> /gm, '> '); // blockquotes + + // Horizontal rules + html = html.replace(/^---+$/gm, '$1');
+
+ // Links [text](url)
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
+
+ // Blockquotes (multi-line support)
+ html = html.replace(/^> (.+)$/gm, '$1'); + // Merge consecutive blockquotes + html = html.replace(/<\/blockquote>\n
/g, '
'); + + // Unordered lists + html = html.replace(/^[\-\*] (.+)$/gm, '$1 '); + // Wrap consecutivein + html = html.replace(/((?:
- .*<\/li>\n?)+)/g, '
$1
'); + + // Ordered lists + html = html.replace(/^\d+\. (.+)$/gm, '$1 '); + html = html.replace(/((?:.*<\/oli>\n?)+)/g, function(match) { + return ' ' + match.replace(/<\/?oli>/g, function(tag) { + return tag.replace('oli', 'li'); + }) + '
'; + }); + + // Paragraphs — wrap remaining loose text lines + const lines = html.split('\n'); + const result = []; + let inBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) { + if (!inBlock) result.push(''); + continue; + } + // Skip if already an HTML block element + if (/^<(h[1-6]|ul|ol|li|blockquote|hr|p|div)/.test(line)) { + result.push(line); + inBlock = /^<(ul|ol|blockquote)/.test(line); + } else if (/<\/(ul|ol|blockquote)>$/.test(line)) { + result.push(line); + inBlock = false; + } else { + result.push('' + line + '
'); + } + } + + return result.join('\n'); + } + + // ─── Date Formatting ───────────────────────────── + function formatSitrepDate(dateStr) { + if (!dateStr) return 'UNKNOWN DATE'; + const d = new Date(dateStr + 'T00:00:00Z'); + const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']; + const day = String(d.getUTCDate()).padStart(2, '0'); + const month = months[d.getUTCMonth()]; + const year = d.getUTCFullYear(); + return `DAILY SITREP // ${day} ${month} ${year} // 0700 HRS`; + } + + function formatArchiveDate(dateStr) { + if (!dateStr) return '???'; + const d = new Date(dateStr + 'T00:00:00Z'); + const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC']; + const day = String(d.getUTCDate()).padStart(2, '0'); + return `${day} ${months[d.getUTCMonth()]} ${d.getUTCFullYear()}`; + } + + function formatTimestamp(ts) { + if (!ts) return '—'; + const d = new Date(ts); + return d.toUTCString().replace('GMT', 'UTC'); + } + + // ─── Crypto Ticker ─────────────────────────────── + function renderCrypto(crypto) { + if (!crypto || !els.cryptoBar) return; + + const tickers = Object.entries(crypto); + if (!tickers.length) { + els.cryptoBar.style.display = 'none'; + return; + } + + let html = ''; + tickers.forEach(([symbol, data], idx) => { + const price = parseFloat(data.price) || 0; + const change = parseFloat(data.change) || 0; + const direction = change >= 0 ? '▲' : '▼'; + const cls = change >= 0 ? 'positive' : 'negative'; + + html += ` ++ ${symbol} + $${price.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})} + ${direction} ${change >= 0 ? '+' : ''}${change.toFixed(2)}% ++ `; + if (idx < tickers.length - 1) { + html += ''; + } + }); + + els.cryptoBar.innerHTML = html; + els.cryptoBar.style.display = 'flex'; + } + + // ─── Navigation ────────────────────────────────── + function updateNav() { + if (!archiveDates.length || !currentDate) { + if (els.btnPrev) els.btnPrev.classList.add('disabled'); + if (els.btnNext) els.btnNext.classList.add('disabled'); + return; + } + + const dates = archiveDates.map(d => d.date); + const idx = dates.indexOf(currentDate); + + if (els.btnPrev) { + if (idx < dates.length - 1) { + els.btnPrev.classList.remove('disabled'); + els.btnPrev.onclick = () => loadSitrep(dates[idx + 1]); + } else { + els.btnPrev.classList.add('disabled'); + els.btnPrev.onclick = null; + } + } + + if (els.btnNext) { + if (idx > 0) { + els.btnNext.classList.remove('disabled'); + els.btnNext.onclick = () => loadSitrep(dates[idx - 1]); + } else { + els.btnNext.classList.add('disabled'); + els.btnNext.onclick = null; + } + } + } + + function updateArchiveHighlight() { + if (!els.archiveList) return; + els.archiveList.querySelectorAll('.sitrep-archive-item').forEach(item => { + item.classList.toggle('active', item.dataset.date === currentDate); + }); + } + + // ─── Load SITREP ───────────────────────────────── + async function loadSitrep(date) { + if (els.content) { + els.content.innerHTML = 'DECRYPTING TRANSMISSION...'; + } + + try { + const url = date ? `${API_BASE}?date=${date}` : API_BASE; + const resp = await fetch(url); + + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + if (els.content) { + els.content.innerHTML = `⚠ ${err.error || 'SITREP not available'}`; + } + if (els.cryptoBar) els.cryptoBar.style.display = 'none'; + return; + } + + const data = await resp.json(); + currentDate = data.date; + + // Date header + if (els.dateTitle) { + els.dateTitle.textContent = formatSitrepDate(data.date); + } + + // Notice + if (els.notice && els.noticeText) { + if (data.notice) { + els.noticeText.textContent = data.notice; + els.notice.classList.add('visible'); + } else { + els.notice.classList.remove('visible'); + } + } + + // Crypto bar + renderCrypto(data.crypto); + + // Briefing content + if (els.content) { + els.content.innerHTML = mdToHtml(data.content); + } + + // Meta + if (els.metaSources) els.metaSources.textContent = data.sources_used || '—'; + if (els.metaModel) els.metaModel.textContent = data.model || '—'; + if (els.metaGenerated) els.metaGenerated.textContent = formatTimestamp(data.generated_at); + + // Nav + updateNav(); + updateArchiveHighlight(); + + } catch (e) { + console.error('SITREP load error:', e); + if (els.content) { + els.content.innerHTML = '⚠ TRANSMISSION FAILED — Unable to reach API'; + } + } + } + + // ─── Load Archive List ──────────────────────────── + async function loadArchive() { + if (!els.archiveList) return; + + try { + const resp = await fetch(`${API_BASE}/list`); + if (!resp.ok) return; + + const data = await resp.json(); + archiveDates = data.dates || []; + + if (!archiveDates.length) { + els.archiveList.innerHTML = 'No archived SITREPs yet.'; + return; + } + + let html = ''; + archiveDates.forEach(entry => { + const isActive = entry.date === currentDate ? ' active' : ''; + html += ` ++ ${formatArchiveDate(entry.date)} + ${escapeHtml(entry.headline || '—')} + ${entry.sources_used || 0} SRC ++ `; + }); + + els.archiveList.innerHTML = html; + updateNav(); + + } catch (e) { + console.error('Archive load error:', e); + } + } + + // ─── Generate SITREP ───────────────────────────── + async function generateSitrep() { + if (!els.btnGenerate) return; + + els.btnGenerate.disabled = true; + els.btnGenerate.textContent = 'GENERATING...'; + + try { + const resp = await fetch(`${API_BASE}/generate`, { method: 'POST' }); + const data = await resp.json(); + + if (data.status === 'ok') { + els.btnGenerate.textContent = '✓ GENERATED'; + // Reload + setTimeout(() => { + loadSitrep(); + loadArchive(); + els.btnGenerate.textContent = 'GENERATE NOW'; + els.btnGenerate.disabled = false; + }, 1000); + } else { + els.btnGenerate.textContent = '✗ FAILED'; + console.error('Generate error:', data); + setTimeout(() => { + els.btnGenerate.textContent = 'GENERATE NOW'; + els.btnGenerate.disabled = false; + }, 3000); + } + } catch (e) { + console.error('Generate error:', e); + els.btnGenerate.textContent = '✗ ERROR'; + setTimeout(() => { + els.btnGenerate.textContent = 'GENERATE NOW'; + els.btnGenerate.disabled = false; + }, 3000); + } + } + + // ─── Utility ────────────────────────────────────── + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + // ─── Public API (for archive clicks) ───────────── + window.__loadSitrep = loadSitrep; + + // ─── Init ──────────────────────────────────────── + function init() { + // Load today's SITREP + loadSitrep(); + + // Load archive + loadArchive(); + + // Generate button + if (els.btnGenerate) { + els.btnGenerate.addEventListener('click', generateSitrep); + } + } + + // Run on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/transmissions/sitrep.html b/transmissions/sitrep.html index 0b045dd..848a86d 100644 --- a/transmissions/sitrep.html +++ b/transmissions/sitrep.html @@ -8,6 +8,8 @@ + + @@ -37,19 +39,68 @@- -TRANSMISSIONS // OUTBOUND SIGNALS+TRANSMISSIONS // DAILY INTELLIGENCESITREP
-> Short-burst situation reports — quick updates on current operations.
+> Automated daily situation report — AI-generated briefing on technology, cybersecurity, and crypto markets
- - -UNDER CONSTRUCTION-This section is being prepared. Content deployment imminent.-CLASSIFICATION: PENDING // STATUS: STANDBY++ + ++ CLASSIFICATION: OPEN SOURCE // JAESWIFT SIGINT // AUTOMATED COLLECTION- + + +++ + +LOADING TRANSMISSION...++ + + +++ ++ + + + + +++ + +++ + + +DECRYPTING TRANSMISSION...+++ +TRANSMISSION ARCHIVE+++Loading archive...+