Skip to content

Commit 9215c41

Browse files
sanjaydocclaude
andcommitted
Add Merge-themed website + operational GUI dashboard
- docs/index.html: marketing landing page (serif display, dark rules, black pill buttons, the stack flowchart) styled after the Merge Labs aesthetic - docs/app/: vanilla-JS control-plane GUI (no build step) with an in-browser engine that faithfully mirrors the Python resolver, so the GitHub Pages demo works with no backend; auto-switches to a live FastAPI backend when one is reachable at the same origin - docs/assets/theme.css: shared light/monochrome/editorial design system - scripts/export_snapshot.py: generates docs/app/data.js from the seeded lab so the browser demo can never drift from the engine - api.py: serve the GUI from docs/ (live mode) at / - README: embed dashboard screenshots + live-demo links Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9BehfGmiCrz3J15W18rc9
1 parent 1b9622d commit 9215c41

13 files changed

Lines changed: 1546 additions & 12 deletions

File tree

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000)
1515
![Deps](https://img.shields.io/badge/core%20deps-zero-success)
1616

17-
[Architecture](#the-stack) · [Quickstart](#quickstart) · [Demo](#what-the-demo-shows) · [Swappable adapters](#swappable-by-design) · [How it maps to the real stack](#how-it-maps-to-the-real-stack)
17+
[Live demo ↗](https://sanjaydoc.github.io/LabSuite/app/) · [Website ↗](https://sanjaydoc.github.io/LabSuite/) · [Architecture](#the-stack) · [Dashboard](#the-web-dashboard) · [Quickstart](#quickstart) · [How it maps to the real stack](#how-it-maps-to-the-real-stack)
1818

1919
</div>
2020

21+
<p align="center">
22+
<img src="docs/media/dashboard-onboard.png" alt="LabSuite dashboard — onboarding a hire across Okta, AD, TrueNAS and Proxmox in one action" width="900" />
23+
</p>
24+
<p align="center"><sub><b>Onboard once, provisioned everywhere.</b> One action creates the hire in Okta, assigns policy groups, syncs to AD, and resolves their exact TrueNAS + Proxmox access — <a href="https://sanjaydoc.github.io/LabSuite/app/">try the live demo</a>.</sub></p>
25+
2126
---
2227

2328
## Why this exists
@@ -113,6 +118,33 @@ and Proxmox. *Upward* is a decision: "can she start VM 101?" resolves her
113118
path → allow/deny, written to the audit log. TrueNAS and Proxmox never see Okta —
114119
they only ask AD for effective groups, exactly as the real systems are wired.
115120

121+
## The web dashboard
122+
123+
A polished, **zero-build web GUI** (vanilla HTML/CSS/JS — no framework) ships with
124+
the project. Run `labsuite serve` for the live, API-backed version, or open the
125+
[**live demo**](https://sanjaydoc.github.io/LabSuite/app/) — it runs entirely in
126+
your browser as a faithful mirror of the Python engine, so you can click through
127+
onboarding, access resolution, and reviews with nothing installed.
128+
129+
<table>
130+
<tr>
131+
<td width="50%"><img src="docs/media/dashboard-explorer.png" alt="Access explorer resolving a user's effective AD groups and access, and testing an allow/deny decision" /></td>
132+
<td width="50%"><img src="docs/media/dashboard-review.png" alt="Access review board with anomaly flags" /></td>
133+
</tr>
134+
<tr>
135+
<td align="center"><sub><b>Access explorer</b> — resolve Okta → AD (with nested groups ⤴) → TrueNAS + Proxmox, and test a single decision with its reason.</sub></td>
136+
<td align="center"><sub><b>Access review</b> — quarterly-style entitlements with flags for stale access, FULL on sensitive shares, and datacenter admin.</sub></td>
137+
</tr>
138+
<tr>
139+
<td width="50%"><img src="docs/media/dashboard-overview.png" alt="Dashboard overview with stat tiles and the identity chain" /></td>
140+
<td width="50%"><img src="docs/media/dashboard-onboard.png" alt="Onboard form and the provisioned result" /></td>
141+
</tr>
142+
<tr>
143+
<td align="center"><sub><b>Overview</b> — the seeded lab at a glance.</sub></td>
144+
<td align="center"><sub><b>Onboard</b> — the form and the resolved downstream access it produces.</sub></td>
145+
</tr>
146+
</table>
147+
116148
## What makes it more than a toy
117149

118150
| | Typical script | **LabSuite** |

docs/app/app.css

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* App-specific styles layered on the shared LabSuite/Merge theme. */
2+
3+
.app { display: grid; grid-template-columns: 232px 1fr; min-height: calc(100vh - 64px); }
4+
5+
.side { border-right: 1.5px solid var(--rule); padding: 22px 14px; }
6+
.side .navitem {
7+
display: flex; align-items: center; gap: .6rem; width: 100%;
8+
padding: .55rem .7rem; border-radius: 10px; border: 0; background: transparent;
9+
font-family: var(--sans); font-size: .92rem; color: var(--ink-2); font-weight: 500;
10+
cursor: pointer; text-align: left; margin-bottom: 2px;
11+
}
12+
.side .navitem:hover { background: var(--paper-2); }
13+
.side .navitem.active { background: var(--ink); color: #fff; }
14+
.side .navitem .k { margin-left: auto; font-size: .7rem; opacity: .5; }
15+
.side .grp { font-size: .7rem; color: var(--faint); letter-spacing: .08em; text-transform: uppercase; margin: 16px 8px 6px; }
16+
17+
.main { padding: 30px 34px 64px; max-width: 940px; }
18+
.view { display: none; }
19+
.view.active { display: block; animation: fade .18s ease; }
20+
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
21+
22+
.view h2 { font-size: 2rem; margin-bottom: .3rem; }
23+
.view .sub { color: var(--muted); margin-bottom: 1.6rem; font-size: .95rem; }
24+
25+
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 8px; }
26+
@media (max-width: 720px) { .stats { grid-template-columns: 1fr 1fr; } }
27+
.stat { border: 1px solid var(--hair); border-radius: var(--radius); padding: 16px 18px; box-shadow: var(--shadow-sm); }
28+
.stat .n { font-family: var(--serif); font-size: 2.2rem; line-height: 1; }
29+
.stat .l { color: var(--muted); font-size: .82rem; margin-top: .35rem; }
30+
31+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
32+
@media (max-width: 820px) { .grid2 { grid-template-columns: 1fr; } }
33+
34+
.field { margin-bottom: 14px; }
35+
.field label { display: block; font-size: .82rem; color: var(--muted); margin-bottom: .35rem; }
36+
input, select {
37+
width: 100%; font-family: var(--sans); font-size: .95rem; color: var(--ink);
38+
padding: .6rem .7rem; border: 1px solid var(--hair); border-radius: 10px; background: var(--paper);
39+
}
40+
input:focus, select:focus { outline: none; border-color: var(--ink); }
41+
42+
.kv { display: grid; grid-template-columns: auto 1fr; gap: .3rem 1rem; font-size: .9rem; }
43+
.kv .k { color: var(--muted); }
44+
45+
.tags { display: flex; flex-wrap: wrap; gap: .4rem; }
46+
.tag { font-size: .78rem; padding: .2rem .55rem; border-radius: var(--pill); border: 1px solid var(--hair); background: var(--paper-2); color: var(--ink-2); }
47+
.tag.nested { border-style: dashed; color: var(--ad); }
48+
49+
.result { margin-top: 18px; }
50+
.flags .flag { display: flex; gap: .5rem; align-items: baseline; padding: .5rem 0; border-bottom: 1px solid var(--hair); font-size: .9rem; }
51+
.flags .flag .bang { color: var(--no); font-weight: 800; }
52+
53+
.audit-row { display: grid; grid-template-columns: 120px 150px 1fr auto; gap: .6rem; font-size: .84rem; padding: .45rem .2rem; border-bottom: 1px solid var(--hair); align-items: center; }
54+
.audit-row .sys { color: var(--muted); font-family: var(--mono, monospace); font-size: .78rem; }
55+
.audit-row .act { font-weight: 600; }
56+
@media (max-width: 720px) { .audit-row { grid-template-columns: 1fr auto; } .audit-row .sys, .audit-row .det { display: none; } }
57+
58+
.modebadge { font-size: .72rem; font-weight: 700; padding: .2rem .55rem; border-radius: var(--pill); }
59+
.modebadge.demo { background: #fff4e5; color: #a2661a; border: 1px solid #f0dcc0; }
60+
.modebadge.live { background: var(--ok-bg); color: var(--ok); border: 1px solid #cfe6da; }
61+
62+
.linkish { cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
63+
64+
.notice { font-size: .82rem; color: var(--muted); background: var(--paper-2); border: 1px solid var(--hair); border-radius: 10px; padding: .7rem .9rem; }
65+
66+
@media (max-width: 720px) {
67+
.app { grid-template-columns: 1fr; }
68+
.side { display: flex; overflow-x: auto; border-right: 0; border-bottom: 1.5px solid var(--rule); padding: 10px; gap: 6px; }
69+
.side .grp { display: none; }
70+
.side .navitem { width: auto; white-space: nowrap; }
71+
.main { padding: 22px 18px 48px; }
72+
}

0 commit comments

Comments
 (0)