Containerised macro-strategy dashboard: 4-panel web UI (indicators, portfolio, flash news, AI strategic log), MariaDB store, hourly ingestion jobs, OpenRouter-backed AI analysis. Ports the four prototype scripts in the parent dir (market_pulse, flash_news, trading212, strategic_log) into async services backed by a persistent DB and served via FastAPI + Jinja2 + HTMX. APScheduler runs as a separate compose service for crash-safety and easier restarts. Portfolio composition + position names come live from Trading 212; news per-ticker headlines reuse those names. Tone (NOVICE/INTERMEDIATE/ PRO) and analysis style (DRY/SPECULATIVE) are env-configurable and stored on each log row so historical entries show what produced them. Default model is deepseek/deepseek-v4-flash (overridable via env). Light/dark theme toggle, sans-serif for prose surfaces, monospace for data. Bearer-token auth, OpenRouter monthly cost cap, RSS feeds auto- disabled on consecutive failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.7 KiB
HTML
71 lines
2.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>{% block title %}Cassandra{% endblock %}</title>
|
|
{# Apply saved theme before stylesheet renders to avoid a flash. #}
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var t = localStorage.getItem('cassandra.theme') || 'dark';
|
|
document.documentElement.dataset.theme = t;
|
|
} catch (e) { document.documentElement.dataset.theme = 'dark'; }
|
|
})();
|
|
</script>
|
|
<link rel="stylesheet" href="{{ url_for('static', path='/css/cassandra.css') }}" />
|
|
<script src="{{ url_for('static', path='/js/htmx.min.js') }}" defer></script>
|
|
<script>
|
|
// Render any <time datetime="..."> in the browser's local timezone.
|
|
// Re-runs after every HTMX swap so freshly-loaded news rows pick up too.
|
|
function formatLocalTimes() {
|
|
document.querySelectorAll('time[datetime]:not([data-local])').forEach(function (t) {
|
|
try {
|
|
var d = new Date(t.getAttribute('datetime'));
|
|
if (isNaN(d.getTime())) return;
|
|
var date = d.toLocaleDateString(undefined, { day: '2-digit', month: 'short' });
|
|
var time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
t.textContent = date + ' ' + time;
|
|
t.title = d.toLocaleString();
|
|
t.setAttribute('data-local', '1');
|
|
} catch (e) {}
|
|
});
|
|
}
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
formatLocalTimes();
|
|
document.body.addEventListener('htmx:afterSwap', formatLocalTimes);
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="app">
|
|
<header class="app-header">
|
|
<div class="brand">Cassandra</div>
|
|
<nav>
|
|
<a href="/" class="{% if request.url.path == '/' %}active{% endif %}">Dashboard</a>
|
|
<a href="/news" class="{% if request.url.path == '/news' %}active{% endif %}">News</a>
|
|
<a href="/log" class="{% if request.url.path.startswith('/log') %}active{% endif %}">Log</a>
|
|
</nav>
|
|
<div class="header-right">
|
|
<button class="theme-toggle" type="button" aria-label="Toggle theme"
|
|
onclick="(function(){var d=document.documentElement;var t=d.dataset.theme==='light'?'dark':'light';d.dataset.theme=t;try{localStorage.setItem('cassandra.theme',t);}catch(e){}})()">
|
|
<span class="theme-toggle__label"></span>
|
|
</button>
|
|
<span class="meta">v0.1 · UTC</span>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="app-main">
|
|
{% block main %}{% endblock %}
|
|
</main>
|
|
|
|
<footer class="app-footer"
|
|
hx-get="/api/health"
|
|
hx-trigger="load, every 30s"
|
|
hx-swap="innerHTML"
|
|
id="ops-footer">
|
|
<span class="led idle"></span> awaiting status…
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>
|