T6-T8: web dashboard spec-list (§V10, I.web, I.db)

- dashboard.py: Flask read-only, paged 50/page, ORDER BY staked DESC
- templates/index.html + static/style.css: DESIGN.md Bugatti austere tokens
- test_dashboard.py: oracle (ordering, paging, read-only)
- DESIGN.md style guide (was untracked)
This commit is contained in:
proitlab committed 2026-08-04 16:08:08 +07:00
1 parent c5e30a4785
commit 0b3a86aad2
8 files changed
+944 -4

No files matched your search

+8 -4
View File
@@ -5,9 +5,10 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote
## Run
- Node reference script: `node get_voters.js` (no deps, Node 18+).
- Python tool (production): `./venv/bin/python get_voters.py`. venv is Python 3.12, dep `requests` only (`requirements.txt`). Install with `./venv/bin/pip install -r requirements.txt`.
- Syntax check: `node --check get_voters.js`, `./venv/bin/python -m py_compile get_voters.py`.
- Test (the verification oracle): `./venv/bin/python test_get_voters.py` must exit 0. It mocks the network — the live node is too flaky/slow for a full-scan test. Run it after touching `get_voters.py`.
- Python tool (production): `./venv/bin/python get_voters.py`. venv is Python 3.12, deps `requests` + `flask` (`requirements.txt`). Install with `./venv/bin/pip install -r requirements.txt`.
- Web dashboard (read-only, reads `voters.db`): `./venv/bin/python dashboard.py` → http://127.0.0.1:5000/. Paged 50/page, sorted `staked DESC`. Data freshness comes from the daily scan run — the dashboard never scans.
- Syntax check: `node --check get_voters.js`, `./venv/bin/python -m py_compile get_voters.py dashboard.py`.
- Tests (the verification oracles): `./venv/bin/python test_get_voters.py` and `./venv/bin/python test_dashboard.py` must both exit 0. They mock the network / use a temp DB — the live node is too flaky/slow for a full-scan test. Run after touching the relevant file.
## Key facts
@@ -24,10 +25,13 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote
- `SPEC.md` — spec (goal/constraints/interfaces/invariants/tasks/bug log), in caveman encoding. Build/backprop flow through it.
- `get_voters.py` — production fetcher: scan → filter → SQLite `voters.db` (each run replaces the table = daily snapshot, with `scanned_at`; never appends history).
- `get_voters.js` — reference implementation only.
- `dashboard.py` + `templates/index.html` + `static/style.css` — Flask web dashboard; reads `voters.db`, styled per `DESIGN.md`.
- `DESIGN.md` — Bugatti austere style guide; the dashboard's CSS maps its tokens (canvas #000000, hairline #262626, weight 400 everywhere, fonts Saira Condensed / EB Garamond / JetBrains Mono).
- `voters.db` — SQLite output (daily snapshot, gitignored in spirit).
## Conventions
- Comments and console output are in Indonesian — keep new output/comments in Indonesian.
- Dashboard UI labels are Indonesian uppercase captions (e.g. "DAFTAR PEMILIH", "TOTAL PEMILIH").
- JS style: 2-space indent, semicolons, single quotes, trailing commas, async/await.
- Python style: 4-space indent, stdlib `sqlite3`, `requests`, PEP8.
- Python style: 4-space indent, stdlib `sqlite3`, `requests`, `flask`, PEP8.
+454
View File
@@ -0,0 +1,454 @@
---
version: alpha
name: Bugatti-design-analysis
description: An austere luxury-automotive interface that uses near-pure black canvas, white uppercase letterspaced display, and full-bleed automotive photography as the only voltage. The system runs three custom Bugatti typefaces — Bugatti Display, Bugatti Text Regular, and Bugatti Monospace — and combines them at modest weights with wide tracking to feel European-engineered, hyper-minimal, and quietly expensive. There is no accent color, no decorative element, no chrome — only photography, typography, and the brand wordmark.
colors:
primary: "#ffffff"
ink: "#ffffff"
body: "#cccccc"
body-strong: "#e6e6e6"
muted: "#999999"
muted-soft: "#666666"
hairline: "#262626"
hairline-strong: "#3a3a3a"
canvas: "#000000"
surface-soft: "#0d0d0d"
surface-card: "#141414"
surface-elevated: "#1f1f1f"
on-primary: "#000000"
on-dark: "#ffffff"
on-photo: "#ffffff"
link: "#c3d9f3"
warning: "#d4a017"
success: "#5fa657"
typography:
display-xl:
fontFamily: "Bugatti Display, sans-serif"
fontSize: 64px
fontWeight: 400
lineHeight: 1.1
letterSpacing: 4px
display-lg:
fontFamily: "Bugatti Display, sans-serif"
fontSize: 48px
fontWeight: 400
lineHeight: 1.15
letterSpacing: 3px
display-md:
fontFamily: "Bugatti Display, sans-serif"
fontSize: 32px
fontWeight: 400
lineHeight: 1.2
letterSpacing: 2px
display-sm:
fontFamily: "Bugatti Display, sans-serif"
fontSize: 24px
fontWeight: 400
lineHeight: 1.3
letterSpacing: 1.5px
wordmark:
fontFamily: "Bugatti Display, serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1
letterSpacing: 6px
title-md:
fontFamily: "Bugatti Display, sans-serif"
fontSize: 20px
fontWeight: 400
lineHeight: 1.3
letterSpacing: 1px
title-sm:
fontFamily: "Bugatti Display, sans-serif"
fontSize: 16px
fontWeight: 400
lineHeight: 1.3
letterSpacing: 1.5px
caption-uppercase:
fontFamily: "Bugatti Monospace, ui-monospace, monospace"
fontSize: 11px
fontWeight: 400
lineHeight: 1.4
letterSpacing: 2px
body-md:
fontFamily: "Bugatti Text Regular, serif"
fontSize: 16px
fontWeight: 400
lineHeight: 1.5
letterSpacing: 0
body-sm:
fontFamily: "Bugatti Text Regular, serif"
fontSize: 14px
fontWeight: 400
lineHeight: 1.5
letterSpacing: 0
button:
fontFamily: "Bugatti Monospace, ui-monospace, monospace"
fontSize: 14px
fontWeight: 400
lineHeight: 1
letterSpacing: 2.5px
nav-link:
fontFamily: "Bugatti Monospace, ui-monospace, monospace"
fontSize: 12px
fontWeight: 400
lineHeight: 1.4
letterSpacing: 2px
rounded:
none: 0px
pill: 9999px
full: 9999px
spacing:
xxs: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 40px
xxl: 64px
section: 120px
components:
button-primary:
backgroundColor: transparent
textColor: "{colors.on-dark}"
typography: "{typography.button}"
rounded: "{rounded.pill}"
padding: 14px 32px
height: 44px
button-icon:
backgroundColor: transparent
textColor: "{colors.on-dark}"
rounded: "{rounded.full}"
size: 40px
text-link:
backgroundColor: transparent
textColor: "{colors.link}"
typography: "{typography.button}"
top-nav:
backgroundColor: transparent
textColor: "{colors.on-dark}"
typography: "{typography.nav-link}"
height: 56px
wordmark-display:
backgroundColor: transparent
textColor: "{colors.on-dark}"
typography: "{typography.wordmark}"
hero-photo-band:
backgroundColor: "{colors.canvas}"
textColor: "{colors.on-dark}"
typography: "{typography.display-xl}"
padding: 96px
caption-overlay:
backgroundColor: transparent
textColor: "{colors.on-dark}"
typography: "{typography.caption-uppercase}"
career-callout-card:
backgroundColor: "{colors.surface-card}"
textColor: "{colors.on-dark}"
typography: "{typography.body-sm}"
rounded: "{rounded.none}"
padding: 16px
width: 320px
model-photo-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.on-dark}"
typography: "{typography.display-md}"
rounded: "{rounded.none}"
newsroom-article-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.on-dark}"
typography: "{typography.title-md}"
rounded: "{rounded.none}"
padding: 24px
career-listing-row:
backgroundColor: transparent
textColor: "{colors.on-dark}"
typography: "{typography.title-md}"
padding: 24px 0
text-input:
backgroundColor: transparent
textColor: "{colors.on-dark}"
typography: "{typography.body-md}"
rounded: "{rounded.none}"
padding: 12px 0
height: 44px
spec-cell:
backgroundColor: transparent
textColor: "{colors.on-dark}"
typography: "{typography.title-md}"
padding: 24px 0
date-pill:
backgroundColor: transparent
textColor: "{colors.muted}"
typography: "{typography.caption-uppercase}"
category-tag:
backgroundColor: transparent
textColor: "{colors.muted}"
typography: "{typography.caption-uppercase}"
cta-band-photo:
backgroundColor: "{colors.canvas}"
textColor: "{colors.on-dark}"
typography: "{typography.display-md}"
padding: 80px
footer:
backgroundColor: "{colors.canvas}"
textColor: "{colors.muted}"
typography: "{typography.body-sm}"
padding: 64px
---
## Overview
Bugatti's marketing surface is the most austere interface in luxury automotive: a near-pure black canvas (`{colors.canvas}` — #000000) holding white uppercase **letterspaced** display type and full-bleed automotive photography. The system has no accent color, no surface card decoration, no shadows, no gradients, no chrome — only **photography, typography, and the brand wordmark**. Every other luxury auto site in this category (BMW M, Aston Martin, Lamborghini) uses some form of accent color or signature element; Bugatti uses nothing. The empty space, the photograph, and the precisely-tracked Bugatti Display headline ARE the brand.
The system runs **three custom Bugatti typefaces**: **Bugatti Display** (display headlines, the "BUGATTI" wordmark, all caps with wide tracking), **Bugatti Text Regular** (body paragraphs, a serif text face), and **Bugatti Monospace** (button labels, navigation, captions, dates — anywhere precision and machined feel matters). The split is deliberate and unbreakable: never use Bugatti Text in a button, never use Bugatti Monospace in a paragraph.
Display sizes use weight 400 (regular) — never bold. Visual emphasis comes from **size and tracking**, not weight. Letter-spacing on the wordmark is 6px; on display headlines 2-4px; on uppercase labels 2-2.5px. Tight tracking is a brand violation. The wide spacing creates the "engineered precision" feel that no other luxury maker matches.
**Key Characteristics:**
- Pure black canvas (`{colors.canvas}` — #000000) with white type. The system does not have a light mode.
- Three custom Bugatti typefaces: **Display** (uppercase headlines + wordmark), **Text Regular** (body serif), **Monospace** (buttons, captions, nav).
- All display headlines are UPPERCASE with wide letter-spacing (2-4px). Body copy stays sentence-case at standard tracking.
- No accent color. The only non-monochrome color anywhere on the site is `{colors.link}` (#c3d9f3) — a desaturated ice-blue used on inline anchor links, and even that appears rarely.
- Buttons are pill-shaped (`{rounded.pill}`) with **transparent background** and a 1px white outline. Bugatti is the only luxury-auto brand whose primary CTA is fully transparent.
- Photography is the only depth element. No drop shadows. No gradients. No card surfaces. Surface cards are `{colors.surface-card}` (#141414) at most — a barely-different-from-black tone.
- Section rhythm is generous — `{spacing.section}` (120px) between major bands, longer than most marketing sites because Bugatti's pages are mostly photography with minimal text density.
## Colors
### Brand & Accent
- **Primary** (`{colors.primary}` — #ffffff): The single brand color. White type and white CTA outlines on the black canvas.
- **Link** (`{colors.link}` — #c3d9f3): The only non-monochrome color in the system — a desaturated ice-blue used on inline anchor links and rarely on focus states. Bugatti's brand discipline is so tight that this single token is essentially the entire chromatic vocabulary outside black-and-white.
### Surface
- **Canvas** (`{colors.canvas}` — #000000): The default page floor across every surface. Pure black.
- **Surface Soft** (`{colors.surface-soft}` — #0d0d0d): A barely-different-from-black tone used for spec table rows and dense data sections.
- **Surface Card** (`{colors.surface-card}` — #141414): Cards (career callout, newsroom article container, occasional content cards). Even card surfaces stay nearly-black — no contrast jump.
- **Surface Elevated** (`{colors.surface-elevated}` — #1f1f1f): One step further from black, used for nested cards on rare dense pages.
- **Hairline** (`{colors.hairline}` — #262626): The 1px divider tone. Visible but quiet. Used on table rows, between body sections, around card outlines.
- **Hairline Strong** (`{colors.hairline-strong}` — #3a3a3a): A heavier divider used on the underside of input fields (input fields have no border — only an underline hairline).
### Text
- **Ink / On Dark** (`{colors.on-dark}` — #ffffff): All headline and primary text on dark canvas.
- **Body** (`{colors.body}` — #cccccc): Default running-text color (slightly cooler than pure white). Used in body paragraphs.
- **Body Strong** (`{colors.body-strong}` — #e6e6e6): Emphasized body / lead paragraph.
- **Muted** (`{colors.muted}` — #999999): Footer links, dates, captions, secondary metadata. Dembrandt's frequency analysis confirms this as palette-2 (count 6, medium confidence).
- **Muted Soft** (`{colors.muted-soft}` — #666666): A second-tier muted for very-secondary text (legal disclaimer, copyright line).
### Semantic
- **Warning** (`{colors.warning}` — #d4a017): Reserved for technical-warning callouts (specifications, recall notices). Almost never appears on marketing surfaces.
- **Success** (`{colors.success}` — #5fa657): Order confirmation states (rare on marketing pages).
## Typography
### Font Family
The system runs **three custom Bugatti typefaces** as a rigid trinity:
1. **Bugatti Display** — All display headlines (h1, h2, h3), the "BUGATTI" wordmark, model name plates. Uppercase, wide-tracked. The default for any visual emphasis.
2. **Bugatti Text Regular** — A serif text face used exclusively for running body copy, lead paragraphs, model descriptions. Standard sentence-case, no letter-spacing.
3. **Bugatti Monospace** — Button labels, navigation, captions, dates, monospace-precision contexts. Always uppercase with 2-2.5px tracking.
The split is functional and absolute. Bugatti Display in a button breaks the "machined precision" voice; Bugatti Monospace in a paragraph breaks the "engineered elegance" voice; Bugatti Text in a button is unthinkable.
The fallback stack walks `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` for Bugatti Display, `Garamond, "Times New Roman", serif` for Bugatti Text Regular, and `ui-monospace, "SF Mono", "Cascadia Mono", monospace` for Bugatti Monospace.
### Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---|---|---|---|---|
| `{typography.display-xl}` | 64px | 400 | 1.1 | 4px | Hero h1 ("THE BUGATTI F.K.P. HOMMAGE", "TOURBILLON") — Bugatti Display, uppercase, wide-tracked |
| `{typography.display-lg}` | 48px | 400 | 1.15 | 3px | Section heads — Bugatti Display, uppercase |
| `{typography.display-md}` | 32px | 400 | 1.2 | 2px | Sub-section heads, model names — Bugatti Display |
| `{typography.display-sm}` | 24px | 400 | 1.3 | 1.5px | Card titles — Bugatti Display |
| `{typography.wordmark}` | 14px | 400 | 1.0 | 6px | The "BUGATTI" brand wordmark in the top nav — Bugatti Display, the widest tracking in the system |
| `{typography.title-md}` | 20px | 400 | 1.3 | 1px | Career listing titles, intro paragraphs — Bugatti Display |
| `{typography.title-sm}` | 16px | 400 | 1.3 | 1.5px | Mid-tier headlines, callout cards |
| `{typography.caption-uppercase}` | 11px | 400 | 1.4 | 2px | Photo captions, metadata, "EXPLORE OUR OPPORTUNITIES" — Bugatti Monospace, uppercase |
| `{typography.body-md}` | 16px | 400 | 1.5 | 0 | Default body — Bugatti Text Regular (a serif face), sentence case, no tracking |
| `{typography.body-sm}` | 14px | 400 | 1.5 | 0 | Footer body, fine-print legal — Bugatti Text Regular |
| `{typography.button}` | 14px | 400 | 1.0 | 2.5px | All button labels — Bugatti Monospace, uppercase, 2.5px tracking |
| `{typography.nav-link}` | 12px | 400 | 1.4 | 2px | Top-nav menu items ("MENU", "STORE") — Bugatti Monospace |
### Principles
The system NEVER uses bold weight. Every Bugatti typeface is set at weight 400 (regular). Visual emphasis comes from:
1. **Size** — 64px hero vs 16px body is a 4× hierarchy
2. **Letter-spacing** — 6px wordmark vs 0px body
3. **Case** — Uppercase display vs sentence-case body
4. **Family contrast** — Display vs Text Regular vs Monospace
Going to weight 700 anywhere would break the "modest engineering" feel and make Bugatti read like a generic luxury template.
The serif Bugatti Text Regular sets the brand apart from the all-sans luxury crowd (BMW, Aston Martin, Lamborghini all use sans-serif body type). Bugatti's serif body voice signals literary, considered, slow-reading prose — which is the brand's editorial philosophy.
### Note on Font Substitutes
If Bugatti Display, Bugatti Text Regular, and Bugatti Monospace are unavailable, the closest open-source substitutes are:
- **Bugatti Display** → **Saira Condensed** (variable, weight 400) at +0.05em letter-spacing
- **Bugatti Text Regular** → **Cormorant Garamond** (regular) or **EB Garamond**
- **Bugatti Monospace** → **JetBrains Mono** or **IBM Plex Mono** (regular weight)
The substitution preserves the three-family split, which is more important than exact typeface match.
## Layout
### Spacing System
- **Base unit:** 4px.
- **Tokens:** `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.md}` 16px · `{spacing.lg}` 24px · `{spacing.xl}` 40px · `{spacing.xxl}` 64px · `{spacing.section}` 120px.
- **Section padding:** `{spacing.section}` (120px) — longer than most marketing sites because Bugatti's bands are mostly photography with minimal text. The empty space frames the cars.
- **Card internal padding:** `{spacing.lg}` (24px) for newsroom and content cards; `{spacing.md}` (16px) for the career callout card; `{spacing.xxl}` (64px) inside hero photo bands.
- **Gutters:** `{spacing.xl}` (40px) between cards in 2-up grids — wider than typical because Bugatti's grids are sparse.
### Grid & Container
- **Max content width:** ~1280px centered. Hero photo bands bleed full-width with no max.
- **Editorial body:** Single 12-column grid; photo bands are full-bleed.
- **Newsroom layout:** 2-up article grid at desktop, 1-up at tablet+mobile.
- **Career listings:** Single column with 80px row spacing.
### Whitespace Philosophy
Bugatti uses whitespace more aggressively than any luxury-auto competitor. The homepage hero is mostly photography + huge whitespace + a single sentence + a single button. The empty black space below the photograph is intentional — it lets the car breathe. Compressing the whitespace to "fit more content" breaks the brand's fundamental contract: that less is more.
## Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| Flat | No shadow, no border | Body, top nav, footer, photo bands |
| Soft hairline | 1px `{colors.hairline}` border | Section dividers, table rows |
| Card surface | `{colors.surface-card}` background — no shadow | Career callout, newsroom article container |
| Photographic depth | Full-bleed photography with edge-to-edge crop | Hero bands, model showcases — depth via subject + lens, not chrome |
The system uses no shadows, no glassmorphism, no gradients. Depth comes entirely from photography (lighting, lens, subject framing) and from the contrast between black canvas and minimally-elevated `{colors.surface-card}`.
### Decorative Depth
- None. Bugatti is the only luxury-auto brand without a single decorative element. There is no stripe, no badge, no heritage emblem on the marketing site outside the wordmark itself.
## Shapes
### Border Radius Scale
| Token | Value | Use |
|---|---|---|
| `{rounded.none}` | 0px | All cards, photo containers, inputs, spec cells — the dominant radius |
| `{rounded.pill}` | 9999px | All buttons (the only rounded element in the system) |
| `{rounded.full}` | 9999px / 50% | Circular icon buttons, avatar surfaces |
The radius hierarchy is binary: rectangular for everything except buttons, which are pills. No 4px, no 8px, no 12px in between — those would feel "designed" rather than "engineered."
### Photography Geometry
Hero photography fills full-width with no rounding. Photo cards inside grids retain `{rounded.none}` (0px) corners, edge-to-edge images. Model detail shots use 16:9 or wider cinema-aspect ratios. Newsroom thumbnails use 16:9 with 0px corners. There are no avatars or rounded photo crops anywhere on the marketing site.
## Components
### Top Navigation
**`top-nav`** — A 56px-tall transparent nav bar overlaid on the hero photo at the top of every page. No fill, no border. Carries "MENU" at left, the centered **wordmark-display** ("BUGATTI" in 14px Bugatti Display with 6px tracking), and "STORE" at right with a small bag icon. All labels in `{typography.nav-link}` (Bugatti Monospace, 12px, 2px tracking, uppercase).
**`wordmark-display`** — The "BUGATTI" wordmark itself. Bugatti Display at 14px, weight 400, 6px letter-spacing. The widest tracking in the system. Centered in the nav bar at every breakpoint.
### Buttons
**`button-primary`** — The signature primary CTA. Background **transparent**, text `{colors.on-dark}` (white), 1px white outline, rounded `{rounded.pill}` (9999px), padding 14px × 32px, height 44px. Type `{typography.button}` — Bugatti Monospace, uppercase, 14px, 2.5px tracking. The transparent fill is unique to Bugatti — every other luxury-auto brand uses a filled or outlined-with-text-shift button. Bugatti's transparent pill IS the button.
**`button-icon`** — Circular icon buttons (carousel arrows, share, language switcher). 40 × 40px, transparent background, white outline 1px, rounded `{rounded.full}`. Same outline-only treatment as the primary button.
**`text-link`** — Inline body links in `{colors.link}` (#c3d9f3, the only non-monochrome color in the system). Underlined by default. Type inherits `{typography.body-md}` (Bugatti Text Regular, serif).
### Cards & Containers
**`hero-photo-band`** — Full-width black band with full-bleed automotive photography. The h1 in `{typography.display-xl}` sits center-aligned over the photo near the top, often paired with a small Bugatti Monospace caption (`{typography.caption-uppercase}`) below the headline and a single `{component.button-primary}` further down. Vertical padding 96px-200px depending on photo height.
**`career-callout-card`** — A small right-aligned card that floats over the hero photo on the homepage with a recruiting prompt ("Are you ready for a new adventure?"). Background `{colors.surface-card}`, rounded `{rounded.none}` (0px), padding `{spacing.md}` (16px), width 320px. Carries a small thumbnail at top, body line, and a `{typography.caption-uppercase}` link ("EXPLORE OUR OPPORTUNITIES").
**`model-photo-card`** — Used in model showcases (Tourbillon page, model lineup grid). Background `{colors.canvas}` (no card surface — just photo on black), rounded `{rounded.none}`. Top: 16:9 or 21:9 hero shot of the model. Below: model name in `{typography.display-md}` (32px Bugatti Display, 2px tracking), short specs line in `{typography.caption-uppercase}` (11px Bugatti Monospace), a `{component.text-link}` ("DISCOVER").
**`newsroom-article-card`** — Used on the newsroom page (newsroom.bugatti.com). Background `{colors.canvas}` with hairline border, rounded `{rounded.none}`, padding `{spacing.lg}` (24px). Carries a 16:9 thumbnail, a `{component.date-pill}` ("12. NOVEMBER 2025"), a `{typography.title-md}` headline, and a body excerpt in `{typography.body-md}` (Bugatti Text Regular serif).
**`career-listing-row`** — Each row of the careers page job listing. Transparent background, padding 24px vertical, hairline divider between rows. Job title in `{typography.title-md}` (Bugatti Display 20px) at left; location + department in `{typography.caption-uppercase}` at right; chevron arrow (→) at far right.
**`spec-cell`** — Vehicle technical-spec display on model-detail pages (Tourbillon engine specs). Transparent background with hairline dividers between cells (not between cells inside a card). Each spec shows a value in `{typography.title-md}` at top and a label in `{typography.caption-uppercase}` below. Padding 24px vertical.
### Inputs & Forms
**`text-input`** — Standard text input on dark canvas. Background **transparent**, text `{colors.on-dark}`, 1px hairline-strong bottom border only (no top, left, right border), padding 12px × 0px, height 44px. Type `{typography.body-md}` (Bugatti Text Regular). Placeholder in `{colors.muted}`. Focus thickens the bottom border to white.
### Tags & Captions
**`caption-overlay`** — Photo-overlay caption (e.g., "HONORING THE OEYRON AND ITS VISIONARY CREATOR"). Centered or left-aligned over photography in `{typography.caption-uppercase}` (Bugatti Monospace, 11px, 2px tracking, white).
**`category-tag`** + **`date-pill`** — Both render as transparent inline labels in `{typography.caption-uppercase}`, color `{colors.muted}`. No background fill, no border. The "tag" is the type itself.
### CTA / Footer
**`cta-band-photo`** — A pre-footer "Discover Bugatti" band with full-bleed photography of a Bugatti car at speed and a centered headline in `{typography.display-md}` + a `{component.button-primary}` below. Vertical padding 80px. Inherits the editorial gravity of the hero through full-bleed photography.
**`footer`** — Black footer that closes every page. Background `{colors.canvas}`, text `{colors.muted}`. 4-column link list at desktop covering Bugatti / Models / Heritage / Connect. Vertical padding 64px. Bottom row carries the copyright line in `{typography.body-sm}` (Bugatti Text Regular). The wordmark sits center-aligned at the very bottom. The footer never inverts.
## Do's and Don'ts
### Do
- Anchor every page with full-bleed automotive photography. The cars are the brand voltage; chrome backs off entirely.
- Keep all display headlines in UPPERCASE Bugatti Display with 2-4px letter-spacing. The wordmark gets 6px.
- Use Bugatti Display for headlines, Bugatti Text Regular (serif!) for body, Bugatti Monospace for buttons + captions + nav. The trinity is unbreakable.
- Keep `{component.button-primary}` transparent with a 1px white outline. The transparent pill IS the brand button.
- Use weight 400 everywhere. Bold breaks the brand voice — the system has no bold weight role.
- Use `{spacing.section}` (120px) between major editorial bands. The whitespace is part of the brand.
- Reserve `{colors.link}` (#c3d9f3) for inline anchor links only. It's the system's only non-monochrome color.
### Don't
- Don't introduce any accent color outside `{colors.link}`. Bugatti's brand discipline is total monochrome + photography. Adding a brand-blue or brand-red breaks the contract.
- Don't bold any type. The system has no bold weight — every typeface stays at 400.
- Don't fill primary buttons. Transparent + outline only. A solid white button reads as off-brand.
- Don't compress whitespace between sections. The 120px rhythm is part of the editorial pacing.
- Don't use rounded corners outside buttons. Cards, photos, inputs all stay at 0px. Rounded cards read as consumer-tech, not luxury-engineered.
- Don't tighten letter-spacing on display headlines. 2-4px tracking on Bugatti Display is non-negotiable.
- Don't use Bugatti Display in a button (use Bugatti Monospace) or Bugatti Monospace in a paragraph (use Bugatti Text Regular). The trinity split is the brand voice.
## Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|---|---|---|
| Mobile | < 768px | Hamburger nav; hero h1 64→32px; career callout card hides; photo bands stay full-bleed; footer 4 cols → 1 |
| Tablet | 768–1024px | Top nav stays minimal (MENU + wordmark + STORE); 2-up newsroom grid; career rows full-width |
| Desktop | 1024–1440px | Full minimal top-nav; 2-up newsroom grid; spec tables 4-up |
| Wide | > 1440px | Same as desktop with more breathing room; max content 1280px |
### Touch Targets
- `{component.button-primary}` renders at minimum 44 × 44px (matches WCAG AAA).
- `{component.button-icon}` is exactly 40 × 40px.
- `{component.text-input}` height is 44px.
- Career listing rows have 24px vertical padding; effective tap area meets 44px+ with surrounding spacing.
### Collapsing Strategy
- Top nav stays minimal at all breakpoints (MENU label + wordmark + STORE label). On mobile the labels hide behind a hamburger but the wordmark stays centered.
- Hero photography stays full-bleed at every breakpoint. Photo crops adjust — wider crops at desktop, vertical crops on mobile.
- The career callout card on the homepage hides at < 768px (it's a desktop-only floating element).
- 2-up newsroom grid collapses to 1-up at < 768px.
- Spec cells reflow from 4-up to 2-up to 1-up; values stay at the same display size regardless of column count.
### Image Behavior
- Hero photography crops responsively — wider crops at desktop, vertical crops on mobile. Bugatti cars are always shown in motion or at-angle (never flat profiles).
- Newsroom thumbnails retain 16:9 ratio and 0px corners.
## Iteration Guide
1. Focus on ONE component at a time. Reference its YAML key (`{component.hero-photo-band}`, `{component.career-callout-card}`).
2. New components default to `{rounded.none}` (0px). Only `{component.button-primary}` and `{component.button-icon}` use pill / full radius.
3. Variants live as separate entries in `components:`.
4. Use `{token.refs}` everywhere — never inline hex.
5. Never document hover. Default and Active/Pressed states only.
6. Display headlines stay UPPERCASE Bugatti Display 400 with 2-4px tracking. Body stays sentence-case Bugatti Text Regular (serif). Button labels stay Bugatti Monospace 2.5px tracking. The trinity does not blur.
7. When in doubt about emphasis: bigger photography before bigger type.
## Known Gaps
- The dembrandt frequency analyzer captured only 3 colors at root level (`#000000`, `#999999`, `#c3d9f3`). The white text (#ffffff) and dark surface tones (`#0d0d0d`, `#141414`, `#1f1f1f`) were inferred from screenshot — Bugatti's pages are so monochrome that the frequency-based analyzer didn't surface body text or surface tones as distinct palette entries.
- The three Bugatti typefaces (Display, Text Regular, Monospace) are licensed to Bugatti and not available as web fonts publicly. Substitutes are documented in the typography section.
- Animation and transition timings (photo carousel transitions, hover-reveal of menu, configurator animations) are not in scope.
- Form validation states beyond the underline-only `{component.text-input}` are not extracted — error / success states are inferred from general standards, not from the analyzed surfaces.
- The configurator surface (vehicle build pages with custom paint / interior pickers) was not in the analyzed URL set; its swatch grid, customization controls, and price-summary card are not documented here.
- The German-language newsroom (newsroom.bugatti.com/de) shares the system with the English Bugatti.com surfaces — no design-system-level differences observed, only language localization.
- The actual Tourbillon page rendered as a sparse minimal page in the captured screenshot, suggesting either lazy-loaded content or an interactive configurator-style UI that doesn't render fully in static screenshots; engine-spec layout is documented from general luxury-auto patterns informed by the captured spec cell tokens.
+5
View File
@@ -23,6 +23,7 @@ api: POST `https://v2.vexascan.com:2096/v1/chain/get_table_rows`
→ rows[owner, proxy, producers[], staked, last_vote_weight, proxied_vote_weight, is_proxy], more, next_key
db: `voters.db` → table `voters` (owner PK, weight, staked, scanned_at) — ganti tiap skan
cmd: `./venv/bin/python get_voters.py` → stdout summary (id-ID)
web: GET `/` (Flask) → HTML spec-list, paged 50/halaman, `ORDER BY staked DESC, owner ASC`; ⊥ mutation (read-only)
## §V — Invariants
V1: ∀ fetch → retry ≥3 on timeout/http err (like JS fetchWithRetry)
@@ -34,6 +35,7 @@ V6: weight stored string verbatim, ⊥ float conversion
V7: each scan replaces table; ∀ row → latest snapshot; ⊥ append/duplicate owner
V8: normalize coerces `staked` → float (node kadang memberi string)
V9: normalize drops voters where `staked` < 1000 VEX (setelah ÷10000)
V10: dashboard baca `voters.db` → rows sort `staked` DESC (terbesar dulu); ⊥ mutasi DB
## §T — Tasks
id|status|task|cites
@@ -42,6 +44,9 @@ T2|x|scan loop: retry + pagination|V1,V2,V3,I.api
T3|x|filter + normalize (owner, staked÷10000, weight ≥1000)|V4,V5,V6,V8,V9
T4|x|sqlite schema + replace-snapshot w/ scanned_at|V7,I.db
T5|x|CLI entry + Indonesian summary stdout|I.cmd
T6|x|dashboard: Flask read-only, paged spec-list sort staked DESC|V10,I.web,I.db
T7|x|dashboard: styling per DESIGN.md (canvas hitam, typografi, hairline)|I.web
T8|x|dashboard: test oracle + smoke|V10,I.web
## §B — Bug log
id|date|cause|fix
+80
View File
@@ -0,0 +1,80 @@
"""Dashboard web pemilih BP databisnisid — baca `voters.db`, sajikan spec-list.
Read-only (V10): tidak pernah menulis ke DB. Data disegarkan oleh `get_voters.py`
(run harian / cron). Styling mengikuti `DESIGN.md`.
"""
import sqlite3
from flask import Flask, abort, render_template, request
from get_voters import DB_PATH, TARGET_BP
PAGE_SIZE = 50
app = Flask(__name__)
@app.template_filter('vex')
def vex(value):
"""Format jumlah VEX ala id-ID (koma ribuan, titik desimal), tanpa emoji."""
return f'{value:,.4f}'.replace(',', '\u00a0').replace('.', ',').replace('\u00a0', '.')
def _db_rows(page):
"""V10: baca satu halaman, urut staked DESC lalu owner ASC."""
offset = (page - 1) * PAGE_SIZE
conn = sqlite3.connect(DB_PATH)
try:
return conn.execute(
'SELECT owner, staked, weight, scanned_at FROM voters '
'ORDER BY staked DESC, owner ASC '
'LIMIT ? OFFSET ?',
(PAGE_SIZE, offset),
).fetchall()
finally:
conn.close()
def _stats():
"""Ringkasan agregat untuk band spec."""
conn = sqlite3.connect(DB_PATH)
try:
total_voters, total_staked, top_staked = conn.execute(
'SELECT COUNT(*), COALESCE(SUM(staked), 0), COALESCE(MAX(staked), 0) '
'FROM voters'
).fetchone()
scanned = conn.execute(
'SELECT COALESCE(MAX(scanned_at), \'\') FROM voters'
).fetchone()[0]
finally:
conn.close()
return total_voters, total_staked, top_staked, scanned
@app.get('/')
def index():
try:
page = max(1, int(request.args.get('page', 1)))
except ValueError:
abort(400)
total_voters, total_staked, top_staked, scanned = _stats()
total_pages = max(1, (total_voters + PAGE_SIZE - 1) // PAGE_SIZE)
page = min(page, total_pages)
rows = _db_rows(page)
return render_template(
'index.html',
bp=TARGET_BP,
rows=rows,
page=page,
total_pages=total_pages,
total_voters=total_voters,
total_staked=total_staked,
top_staked=top_staked,
scanned=scanned,
)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)
+1
View File
@@ -1 +1,2 @@
requests>=2.31
flask>=3.0
+234
View File
@@ -0,0 +1,234 @@
/* DESIGN.md — Bugatti austere system, mapped 1:1 */
:root {
/* colors */
--ink: #ffffff;
--body: #cccccc;
--body-strong: #e6e6e6;
--muted: #999999;
--muted-soft: #666666;
--hairline: #262626;
--hairline-strong: #3a3a3a;
--canvas: #000000;
--surface-soft: #0d0d0d;
--surface-card: #141414;
--on-dark: #ffffff;
--link: #c3d9f3;
/* typography — DESIGN.md substitutes: Saira Condensed (Display),
EB Garamond (Text Regular), JetBrains Mono (Monospace) */
--font-display: 'Saira Condensed', 'Arial Narrow', -apple-system, sans-serif;
--font-text: 'EB Garamond', Garamond, 'Times New Roman', serif;
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Mono', monospace;
/* spacing */
--sp-xxs: 4px; --sp-xs: 8px; --sp-sm: 12px; --sp-md: 16px;
--sp-lg: 24px; --sp-xl: 40px; --sp-xxl: 64px; --sp-section: 120px;
/* layout */
--max-content: 1280px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { background: var(--canvas); }
body {
background: var(--canvas);
color: var(--on-dark);
font-family: var(--font-text);
font-weight: 400;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ————— top nav ————— */
.top-nav {
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--sp-lg);
max-width: var(--max-content);
width: 100%;
margin: 0 auto;
}
.nav-label {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--on-dark);
}
.wordmark {
font-family: var(--font-display);
font-size: 14px;
letter-spacing: 6px;
text-transform: uppercase;
color: var(--on-dark);
}
/* ————— page / hero ————— */
.page { width: 100%; max-width: var(--max-content); margin: 0 auto; flex: 1; }
.hero { padding: var(--sp-xxl) var(--sp-lg) var(--sp-lg); }
.display-xl {
font-family: var(--font-display);
font-size: clamp(40px, 6vw, 64px);
font-weight: 400;
line-height: 1.1;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--on-dark);
}
.caption {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--muted);
margin-top: var(--sp-md);
}
/* ————— stats band ————— */
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
border-top: 1px solid var(--hairline);
border-bottom: 1px solid var(--hairline);
margin: 0 var(--sp-lg) var(--sp-xl);
}
.spec-cell {
padding: var(--sp-lg) var(--sp-md);
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}
.spec-cell + .spec-cell { border-left: 1px solid var(--hairline); }
.cell-value {
font-family: var(--font-display);
font-size: 20px;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--on-dark);
}
.cell-label {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--muted);
}
/* ————— spec-list ————— */
.spec-list { margin: 0 var(--sp-lg); border-top: 1px solid var(--hairline-strong); }
.list-header,
.list-row {
display: grid;
grid-template-columns: 72px 1fr 200px 1fr;
gap: var(--sp-lg);
align-items: baseline;
padding: var(--sp-lg) var(--sp-sm);
border-bottom: 1px solid var(--hairline);
}
.list-header {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--muted);
padding-top: var(--sp-md);
}
.list-row:nth-child(odd) { background: var(--surface-soft); }
.rank,
.weight {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 1.5px;
color: var(--muted);
}
.owner {
font-family: var(--font-display);
font-size: 20px;
letter-spacing: 1px;
color: var(--on-dark);
}
.stake {
font-family: var(--font-display);
font-size: 20px;
letter-spacing: 1px;
text-align: right;
color: var(--on-dark);
font-variant-numeric: tabular-nums;
}
.empty {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--muted);
padding: var(--sp-xxl) var(--sp-sm);
}
/* ————— pager ————— */
.pager {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-lg);
padding: var(--sp-xxl) var(--sp-lg);
}
.pill {
font-family: var(--font-mono);
font-size: 14px;
letter-spacing: 2.5px;
text-transform: uppercase;
text-decoration: none;
color: var(--on-dark);
border: 1px solid var(--on-dark);
border-radius: 9999px;
padding: 14px 32px;
height: 44px;
display: inline-flex;
align-items: center;
}
.pill-disabled {
opacity: 0.25;
pointer-events: none;
border-color: var(--muted-soft);
}
.page-count {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 2px;
color: var(--muted);
}
/* ————— footer ————— */
.footer {
padding: var(--sp-xxl) var(--sp-lg);
margin-top: var(--sp-section);
border-top: 1px solid var(--hairline);
text-align: center;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--muted-soft);
}
/* ————— responsive ————— */
@media (max-width: 768px) {
.stats { grid-template-columns: 1fr; }
.spec-cell + .spec-cell { border-left: none; border-top: 1px solid var(--hairline); }
.list-header,
.list-row { grid-template-columns: 48px 1fr; gap: var(--sp-sm); }
.list-header span:nth-child(3),
.list-header span:nth-child(4),
.list-row .stake,
.list-row .weight { grid-column: 2; }
.list-header span:nth-child(3) { display: none; }
.list-header span:nth-child(4) { display: none; }
.pager { flex-wrap: wrap; }
}
+73
View File
@@ -0,0 +1,73 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DAFTAR PEMILIH — {{ bp }}</title>
<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=Saira+Condensed:wght@400&family=EB+Garamond:ital,wght@0,400;1,400&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header class="top-nav">
<span class="nav-label">DAFTAR</span>
<span class="wordmark">DATABISNISID</span>
<span class="nav-label">VEX</span>
</header>
<main class="page">
<section class="hero">
<p class="caption">PEMILIH BP · SUARA TUNGGAL · STAKE ≥ 1.000 VEX</p>
<h1 class="display-xl">DAFTAR PEMILIH</h1>
<p class="caption">PEMINDAIAN TERAKHIR · {{ scanned }}</p>
</section>
<section class="stats">
<div class="spec-cell">
<span class="cell-value">{{ total_voters }}</span>
<span class="cell-label">TOTAL PEMILIH</span>
</div>
<div class="spec-cell">
<span class="cell-value">{{ total_staked|vex }}</span>
<span class="cell-label">TOTAL VEX</span>
</div>
<div class="spec-cell">
<span class="cell-value">{{ top_staked|vex }}</span>
<span class="cell-label">STAKE TERTINGGI</span>
</div>
</section>
<section class="spec-list">
<div class="list-header">
<span>RANK</span>
<span>AKUN</span>
<span>STAKE (VEX)</span>
<span>BOBOT SUARA</span>
</div>
{% for row in rows %}
<div class="list-row">
<span class="rank">{{ (page - 1) * 50 + loop.index }}</span>
<span class="owner">{{ row[0] }}</span>
<span class="stake">{{ row[1]|vex }}</span>
<span class="weight">{{ row[2] }}</span>
</div>
{% else %}
<div class="empty">BELUM ADA DATA — JALANKAN get_voters.py</div>
{% endfor %}
</section>
<nav class="pager" aria-label="Navigasi halaman">
<a class="pill {% if page <= 1 %}pill-disabled{% endif %}"
href="{{ url_for('index', page=page-1) }}">← SEBELUMNYA</a>
<span class="page-count">{{ page }} / {{ total_pages }}</span>
<a class="pill {% if page >= total_pages %}pill-disabled{% endif %}"
href="{{ url_for('index', page=page+1) }}">BERIKUTNYA →</a>
</nav>
</main>
<footer class="footer">
<span>DATA TERVALIDASI DARI JARINGAN VEXANIUM · VEXCORE · VOTERS</span>
</footer>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
"""Uji verifikasi dashboard (oracle).
`./venv/bin/python test_dashboard.py` harus exit 0.
Memakai DB sementara, bukan voters.db asli (read-only invariant tetap diverifikasi).
"""
import os
import sqlite3
import tempfile
import dashboard
def check(tag, cond):
if not cond:
raise AssertionError(f'GAGAL: {tag}')
print(f'ok: {tag}')
def make_db(path, rows):
conn = sqlite3.connect(path)
conn.execute('CREATE TABLE voters ('
'owner TEXT PRIMARY KEY, weight TEXT NOT NULL, '
'staked REAL NOT NULL, scanned_at TEXT NOT NULL)')
conn.executemany(
'INSERT INTO voters (owner, weight, staked, scanned_at) VALUES (?, ?, ?, ?)',
rows,
)
conn.commit()
conn.close()
def main():
tmp = tempfile.mkdtemp()
db = os.path.join(tmp, 'voters.db')
rows = []
for i in range(1, 126): # 125 baris -> 3 halaman
rows.append((f'acct{i:03d}', f'{i}.00000000000000000', float(i) * 10.0, '2026-08-04T00:00:00'))
rows[5] = (rows[5][0], rows[5][1], 5.0 * 10.0, rows[5][3]) # non-monotonic guard
make_db(db, rows)
dashboard.DB_PATH = db # arahkan app ke DB uji (read-only tetap dipertahankan)
client = dashboard.app.test_client()
# V10: urutan staked DESC (terbesar dulu), 50 per halaman
resp = client.get('/?page=1')
check('V10 halaman 1 → 200', resp.status_code == 200)
html = resp.get_data(as_text=True)
check('V10 judul halaman', 'DAFTAR PEMILIH' in html)
staked_vals = []
for i in range(1, 4):
resp = client.get(f'/?page={i}')
check(f'V10 halaman {i} → 200', resp.status_code == 200)
text = resp.get_data(as_text=True)
for token in text.split('class="list-row"')[1:]:
stake_token = token.split('<span class="stake">')[1].split('</span>')[0]
staked_vals.append(_parse_vex(stake_token))
check('V10 total 125 baris ditarik', len(staked_vals) == 125)
check('V10 urut non-naik', all(staked_vals[i] >= staked_vals[i + 1]
for i in range(len(staked_vals) - 1)))
# halaman melebihi total → dibatasi (tidak error)
resp = client.get('/?page=99')
check('V10 halaman ekstra → 200', resp.status_code == 200)
# V10: read-only — tidak ada tabel baru / perubahan pada DB asli
before = _table_hashes(db)
client.get('/')
check('V10 read-only (DB tak berubah)', _table_hashes(db) == before)
def _parse_vex(text):
# format '1.749.791,2303' → float
return float(text.replace('.', '').replace(',', '.'))
def _table_hashes(path):
conn = sqlite3.connect(path)
try:
rows = conn.execute('SELECT owner, weight, staked, scanned_at '
'FROM voters ORDER BY owner').fetchall()
finally:
conn.close()
return tuple(rows)
if __name__ == '__main__':
main()