initial commit — cassandra v0.1
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>
This commit is contained in:
commit
a10409c02b
61 changed files with 4890 additions and 0 deletions
73
app/static/js/chat.js
Normal file
73
app/static/js/chat.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Cassandra chat sidebar — ephemeral, client-state conversation.
|
||||
// No persistence: page refresh starts a new chat.
|
||||
(() => {
|
||||
const thread = document.getElementById('chat-thread');
|
||||
const form = document.getElementById('chat-form');
|
||||
const input = document.getElementById('chat-input');
|
||||
const send = document.getElementById('chat-send');
|
||||
if (!thread || !form || !input || !send) return;
|
||||
|
||||
/** @type {{role: string, content: string}[]} */
|
||||
const messages = [];
|
||||
|
||||
function escapeHTML(s) {
|
||||
return s.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function append(role, html_or_text, opts) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'chat-msg chat-msg--' + role;
|
||||
if (opts && opts.html) {
|
||||
div.innerHTML = html_or_text;
|
||||
} else {
|
||||
div.textContent = html_or_text;
|
||||
}
|
||||
thread.appendChild(div);
|
||||
thread.scrollTop = thread.scrollHeight;
|
||||
return div;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const text = input.value.trim();
|
||||
if (!text || send.disabled) return;
|
||||
messages.push({role: 'user', content: text});
|
||||
append('user', text);
|
||||
input.value = '';
|
||||
send.disabled = true;
|
||||
const thinking = append('assistant pending', '…');
|
||||
try {
|
||||
const r = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({messages}),
|
||||
});
|
||||
if (!r.ok) {
|
||||
const msg = await r.text();
|
||||
thinking.className = 'chat-msg chat-msg--error';
|
||||
thinking.textContent = 'HTTP ' + r.status + ': ' + msg.slice(0, 300);
|
||||
return;
|
||||
}
|
||||
const data = await r.json();
|
||||
thinking.className = 'chat-msg chat-msg--assistant';
|
||||
thinking.innerHTML = data.content_html || escapeHTML(data.content);
|
||||
messages.push({role: 'assistant', content: data.content});
|
||||
} catch (err) {
|
||||
thinking.className = 'chat-msg chat-msg--error';
|
||||
thinking.textContent = 'error: ' + err.message;
|
||||
} finally {
|
||||
send.disabled = false;
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Enter to send; Shift+Enter for newline.
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
})();
|
||||
1
app/static/js/htmx.min.js
vendored
Normal file
1
app/static/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue