phase G: data minimisation + passwordless auth + DeepSeek-first LLM
Server no longer holds portfolios. Holdings live in the browser (localStorage); the server publishes an anonymous ticker_universe and a gzipped /api/universe payload identical for every authenticated user, so access patterns can't betray which tickers a user holds. AI commentary is generated ephemerally from the browser-supplied pie and the cost ledger row records no positions. Migrations 0009-0011 added the universe table and dropped positions / portfolio_snapshots / portfolios. Authentication is now e-mail OTP only. Migration 0010 dropped password_hash and email_verified (every active session is by construction proof of email control). The /signup endpoint is gone; signup and login share a single email-entry page. Email rendering is HTML+plain-text multipart with a shared brand palette (app/branding.py) asserted in sync with the CSS by a drift-detection test. LLM provider defaults to DeepSeek-direct (cheaper, api.deepseek.com) with OpenRouter as automatic fallback if DeepSeek fails. ai_log_job and indicator_summary_job now iterate the two tones (NOVICE, INTERMEDIATE) per cycle so the dashboard's tone toggle is instant; PROMPT_VERSION bumped to 6 with an educational anti-TA / anti-gambling stance baked into _CORE. NOVICE mode renders a curated glossary inline (CBOE VIX, yield curve, HY OAS, etc.) with JS-positioned tooltips that survive viewport edges and sticky bars. Model name and tokens hidden from the user UI; still recorded in StrategicLog.model and AICall for admin. Layout adds a sticky top nav, a sticky bottom markets bar (one chip per exchange with status LED + headline index + 1d change), and Phase H feedback reporting is queued in tasks/todo.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
480fd311c5
commit
6e7f57c6b2
54 changed files with 5005 additions and 916 deletions
|
|
@ -75,6 +75,9 @@ a:hover { text-decoration: underline; }
|
|||
background: var(--surface);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
.app-header .brand {
|
||||
color: var(--accent);
|
||||
|
|
@ -104,6 +107,33 @@ a:hover { text-decoration: underline; }
|
|||
.theme-toggle__label::before { content: "◐ dark"; }
|
||||
[data-theme="light"] .theme-toggle__label::before { content: "◐ light"; }
|
||||
|
||||
/* Tone toggle (segmented control: Novice | Intermediate) */
|
||||
.tone-toggle {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tone-toggle button {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 0;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-transform: inherit;
|
||||
}
|
||||
.tone-toggle button + button { border-left: 1px solid var(--border); }
|
||||
.tone-toggle button:hover { color: var(--accent); }
|
||||
.tone-toggle[data-tone="NOVICE"] button[data-value="NOVICE"],
|
||||
.tone-toggle[data-tone="INTERMEDIATE"] button[data-value="INTERMEDIATE"] {
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
|
|
@ -124,9 +154,18 @@ a:hover { text-decoration: underline; }
|
|||
|
||||
#indicators-panel { grid-area: indicators; }
|
||||
#portfolio-panel { grid-area: portfolio; }
|
||||
#log-panel { grid-area: log; }
|
||||
#log-panel {
|
||||
grid-area: log;
|
||||
/* Don't stretch to fill both grid rows; if the log is shorter than
|
||||
the portfolio next to it, the surplus below would render as a big
|
||||
empty white box. Aligning to the start makes the panel shrink to
|
||||
its content and the dashboard background fills any gap. */
|
||||
align-self: start;
|
||||
}
|
||||
#news-panel { grid-area: news; }
|
||||
|
||||
/* Legacy footer rules — kept for the /api/health page which still uses
|
||||
the old class via the standalone HTML template. */
|
||||
.app-footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 8px 18px;
|
||||
|
|
@ -138,6 +177,27 @@ a:hover { text-decoration: underline; }
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Sticky bottom markets bar — uses the same .mkt chip styling as the
|
||||
old dashboard header, extended with each market's headline index. */
|
||||
.markets-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.markets-bar__inner {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--border);
|
||||
border: 0;
|
||||
}
|
||||
.markets-bar .mkt {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* --- Panels ----------------------------------------------------------- */
|
||||
|
||||
.panel {
|
||||
|
|
@ -193,6 +253,10 @@ table.dense td[title] {
|
|||
border-bottom: 1px dotted color-mix(in srgb, var(--accent) 40%, transparent);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.pf-name.has-tip {
|
||||
cursor: help;
|
||||
border-bottom: 1px dotted color-mix(in srgb, var(--accent) 50%, transparent);
|
||||
}
|
||||
table.dense tr:hover td { background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||
|
||||
.pos { color: var(--positive); }
|
||||
|
|
@ -251,7 +315,8 @@ table.dense tr.row-stale td { color: var(--dim); }
|
|||
}
|
||||
.mkt__dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
grid-row: 1; grid-column: 1;
|
||||
grid-row: 1 / span 2; grid-column: 1;
|
||||
align-self: center;
|
||||
}
|
||||
.mkt--open .mkt__dot { background: var(--positive); box-shadow: 0 0 6px var(--positive); }
|
||||
.mkt--closed .mkt__dot { background: var(--dim); }
|
||||
|
|
@ -263,13 +328,30 @@ table.dense tr.row-stale td { color: var(--dim); }
|
|||
.mkt__state {
|
||||
grid-row: 1; grid-column: 3;
|
||||
font-size: 9.5px; letter-spacing: 0.08em;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.mkt--open .mkt__state { color: var(--positive); }
|
||||
.mkt--closed .mkt__state { color: var(--dim); }
|
||||
.mkt__index {
|
||||
grid-row: 2; grid-column: 2;
|
||||
font-size: 10.5px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mkt__index-label { color: var(--dim); }
|
||||
.mkt__index-price { color: var(--text); }
|
||||
.mkt__index-change.pos { color: var(--positive); }
|
||||
.mkt__index-change.neg { color: var(--negative); }
|
||||
.mkt__index-change.neu { color: var(--muted); }
|
||||
.mkt__index--empty { color: var(--dim); font-size: 10px; }
|
||||
.mkt__when {
|
||||
grid-row: 2; grid-column: 2 / -1;
|
||||
grid-row: 2; grid-column: 3;
|
||||
color: var(--muted); font-size: 10px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
.mkt__when-label { color: var(--dim); }
|
||||
|
||||
|
|
@ -334,6 +416,41 @@ table.dense tr.row-stale td { color: var(--dim); }
|
|||
.ind-summary--pending { color: var(--dim); font-style: italic; }
|
||||
.ind-summary--pending .ind-summary__body { color: var(--dim); font-size: 12px; }
|
||||
|
||||
/* --- Glossary tooltips (Novice mode) --------------------------------- */
|
||||
/* The term gets a dotted underline. The actual tooltip is a single shared
|
||||
element (#glossary-tooltip) positioned by JS so it can flip on viewport
|
||||
edges and never clip behind sticky bars (which sit at z-index 50). */
|
||||
|
||||
.glossary {
|
||||
border-bottom: 1px dotted var(--accent);
|
||||
cursor: help;
|
||||
/* Same colour as surrounding text — only the underline signals "tooltip
|
||||
available", keeping the paragraph visually quiet. */
|
||||
}
|
||||
.glossary:focus { outline: 1px dotted var(--accent); outline-offset: 2px; }
|
||||
|
||||
#glossary-tooltip {
|
||||
position: fixed;
|
||||
z-index: 200; /* Above sticky bars (z-index 50). */
|
||||
max-width: 300px;
|
||||
padding: 9px 12px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--accent);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.35);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 90ms ease;
|
||||
}
|
||||
#glossary-tooltip[data-visible="1"] { opacity: 1; }
|
||||
#glossary-tooltip[hidden] { display: none; }
|
||||
|
||||
/* --- Group tabs ------------------------------------------------------- */
|
||||
|
||||
.group-tabs {
|
||||
|
|
@ -407,6 +524,86 @@ table.dense tr.row-stale td { color: var(--dim); }
|
|||
.pf-stat-value.neu { color: var(--muted); }
|
||||
.pf-ccy { color: var(--dim); font-size: 11px; margin-left: 2px; }
|
||||
.pf-pct { color: var(--dim); font-size: 11px; margin-left: 4px; }
|
||||
.pf-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
||||
.pf-pill {
|
||||
font-size: 10.5px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--muted);
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 6px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.pf-warn {
|
||||
border-left: 3px solid var(--alert);
|
||||
background: color-mix(in srgb, var(--alert) 6%, transparent);
|
||||
color: var(--alert);
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.pf-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.pf-actions button {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
background: var(--surface-2);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--border);
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pf-actions button:hover { border-color: var(--accent); }
|
||||
.pf-actions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.pf-actions .pf-secondary { color: var(--muted); }
|
||||
.pf-actions .pf-secondary:hover { color: var(--negative); border-color: var(--negative); }
|
||||
.pf-analysis {
|
||||
margin-top: 14px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.pf-analysis__details { padding: 0; }
|
||||
.pf-analysis__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
list-style: none; /* hide native marker in Firefox */
|
||||
}
|
||||
.pf-analysis__head::-webkit-details-marker { display: none; }
|
||||
.pf-analysis__head-left::before {
|
||||
content: "▸ ";
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
color: var(--accent);
|
||||
transition: transform 120ms ease;
|
||||
}
|
||||
details[open] .pf-analysis__head-left::before { content: "▾ "; }
|
||||
.pf-analysis__head:hover { color: var(--accent); }
|
||||
.pf-analysis__head:hover .pf-analysis__head-left::before { color: var(--accent); }
|
||||
.pf-analysis__details[open] .pf-analysis__head {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.pf-analysis__body {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--text);
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
/* --- Log panel -------------------------------------------------------- */
|
||||
|
||||
|
|
@ -583,13 +780,15 @@ table.dense tr.row-stale td { color: var(--dim); }
|
|||
/* --- Log metadata footer ---------------------------------------------- */
|
||||
|
||||
.log-meta {
|
||||
padding: 8px clamp(20px, 4vw, 56px) 16px;
|
||||
padding: 4px clamp(20px, 4vw, 56px) 6px;
|
||||
max-width: 76ch;
|
||||
margin: 0 auto;
|
||||
border-top: 1px dashed var(--border);
|
||||
color: var(--dim);
|
||||
font-size: 10.5px;
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.log-meta__row { display: flex; flex-wrap: wrap; align-items: center; gap: 0; margin-top: 6px; }
|
||||
.log-meta__row--dim { color: var(--dim); font-size: 10.5px; }
|
||||
|
||||
/* --- Auth pages (login / signup, standalone — no app chrome) -------- */
|
||||
|
||||
|
|
@ -674,6 +873,32 @@ table.dense tr.row-stale td { color: var(--dim); }
|
|||
margin-bottom: 14px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.auth-info {
|
||||
border-left: 3px solid var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 6%, transparent);
|
||||
color: var(--accent);
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 14px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.auth-card__lede {
|
||||
font-size: 12.5px;
|
||||
color: var(--muted);
|
||||
margin: 0 0 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.auth-card__lede strong { color: var(--text); font-weight: normal; }
|
||||
.auth-card__resend {
|
||||
background: transparent !important;
|
||||
color: var(--muted) !important;
|
||||
border: 1px dashed var(--border) !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
.auth-card__resend:hover {
|
||||
color: var(--accent) !important;
|
||||
border-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
/* User chip in header */
|
||||
.user-chip {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue