SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,555
Folders
422
Scanned Size
3.85 GB
PHP Files
920
Editable Text Files
12,816
File viewer
guarded to /htdocs
/live/feed_manager.php
<?php declare(strict_types=1); require __DIR__ . '/bootstrap.php'; $feeds = simon_read_json_file(SIMON_FEEDS_FILE, []); if (!is_array($feeds)) $feeds = []; $engines = []; foreach (glob(__DIR__ . '/providers/*.php') ?: [] as $file) $engines[] = basename($file, '.php'); sort($engines); $categories = array_values(array_unique(array_filter(array_map(fn($f)=>(string)($f['category'] ?? ''), $feeds)))); sort($categories); $message = ''; $error = ''; $editId = (string)($_GET['edit'] ?? ''); function fm_redirect(string $msg = ''): never { $url = 'feed_manager.php'; if ($msg !== '') $url .= '?msg=' . rawurlencode($msg); header('Location: ' . $url); exit; } function fm_find_index(array $feeds, string $id): int { foreach ($feeds as $i => $f) if (($f['id'] ?? '') === $id) return (int)$i; return -1; } function fm_parse_json_field(string $raw, string $field): array { $raw = trim($raw); if ($raw === '') return []; $data = json_decode($raw, true); if (!is_array($data)) throw new RuntimeException($field . ' must be valid JSON object.'); return $data; } $templates = [ 'rss' => ['id'=>'new_rss_feed','name'=>'New RSS Feed','category'=>'news','engine'=>'rss','enabled'=>true,'url'=>'https://example.com/feed.xml','refresh'=>900,'priority'=>70,'color'=>'#4be3ff'], 'json' => ['id'=>'new_json_api','name'=>'New JSON API','category'=>'data','engine'=>'json','enabled'=>true,'url'=>'https://example.com/api.json','refresh'=>900,'priority'=>70,'color'=>'#7a5cff','map'=>['title'=>'title','summary'=>'description','url'=>'url','published'=>'published_at','image'=>'image']], 'reddit' => ['id'=>'reddit_subreddit','name'=>'Reddit Subreddit','category'=>'social','engine'=>'reddit','enabled'=>true,'url'=>'https://www.reddit.com/r/worldnews/top.json?t=day&limit=30','refresh'=>900,'priority'=>70,'color'=>'#ff5bd1'], 'weather' => ['id'=>'weather_location','name'=>'Weather Location','category'=>'weather','engine'=>'weather','enabled'=>true,'lat'=>32.7767,'lon'=>-96.7970,'place'=>'Dallas, TX','refresh'=>600,'priority'=>74,'color'=>'#34f0c8'], 'webhook' => ['id'=>'webhook_stream','name'=>'Webhook Stream','category'=>'simon','engine'=>'webhook','enabled'=>true,'stream'=>'default','refresh'=>60,'priority'=>95,'color'=>'#9df4ff'], 'social' => ['id'=>'social_profile','name'=>'Social Profile','category'=>'social','engine'=>'social','enabled'=>true,'url'=>'https://example.com/profile','refresh'=>3600,'priority'=>65,'color'=>'#ff5bd1','params'=>['platform'=>'Social','handle'=>'@handle','connected_at'=>'2026-07-13T00:00:00Z']], ]; if (isset($_GET['msg'])) $message = (string)$_GET['msg']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = (string)($_POST['action'] ?? ''); try { if ($action === 'toggle') { $id = (string)($_POST['id'] ?? ''); $idx = fm_find_index($feeds, $id); if ($idx < 0) throw new RuntimeException('Feed not found.'); simon_backup_file(SIMON_FEEDS_FILE, 'toggle'); $feeds[$idx]['enabled'] = empty($feeds[$idx]['enabled']); simon_write_json_file(SIMON_FEEDS_FILE, $feeds); fm_redirect('Feed toggled.'); } if ($action === 'delete') { $id = (string)($_POST['id'] ?? ''); simon_backup_file(SIMON_FEEDS_FILE, 'delete'); $feeds = array_values(array_filter($feeds, fn($f)=>($f['id'] ?? '') !== $id)); simon_write_json_file(SIMON_FEEDS_FILE, $feeds); fm_redirect('Feed deleted. Backup created.'); } if ($action === 'save') { $originalId = (string)($_POST['original_id'] ?? ''); $params = fm_parse_json_field((string)($_POST['params_json'] ?? ''), 'Params'); $map = fm_parse_json_field((string)($_POST['map_json'] ?? ''), 'Map'); $input = [ 'id' => (string)($_POST['id'] ?? ''), 'name' => (string)($_POST['name'] ?? ''), 'category' => (string)($_POST['category'] ?? ''), 'engine' => (string)($_POST['engine'] ?? ''), 'enabled' => !empty($_POST['enabled']), 'url' => (string)($_POST['url'] ?? ''), 'stream' => (string)($_POST['stream'] ?? ''), 'lat' => (string)($_POST['lat'] ?? ''), 'lon' => (string)($_POST['lon'] ?? ''), 'place' => (string)($_POST['place'] ?? ''), 'refresh' => (string)($_POST['refresh'] ?? '900'), 'max_items' => (string)($_POST['max_items'] ?? '30'), 'priority' => (string)($_POST['priority'] ?? '70'), 'color' => (string)($_POST['color'] ?? '#4be3ff'), 'params' => $params, 'map' => $map, ]; $feed = simon_normalize_feed($input); $dupe = fm_find_index($feeds, $feed['id']); $idx = $originalId !== '' ? fm_find_index($feeds, $originalId) : -1; if ($dupe >= 0 && $dupe !== $idx) throw new RuntimeException('Feed ID already exists. Use another ID.'); simon_backup_file(SIMON_FEEDS_FILE, $idx >= 0 ? 'edit' : 'add'); if ($idx >= 0) $feeds[$idx] = $feed; else $feeds[] = $feed; simon_write_json_file(SIMON_FEEDS_FILE, $feeds); fm_redirect($idx >= 0 ? 'Feed updated. Backup created.' : 'Feed added. Backup created.'); } if ($action === 'import_json') { $raw = trim((string)($_POST['import_json'] ?? '')); $data = json_decode($raw, true); if (!is_array($data)) throw new RuntimeException('Import must be valid JSON.'); $incoming = array_is_list($data) ? $data : [$data]; simon_backup_file(SIMON_FEEDS_FILE, 'import'); $added = 0; $updated = 0; foreach ($incoming as $row) { if (!is_array($row)) continue; $feed = simon_normalize_feed($row); $idx = fm_find_index($feeds, $feed['id']); if ($idx >= 0) { $feeds[$idx] = $feed; $updated++; } else { $feeds[] = $feed; $added++; } } simon_write_json_file(SIMON_FEEDS_FILE, $feeds); fm_redirect("Import complete. Added $added, updated $updated."); } if ($action === 'restore_backup') { $file = basename((string)($_POST['file'] ?? '')); $path = SIMON_BACKUP_DIR . '/' . $file; if (!is_file($path)) throw new RuntimeException('Backup not found.'); $data = simon_read_json_file($path, null); if (!is_array($data)) throw new RuntimeException('Backup is not valid JSON.'); simon_backup_file(SIMON_FEEDS_FILE, 'before_restore'); simon_write_json_file(SIMON_FEEDS_FILE, $data); fm_redirect('Backup restored.'); } if ($action === 'clear_cache') { $n = 0; foreach (glob(SIMON_CACHE_DIR . '/*.json') ?: [] as $file) { if (@unlink($file)) $n++; } fm_redirect("Cache cleared: $n files."); } } catch (Throwable $e) { $error = $e->getMessage(); } } $editFeed = null; if ($editId !== '') { $idx = fm_find_index($feeds, $editId); if ($idx >= 0) $editFeed = $feeds[$idx]; } if (!$editFeed) { $tpl = (string)($_GET['template'] ?? ''); $editFeed = $templates[$tpl] ?? ['id'=>'','name'=>'','category'=>'news','engine'=>'rss','enabled'=>true,'url'=>'','refresh'=>900,'priority'=>70,'color'=>'#4be3ff']; } $backups = array_slice(array_reverse(glob(SIMON_BACKUP_DIR . '/feeds_*.json') ?: []), 0, 8); $enabledCount = count(array_filter($feeds, fn($f)=>!empty($f['enabled']))); $disabledCount = count($feeds) - $enabledCount; $providerMissing = []; foreach ($feeds as $f) { $e = (string)($f['engine'] ?? ''); if ($e && !in_array($e, $engines, true)) $providerMissing[$e] = true; } function h($v): string { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); } function pretty_json($v): string { return $v ? json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : ''; } ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>SIMON Feed Manager · Phase 2</title> <style> :root{--bg:#05041a;--panel:rgba(16,14,48,.78);--panel2:rgba(10,8,32,.92);--text:#eaf2ff;--muted:#9aa3c4;--line:rgba(122,92,255,.28);--cyan:#4be3ff;--teal:#34f0c8;--red:#ff6b8f;--amber:#ffb347;--pink:#ff5bd1;--green:#7cff9e} *{box-sizing:border-box}body{margin:0;background:radial-gradient(900px 500px at 20% 0,rgba(122,92,255,.22),transparent),linear-gradient(180deg,#05041a,#08072a);color:var(--text);font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}.wrap{max-width:1320px;margin:0 auto;padding:18px}.top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.brand{font-size:28px;font-weight:900}.pill,a.btn,button,input,select,textarea{border:1px solid var(--line);background:rgba(255,255,255,.055);color:var(--text);border-radius:14px;padding:10px 12px;text-decoration:none}button,.btn{cursor:pointer;font-weight:800}.btn.primary,button.primary{background:linear-gradient(135deg,var(--cyan),#7a5cff);color:#08072a;border:0}.grid{display:grid;grid-template-columns:1fr 420px;gap:16px}.panel{border:1px solid var(--line);background:linear-gradient(180deg,var(--panel),var(--panel2));border-radius:22px;padding:16px;margin:14px 0;box-shadow:0 18px 60px rgba(0,0,0,.25)}.stats{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:10px}.stat{border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:13px;background:rgba(255,255,255,.04)}.stat b{font-size:28px}.muted{color:var(--muted)}.ok{color:var(--green)}.bad{color:var(--red)}.warn{color:var(--amber)}table{width:100%;border-collapse:collapse}td,th{padding:10px;border-bottom:1px solid rgba(255,255,255,.08);text-align:left;vertical-align:top}th{color:#cfe8ff;font-size:11px;letter-spacing:.12em;text-transform:uppercase}.formgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.formgrid .wide{grid-column:1/-1}label{display:grid;gap:5px;color:#cfe8ff;font-size:12px;font-weight:800}input,select,textarea{width:100%;font:inherit}textarea{min-height:105px;border-radius:16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.actions{display:flex;gap:8px;flex-wrap:wrap}.tag{display:inline-flex;border:1px solid var(--line);border-radius:999px;padding:3px 8px;color:var(--muted);font-size:11px}.msg{border-color:rgba(124,255,158,.3);color:var(--green)}.err{border-color:rgba(255,107,143,.4);color:var(--red)}.small{font-size:12px}.search{max-width:360px;margin-left:auto}.rowbtn{padding:7px 9px;border-radius:999px;font-size:12px}.templates{display:flex;gap:8px;flex-wrap:wrap}.color-dot{width:14px;height:14px;border-radius:50%;display:inline-block;vertical-align:middle;margin-right:6px}.jsonbox{white-space:pre-wrap;word-break:break-word;background:rgba(0,0,0,.25);border-radius:14px;padding:10px;color:#d8e8ff;max-height:260px;overflow:auto}@media(max-width:1040px){.grid{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,1fr)}}@media(max-width:720px){.formgrid,.stats{grid-template-columns:1fr}table{font-size:12px}.hide-sm{display:none}.brand{font-size:22px}} </style> </head> <body> <div class="wrap"> <div class="top"> <div class="brand">SIMON Feed Manager</div> <span class="tag">Phase 2</span> <a class="btn" href="index.php">Dashboard</a> <a class="btn" href="news_reader.php">SIMON Daily</a> <a class="btn" href="feed_proxy.php?limit=50" target="_blank">JSON API</a> <a class="btn" href="webhook_test.php" target="_blank">Webhook Test</a> <form method="post" style="margin-left:auto"><button class="btn" name="action" value="clear_cache">Clear Cache</button></form> </div> <?php if($message): ?><div class="panel msg"><?=h($message)?></div><?php endif; ?> <?php if($error): ?><div class="panel err"><?=h($error)?></div><?php endif; ?> <div class="stats"> <div class="stat"><div class="muted">Total Feeds</div><b><?=count($feeds)?></b></div> <div class="stat"><div class="muted">Enabled</div><b class="ok"><?=$enabledCount?></b></div> <div class="stat"><div class="muted">Disabled</div><b class="warn"><?=$disabledCount?></b></div> <div class="stat"><div class="muted">Providers</div><b><?=count($engines)?></b></div> </div> <?php if($providerMissing): ?><div class="panel err"><b>Missing provider files:</b> <?=h(implode(', ', array_keys($providerMissing)))?></div><?php endif; ?> <div class="grid"> <main> <div class="panel"> <div class="top"> <h2 style="margin:0">Feeds</h2> <input class="search" id="feedSearch" placeholder="Filter table…"> </div> <table id="feedTable"> <thead><tr><th>Status</th><th>Name</th><th>Engine</th><th>Route</th><th>Actions</th></tr></thead> <tbody> <?php foreach($feeds as $f): $id=(string)($f['id']??''); $test='feed_proxy.php?source='.rawurlencode($id).'&limit=10&force=1'; ?> <tr data-row="<?=h(strtolower(json_encode($f)))?>"> <td><span class="<?=!empty($f['enabled'])?'ok':'bad'?>"><?=!empty($f['enabled'])?'ON':'OFF'?></span><br><span class="small muted">priority <?=h($f['priority']??'')?></span></td> <td><b><?=h($f['name']??'')?></b><br><span class="muted small"><?=h($id)?></span><br><span class="tag"><span class="color-dot" style="background:<?=h($f['color']??'#4be3ff')?>"></span><?=h($f['category']??'')?></span></td> <td><?=h($f['engine']??'')?><br><span class="muted small">refresh <?=h($f['refresh']??'')?>s</span></td> <td class="small hide-sm"><?=h($f['url']??($f['stream']??(($f['place']??''))))?></td> <td> <div class="actions"> <a class="btn rowbtn" href="feed_manager.php?edit=<?=rawurlencode($id)?>">Edit</a> <a class="btn rowbtn" href="<?=$test?>" target="_blank">Test</a> <form method="post" style="display:inline"><input type="hidden" name="id" value="<?=h($id)?>"><button class="rowbtn" name="action" value="toggle">Toggle</button></form> <form method="post" style="display:inline" onsubmit="return confirm('Delete <?=h($id)?>? A backup will be created first.')"><input type="hidden" name="id" value="<?=h($id)?>"><button class="rowbtn" name="action" value="delete">Delete</button></form> </div> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <div class="panel"> <h2>Import JSON</h2> <p class="muted">Paste one feed object or an array of feed objects. Existing IDs update in place. Backup is created before import.</p> <form method="post"> <input type="hidden" name="action" value="import_json"> <textarea name="import_json" placeholder='[{"id":"example","name":"Example RSS","engine":"rss","category":"news","url":"https://example.com/feed.xml","enabled":true}]'></textarea> <p><button class="primary">Import / Update Feeds</button></p> </form> </div> <div class="panel"> <h2>Backups</h2> <?php if(!$backups): ?><p class="muted">No backups yet. Saving, toggling, deleting, importing, or restoring creates backups automatically.</p><?php endif; ?> <?php foreach($backups as $b): $base=basename($b); ?> <form method="post" class="top" onsubmit="return confirm('Restore this backup? Current feeds.json will be backed up first.')"> <input type="hidden" name="file" value="<?=h($base)?>"> <span class="pill"><?=h($base)?></span> <button name="action" value="restore_backup">Restore</button> </form> <?php endforeach; ?> </div> </main> <aside> <div class="panel"> <h2><?= $editId ? 'Edit Feed' : 'Add Feed' ?></h2> <div class="templates"> <?php foreach($templates as $k=>$t): ?><a class="btn rowbtn" href="feed_manager.php?template=<?=h($k)?>">+ <?=h(strtoupper($k))?></a><?php endforeach; ?> </div> <form method="post" style="margin-top:12px"> <input type="hidden" name="action" value="save"> <input type="hidden" name="original_id" value="<?=h($editId)?>"> <div class="formgrid"> <label>ID<input name="id" required value="<?=h($editFeed['id']??'')?>" placeholder="nasa_news"></label> <label>Enabled<select name="enabled"><option value="1" <?=!empty($editFeed['enabled'])?'selected':''?>>Enabled</option><option value="" <?=empty($editFeed['enabled'])?'selected':''?>>Disabled</option></select></label> <label class="wide">Name<input name="name" required value="<?=h($editFeed['name']??'')?>"></label> <label>Category<input name="category" list="categoryList" value="<?=h($editFeed['category']??'news')?>"></label> <label>Engine<select name="engine"><?php foreach($engines as $e): ?><option value="<?=h($e)?>" <?=($editFeed['engine']??'')===$e?'selected':''?>><?=h($e)?></option><?php endforeach; ?></select></label> <label class="wide">URL<input name="url" value="<?=h($editFeed['url']??'')?>" placeholder="RSS/API URL"></label> <label>Webhook Stream<input name="stream" value="<?=h($editFeed['stream']??'')?>" placeholder="default"></label> <label>Place<input name="place" value="<?=h($editFeed['place']??'')?>" placeholder="Dallas, TX"></label> <label>Latitude<input name="lat" value="<?=h($editFeed['lat']??'')?>"></label> <label>Longitude<input name="lon" value="<?=h($editFeed['lon']??'')?>"></label> <label>Refresh Seconds<input name="refresh" type="number" min="30" value="<?=h($editFeed['refresh']??900)?>"></label> <label>Items Per Feed<input name="max_items" type="number" min="1" max="100" value="<?=h($editFeed['max_items']??30)?>"></label> <label>Priority 0-100<input name="priority" type="number" min="0" max="100" value="<?=h($editFeed['priority']??70)?>"></label> <label>Color<input name="color" value="<?=h($editFeed['color']??'#4be3ff')?>"></label> <label class="wide">Params JSON<textarea name="params_json" placeholder='{"country":"us","pageSize":"50"}'><?=h(pretty_json($editFeed['params']??[]))?></textarea></label> <label class="wide">Map JSON<textarea name="map_json" placeholder='{"title":"name","summary":"details","url":"links.webcast","published":"date_utc"}'><?=h(pretty_json($editFeed['map']??[]))?></textarea></label> </div> <p class="actions"><button class="primary">Save Feed</button><a class="btn" href="feed_manager.php">Cancel</a><?php if($editId): ?><a class="btn" target="_blank" href="feed_proxy.php?source=<?=rawurlencode($editId)?>&limit=10&force=1">Test Current</a><?php endif; ?></p> </form> </div> <div class="panel"> <h2>Provider Engines</h2> <p class="muted">Provider files currently installed in <code>providers/</code>.</p> <div class="actions"><?php foreach($engines as $e): ?><span class="tag"><?=h($e)?></span><?php endforeach; ?></div> </div> <div class="panel"> <h2>Current feeds.json</h2> <div class="jsonbox"><?=h(json_encode($feeds, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE))?></div> </div> </aside> </div> </div> <datalist id="categoryList"><?php foreach($categories as $c): ?><option value="<?=h($c)?>"><?php endforeach; ?></datalist> <script> const s=document.getElementById('feedSearch'); s&&s.addEventListener('input',()=>{const q=s.value.toLowerCase();document.querySelectorAll('#feedTable tbody tr').forEach(r=>r.style.display=r.dataset.row.includes(q)?'':'none')}); </script> </body> </html>
Save file
Quick jump
open a path
Open