diff --git a/AGENTS.md b/AGENTS.md index 3ea213d..c70e92f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..c6067fe --- /dev/null +++ b/DESIGN.md @@ -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. diff --git a/SPEC.md b/SPEC.md index dfc3740..d518cb7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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 diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 0000000..f603a39 --- /dev/null +++ b/dashboard.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 535409c..02472e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests>=2.31 +flask>=3.0 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b727f2f --- /dev/null +++ b/static/style.css @@ -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; } +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..119c7eb --- /dev/null +++ b/templates/index.html @@ -0,0 +1,73 @@ + + + + + + DAFTAR PEMILIH — {{ bp }} + + + + + + +
+ DAFTAR + DATABISNISID + VEX +
+ +
+
+

PEMILIH BP · SUARA TUNGGAL · STAKE ≥ 1.000 VEX

+

DAFTAR PEMILIH

+

PEMINDAIAN TERAKHIR · {{ scanned }}

+
+ +
+
+ {{ total_voters }} + TOTAL PEMILIH +
+
+ {{ total_staked|vex }} + TOTAL VEX +
+
+ {{ top_staked|vex }} + STAKE TERTINGGI +
+
+ +
+
+ RANK + AKUN + STAKE (VEX) + BOBOT SUARA +
+ {% for row in rows %} +
+ {{ (page - 1) * 50 + loop.index }} + {{ row[0] }} + {{ row[1]|vex }} + {{ row[2] }} +
+ {% else %} +
BELUM ADA DATA — JALANKAN get_voters.py
+ {% endfor %} +
+ + +
+ + + + diff --git a/test_dashboard.py b/test_dashboard.py new file mode 100644 index 0000000..0131ac4 --- /dev/null +++ b/test_dashboard.py @@ -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('')[1].split('')[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()