new config file for job status.

This commit is contained in:
zipfriis
2026-05-27 13:40:25 +02:00
parent 47072ef0b9
commit 7ff604143c
8 changed files with 50 additions and 749 deletions
-8
View File
@@ -3,11 +3,3 @@
-. Game list -. Game list
- tech tools and eq research sections. - tech tools and eq research sections.
- phone support - phone support
- Games
- reasearch
- lsit fo eq researh work, make DD
- make 3.
- mstr as short
-
+3
View File
@@ -0,0 +1,3 @@
{
"job": "none"
}
+31 -30
View File
@@ -1,30 +1,25 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os"
) )
// ---------- HTTP Handlers ---------- type Config struct {
Job string `json:"job"`
func HelloHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/hello" {
http.Error(w, "404 not found", http.StatusNotFound)
return
}
if r.Method != "GET" {
http.Error(w, "method is not supported", http.StatusNotFound)
return
}
fmt.Fprintf(w, "Hello You")
} }
func staticPage(path, file string) { func loadConfig() (*Config, error) {
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { data, err := os.ReadFile("data.json")
http.ServeFile(w, r, "./static/"+file) if err != nil {
}) return nil, err
}
var c Config
return &c, json.Unmarshal(data, &c)
} }
// ---------- Main ---------- // ---------- Main ----------
@@ -36,23 +31,29 @@ func main() {
fs := http.FileServer(http.Dir("static")) fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs)) http.Handle("/static/", http.StripPrefix("/static/", fs))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/about.html") })
if r.URL.Path != "/" { http.HandleFunc("GET /infra", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/infra.html") })
http.Redirect(w, r, "/", http.StatusFound) http.HandleFunc("GET /finance", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/finance.html") })
http.HandleFunc("GET /cinema", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cinema.html") })
http.HandleFunc("GET /engine", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/engine.html") })
http.HandleFunc("GET /42", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/42.html") })
http.HandleFunc("GET /secret", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/secret.html") })
http.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/icon.png") })
http.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) {
config, err := loadConfig()
if err != nil {
http.Error(w, "failed to load config", http.StatusInternalServerError)
return return
} }
http.ServeFile(w, r, "./static/about.html")
})
//http.HandleFunc("/portfolio", Portfolio)
staticPage("/infra", "infra.html")
staticPage("/finance", "finance.html")
staticPage("/cinema", "cinema.html")
staticPage("/engine", "engine.html")
staticPage("/42", "42.html")
staticPage("/secret", "secret.html")
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html")
http.ServeFile(w, r, "./static/icon.png")
if config.Job == "" || config.Job == "none" {
fmt.Fprint(w, `<span><span class="dot dot--available"></span>available for work</span>`)
} else {
fmt.Fprintf(w, `<span><span class="dot dot--busy"></span>working at: <b>%s</b></span>`, config.Job)
}
}) })
//http.HandleFunc("/portfolio", Portfolio) //http.HandleFunc("/portfolio", Portfolio)
BIN
View File
Binary file not shown.
+2 -9
View File
@@ -9,8 +9,8 @@
<link <link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap"
rel="stylesheet" /> rel="stylesheet" />
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<link rel="stylesheet" href="/static/style/styles2.css"/> <link rel="stylesheet" href="/static/style/styles2.css"/>
<link rel="stylesheet" href="/static/style/game.css" />
</head> </head>
<body class="theme-root day " id="themeRoot"> <body class="theme-root day " id="themeRoot">
@@ -87,13 +87,6 @@
</div> </div>
<div style="flex: 1;"></div> <div style="flex: 1;"></div>
<!--
<div class="sidebar-links">
<p class="sidebar-label">mini games</p>
<a href="#alice" onclick="unhideGame()"> Follow the rabbit</a>
<a href="#tetris" onclick="unhideTetris()" >Alexey Pajitnov</a>
</div>
-->
</aside> </aside>
<main id="about"> <main id="about">
@@ -107,7 +100,7 @@
and financial analysis. Analytical, detail-oriented, and efficiency-focused.</p> and financial analysis. Analytical, detail-oriented, and efficiency-focused.</p>
<div class="hero-meta"> <div class="hero-meta">
<span> <span>
<span class="dot"></span>available for work</span> <div hx-get="/status" hx-trigger="load" hx-swap="outerHTML"></div>
<span>samantha42.xyz</span> <span>samantha42.xyz</span>
<span>me@samantha42.xyz</span> <span>me@samantha42.xyz</span>
</div> </div>
-637
View File
@@ -1,637 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Portfolio — Samantha Friis</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Fraunces:ital,wght@0,300;0,400;0,600;1,300;1,400&display=swap" rel="stylesheet"/>
<style>
:root {
--bg: #0c0c0e;
--surface: #111114;
--surface2:#17171b;
--border: #232329;
--border2: #2e2e36;
--text: #e8e6e0;
--muted: #5a5a68;
--muted2: #3d3d4a;
--accent: #c8a96e;
--up: #5aad85;
--down: #c96b6b;
--mono: 'Space Mono', monospace;
--serif: 'Fraunces', Georgia, serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 15px; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--mono);
font-size: 0.8rem;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.container { max-width: 1080px; margin: 0 auto; padding: 3rem 2rem 5rem; }
/* ── Header ── */
header { margin-bottom: 3rem; border-bottom: 1px solid var(--border); padding-bottom: 2rem; }
.back { display: inline-block; color: var(--muted); text-decoration: none; font-size: 0.72rem; letter-spacing: 0.05em; margin-bottom: 1.8rem; transition: color 0.15s; }
.back:hover { color: var(--accent); }
.tag { font-size: 0.68rem; color: var(--accent); letter-spacing: 0.12em; margin-bottom: 0.6rem; }
h1 { font-family: var(--serif); font-size: clamp(2.4rem, 5vw, 3.6rem); font-weight: 300; line-height: 1.1; color: var(--text); margin-bottom: 0.75rem; }
h1 span { font-style: italic; color: var(--accent); }
.header-sub { font-size: 0.7rem; color: var(--muted); letter-spacing: 0.08em; }
/* ── Status bar ── */
.status-bar {
display: flex; align-items: center; gap: 0.6rem;
font-size: 0.65rem; color: var(--muted);
margin-bottom: 2rem; padding: 0.6rem 1rem;
background: var(--surface); border: 1px solid var(--border); border-radius: 5px;
}
.status-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--muted2); }
.status-dot.ok { background: var(--up); }
.status-dot.loading { background: var(--accent); animation: blink 1s ease-in-out infinite; }
.status-dot.error { background: var(--down); }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.2} }
.status-sep { color: var(--muted2); margin: 0 0.25rem; }
.status-spacer { flex: 1; }
.refresh-btn {
background: none; border: 1px solid var(--border2); border-radius: 3px;
color: var(--muted); font-family: var(--mono); font-size: 0.62rem;
padding: 2px 9px; cursor: pointer; letter-spacing: 0.05em; transition: all 0.15s;
}
.refresh-btn:hover { border-color: var(--accent); color: var(--accent); }
/* ── Summary cards ── */
.cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; margin-bottom: 2.5rem; }
.card { background: var(--surface); padding: 1.1rem 1.25rem; }
.card-label { font-size: 0.62rem; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.45rem; }
.card-value { font-family: var(--serif); font-size: 1.65rem; font-weight: 300; color: var(--text); }
.card-value.dim { color: var(--muted2); font-size: 1rem; }
/* ── Section label ── */
.section-label { font-size: 0.62rem; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.75rem; }
.section-label span { color: var(--accent); }
/* ── Tables ── */
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 2.5rem; }
table { width: 100%; border-collapse: collapse; font-size: 0.75rem; }
thead tr { border-bottom: 1px solid var(--border2); background: var(--surface); }
th { color: var(--muted); font-weight: 400; font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.75rem 1rem; text-align: left; white-space: nowrap; }
td { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); color: var(--text); white-space: nowrap; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: rgba(200,169,110,0.03); }
.ticker { font-weight: 700; font-size: 0.72rem; letter-spacing: 0.04em; color: var(--text); margin-right: 6px; }
.currency-badge { display: inline-block; font-size: 0.58rem; padding: 1px 5px; background: var(--surface2); border: 1px solid var(--border2); border-radius: 3px; color: var(--muted); letter-spacing: 0.06em; vertical-align: middle; }
.weight-cell { min-width: 100px; }
.weight-bar-track { height: 2px; background: var(--border2); border-radius: 1px; margin-top: 5px; }
.weight-bar-fill { height: 2px; background: var(--accent); border-radius: 1px; }
.state-row td { color: var(--muted2); font-size: 0.7rem; text-align: center; padding: 2rem; border-bottom: none; }
/* ── Trades accordion ── */
.trades-section { margin-top: 0; }
.trades-toggle {
display: flex; align-items: center; justify-content: space-between;
cursor: pointer; background: none; border: none;
border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
width: 100%; padding: 0.85rem 0; color: inherit;
font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em;
text-align: left; user-select: none; text-transform: uppercase;
}
.trades-toggle:hover .toggle-label { color: var(--accent); }
.toggle-label { color: var(--muted); transition: color 0.15s; }
.toggle-label span { color: var(--accent); }
.toggle-icon { color: var(--muted2); font-size: 0.9rem; transition: transform 0.25s ease; }
.trades-toggle[aria-expanded="true"] .toggle-icon { transform: rotate(180deg); }
.trades-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s ease; }
.trades-body.open { grid-template-rows: 1fr; }
.trades-inner { overflow: hidden; }
.trades-filter { display: flex; gap: 0.4rem; flex-wrap: wrap; padding: 1rem 0 0.75rem; }
.filter-btn {
background: none; border: 1px solid var(--border2); border-radius: 3px;
color: var(--muted); font-family: var(--mono); font-size: 0.62rem;
letter-spacing: 0.08em; padding: 3px 10px; cursor: pointer;
transition: all 0.15s; text-transform: uppercase;
}
.filter-btn:hover, .filter-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(200,169,110,0.06); }
.trades-count { font-size: 0.62rem; color: var(--muted2); letter-spacing: 0.06em; padding-bottom: 0.5rem; }
.trades-table-wrap { overflow-x: auto; padding-bottom: 1.5rem; border: 1px solid var(--border); border-radius: 6px; }
.trades-table { width: 100%; border-collapse: collapse; font-size: 0.73rem; }
.trades-table thead tr { border-bottom: 1px solid var(--border2); background: var(--surface); }
.trades-table th { color: var(--muted); font-weight: 400; letter-spacing: 0.1em; font-size: 0.6rem; text-transform: uppercase; padding: 0.6rem 0.9rem; text-align: left; white-space: nowrap; }
.trades-table td { padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--border); white-space: nowrap; color: var(--text); }
.trades-table tbody tr:last-child td { border-bottom: none; }
.trades-table tbody tr:hover td { background: rgba(200,169,110,0.03); }
.trades-table tr.hidden-row { display: none; }
.dir-buy { color: var(--up); }
.dir-sell { color: var(--down); }
.trade-code { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.6rem; background: var(--surface2); border: 1px solid var(--border2); color: var(--muted); letter-spacing: 0.05em; }
.trade-ticker { font-weight: 700; color: var(--text); letter-spacing: 0.03em; }
/* ── Footer ── */
footer { margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
footer a { color: var(--muted); font-size: 0.68rem; text-decoration: none; letter-spacing: 0.04em; transition: color 0.15s; }
footer a:hover { color: var(--accent); }
.footer-copy { font-size: 0.65rem; color: var(--muted2); letter-spacing: 0.06em; }
</style>
</head>
<body>
<div class="container">
<header>
<a href="/" class="back">← back</a>
<p class="tag">// portfolio</p>
<h1>Investment<br/><span>Portfolio</span></h1>
<p class="header-sub">Equity positions &nbsp;·&nbsp; Personal research</p>
<div class="health-badge" id="healthBadge" title="API server status">
<span class="health-dot loading" id="healthDot"></span>
<span id="healthText">connecting to api…</span>
</div>
<p>
Test of API:
Source at
<a href="https://git.samantha42.xyz/samantha/FPandA-Engine"
style="color:var(--accent);text-decoration:none"
target="_blank">git.samantha42.xyz/samantha/FPandA-Engine</a>
</p>
</header>
<!-- Summary cards (populated dynamically from /positions/list) -->
<div class="cards">
<div class="card">
<p class="card-label">Total Weight</p>
<p class="card-value neutral" id="sumWeight"></p>1
</div>
<div class="card">
<p class="card-label">Positions</p>
<p class="card-value neutral" id="sumPositions"></p>
</div>
<div class="card">
<p class="card-label">Total Cost Basis</p>
<p class="card-value neutral" id="sumCostBasis"></p>
</div>
<div class="card">
<p class="card-label">Total Shares</p>
<p class="card-value neutral" id="sumShares"></p>
</div>
</div>
<!-- Positions table -->
<div class="section-header-row">
<p class="section-label" style="margin:0">// positions</p>
<span class="price-status" id="priceStatus">
<span class="price-dot loading" id="priceDot"></span>
<span id="priceStatusText">fetching prices…</span>
</span>
<button class="refresh-btn" onclick="fetchHoldingPrices()">↺ refresh</button>
</div>
<!-- Backend health status bar -->
<div class="status-bar">
<span class="status-dot loading" id="statusDot"></span>
<span id="statusText">connecting to backend…</span>
<span class="status-sep">·</span>
<span id="statusUptime" style="color:var(--muted)">uptime —</span>
<span class="status-sep">·</span>
<span id="statusDb" style="color:var(--muted)">db —</span>
<span class="status-spacer"></span>
<button class="refresh-btn" onclick="loadAll()">↺ refresh</button>
</div>
<!-- Summary cards — populated from API -->
<div class="cards">
<div class="card">
<p class="card-label">Positions</p>
<p class="card-value dim" id="cardPositions"></p>
</div>
<div class="card">
<p class="card-label">Total Shares</p>
<p class="card-value dim" id="cardShares"></p>
</div>
<div class="card">
<p class="card-label">Total Trades</p>
<p class="card-value dim" id="cardTrades"></p>
</div>
<div class="card">
<p class="card-label">Currencies</p>
<p class="card-value dim" id="cardCurrencies"></p>
</div>
</div>
<!-- Positions — GET /positions/list -->
<p class="section-label">// <span>positions</span></p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Currency</th>
<th>Shares</th>
<th>Cost Basis</th>
<th>Weight</th>
<th>Market Price</th>
</tr>
</thead>
<tbody id="positionsBody">
<tr class="skeleton-row"><td><div class="skeleton-cell" style="width:55%"></div></td><td><div class="skeleton-cell" style="width:40%"></div></td><td><div class="skeleton-cell" style="width:40%"></div></td><td><div class="skeleton-cell" style="width:60%"></div></td><td><div class="skeleton-cell" style="width:50%"></div></td><td><div class="skeleton-cell" style="width:50%"></div></td></tr>
<tr class="skeleton-row"><td><div class="skeleton-cell" style="width:65%"></div></td><td><div class="skeleton-cell" style="width:35%"></div></td><td><div class="skeleton-cell" style="width:45%"></div></td><td><div class="skeleton-cell" style="width:55%"></div></td><td><div class="skeleton-cell" style="width:48%"></div></td><td><div class="skeleton-cell" style="width:52%"></div></td></tr>
<tr class="skeleton-row"><td><div class="skeleton-cell" style="width:60%"></div></td><td><div class="skeleton-cell" style="width:42%"></div></td><td><div class="skeleton-cell" style="width:38%"></div></td><td><div class="skeleton-cell" style="width:58%"></div></td><td><div class="skeleton-cell" style="width:44%"></div></td><td><div class="skeleton-cell" style="width:56%"></div></td></tr>
</tr>
</thead>
<tbody id="positionsBody">
<tr class="state-row"><td colspan="5">loading…</td></tr>
</tbody>
</table>
</div>
<!-- Trades accordion -->
<!-- Companies — GET /company/list -->
<p class="section-label">// <span>companies</span> — tracked universe</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Currency</th>
<th>Price</th>
<th>Shares Outstanding</th>
<th>Market Cap</th>
</tr>
</thead>
<tbody id="companiesBody">
<tr class="state-row"><td colspan="5">loading…</td></tr>
</tbody>
</table>
</div>
<!-- Trades — GET /trade/list — accordion -->
<div class="trades-section">
<button class="trades-toggle" aria-expanded="false" onclick="toggleTrades(this)">
<span class="toggle-label">// <span>trade history</span> — all executed orders</span>
<span class="toggle-icon"></span>
</button>
<div class="trades-body" id="tradesBody">
<div>
<div class="trades-header-row">
<div class="trades-filter">
<button class="filter-btn active" onclick="filterTrades('all', this)">All</button>
<button class="filter-btn" onclick="filterTrades('stock', this)">Stocks</button>
<button class="filter-btn" onclick="filterTrades('option', this)">Options</button>
<button class="filter-btn" onclick="filterTrades('buy', this)">Buy</button>
<button class="filter-btn" onclick="filterTrades('sell', this)">Sell</button>
</div>
<button class="add-trade-btn" onclick="openAddTrade()">+ add trade</button>
<div class="trades-body" id="tradesAccordion">
<div class="trades-inner">
<div class="trades-filter" id="tradeFilters">
<button class="filter-btn active" onclick="filterTrades('all', this)">All</button>
</div>
<p class="trades-count" id="tradesCount"></p>
<div class="trades-table-wrap">
<table class="trades-table">
<thead>
<tr>
<th>Symbol</th>
<th>Asset</th>
<th>Ccy</th>
<th>Currency</th>
<th>Date</th>
<th>Dir</th>
<th>Qty</th>
<th>Price</th>
</tr>
</thead>
<tbody id="tradesBody_rows">
<tr><td colspan="7" class="trades-empty">open to load trades</td></tr>
<tbody id="tradesBody">
<tr class="state-row"><td colspan="7">expand to load trades</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<footer>
<a href="mailto:me@smantha42.xyz">me@smantha42.xyz</a>
<a href="mailto:samantha@friis.dk">samantha@friis.dk</a>
<p class="footer-copy">&copy; 2026 — All rights reserved</p>
</footer>
</div>
<!-- Add Trade Modal -->
<div class="modal-overlay" id="addTradeModal" onclick="handleOverlayClick(event)">
<div class="modal">
<p class="modal-title">// <span>add trade</span> — new executed order</p>
<button class="modal-close" onclick="closeAddTrade()"></button>
<div class="form-grid">
<!-- API uses "symbol" + "currency_code", not tickerId -->
<div class="form-field full">
<label>SYMBOL</label>
<input type="text" id="f-symbol" placeholder="NOVO-B.CO" style="text-transform:uppercase"/>
</div>
<div class="form-field">
<label>SHARES</label>
<input type="number" id="f-shares" min="1" placeholder="10" />
</div>
<div class="form-field">
<label>PRICE</label>
<input type="number" id="f-price" step="0.01" placeholder="251.00" />
</div>
<div class="form-field">
<label>CURRENCY CODE</label>
<input type="text" id="f-currency" placeholder="DKK" maxlength="8" style="text-transform:uppercase"/>
</div>
<div class="form-field">
<label>PRODUCT</label>
<select id="f-product">
<option value="0">0 — Stock</option>
<option value="1">1 — Option Call</option>
<option value="2">2 — Option Put</option>
<option value="3">3 — Currency</option>
<option value="4">4 — Bond</option>
</select>
</div>
<div class="form-field">
<label>DIRECTION</label>
<select id="f-type">
<option value="false">Buy</option>
<option value="true">Sell</option>
</select>
</div>
<div class="form-field full">
<label>TRADE DATE</label>
<input type="datetime-local" id="f-date" />
</div>
</div>
<div class="form-error" id="formError"></div>
<div class="form-actions">
<button class="btn-cancel" onclick="closeAddTrade()">cancel</button>
<button class="btn-submit" id="submitBtn" onclick="submitTrade()">submit trade</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API = 'http://localhost:8080';
const PRODUCT_LABELS = { 0:'Stock', 1:'Call Option', 2:'Put Option', 3:'Currency', 4:'Bond' };
/* ── helpers ─────────────────────────────────────────────────────────── */
function n(v, dec = 2) {
if (v == null || isNaN(v)) return '—';
return Number(v).toLocaleString('da-DK', { minimumFractionDigits: dec, maximumFractionDigits: dec });
}
function fmtDate(iso) {
return iso ? iso.slice(0, 10) : '—';
}
async function get(path) {
const r = await fetch(API + path);
if (!r.ok) throw new Error(path + ' ' + r.status);
return r.json();
}
/* ── health ─────────────────────────────────────────────────────────── */
async function loadHealth() {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
try {
const h = await get('/health');
const ok = h.status === 'ok';
dot.className = 'status-dot ' + (ok ? 'ok' : 'error');
text.textContent = 'backend ' + h.status;
document.getElementById('statusUptime').textContent = 'up ' + (h.uptime || '—');
const db = h.database || {};
document.getElementById('statusDb').textContent =
'db ' + (db.status || '—') + (db.latency ? ' · ' + db.latency : '');
} catch (e) {
dot.className = 'status-dot error';
text.textContent = 'backend unreachable';
document.getElementById('statusUptime').textContent = e.message;
}
}
/* ── currencies ─────────────────────────────────────────────────────── */
let ccyMap = {}; // id → code
async function loadCurrencies() {
try {
const list = await get('/currency/list');
const arr = Array.isArray(list) ? list : (list ? [list] : []);
arr.forEach(c => {
// field names may vary
const id = c.ID ?? c.id;
const code = c.Code ?? c.code ?? c.CodeName ?? '—';
if (id != null) ccyMap[id] = code;
});
set('cardCurrencies', arr.length);
} catch (_) {
set('cardCurrencies', 'err');
}
}
/* ── positions ───────────────────────────────────────────────────────── */
async function loadPositions() {
const tbody = document.getElementById('positionsBody');
try {
const raw = await get('/positions/list');
const list = Array.isArray(raw) ? raw : (raw ? [raw] : []);
if (!list.length) {
tbody.innerHTML = stateRow(5, 'no positions');
set('cardPositions', 0); set('cardShares', 0);
return;
}
const totalCost = list.reduce((s, p) => s + (p.CostBasis || 0), 0);
const totalShares = list.reduce((s, p) => s + (p.Shares || 0), 0);
tbody.innerHTML = list.map(p => {
const ccy = p.CurrencyCode || ccyMap[p.CurrencyID] || '—';
const w = totalCost > 0 ? (p.CostBasis / totalCost * 100) : 0;
return `<tr>
<td><span class="ticker">${p.Symbol || '—'}</span></td>
<td><span class="currency-badge">${ccy}</span></td>
<td>${n(p.Shares, 0)}</td>
<td>${n(p.CostBasis)}</td>
<td><div class="weight-cell">${w.toFixed(1)}%
<div class="weight-bar-track">
<div class="weight-bar-fill" style="width:${Math.min(Math.round(w),100)}%"></div>
</div>
</div></td>
</tr>`;
}).join('');
set('cardPositions', list.length);
set('cardShares', n(totalShares, 0));
} catch (e) {
tbody.innerHTML = stateRow(5, 'load failed: ' + e.message);
}
}
/* ── companies ───────────────────────────────────────────────────────── */
async function loadCompanies() {
const tbody = document.getElementById('companiesBody');
try {
const raw = await get('/company/list');
const list = Array.isArray(raw) ? raw : (raw ? [raw] : []);
if (!list.length) {
tbody.innerHTML = stateRow(5, 'no companies');
return;
}
tbody.innerHTML = list.map(c => {
const ccy = ccyMap[c.CurrencyID] || '—';
const mcap = (c.SharesOutstanding && c.Price)
? n(c.SharesOutstanding * c.Price / 1_000_000, 0) + ' M'
: '—';
return `<tr>
<td><span class="ticker">${c.Symbol || '—'}</span></td>
<td><span class="currency-badge">${ccy}</span></td>
<td>${n(c.Price)}</td>
<td>${n(c.SharesOutstanding, 0)}</td>
<td>${mcap}</td>
</tr>`;
}).join('');
} catch (e) {
tbody.innerHTML = stateRow(5, 'load failed: ' + e.message);
}
}
/* ── trades ──────────────────────────────────────────────────────────── */
let allTrades = [];
let tradesLoaded = false;
async function loadTrades() {
const tbody = document.getElementById('tradesBody');
const filters = document.getElementById('tradeFilters');
try {
const raw = await get('/trade/list');
allTrades = Array.isArray(raw) ? raw : (raw ? [raw] : []);
tradesLoaded = true;
// Build per-symbol filter buttons
const syms = [...new Set(allTrades.map(t => t.Symbol).filter(Boolean))];
syms.forEach(sym => {
if (!filters.querySelector(`[data-sym="${sym}"]`)) {
const btn = Object.assign(document.createElement('button'), {
className: 'filter-btn',
textContent: sym,
onclick() { filterTrades(sym, this); },
});
btn.dataset.sym = sym;
filters.appendChild(btn);
}
});
set('cardTrades', allTrades.length);
renderTrades('all');
} catch (e) {
tbody.innerHTML = stateRow(7, 'load failed: ' + e.message);
}
}
function renderTrades(sym) {
const tbody = document.getElementById('tradesBody');
const countEl = document.getElementById('tradesCount');
const filtered = sym === 'all' ? allTrades : allTrades.filter(t => t.Symbol === sym);
if (!filtered.length) {
tbody.innerHTML = stateRow(7, 'no trades');
countEl.textContent = '0 trades';
return;
}
tbody.innerHTML = filtered.map(t => {
const buy = t.Type === true || t.Type === 'true';
const prod = PRODUCT_LABELS[t.Product] ?? ('type ' + t.Product);
return `<tr>
<td><span class="trade-ticker">${t.Symbol || '—'}</span></td>
<td>${prod}</td>
<td>${t.CurrencyCode || '—'}</td>
<td>${fmtDate(t.Date)}</td>
<td class="${buy ? 'dir-buy' : 'dir-sell'}">${buy ? 'BUY' : 'SELL'}</td>
<td>${n(t.Shares, 0)}</td>
<td>${n(t.Price)}</td>
</tr>`;
}).join('');
countEl.textContent = filtered.length + ' trade' + (filtered.length !== 1 ? 's' : '');
}
/* ── accordion / filter ──────────────────────────────────────────────── */
function toggleTrades(btn) {
const body = document.getElementById('tradesAccordion');
const expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!expanded));
body.classList.toggle('open', !expanded);
if (!expanded && !tradesLoaded) loadTrades();
}
function filterTrades(sym, btn) {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderTrades(sym);
}
/* ── misc helpers ────────────────────────────────────────────────────── */
function set(id, val) {
const el = document.getElementById(id);
if (el) { el.textContent = val; el.classList.remove('dim'); }
}
function stateRow(cols, msg) {
return `<tr class="state-row"><td colspan="${cols}">${msg}</td></tr>`;
}
/* ── boot ────────────────────────────────────────────────────────────── */
async function loadAll() {
// Reset status
document.getElementById('statusDot').className = 'status-dot loading';
document.getElementById('statusText').textContent = 'connecting…';
await loadHealth();
await loadCurrencies(); // currency map must be ready before tables
await Promise.all([loadPositions(), loadCompanies()]);
// Trades loaded lazily on accordion open
}
document.addEventListener('DOMContentLoaded', loadAll);
</script>
</body>
</html>
-57
View File
@@ -1,57 +0,0 @@
.play {
padding: 2rem 0;
}
.play .section-head h2 {
font-family: var(--ff-mono);
font-size: 1rem;
font-weight: 400;
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 1.5rem;
}
.play .story-text {
font-family: var(--ff-mono);
font-size: 0.85rem;
line-height: 1.9;
color: var(--fg);
max-width: 560px;
margin-bottom: 2rem;
white-space: pre-wrap;
border: 1px solid rgb(167, 205, 250);
border-radius: 10px;
padding: 1rem 1.25rem;
}
.play .choices {
display: flex;
flex-direction: column;
gap: 0.6rem;
max-width: 560px;
}
.play .choices a,
.play .choices button {
font-family: var(--ff-mono);
font-size: 0.8rem;
color: var(--muted);
background: none;
border: none;
border-left: 1px solid var(--muted);
padding: 0.3rem 0.75rem;
cursor: pointer;
text-align: left;
text-decoration: none;
transition: color 0.15s, border-color 0.15s;
}
.play .choices a:hover,
.play .choices button:hover {
color: var(--fg);
border-color: var(--fg);
}
.idx {
color: blueviolet;
padding: 10px;
}
+11 -5
View File
@@ -157,11 +157,7 @@ h1 span{color:var(--accent);}
font-family:var(--ff-mono);font-size:0.72rem;color:var(--dim); font-family:var(--ff-mono);font-size:0.72rem;color:var(--dim);
} }
.hero-meta span{display:flex;align-items:center;gap:5px;} .hero-meta span{display:flex;align-items:center;gap:5px;}
.dot{
width:6px;height:6px;border-radius:50%;
background:var(--green);display:inline-block;
animation:pulse 2s infinite;
}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.35;}} @keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.35;}}
.section-head{display:flex;align-items:center;gap:10px;margin-bottom:1.8rem;} .section-head{display:flex;align-items:center;gap:10px;margin-bottom:1.8rem;}
@@ -591,3 +587,13 @@ a{
letter-spacing: 0.08em; letter-spacing: 0.08em;
padding-left: 1.1rem; padding-left: 1.1rem;
} }
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
animation: pulse 2s infinite;
}
.dot--available { background: var(--green); }
.dot--busy { background: red; }