Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions docs/frontend_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,8 @@ The application uses a **single-page template** (`index.html`) with tab-based na
```
┌─────────────────────────────────────────────────────────┐
│ Header (site-header) │
│ ├── Brand (icon + title + subtitle) │
│ ├── Parameters bar [ticker] [Run] │ ← not a tab, always visible
│ └── Theme toggle │
│ Brand (icon+title, left) [ticker] [✅/❌] [Run] (center) │ ← not a tab, always visible
│ Theme toggle (right)│
├─────────────────────────────────────────────────────────┤
│ Sidebar │ Main Panel │
│ (tab-nav) │ (tab-content) │
Expand All @@ -144,16 +143,31 @@ The application uses a **single-page template** (`index.html`) with tab-based na
```

The **Parameters bar** (`templates/partials/parameters_bar.html`, batch B6; folded into
the header post-B10) renders inside `.site-header`, between the brand and the theme
toggle, and owns exactly one visible input — `ticker` — plus the Run button and the
ticker-validation badges. Every other parameter lives in its module's toolbar (batch
B7); the bar also carries two hidden `start_time`/`end_time` inputs that `POST /`
validates and uses to size the readiness prefetch, kept in sync by
`state/marketParamsState.js`. It is **not** a tab: it survives tab switches, and it is
always visible — there is no collapse toggle. Because `.site-header` stays dark in both
themes, the bar's own controls (label, input) use light-on-dark styling rather than the
page's light-theme tokens; the ticker-validation badges and the error alert keep their
own self-contained colors, so they read the same as before.
the header post-B10) renders inside `.site-header` and owns exactly one visible input —
`ticker` — plus the Run button and a validity icon per ticker. `.header-inner` is a
3-column grid (`1fr auto 1fr`: brand / bar / actions); the bar sits in the `auto` middle
column, so it is truly centered on the row regardless of how wide the brand or the theme
toggle are, not just left-packed next to the brand.
`.parameters-bar-main` (the label + input + validity icons + Run) is deliberately
`flex-wrap: nowrap`: a multi-line flex container's intrinsic width is only its widest
single child, not the sum, which would starve the grid column of the width it needs —
wrapping only comes back below the 768px breakpoint, where the bar is stretched to the
header's full width by `grid-template-areas` instead of being intrinsically sized, so
wrapping there is safe.

Every other parameter lives in its module's toolbar (batch B7); the bar also carries two
hidden `start_time`/`end_time` inputs that `POST /` validates and uses to size the
readiness prefetch, kept in sync by `state/marketParamsState.js`. It is **not** a tab: it
survives tab switches, and it is always visible — there is no collapse toggle. Because
`.site-header` stays dark in both themes, the bar's own controls (label, input) use
light-on-dark styling rather than the page's light-theme tokens.

**Ticker validity feedback** (`#ticker-badges`, `static/main.js::validateTicker`) is a
single ✅/❌ glyph per parsed ticker, rendered immediately to the right of the input —
not the pre-existing pill badges (ticker + price, colored background) and not the
`▸ N ticker(s) valid` sentence that used to sit below the input; both are gone. The glyph
carries the ticker (and price, if valid) as its `title` tooltip. Debounced 500ms after
input, same as before.

### Peek Sidebar

Expand Down
33 changes: 12 additions & 21 deletions static/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ const FormManager = {
}
};

// Validity feedback is a single ✅/❌ icon per ticker, rendered to the right
// of the input (#ticker-badges) — no separate status sentence.
let validationTimeout;
function validateTicker() {
const rawInput = document.getElementById('ticker').value.trim().toUpperCase();
const validationDiv = document.getElementById('ticker-validation');
const badgesDiv = document.getElementById('ticker-badges');

if (!rawInput) {
if (validationDiv) validationDiv.innerHTML = '';
if (badgesDiv) badgesDiv.innerHTML = '';
currentPrice = null;
return;
Expand All @@ -120,10 +120,12 @@ function validateTicker() {
validationTimeout = setTimeout(() => {
const tickers = parseTickers(rawInput);
if (tickers.length === 0) {
if (validationDiv) validationDiv.innerHTML = 'No valid symbols';
if (badgesDiv) {
badgesDiv.innerHTML = '<span class="ticker-badge invalid" title="No valid symbols">❌</span>';
}
return;
}
if (validationDiv) validationDiv.innerHTML = 'Validating...';
if (badgesDiv) badgesDiv.innerHTML = '';

fetch('/api/validate_tickers', {
method: 'POST',
Expand All @@ -137,35 +139,24 @@ function validateTicker() {
if (badgesDiv) {
badgesDiv.innerHTML = Object.entries(results).map(([t, info]) => {
const cls = info.valid ? 'ticker-badge valid' : 'ticker-badge invalid';
const symbol = info.valid ? '✅' : '❌';
const priceTxt = info.valid && info.price ? ` $${info.price.toFixed(2)}` : '';
return `<span class="${cls}">${escapeHtml(t)}${priceTxt}</span>`;
}).join(' ');
const title = `${escapeHtml(t)}${priceTxt}`;
return `<span class="${cls}" title="${title}">${symbol}</span>`;
}).join('');
}
const firstValid = Object.entries(results).find(([, info]) => info.valid);
currentPrice = firstValid ? firstValid[1].price : null;

const validCount = Object.values(results).filter(r => r.valid).length;
const totalCount = Object.keys(results).length;
if (validationDiv) {
if (validCount === totalCount) {
validationDiv.innerHTML = `${validCount} ticker(s) valid`;
validationDiv.className = 'ticker-validation valid';
} else {
validationDiv.innerHTML = `${validCount}/${totalCount} valid`;
validationDiv.className = 'ticker-validation warning';
}
}

const validTickers = Object.entries(results)
.filter(([, info]) => info.valid)
.map(([t]) => t);
if (validTickers.length > 0) preloadOptionChains(validTickers);
}
})
.catch(() => {
if (validationDiv) {
validationDiv.innerHTML = 'Error';
validationDiv.className = 'ticker-validation warning';
if (badgesDiv) {
badgesDiv.innerHTML = '<span class="ticker-badge invalid" title="Error checking ticker validity">❌</span>';
}
currentPrice = null;
});
Expand Down
111 changes: 52 additions & 59 deletions static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,18 @@ body {
box-shadow: 0 1px 0 rgba(255, 255, 255, .06), var(--shadow-md);
}

/* Three columns (brand / ticker form / actions) so the middle column —
the Parameters bar — sits truly centered on the row, not just left-packed
next to the brand: the two outer `1fr` tracks absorb the leftover space
equally regardless of how wide the brand or the actions are. */
.header-inner {
width: 100%;
max-width: 1600px;
margin: 0 auto;
padding: .6rem 1.5rem;
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
justify-content: space-between;
gap: .6rem 1rem;
}

Expand All @@ -172,6 +175,8 @@ body {
align-items: center;
gap: .75rem;
color: #fff;
justify-self: start;
min-width: 0;
}

.brand-icon {
Expand Down Expand Up @@ -212,6 +217,7 @@ body {
align-items: center;
gap: .9rem;
flex-shrink: 0;
justify-self: end;
}

/* Theme toggle — sits on the sticky header, which stays dark in BOTH
Expand Down Expand Up @@ -609,24 +615,6 @@ body {
border-radius: var(--radius-sm);
}

/* Ticker validation */
.ticker-validation {
font-size: .75rem;
font-weight: 500;
}

.ticker-validation.valid {
color: var(--success);
}

.ticker-validation.invalid {
color: var(--error);
}

.ticker-validation.warning {
color: var(--warning);
}

/* ── Collapsible Positions ── */
.form-card.collapsible .form-card-title {
display: flex;
Expand Down Expand Up @@ -1116,11 +1104,33 @@ body {
display: none;
}

/* Brand + theme toggle share the first line; the ticker form (flex-basis
100%) is forced onto its own line below, same as the old separate bar. */
/* Brand + theme toggle share the first line (their own grid track each);
the ticker form drops to a second, full-width row below — same as the
old separate bar — instead of trying to stay centered on one line. */
.header-inner {
grid-template-columns: 1fr auto;
grid-template-areas:
"brand actions"
"ticker ticker";
}

.header-brand {
grid-area: brand;
}

.header-actions {
grid-area: actions;
}

.parameters-bar {
order: 3;
flex-basis: 100%;
grid-area: ticker;
justify-self: stretch;
}

/* Full-width here (not intrinsically sized), so wrapping is safe and
needed on narrow phones. */
.parameters-bar-main {
flex-wrap: wrap;
}
}

Expand Down Expand Up @@ -1895,35 +1905,19 @@ body {
.ticker-badges {
display: flex;
flex-wrap: wrap;
gap: .35rem;
margin-top: .4rem;
align-items: center;
gap: .25rem;
}

/* One ✅/❌ glyph per ticker — the emoji itself carries the valid/invalid
color, so `.valid`/`.invalid` are kept only as hooks (title tooltip has
the ticker + price / reason) rather than for extra styling. */
.ticker-badge {
display: inline-flex;
align-items: center;
gap: .25rem;
font-size: .73rem;
font-weight: 600;
padding: .2rem .55rem;
border-radius: 999px;
white-space: nowrap;
}

.ticker-badge.valid {
background: #ecfdf5;
color: #065f46;
border: 1px solid #a7f3d0;
}

.ticker-badge.invalid {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}

.ticker-badge i {
font-size: .65rem;
font-size: 1rem;
line-height: 1;
cursor: default;
}

/* Ticker tab navigation (for multi-ticker results) */
Expand Down Expand Up @@ -3175,14 +3169,21 @@ textarea:focus-visible,
display: flex;
flex-direction: column;
gap: 4px;
flex: 1 1 320px;
justify-self: center;
min-width: 0;
font-family: var(--font);
}

/* `flex-wrap: nowrap` here is load-bearing for the grid `auto` column above:
a multi-line flex container's intrinsic (max-content) width is only its
*widest single child*, not the sum of all of them, which collapsed this
row to one item's width and wrapped the rest. Single-line intrinsic width
is the sum, which is what actually sizes the column to fit the row. Wrap
is re-enabled in the mobile media query, where this row is stretched to
the header's full width instead of being intrinsically sized. */
.parameters-bar-main {
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
gap: 10px;
}
Expand Down Expand Up @@ -3222,14 +3223,6 @@ textarea:focus-visible,
margin: 0;
}

.parameters-bar .ticker-validation {
color: var(--slate-300);
}

.parameters-bar .btn-primary {
margin-left: auto;
}

/* ============================================================
Module toolbars (batch B7) — each module owns its parameters.
They sit under the panel header; the values travel on the module's
Expand Down
6 changes: 3 additions & 3 deletions templates/partials/parameters_bar.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
index.html header comment).

Owns exactly ONE input that every data module shares: `ticker` — plus the Run
button and the ticker-validation badges. It is not a tab: it renders inside
button and a validity icon (✅/❌ per ticker, static/main.js::validateTicker)
rendered to the input's right. It is not a tab: it renders inside
`.site-header`, so it stays put while the user switches tabs and never hides.

Batch B7 moved every other parameter to the module that owns it: the horizon
Expand All @@ -19,7 +20,7 @@
<input type="text" id="ticker" name="ticker" class="parameters-bar-ticker"
value="{{ ticker or '' }}" placeholder="e.g. AAPL, SPY, ^SPX"
autocomplete="off" required>
<div id="ticker-validation-container" class="ticker-badges"></div>
<div id="ticker-badges" class="ticker-badges" aria-live="polite"></div>

<button type="submit" class="btn-primary" id="parameters-bar-run">Run</button>
</div>
Expand All @@ -28,7 +29,6 @@
{% if error %}
<div class="alert alert-error parameters-bar-alert"><span>{{ error }}</span></div>
{% endif %}
<div class="ticker-validation" id="ticker-validation"></div>

{# See the header comment: submit-only mirror of the marketParams store. #}
<input type="hidden" id="start_time" name="start_time">
Expand Down
Loading