in progress
This commit is contained in:
parent
96e6ae3bfc
commit
ac007fb1e6
43
main.py
43
main.py
|
|
@ -46,6 +46,14 @@ async def graphs():
|
||||||
with open("templates/graphs.html", "r", encoding="utf-8") as f:
|
with open("templates/graphs.html", "r", encoding="utf-8") as f:
|
||||||
return HTMLResponse(f.read())
|
return HTMLResponse(f.read())
|
||||||
|
|
||||||
|
# -----------------------
|
||||||
|
# PAGE 3: /portgraphs
|
||||||
|
# -----------------------
|
||||||
|
@app.get("/port")
|
||||||
|
async def port():
|
||||||
|
with open("templates/portfoliograph.html", "r", encoding="utf-8") as f:
|
||||||
|
return HTMLResponse(f.read())
|
||||||
|
|
||||||
|
|
||||||
# -----------------------
|
# -----------------------
|
||||||
# API 1 (used by /)
|
# API 1 (used by /)
|
||||||
|
|
@ -67,7 +75,7 @@ async def api_data():
|
||||||
# -----------------------
|
# -----------------------
|
||||||
# API 2 (used by /graphs)
|
# API 2 (used by /graphs)
|
||||||
# -----------------------
|
# -----------------------
|
||||||
from fastapi import Depends, Query
|
|
||||||
|
|
||||||
@app.get("/api/graphs")
|
@app.get("/api/graphs")
|
||||||
async def api_graphs(
|
async def api_graphs(
|
||||||
|
|
@ -103,6 +111,39 @@ async def api_graphs(
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/portfolio")
|
||||||
|
async def api_folio(
|
||||||
|
ticker: str = Query(...),
|
||||||
|
uuid: int = Query(0),
|
||||||
|
):
|
||||||
|
tickers = [t.strip().upper() for t in ticker.split(",") if t.strip()]
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for t in tickers:
|
||||||
|
query = text("""
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
s.CurPrice,
|
||||||
|
(s.CurPrice * p.SharesOwned) AS CurrentValue
|
||||||
|
FROM STOCKMARKET_PORTFOLIO p
|
||||||
|
JOIN STOCKMARKET_STOCKS s
|
||||||
|
ON s.Ticker = p.Ticker
|
||||||
|
WHERE p.Ticker = :ticker
|
||||||
|
AND p.UUID = :uuid;
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = conn.execute(query, {
|
||||||
|
"ticker": t,
|
||||||
|
"uuid": uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
rows = [dict(r._mapping) for r in result]
|
||||||
|
results[t] = rows
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
# -----------------------
|
# -----------------------
|
||||||
# RUN
|
# RUN
|
||||||
# -----------------------
|
# -----------------------
|
||||||
|
|
|
||||||
282
static/style.css
282
static/style.css
|
|
@ -1,14 +1,74 @@
|
||||||
|
/* =========================
|
||||||
|
BASE / GLOBAL
|
||||||
|
========================= */
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #ffffff;
|
background: #0f1115;
|
||||||
color: #ffffff;
|
color: #e6e6e6;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
overflow: hidden; /* prevents page scroll messing with chart height */
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header area */
|
/* =========================
|
||||||
|
LAYOUT SYSTEM
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
SIDEBAR
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 220px;
|
||||||
|
background: #1a1d23;
|
||||||
|
padding: 15px;
|
||||||
|
border-right: 1px solid #2a2f3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #b5b5b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #2a2f3a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #3a3f4b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
TYPOGRAPHY
|
||||||
|
========================= */
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 10px 15px 5px 15px;
|
margin: 10px 15px 5px 15px;
|
||||||
}
|
}
|
||||||
|
|
@ -24,18 +84,226 @@ a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FULL SCREEN CHART CONTAINER */
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
CHART (IMPORTANT)
|
||||||
|
========================= */
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 80px); /* subtract header space */
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CRITICAL: canvas must stretch */
|
|
||||||
#chart {
|
#chart {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
EDITOR PANEL
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
width: 420px;
|
||||||
|
background: #1a1d23;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-wrap {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
FORMS
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.form-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #151922;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa4b2;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px;
|
||||||
|
background: #0f1115;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
BUTTONS
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #5865f2;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #4752c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-btn {
|
||||||
|
background: #3a3f4b;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
ADMIN PANEL
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.admin-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-left {
|
||||||
|
width: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1a1d23;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-right {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
SEARCH / USERS
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #0f1115;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card:hover {
|
||||||
|
background: #2a2f3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card.active {
|
||||||
|
background: #3a3f4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
INFO BLOCKS
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.info-block {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #151922;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #9aa4b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
TOASTS
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: white;
|
||||||
|
background: #2a2f3a;
|
||||||
|
border: 1px solid #3a3f4b;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background: #1f3b2c;
|
||||||
|
border-color: #2f6b4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background: #3b1f1f;
|
||||||
|
border-color: #6b2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
HEADERS
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
.edit-header {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-header span {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
@ -8,11 +8,30 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h2 id="title">Graphs</h2>
|
<div class="layout">
|
||||||
<a href="/">Back</a>
|
|
||||||
|
|
||||||
<div class="chart-container">
|
<!-- SIDEBAR (optional placeholder - remove if not needed) -->
|
||||||
<canvas id="chart"></canvas>
|
<div class="sidebar">
|
||||||
|
<div class="nav-item active">Graphs</div>
|
||||||
|
<div class="nav-item">Dashboard</div>
|
||||||
|
<div class="nav-item">Settings</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN CONTENT -->
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<h2 class="section-title">Graphs</h2>
|
||||||
|
<a href="/">Back</a>
|
||||||
|
|
||||||
|
<div class="chart-container" style="height: calc(100vh - 140px);">
|
||||||
|
<canvas id="chart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -50,7 +69,6 @@ async function loadGraph() {
|
||||||
|
|
||||||
if (raw.length === 0) return;
|
if (raw.length === 0) return;
|
||||||
|
|
||||||
// Company name comes from FIRST row
|
|
||||||
const companyName = `${raw[0].CompanyName} (${ticker})` || ticker;
|
const companyName = `${raw[0].CompanyName} (${ticker})` || ticker;
|
||||||
|
|
||||||
const prices = raw
|
const prices = raw
|
||||||
|
|
@ -72,12 +90,10 @@ async function loadGraph() {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: companyName,
|
label: companyName,
|
||||||
data: prices,
|
data: prices,
|
||||||
|
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: false,
|
fill: false,
|
||||||
|
|
||||||
pointBackgroundColor: pointColors,
|
pointBackgroundColor: pointColors,
|
||||||
pointBorderColor: pointColors,
|
pointBorderColor: pointColors,
|
||||||
pointRadius: pointSizes,
|
pointRadius: pointSizes,
|
||||||
|
|
@ -85,9 +101,6 @@ async function loadGraph() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// X AXIS LABELS (14 → 1 numeric)
|
|
||||||
// -----------------------------
|
|
||||||
const labels = [];
|
const labels = [];
|
||||||
const totalDays = Math.ceil(basePrices.length / 4);
|
const totalDays = Math.ceil(basePrices.length / 4);
|
||||||
let dayCounter = totalDays;
|
let dayCounter = totalDays;
|
||||||
|
|
@ -101,9 +114,6 @@ async function loadGraph() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// CHART
|
|
||||||
// -----------------------------
|
|
||||||
new Chart(document.getElementById("chart"), {
|
new Chart(document.getElementById("chart"), {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -138,7 +148,6 @@ async function loadGraph() {
|
||||||
maxRotation: 0
|
maxRotation: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
y: {
|
y: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Portfolio Debug View</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-box {
|
||||||
|
flex: 1;
|
||||||
|
background: #0f1115;
|
||||||
|
border: 1px solid #2a2f3a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa4b2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="nav-item">Dashboard</div>
|
||||||
|
<div class="nav-item active">Debug View</div>
|
||||||
|
<div class="nav-item">Settings</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h2 class="section-title">Portfolio Debug View</h2>
|
||||||
|
<a href="/">Back</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta" id="meta"></div>
|
||||||
|
|
||||||
|
<div id="output" class="json-box">Loading...</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getParams() {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tickers: url.searchParams.get("ticker"),
|
||||||
|
uuid: url.searchParams.get("uuid")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const { tickers, uuid } = getParams();
|
||||||
|
|
||||||
|
const meta = document.getElementById("meta");
|
||||||
|
const output = document.getElementById("output");
|
||||||
|
|
||||||
|
if (!tickers || !uuid) {
|
||||||
|
output.innerHTML = "Missing ?ticker or ?uuid parameter";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.innerText = `Requesting: ticker=${tickers} | uuid=${uuid}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/portfolio?ticker=${encodeURIComponent(tickers)}&uuid=${encodeURIComponent(uuid)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
output.innerText = JSON.stringify(data, null, 4);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
output.innerHTML = `<div class="error">Error loading data: ${err}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue