Init Of work

This commit is contained in:
Chase Eller 2026-04-13 04:33:39 -04:00
parent d0fc01b98f
commit 96e6ae3bfc
No known key found for this signature in database
GPG Key ID: 9233D42F495E657C
10 changed files with 435 additions and 2 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.env
/__pycache__
/__pycache__
*.pyc

14
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lua",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/Untitled-1.lua"
}
]
}

View File

@ -1,3 +1,16 @@
# STONKS
# stonk GUI App
Dev for handling stonk data so i can figure all this out web wise.
## Setup
1. Install dependencies:
pip install -r requirements.txt
2. Update .env with your Authentik details
3. Update DB connection string in main.py
4. Run:
uvicorn main:app --reload
5. Open:
http://localhost:8000

15
dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM python:3.12-slim
WORKDIR /usr/src/app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of your code (do not copy .env)
COPY . .
# Expose port if web app
EXPOSE 8000
CMD ["python", "main.py"]

111
main.py Normal file
View File

@ -0,0 +1,111 @@
import os
from fastapi import FastAPI, Depends, Query
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from sqlalchemy import create_engine, text
from dotenv import load_dotenv
load_dotenv()
DB_USER = os.getenv("DB_USER")
DB_PASS = os.getenv("DB_PASS")
DB_HOST = os.getenv("DB_HOST")
DB_NAME = os.getenv("DB_NAME")
if not all([DB_USER, DB_PASS, DB_HOST, DB_NAME]):
raise EnvironmentError("Missing DB environment variables")
app = FastAPI()
engine = create_engine(
f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}/{DB_NAME}",
pool_pre_ping=True
)
app.mount("/static", StaticFiles(directory="static"), name="static")
# -----------------------
# PAGE 1: DEFAULT /
# -----------------------
@app.get("/")
async def index():
with open("templates/index.html", "r", encoding="utf-8") as f:
return HTMLResponse(f.read())
@app.get("/embed")
async def index():
with open("templates/embed.html", "r", encoding="utf-8") as f:
return HTMLResponse(f.read())
# -----------------------
# PAGE 2: /graphs
# -----------------------
@app.get("/graphs")
async def graphs():
with open("templates/graphs.html", "r", encoding="utf-8") as f:
return HTMLResponse(f.read())
# -----------------------
# API 1 (used by /)
# -----------------------
@app.get("/api/data")
async def api_data():
query = text("SELECT * FROM STOCKMARKET_HISTORY WHERE Ticker = 'FAT' ORDER BY ChangeID DESC LIMIT 28")
with engine.connect() as conn:
result = conn.execute(query)
rows = [dict(r._mapping) for r in result]
return {
"columns": list(rows[0].keys()) if rows else [],
"rows": rows
}
# -----------------------
# API 2 (used by /graphs)
# -----------------------
from fastapi import Depends, Query
@app.get("/api/graphs")
async def api_graphs(
ticker: str = Query(...),
limit: int = Query(28),
):
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
h.*,
s.CompanyName
FROM STOCKMARKET_HISTORY h
JOIN STOCKMARKET_STOCKS s
ON s.Ticker = h.Ticker
WHERE h.Ticker = :ticker
ORDER BY h.ChangeID DESC
LIMIT :limit;
""")
result = conn.execute(query, {
"ticker": t,
"limit": limit * 4
})
rows = [dict(r._mapping) for r in result]
results[t] = rows
return results
# -----------------------
# RUN
# -----------------------
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
fastapi
uvicorn
sqlalchemy
pymysql
python-jose[cryptography]
authlib
httpx
python-dotenv
itsdangerous
cryptography

41
static/style.css Normal file
View File

@ -0,0 +1,41 @@
html, body {
height: 100%;
margin: 0;
padding: 0;
background: #ffffff;
color: #ffffff;
font-family: Arial, sans-serif;
overflow: hidden; /* prevents page scroll messing with chart height */
}
/* Header area */
h2 {
margin: 10px 15px 5px 15px;
}
a {
color: #4da3ff;
margin: 0 15px 10px 15px;
text-decoration: none;
display: inline-block;
}
a:hover {
text-decoration: underline;
}
/* FULL SCREEN CHART CONTAINER */
.chart-container {
width: 100%;
height: calc(100vh - 80px); /* subtract header space */
display: flex;
padding: 10px 15px;
box-sizing: border-box;
}
/* CRITICAL: canvas must stretch */
#chart {
width: 100% !important;
height: 100% !important;
display: block;
}

17
templates/embed.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h2>Main Dashboard</h2>
<iframe
src="/graphs?ticker=FAT,ALB,TSLA&limit=28"
style="width:33%; height:500px; border:0;"
></iframe>
</body>
</html>

162
templates/graphs.html Normal file
View File

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html>
<head>
<title>Graphs</title>
<link rel="stylesheet" href="/static/style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<h2 id="title">Graphs</h2>
<a href="/">Back</a>
<div class="chart-container">
<canvas id="chart"></canvas>
</div>
<script>
function getParams() {
const url = new URL(window.location.href);
return {
tickers: url.searchParams.get("ticker"),
limit: parseInt(url.searchParams.get("limit") || 28)
};
}
async function loadGraph() {
const { tickers, limit } = getParams();
if (!tickers) {
document.body.innerHTML = "<h3>Missing ?ticker= parameter</h3>";
return;
}
const tickerList = tickers.split(",").map(t => t.trim());
const res = await fetch(
`/api/graphs?ticker=${tickers}&limit=${limit}`
);
const data = await res.json();
const datasets = [];
let basePrices = [];
tickerList.forEach((ticker, index) => {
const raw = data[ticker] || [];
if (raw.length === 0) return;
// Company name comes from FIRST row
const companyName = `${raw[0].CompanyName} (${ticker})` || ticker;
const prices = raw
.map(r => parseFloat(r.NewPrice))
.reverse();
if (index === 0) basePrices = prices;
const color = `hsl(${(index * 360) / tickerList.length}, 70%, 50%)`;
const pointColors = prices.map((_, i) =>
i % 4 === 0 ? "rgba(255, 99, 132, 1)" : color
);
const pointSizes = prices.map((_, i) =>
i % 4 === 0 ? 5 : 3
);
datasets.push({
label: companyName,
data: prices,
borderColor: color,
backgroundColor: color,
borderWidth: 2,
fill: false,
pointBackgroundColor: pointColors,
pointBorderColor: pointColors,
pointRadius: pointSizes,
pointHoverRadius: 6
});
});
// -----------------------------
// X AXIS LABELS (14 → 1 numeric)
// -----------------------------
const labels = [];
const totalDays = Math.ceil(basePrices.length / 4);
let dayCounter = totalDays;
for (let i = 0; i < basePrices.length; i++) {
if (i % 4 === 0) {
labels.push(String(dayCounter));
dayCounter--;
} else {
labels.push("");
}
}
// -----------------------------
// CHART
// -----------------------------
new Chart(document.getElementById("chart"), {
type: "line",
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `${context.dataset.label}: $${context.parsed.y}`;
}
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: "Days"
},
ticks: {
autoSkip: false,
maxRotation: 0
}
},
y: {
title: {
display: true,
text: "Price ($)"
},
ticks: {
callback: function(value) {
return `$${value}`;
}
}
}
}
}
});
}
loadGraph();
</script>
</body>
</html>

46
templates/index.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h2>Main Dashboard</h2>
<a href="/graphs">Go to Graphs</a>
<table id="table"></table>
<script>
async function load() {
const res = await fetch("/api/data");
const data = await res.json();
const table = document.getElementById("table");
if (!data.rows.length) {
table.innerHTML = "<tr><td>No data</td></tr>";
return;
}
const cols = data.columns;
let html = "<tr>" +
cols.map(c => `<th>${c}</th>`).join("") +
"</tr>";
html += data.rows.map(row =>
"<tr>" +
cols.map(c => `<td>${row[c]}</td>`).join("") +
"</tr>"
).join("");
table.innerHTML = html;
}
load();
</script>
</body>
</html>