new theme
This commit is contained in:
@@ -34,9 +34,11 @@ func Infra(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./s
|
|||||||
func Finance(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/finance.html") }
|
func Finance(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/finance.html") }
|
||||||
func Cinema(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cinema.html") }
|
func Cinema(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cinema.html") }
|
||||||
func Cyber(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cyber.html") }
|
func Cyber(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/cyber.html") }
|
||||||
|
func Engine(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/engine.html") }
|
||||||
func About(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/about.html") }
|
func About(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/about.html") }
|
||||||
|
|
||||||
func Styles(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles.css") }
|
func Styles(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles.css") }
|
||||||
|
func Styles2(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/styles2.css") }
|
||||||
|
|
||||||
// TradesHandler handles POST /trades
|
// TradesHandler handles POST /trades
|
||||||
// It accepts a CSV file upload (field name: "file") and returns enriched trades as JSON.
|
// It accepts a CSV file upload (field name: "file") and returns enriched trades as JSON.
|
||||||
@@ -94,6 +96,7 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
http.HandleFunc("/styles.css", Styles)
|
http.HandleFunc("/styles.css", Styles)
|
||||||
|
http.HandleFunc("/styles2.css", Styles2)
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
@@ -106,6 +109,7 @@ func main() {
|
|||||||
http.HandleFunc("/finance", Finance)
|
http.HandleFunc("/finance", Finance)
|
||||||
http.HandleFunc("/cinema", Cinema)
|
http.HandleFunc("/cinema", Cinema)
|
||||||
http.HandleFunc("/cyber", Cyber)
|
http.HandleFunc("/cyber", Cyber)
|
||||||
|
http.HandleFunc("/engine", Engine)
|
||||||
http.HandleFunc("/git", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/git", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "https://git.samantha42.xyz", http.StatusFound)
|
http.Redirect(w, r, "https://git.samantha42.xyz", http.StatusFound)
|
||||||
})
|
})
|
||||||
|
|||||||
+112
-169
@@ -1,201 +1,144 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>About — Samantha Vero Friis</title>
|
<title>Samantha Vero Friis</title>
|
||||||
<link rel="stylesheet" href="styles.css" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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" rel="stylesheet"/>
|
||||||
<link
|
<link rel="stylesheet" href="styles2.css" />
|
||||||
href="https://fonts.googleapis.com/css2?family=Libre+Caslon+Text:ital,wght@0,400;0,700;1,400&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
|
||||||
<!-- PAGE NAV — place right before <header> -->
|
<nav>
|
||||||
<nav class="page-nav">
|
<a class="nav-logo" href="#">samantha_vero_friis</a>
|
||||||
<a href="#about" class="active"><span>about</span></a>
|
<div class="nav-links">
|
||||||
<a href="/git" target="_blank"><span>git</span></a>
|
<a href="/engine">engine</a>
|
||||||
<br /><span><p class="section-label">//Opinion</p></span>
|
<a href="/cyber">cyber</a>
|
||||||
<a href="/cinema"><span>Cinema</span></a>
|
<a href="/cinema">cinema</a>
|
||||||
<a href="/finance"><span>Finance</span></a>
|
<a href="/cinema">finance</a>
|
||||||
<a href="/cyber"><span>cyber</span></a>
|
<a href="/infra">infrastructure</a>
|
||||||
<a href="/infra"><span>infra</span></a>
|
<a href="https://samantha42.xyz/git">git</a>
|
||||||
|
<a class="cta" href="mailto:me@samantha42.xyz">contact</a>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<header>
|
|
||||||
<p class="tag">// about me</p>
|
|
||||||
<h1>Samantha<br /><span>Vero Friis</span></h1>
|
|
||||||
<p class="header-sub">
|
|
||||||
Denmark, Aalborg · samantha42.xyz
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
<div class="shell">
|
||||||
<p class="summary">
|
<aside>
|
||||||
Bachelor's student in Business Administration seeking an
|
|
||||||
entry-level analyst or finance-related role while studying.
|
|
||||||
Strong numerical skills, advanced Excel, and experience in
|
|
||||||
company, risk, and financial analysis. Analytical,
|
|
||||||
detail-oriented, and efficiency-focused.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Skills -->
|
|
||||||
<section>
|
|
||||||
<p class="section-label">// skills</p>
|
|
||||||
<div class="skills-list">
|
|
||||||
<div class="skill-row">
|
|
||||||
<span>Excel</span>
|
|
||||||
<div class="skill-bar-track">
|
|
||||||
<div
|
|
||||||
class="skill-bar-fill"
|
|
||||||
style="width: 85%; animation-delay: 0.1s"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="skill-row">
|
|
||||||
<span>Python</span>
|
|
||||||
<div class="skill-bar-track">
|
|
||||||
<div
|
|
||||||
class="skill-bar-fill"
|
|
||||||
style="width: 75%; animation-delay: 0.2s"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="skill-row">
|
|
||||||
<span>SQL</span>
|
|
||||||
<div class="skill-bar-track">
|
|
||||||
<div
|
|
||||||
class="skill-bar-fill"
|
|
||||||
style="width: 60%; animation-delay: 0.3s"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="skill-row">
|
|
||||||
<span>Go</span>
|
|
||||||
<div class="skill-bar-track">
|
|
||||||
<div
|
|
||||||
class="skill-bar-fill"
|
|
||||||
style="width: 70%; animation-delay: 0.4s"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="skill-row">
|
|
||||||
<span>Git</span>
|
|
||||||
<div class="skill-bar-track">
|
|
||||||
<div
|
|
||||||
class="skill-bar-fill"
|
|
||||||
style="width: 65%; animation-delay: 0.5s"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Experience -->
|
|
||||||
<section>
|
|
||||||
<p class="section-label">// experience</p>
|
|
||||||
|
|
||||||
<div class="exp-block">
|
|
||||||
<h2>Independent Equity Research</h2>
|
|
||||||
<p>
|
|
||||||
Perform fundamental analysis using SEC filings
|
|
||||||
(10-K, 10-Q) and earnings reports. Assess
|
|
||||||
profitability, liquidity, risk factors, and
|
|
||||||
long-term business sustainability. Handle incomplete
|
|
||||||
or low-transparency information through structured
|
|
||||||
cross-validation.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="exp-block">
|
|
||||||
<h2>Technical Tools & Automation</h2>
|
|
||||||
<p>
|
|
||||||
Build lightweight backend tools to automate
|
|
||||||
repetitive analytical and operational tasks. Use
|
|
||||||
Python for data processing, validation, and workflow
|
|
||||||
automation. Develop lightweight online services in
|
|
||||||
Go, focusing on speed and reliability.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Education -->
|
|
||||||
<section>
|
|
||||||
<p class="section-label">// education</p>
|
|
||||||
|
|
||||||
<div class="edu-item">
|
|
||||||
<span class="edu-year">2025 — now</span>
|
|
||||||
<div>
|
<div>
|
||||||
<p class="edu-title">
|
<p class="sidebar-label">navigate</p>
|
||||||
Bachelor of Economics & Business Administration
|
<div class="sidebar-links">
|
||||||
(HA)
|
<a href="#about">about</a>
|
||||||
</p>
|
<a href="#experience">experience</a>
|
||||||
<p class="edu-sub">
|
<a href="#education">education</a>
|
||||||
Aalborg University · Expected 2028
|
<a href="#opinion">opinion</a>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edu-item">
|
|
||||||
<span class="edu-year">2020 — 2023</span>
|
|
||||||
<div>
|
<div>
|
||||||
<p class="edu-title">
|
<p class="sidebar-label">skills</p>
|
||||||
HTX – Technical Upper Secondary Education
|
<div class="skill-list">
|
||||||
</p>
|
<div class="skill-item">Excel</div>
|
||||||
<p class="edu-sub">
|
<div class="skill-item">Python</div>
|
||||||
Viden Gymnasier · Mathematics A,
|
<div class="skill-item">SQL</div>
|
||||||
Biology B<br />Study project: Mathematics &
|
<div class="skill-item">Go</div>
|
||||||
Technology
|
<div class="skill-item">Git</div>
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="sidebar-label">location</p>
|
||||||
|
<div style="font-family:var(--ff-mono);font-size:0.75rem;color:var(--muted);">Denmark<br>Aalborg</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main id="about">
|
||||||
|
|
||||||
|
<div class="hero">
|
||||||
|
<p class="hero-eyebrow">portfolio · 2026</p>
|
||||||
|
<h1>Samantha<br><span>Vero Friis</span></h1>
|
||||||
|
<p class="hero-bio">Bachelor's student in Business Administration seeking an entry-level analyst or finance-related role while studying. Strong numerical skills, advanced Excel, and experience in company, risk, and financial analysis. Analytical, detail-oriented, and efficiency-focused.</p>
|
||||||
|
<div class="hero-meta">
|
||||||
|
<span><span class="dot"></span>available for work</span>
|
||||||
|
<span>samantha42.xyz</span>
|
||||||
|
<span>contact@samantha42.xyz</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="experience">
|
||||||
|
<div class="section-head"><h2>experience</h2></div>
|
||||||
|
<div class="exp-grid">
|
||||||
|
<div class="exp-card">
|
||||||
|
<div class="exp-header">
|
||||||
|
<span class="exp-title">Independent Equity Research</span>
|
||||||
|
<span class="exp-tag">finance</span>
|
||||||
|
</div>
|
||||||
|
<p class="exp-body">Fundamental analysis using SEC filings (10-K, 10-Q) and earnings reports. Assess profitability, liquidity, risk factors, and long-term business sustainability. Handle incomplete or low-transparency information through structured cross-validation.</p>
|
||||||
|
</div>
|
||||||
|
<div class="exp-card">
|
||||||
|
<div class="exp-header">
|
||||||
|
<span class="exp-title">Technical Tools & Automation</span>
|
||||||
|
<span class="exp-tag">engineering</span>
|
||||||
|
</div>
|
||||||
|
<p class="exp-body">Build lightweight backend tools to automate repetitive analytical and operational tasks. Python for data processing, validation, and workflow automation. Lightweight online services in Go, focusing on speed and reliability.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Languages -->
|
<section id="education">
|
||||||
<section>
|
<div class="section-head"><h2>education</h2></div>
|
||||||
<p class="section-label">// languages</p>
|
<div class="edu-grid">
|
||||||
<div class="skills-list">
|
<div class="edu-card">
|
||||||
<div class="skill-row">
|
<div class="edu-year">2025 —<br>present</div>
|
||||||
<span>Danish</span>
|
<div>
|
||||||
<div class="skill-bar-track">
|
<div class="edu-degree">Bachelor of Economics & Business Administration (HA)</div>
|
||||||
<div
|
<div class="edu-inst">Aalborg University · Expected 2028</div>
|
||||||
class="skill-bar-fill"
|
|
||||||
style="width: 100%; animation-delay: 0.1s"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-row">
|
<div class="edu-card">
|
||||||
<span>English</span>
|
<div class="edu-year">2020 —<br>2023</div>
|
||||||
<div class="skill-bar-track">
|
<div>
|
||||||
<div
|
<div class="edu-degree">HTX – Technical Upper Secondary Education</div>
|
||||||
class="skill-bar-fill"
|
<div class="edu-inst">Viden Gymnasier</div>
|
||||||
style="width: 80%; animation-delay: 0.2s"
|
<div class="edu-detail">Mathematics A, Biology B · Study project: Mathematics & Technology</div>
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Interests -->
|
<section id="opinion">
|
||||||
<section>
|
<div class="section-head"><h2>opinion</h2></div>
|
||||||
<p class="section-label">// interests</p>
|
<div class="opinion-grid">
|
||||||
<div class="tags">
|
<a class="opinion-link" href="https://samantha42.xyz/cinema">cinema</a>
|
||||||
<span class="tag-pill">Gaming</span>
|
<a class="opinion-link" href="https://samantha42.xyz/finance">finance</a>
|
||||||
<span class="tag-pill">Board games</span>
|
<a class="opinion-link" href="https://samantha42.xyz/cyber">cyber</a>
|
||||||
<span class="tag-pill">Programming</span>
|
<a class="opinion-link" href="https://samantha42.xyz/infra">infra</a>
|
||||||
<span class="tag-pill">Financial markets</span>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="section-head"><h2>languages</h2></div>
|
||||||
|
<div class="lang-row">
|
||||||
|
<span class="lang-badge">Danish</span>
|
||||||
|
<span class="lang-badge">English</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="section-head"><h2>interests</h2></div>
|
||||||
|
<div class="interests-row">
|
||||||
|
<span class="interest-chip">gaming</span>
|
||||||
|
<span class="interest-chip">board-games</span>
|
||||||
|
<span class="interest-chip">programming</span>
|
||||||
|
<span class="interest-chip">financial-markets</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<a href="mailto:contact@samantha42.xyz"
|
<div class="footer-contact"><a href="mailto:me@samantha42.xyz">contact@samantha42.xyz</a></div>
|
||||||
>contact@samantha42.xyz</a
|
<div class="footer-copy">© 2026 — all rights reserved</div>
|
||||||
>
|
|
||||||
<p class="footer-copy">© 2026 — All rights reserved</p>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>FP&A Budgeting Engine — Samantha Vero Friis</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Libre+Caslon+Text:wght@400;700;800&family=Syne:wght@400;700;800&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<style>
|
||||||
|
/* ── Page-specific layout ── */
|
||||||
|
header { padding: 2.4rem 0 2rem; }
|
||||||
|
main { padding: 3rem 0 5rem; }
|
||||||
|
|
||||||
|
.prose-block { max-width: 660px; }
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: "DM Mono", monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 1.4rem 1.6rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1.6rem 0;
|
||||||
|
color: #c8c8c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stat cards at top */
|
||||||
|
.cards { margin: 2rem 0 2.5rem; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 1.6rem 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
footer a { color: var(--muted); text-decoration: none; }
|
||||||
|
footer a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.cards { grid-template-columns: 1fr 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ── HEADER ── -->
|
||||||
|
<header>
|
||||||
|
<div class="container">
|
||||||
|
<a class="back" href="https://samantha42.xyz/">← back</a>
|
||||||
|
|
||||||
|
<div class="tag">project</div>
|
||||||
|
|
||||||
|
<h1>FP&A <span>Budgeting Engine</span></h1>
|
||||||
|
|
||||||
|
<div class="byline">
|
||||||
|
<span>Samantha Vero Friis</span>
|
||||||
|
<span style="color:var(--muted)">Go · REST API · Finance</span>
|
||||||
|
<span style="color:var(--muted)">March 2026</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ── STAT CARDS ── -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Language</div>
|
||||||
|
<div class="card-value" style="color:var(--accent)">Go</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Database</div>
|
||||||
|
<div class="card-value" style="color:var(--accent)">SQLite</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Architecture</div>
|
||||||
|
<div class="card-value" style="font-size:1.1rem;color:var(--accent)">REST API</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-label">Infrastructure</div>
|
||||||
|
<div class="card-value" style="font-size:1.1rem;color:var(--green)">Zero deps</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── MAIN ── -->
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
<div class="prose-block">
|
||||||
|
|
||||||
|
<p class="lead">
|
||||||
|
A REST API for corporate budget management, actuals ingestion, and variance
|
||||||
|
reporting — built to replace the manual Excel workflow that smaller finance
|
||||||
|
teams still run every month. Single binary, no infrastructure, deploys anywhere.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>The problem it solves</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
At companies below a certain size — typically below 500 employees, or without
|
||||||
|
the budget for enterprise planning platforms like Anaplan or Adaptive Planning —
|
||||||
|
the monthly FP&A close still runs on spreadsheets. Actuals are exported from
|
||||||
|
the ERP, pasted into a workbook, and variances are computed by hand across
|
||||||
|
departments and GL accounts. The process is slow, error-prone, and breaks the
|
||||||
|
moment someone edits the wrong cell.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Large enterprises have solved this with dedicated tooling. The gap is the
|
||||||
|
mid-market: companies that have outgrown Excel but cannot justify a six-figure
|
||||||
|
implementation project. This engine sits in that space — structured schema,
|
||||||
|
versioned budgets, on-demand actuals ingestion, and instant variance reports
|
||||||
|
with no pivot tables and no shared file conflicts.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
The goal was not to build something clever.<br>
|
||||||
|
It was to build something that solves a real problem<br>
|
||||||
|
<span>finance teams in the mid-market actually have.</span>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h2>Stack</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Most finance tooling is Python notebooks or Excel macros — useful for analysis,
|
||||||
|
fragile in production. Go compiles to a single binary with no runtime
|
||||||
|
dependencies, makes every error path explicit, and handles concurrency safely.
|
||||||
|
SQLite was chosen over Postgres deliberately: for a tool running inside a
|
||||||
|
finance team's environment, zero infrastructure matters. The database is a
|
||||||
|
single file. Backups are a file copy.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Layer</th>
|
||||||
|
<th style="text-align:left">Choice</th>
|
||||||
|
<th style="text-align:left">Reason</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="ticker">Language</td>
|
||||||
|
<td style="text-align:left">Go</td>
|
||||||
|
<td style="text-align:left;color:var(--muted)">Single binary, no runtime, explicit errors</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="ticker">Routing</td>
|
||||||
|
<td style="text-align:left">net/http (stdlib)</td>
|
||||||
|
<td style="text-align:left;color:var(--muted)">No deps; method+path routing since Go 1.22</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="ticker">Database</td>
|
||||||
|
<td style="text-align:left">SQLite</td>
|
||||||
|
<td style="text-align:left;color:var(--muted)">Zero infrastructure, file-based, ships with binary</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="ticker">Schema</td>
|
||||||
|
<td style="text-align:left">Auto-migrated</td>
|
||||||
|
<td style="text-align:left;color:var(--muted)"><code>CREATE TABLE IF NOT EXISTS</code> on startup</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>API surface</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Three resource groups. Budgets support full CRUD by department, GL account,
|
||||||
|
fiscal period, and version. Actuals are ingested via a single upsert endpoint
|
||||||
|
— idempotent by period, department, and GL code, designed to accept ERP
|
||||||
|
JSON exports directly. Variance endpoints return computed reports immediately.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>POST /api/v1/budgets Create a budget line
|
||||||
|
PUT /api/v1/budgets/{id} Update amount or notes
|
||||||
|
DELETE /api/v1/budgets/{id} Remove a budget line
|
||||||
|
|
||||||
|
POST /api/v1/actuals/ingest Upsert an actual (idempotent)
|
||||||
|
|
||||||
|
GET /api/v1/variance Full variance report
|
||||||
|
GET /api/v1/variance/alerts Lines exceeding threshold
|
||||||
|
|
||||||
|
GET /api/v1/health Returns 200 if DB is reachable</pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Variance endpoints accept <code>year</code>, <code>period</code>,
|
||||||
|
<code>dept</code>, <code>version</code>, and <code>threshold</code>
|
||||||
|
as query parameters.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Finance concepts implemented correctly</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong style="color:var(--text)">Favourability logic.</strong> Whether a
|
||||||
|
variance is good or bad depends on account type. Revenue accounts are
|
||||||
|
favourable when actuals exceed budget; cost accounts when actuals come in
|
||||||
|
under. The engine handles both via a <code>favour_high</code> flag per GL
|
||||||
|
account — a distinction generic reporting tools consistently leave to manual
|
||||||
|
configuration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong style="color:var(--text)">Budget versioning.</strong> Original budget
|
||||||
|
and up to three forecast revisions are stored separately, enabling a full
|
||||||
|
budget vs. forecast vs. actual three-way comparison without overwriting
|
||||||
|
history — the standard FP&A reforecast workflow.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong style="color:var(--text)">GL account typing.</strong> Accounts are
|
||||||
|
typed as <code>revenue</code>, <code>cogs</code>, <code>opex</code>,
|
||||||
|
<code>capex</code>, or <code>headcount</code>. This drives the P&L
|
||||||
|
rollup structure and the favourability logic together, reflecting how an
|
||||||
|
actual chart of accounts is structured.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong style="color:var(--text)">Fiscal period decoupling.</strong> Periods
|
||||||
|
are stored as integers 1–12, decoupled from calendar months — required for
|
||||||
|
any company not on a January fiscal year.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Example output</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Engineering department, FY2024 P09. Cloud infrastructure came in under budget.
|
||||||
|
Consulting exceeded by 51%.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>GL</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Budget</th>
|
||||||
|
<th>Actual</th>
|
||||||
|
<th>Var %</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>6100</td>
|
||||||
|
<td style="text-align:left;color:var(--text)">Salaries & Wages</td>
|
||||||
|
<td>4,200k</td>
|
||||||
|
<td>4,380k</td>
|
||||||
|
<td class="down">−4.29%</td>
|
||||||
|
<td class="down">unfavourable</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>6500</td>
|
||||||
|
<td style="text-align:left;color:var(--text)">Consulting & Contractors</td>
|
||||||
|
<td>450k</td>
|
||||||
|
<td>680k</td>
|
||||||
|
<td class="down">−51.11%</td>
|
||||||
|
<td class="down">unfavourable</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>5000</td>
|
||||||
|
<td style="text-align:left;color:var(--text)">Cloud Infrastructure</td>
|
||||||
|
<td>850k</td>
|
||||||
|
<td>791k</td>
|
||||||
|
<td class="up">+6.94%</td>
|
||||||
|
<td class="up">favourable</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Running it</h2>
|
||||||
|
|
||||||
|
<pre>git clone https://git.samantha42.xyz/samantha/FPandA-Engine
|
||||||
|
cd FPandA-Engine
|
||||||
|
cp .env.example .env
|
||||||
|
go mod tidy
|
||||||
|
go run ./cmd/server
|
||||||
|
# → http://localhost:8080
|
||||||
|
# → fpa.db created on first run</pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Two environment variables: <code>DB_PATH</code> (use <code>:memory:</code>
|
||||||
|
for tests) and <code>PORT</code>. Demo seed data for Engineering, Sales,
|
||||||
|
and Marketing is included in <code>scripts/seed_demo.sql</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container" style="display:flex;justify-content:space-between;width:100%">
|
||||||
|
<a href="mailto:contact@samantha42.xyz">contact@samantha42.xyz</a>
|
||||||
|
<span>© 2026 — All rights reserved</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+346
-275
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,231 @@
|
|||||||
|
:root{
|
||||||
|
--bg:#f7f6f3;
|
||||||
|
--surface:#ffffff;
|
||||||
|
--surface2:#f0eeea;
|
||||||
|
--border:#e2ddd6;
|
||||||
|
--border2:#ccc7be;
|
||||||
|
--text:#1a1917;
|
||||||
|
--muted:#6b6560;
|
||||||
|
--dim:#b0aa9f;
|
||||||
|
--accent:#1a56db;
|
||||||
|
--accent2:#1447b8;
|
||||||
|
--green:#0a7c52;
|
||||||
|
--amber:#92620a;
|
||||||
|
--ff-mono:'IBM Plex Mono',monospace;
|
||||||
|
--ff-sans:'IBM Plex Sans',sans-serif;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0;}
|
||||||
|
html{font-size:15px;scroll-behavior:smooth;}
|
||||||
|
body{
|
||||||
|
background:var(--bg);
|
||||||
|
color:var(--text);
|
||||||
|
font-family:var(--ff-sans);
|
||||||
|
font-weight:300;
|
||||||
|
line-height:1.7;
|
||||||
|
min-height:100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* subtle dot grid */
|
||||||
|
body::before{
|
||||||
|
content:'';
|
||||||
|
position:fixed;inset:0;
|
||||||
|
background-image:radial-gradient(circle, var(--border) 1px, transparent 1px);
|
||||||
|
background-size:24px 24px;
|
||||||
|
opacity:0.5;
|
||||||
|
pointer-events:none;z-index:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav{
|
||||||
|
position:sticky;top:0;z-index:100;
|
||||||
|
background:rgba(247,246,243,0.94);
|
||||||
|
backdrop-filter:blur(10px);
|
||||||
|
border-bottom:1px solid var(--border);
|
||||||
|
padding:0 clamp(1rem,5vw,3rem);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
height:52px;
|
||||||
|
}
|
||||||
|
.nav-logo{
|
||||||
|
font-family:var(--ff-mono);font-size:0.78rem;font-weight:500;
|
||||||
|
color:var(--accent);text-decoration:none;
|
||||||
|
display:flex;align-items:center;gap:6px;
|
||||||
|
}
|
||||||
|
.nav-logo::before{content:'>';color:var(--green);font-weight:500;}
|
||||||
|
.nav-links{display:flex;gap:0.2rem;align-items:center;}
|
||||||
|
.nav-links a{
|
||||||
|
font-family:var(--ff-mono);font-size:0.72rem;
|
||||||
|
color:var(--muted);text-decoration:none;
|
||||||
|
padding:0.3rem 0.7rem;border-radius:4px;
|
||||||
|
transition:color 0.15s,background 0.15s;
|
||||||
|
}
|
||||||
|
.nav-links a:hover{color:var(--text);background:var(--surface2);}
|
||||||
|
.nav-links .cta{color:var(--accent);border:1px solid var(--accent2);margin-left:0.4rem;}
|
||||||
|
.nav-links .cta:hover{background:var(--accent2);color:#fff;}
|
||||||
|
|
||||||
|
.shell{
|
||||||
|
position:relative;z-index:1;
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:220px 1fr;
|
||||||
|
min-height:calc(100vh - 52px);
|
||||||
|
max-width:1100px;
|
||||||
|
margin:0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside{
|
||||||
|
border-right:1px solid var(--border);
|
||||||
|
padding:2.5rem 1.5rem;
|
||||||
|
display:flex;flex-direction:column;gap:2.5rem;
|
||||||
|
position:sticky;top:52px;height:calc(100vh - 52px);
|
||||||
|
overflow-y:auto;
|
||||||
|
background:var(--surface);
|
||||||
|
}
|
||||||
|
.sidebar-label{
|
||||||
|
font-family:var(--ff-mono);font-size:0.65rem;
|
||||||
|
letter-spacing:0.2em;text-transform:uppercase;
|
||||||
|
color:var(--dim);margin-bottom:0.8rem;
|
||||||
|
}
|
||||||
|
.sidebar-links{display:flex;flex-direction:column;gap:2px;}
|
||||||
|
.sidebar-links a{
|
||||||
|
font-family:var(--ff-mono);font-size:0.78rem;color:var(--muted);
|
||||||
|
text-decoration:none;padding:0.3rem 0.6rem;border-radius:4px;
|
||||||
|
transition:color 0.15s,background 0.15s;
|
||||||
|
display:flex;align-items:center;gap:6px;
|
||||||
|
}
|
||||||
|
.sidebar-links a::before{content:'//';color:var(--dim);font-size:0.65rem;}
|
||||||
|
.sidebar-links a:hover{color:var(--text);background:var(--surface2);}
|
||||||
|
.skill-list{display:flex;flex-direction:column;gap:4px;}
|
||||||
|
.skill-item{
|
||||||
|
font-family:var(--ff-mono);font-size:0.78rem;
|
||||||
|
color:var(--muted);padding:0.3rem 0.6rem;
|
||||||
|
border-left:2px solid var(--border2);
|
||||||
|
transition:border-color 0.15s,color 0.15s;cursor:default;
|
||||||
|
}
|
||||||
|
.skill-item:hover{border-color:var(--accent);color:var(--text);}
|
||||||
|
|
||||||
|
main{
|
||||||
|
padding:3rem clamp(1.5rem,4vw,3rem);
|
||||||
|
display:flex;flex-direction:column;gap:4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero{padding-bottom:3rem;border-bottom:1px solid var(--border);}
|
||||||
|
.hero-eyebrow{
|
||||||
|
font-family:var(--ff-mono);font-size:0.72rem;color:var(--green);
|
||||||
|
letter-spacing:0.18em;margin-bottom:1rem;
|
||||||
|
display:flex;align-items:center;gap:8px;
|
||||||
|
}
|
||||||
|
.hero-eyebrow::before{content:'';display:block;width:20px;height:1px;background:var(--green);}
|
||||||
|
h1{
|
||||||
|
font-family:var(--ff-mono);
|
||||||
|
font-size:clamp(1.8rem,4vw,2.8rem);
|
||||||
|
font-weight:500;color:var(--text);
|
||||||
|
letter-spacing:-0.02em;line-height:1.1;
|
||||||
|
}
|
||||||
|
h1 span{color:var(--accent);}
|
||||||
|
.hero-bio{
|
||||||
|
margin-top:1.2rem;font-size:0.9rem;color:var(--muted);
|
||||||
|
max-width:560px;line-height:1.8;
|
||||||
|
}
|
||||||
|
.hero-meta{
|
||||||
|
margin-top:1.5rem;display:flex;gap:1.5rem;flex-wrap:wrap;
|
||||||
|
font-family:var(--ff-mono);font-size:0.72rem;color:var(--dim);
|
||||||
|
}
|
||||||
|
.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;}}
|
||||||
|
|
||||||
|
.section-head{display:flex;align-items:center;gap:10px;margin-bottom:1.8rem;}
|
||||||
|
.section-head h2{
|
||||||
|
font-family:var(--ff-mono);font-size:0.72rem;font-weight:400;
|
||||||
|
letter-spacing:0.25em;text-transform:uppercase;color:var(--muted);
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.section-head::after{content:'';flex:1;height:1px;background:var(--border);}
|
||||||
|
|
||||||
|
.exp-grid{display:flex;flex-direction:column;gap:8px;}
|
||||||
|
.exp-card{
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:6px;padding:1.4rem 1.5rem;
|
||||||
|
transition:border-color 0.2s,box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.exp-card:hover{border-color:var(--border2);box-shadow:0 2px 12px rgba(0,0,0,0.06);}
|
||||||
|
.exp-header{
|
||||||
|
display:flex;align-items:baseline;justify-content:space-between;
|
||||||
|
margin-bottom:0.6rem;flex-wrap:wrap;gap:0.5rem;
|
||||||
|
}
|
||||||
|
.exp-title{font-family:var(--ff-mono);font-size:0.9rem;font-weight:500;color:var(--text);}
|
||||||
|
.exp-tag{
|
||||||
|
font-family:var(--ff-mono);font-size:0.65rem;
|
||||||
|
letter-spacing:0.1em;text-transform:uppercase;
|
||||||
|
background:rgba(26,86,219,0.07);color:var(--accent);
|
||||||
|
border:1px solid rgba(26,86,219,0.2);
|
||||||
|
padding:2px 8px;border-radius:3px;
|
||||||
|
}
|
||||||
|
.exp-body{font-size:0.85rem;color:var(--muted);line-height:1.8;}
|
||||||
|
|
||||||
|
.edu-grid{display:flex;flex-direction:column;gap:8px;}
|
||||||
|
.edu-card{
|
||||||
|
display:grid;grid-template-columns:80px 1fr;gap:1.2rem;
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
border-radius:6px;padding:1.2rem 1.4rem;
|
||||||
|
transition:border-color 0.2s,box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.edu-card:hover{border-color:var(--border2);box-shadow:0 2px 12px rgba(0,0,0,0.06);}
|
||||||
|
.edu-year{font-family:var(--ff-mono);font-size:0.7rem;color:var(--amber);padding-top:2px;line-height:1.6;}
|
||||||
|
.edu-degree{font-size:0.9rem;font-weight:500;color:var(--text);}
|
||||||
|
.edu-inst{font-family:var(--ff-mono);font-size:0.75rem;color:var(--muted);margin-top:2px;}
|
||||||
|
.edu-detail{font-size:0.82rem;color:var(--dim);margin-top:4px;}
|
||||||
|
|
||||||
|
.lang-row{display:flex;gap:0.6rem;flex-wrap:wrap;}
|
||||||
|
.lang-badge{
|
||||||
|
font-family:var(--ff-mono);font-size:0.78rem;color:var(--text);
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
padding:0.35rem 0.9rem;border-radius:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interests-row{display:flex;gap:0.6rem;flex-wrap:wrap;}
|
||||||
|
.interest-chip{
|
||||||
|
font-family:var(--ff-mono);font-size:0.75rem;color:var(--muted);
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
padding:0.3rem 0.75rem;border-radius:4px;
|
||||||
|
}
|
||||||
|
.interest-chip::before{content:'# ';color:var(--dim);}
|
||||||
|
|
||||||
|
.opinion-grid{display:flex;flex-wrap:wrap;gap:0.6rem;}
|
||||||
|
.opinion-link{
|
||||||
|
font-family:var(--ff-mono);font-size:0.78rem;
|
||||||
|
color:var(--muted);text-decoration:none;
|
||||||
|
background:var(--surface);border:1px solid var(--border);
|
||||||
|
padding:0.4rem 0.9rem;border-radius:4px;
|
||||||
|
display:flex;align-items:center;gap:6px;
|
||||||
|
transition:border-color 0.2s,color 0.2s,box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.opinion-link::before{content:'→';color:var(--dim);font-size:0.75rem;}
|
||||||
|
.opinion-link:hover{border-color:var(--accent2);color:var(--text);box-shadow:0 2px 8px rgba(0,0,0,0.06);}
|
||||||
|
|
||||||
|
footer{
|
||||||
|
position:relative;z-index:1;
|
||||||
|
border-top:1px solid var(--border);
|
||||||
|
padding:1.4rem clamp(1rem,5vw,3rem);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
flex-wrap:wrap;gap:0.5rem;
|
||||||
|
background:var(--surface);
|
||||||
|
}
|
||||||
|
.footer-contact a{font-family:var(--ff-mono);font-size:0.78rem;color:var(--accent);text-decoration:none;}
|
||||||
|
.footer-contact a:hover{text-decoration:underline;}
|
||||||
|
.footer-copy{font-family:var(--ff-mono);font-size:0.7rem;color:var(--dim);}
|
||||||
|
|
||||||
|
@keyframes fadeUp{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:translateY(0);}}
|
||||||
|
.hero{animation:fadeUp 0.6s ease both;}
|
||||||
|
.exp-card:nth-child(1){animation:fadeUp 0.5s 0.05s ease both;}
|
||||||
|
.exp-card:nth-child(2){animation:fadeUp 0.5s 0.12s ease both;}
|
||||||
|
.edu-card:nth-child(1){animation:fadeUp 0.5s 0.08s ease both;}
|
||||||
|
.edu-card:nth-child(2){animation:fadeUp 0.5s 0.15s ease both;}
|
||||||
|
|
||||||
|
@media(max-width:700px){
|
||||||
|
.shell{grid-template-columns:1fr;}
|
||||||
|
aside{display:none;}
|
||||||
|
.edu-card{grid-template-columns:1fr;gap:0.3rem;}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user