Skip to content

Commit 9ffcda1

Browse files
feat: add logo component and update styles for header alignment and theme variables
1 parent 45079ee commit 9ffcda1

7 files changed

Lines changed: 66 additions & 13 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ vite.config.js.timestamp-*.mjs
1919
_to_delete/
2020
# obsidian workspace
2121
.obsidian/
22+
# screenshot build folders
23+
.shot*/

‎frontend/src/App.css‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
/* header */
1111
.app-header {
1212
display: flex;
13-
align-items: baseline;
13+
align-items: center;
1414
justify-content: space-between;
1515
padding: 20px 0 14px;
1616
border-bottom: 1px solid var(--border);
1717
}
1818

1919
.brand {
2020
display: flex;
21-
align-items: baseline;
21+
align-items: center;
2222
gap: 10px;
2323
}
2424

‎frontend/src/App.jsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Queue from './views/Queue.jsx'
1515
import Team from './views/Team.jsx'
1616
import { period, leads, opsPeople, members, projectLeads, projectsLedBy, projectState, activeSwitcherPeople } from './data/mockData.js'
1717
import { SessionProvider, useSession, actorFor, RoleChip } from './session.jsx'
18+
import Logo from './components/Logo.jsx'
1819

1920
const TABS = [
2021
{ id: 'analytics', label: 'Overview', component: Analytics, opsOnly: true },
@@ -99,6 +100,7 @@ function AppInner() {
99100
<div className="app">
100101
<header className="app-header">
101102
<div className="brand">
103+
<Logo height={30} />
102104
<span className="brand-name">faircenter</span>
103105
</div>
104106
<div className="header-right">

‎frontend/src/components/Logo.jsx‎

Lines changed: 13 additions & 0 deletions
Large diffs are not rendered by default.

‎frontend/src/data/mockData.js‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,9 @@ export function loadSeries(projList, t0, t1, keyOf, opts) {
619619
const bucketOf = {}; for (const p of projList) bucketOf[p.id] = keyOf(p)
620620
const set = new Set(projList.map((p) => p.id))
621621
const buckets = [...new Set(projList.map((p) => keyOf(p)))]
622-
const emit = (cj, cr) => { const row = {}; for (const b of buckets) { if (split) { row[b] = cj[b]; row[b + RESV_SFX] = cr[b] } else { row[b] = cj[b] + cr[b] } } return row }
622+
// a reservation is a hold: usage under it is absorbed inside the reservation, and only
623+
// usage above it adds its own band. Job band = max(0, used - reserved); total = max(used, reserved).
624+
const emit = (cj, cr) => { const row = {}; for (const b of buckets) { const over = Math.max(0, cj[b] - cr[b]); if (split) { row[b] = over; row[b + RESV_SFX] = cr[b] } else { row[b] = cr[b] + over } } return row }
623625
const raw = (t1 - t0) <= 14 * DAY
624626
if (raw) {
625627
const evts = []
@@ -632,15 +634,38 @@ export function loadSeries(projList, t0, t1, keyOf, opts) {
632634
return { rows, buckets, mode: 'raw' }
633635
}
634636
const d0 = Math.floor(t0 / DAY), d1 = Math.ceil(t1 / DAY), nDays = d1 - d0
635-
const gj = new Map(), gr = new Map()
637+
const gj = new Map() // jobs: mean GPUs over the day (GPU-hours / 24)
638+
const gr = new Map() // reservations: the reserved level, kept as a rectangular block
636639
for (let d = 0; d < nDays; d++) { gj.set(d, Object.fromEntries(buckets.map((b) => [b, 0]))); gr.set(d, Object.fromEntries(buckets.map((b) => [b, 0]))) }
640+
// jobs -> time-averaged GPUs per day (actual usage genuinely varies within a day)
637641
for (const u of units) {
638-
if (!set.has(u.projectId) || u.end <= t0 || u.start >= t1) continue
639-
const b = bucketOf[u.projectId]; const grid = u.kind === 'resv' ? gr : gj; let s = Math.max(u.start, t0); const e = Math.min(u.end, t1)
640-
while (s < e) { const di = Math.floor(s / DAY) - d0; const de = (Math.floor(s / DAY) + 1) * DAY; const seg = Math.min(e, de) - s; if (grid.has(di)) grid.get(di)[b] += u.gpus * seg / HOUR; s = de }
642+
if (u.kind === 'resv' || !set.has(u.projectId) || u.end <= t0 || u.start >= t1) continue
643+
const b = bucketOf[u.projectId]; let s = Math.max(u.start, t0); const e = Math.min(u.end, t1)
644+
while (s < e) { const di = Math.floor(s / DAY) - d0; const de = (Math.floor(s / DAY) + 1) * DAY; const seg = Math.min(e, de) - s; if (gj.has(di)) gj.get(di)[b] += u.gpus * seg / HOUR; s = de }
645+
}
646+
// reservations -> the reserved GPU level, not a time-average: a hold is a rectangular
647+
// block, so per day and bucket take the peak concurrent reserved GPUs. A single
648+
// reservation then reads flat at its size across every day it is active.
649+
const resvUnits = units.filter((u) => u.kind === 'resv' && set.has(u.projectId) && u.end > t0 && u.start < t1)
650+
for (let d = 0; d < nDays; d++) {
651+
const dayS = (d0 + d) * DAY, dayE = dayS + DAY
652+
for (const b of buckets) {
653+
const ev = []
654+
for (const u of resvUnits) {
655+
if (bucketOf[u.projectId] !== b) continue
656+
const s = Math.max(u.start, dayS, t0), e = Math.min(u.end, dayE, t1)
657+
if (e <= s) continue
658+
ev.push([s, u.gpus]); ev.push([e, -u.gpus])
659+
}
660+
if (!ev.length) continue
661+
ev.sort((x, y) => x[0] - y[0])
662+
let cur = 0, peak = 0
663+
for (const [, g] of ev) { cur += g; if (cur > peak) peak = cur }
664+
gr.get(d)[b] = peak
665+
}
641666
}
642667
const rows = []
643-
for (let d = 0; d < nDays; d++) { const cj = Object.fromEntries(buckets.map((b) => [b, +(gj.get(d)[b] / 24).toFixed(1)])); const cr = Object.fromEntries(buckets.map((b) => [b, +(gr.get(d)[b] / 24).toFixed(1)])); rows.push({ t: (d0 + d) * DAY, ...emit(cj, cr) }) }
668+
for (let d = 0; d < nDays; d++) { const cj = Object.fromEntries(buckets.map((b) => [b, +(gj.get(d)[b] / 24).toFixed(1)])); const cr = Object.fromEntries(buckets.map((b) => [b, +gr.get(d)[b].toFixed(1)])); rows.push({ t: (d0 + d) * DAY, ...emit(cj, cr) }) }
644669
return { rows, buckets, mode: 'daily' }
645670
}
646671

‎frontend/src/index.css‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
--series-3: #1baf7a;
2424
--series-4: #eda100;
2525

26+
/* logo (adapts to theme; body/ink/yarn fixed, outline flips) */
27+
--logo-body: #f6f1e4;
28+
--logo-line: #1d1b18;
29+
--logo-ink: #1d1b18;
30+
--logo-yarn: #e8733d;
31+
--logo-nose: #e58b9a;
32+
2633
--radius: 8px;
2734
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
2835
font-size: 14px;
@@ -41,6 +48,8 @@
4148
--line: #383835;
4249
--border: rgba(255, 255, 255, 0.10);
4350

51+
--logo-line: #f1ede0;
52+
4453
--accent: #3987e5;
4554
--series-1: #3987e5;
4655
--series-2: #d95926;

‎frontend/src/views/Overview.jsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Operations overview: tiles and the "needs attention" list, shown at the top of Analytics.
22
import {
33
projects, reservations, parameters, period, effectiveGpus, runningAt, activeProjects,
4-
projRate, queueSnapshot, requests as changeRequests, poolRealisation,
4+
projRate, queueSnapshot, requests as changeRequests, poolRealisation, projectState,
55
} from '../data/mockData.js'
66
import { fmt } from '../format.js'
77
import { useSession } from '../session.jsx'
@@ -24,6 +24,8 @@ export default function Overview({ onNav }) {
2424
const qMedMin = qw.length ? qw[Math.floor(qw.length / 2)] / 60000 : 0
2525
const pending = changeRequests.filter((r) => r.status === 'pending')
2626
const activeTeam = active.filter((p) => p.funding !== 'person')
27+
// the team-project roster still on the books: those running now plus those yet to start
28+
const plannedTeam = projects.filter((p) => p.funding !== 'person' && projectState(p) !== 'finished')
2729
const realis = poolRealisation()
2830
const pctLabel = (v) => (v == null ? '—' : Math.round(v * 100) + '%')
2931

@@ -44,10 +46,10 @@ export default function Overview({ onNav }) {
4446
<div className="tiles">
4547
<Tile label="Utilisation now" value={util + '%'} note={`${fmt(inUse)} of ${fmt(capNow)} GPUs`} onClick={() => onNav && onNav('consumption')} />
4648
<Tile label="In queue" value={fmt(q.length)} note={`${fmt(q.reduce((s, r) => s + r.gpus, 0))} GPUs waiting`} onClick={() => onNav && onNav('queue')} />
47-
<Tile label="Queue wait" value={q.length ? medLabel(qMedMin) : '—'} note="median, jobs waiting now" onClick={() => onNav && onNav('queue')} />
48-
<Tile label="Project budget realisation" value={pctLabel(realis.project)} note="used vs allocated, to date" onClick={() => onNav && onNav('review')} />
49-
<Tile label="Person budget realisation" value={pctLabel(realis.person)} note="used vs allocated, to date" onClick={() => onNav && onNav('budgets')} />
50-
<Tile label="Active projects" value={fmt(activeTeam.length)} note="team projects running now" onClick={() => onNav && onNav('review')} />
49+
<Tile label="Queue wait" value={q.length ? medLabel(qMedMin) : '—'} note="median of waiting jobs" onClick={() => onNav && onNav('queue')} />
50+
<Tile label="Project budget used" value={pctLabel(realis.project)} note="vs allocated" onClick={() => onNav && onNav('review')} />
51+
<Tile label="Person budget used" value={pctLabel(realis.person)} note="vs allocated" onClick={() => onNav && onNav('budgets')} />
52+
<Tile label="Active projects" value={fmt(activeTeam.length)} note={`of ${fmt(plannedTeam.length)} planned`} onClick={() => onNav && onNav('review')} />
5153
</div>
5254

5355
<div className="card">

0 commit comments

Comments
 (0)