diff --git a/docs/architecture/ADR-003-official-veeam-service-naming.md b/docs/architecture/ADR-003-official-veeam-service-naming.md new file mode 100644 index 0000000..0919a1a --- /dev/null +++ b/docs/architecture/ADR-003-official-veeam-service-naming.md @@ -0,0 +1,66 @@ +# ADR-003: Adopt Official Veeam Help-Center Service Names + +**Status**: Accepted + +## Context + +The UI previously used a mix of ad-hoc service names produced from the YAML keys (`vdc_vault`, `vdc_m365`, `vdc_entra_id`, `vdc_salesforce`, `vdc_azure_backup`). The `serviceDisplayNames` map rendered them as a hybrid of ALL-CAPS brand acronyms and title-case product nouns — e.g. `M365 Protection`, `ENTRA ID Protection`, `SALESFORCE Protection`, `AZURE Protection`. The Vault row used short `Vault` / full `VAULT`. + +Two problems emerged from this: + +- The names didn’t match the section headings used in the official Veeam Data Cloud help-center user guide. Anyone reading both side-by-side had to mentally translate. +- The service-filter checkbox label `Azure` collided with the Azure cloud provider label in the provider filter directly above it. Users could mistake the service filter for a provider filter. + +The names appear in three runtime contexts: the multi-select filter dropdown (short label), the filter-button label when one service is selected (short label), and the popup row per service (full label). + +## Decision Drivers + +- Vocabulary parity with the official Veeam documentation users are most likely to read alongside the map. +- Unambiguous filter labels — no two top-level controls should share a label. +- A single source of truth (`serviceDisplayNames`) drives every UI surface. + +## Decision + +Adopt the official help-center section names verbatim as the `full` display names: + +| YAML key | `short` (filter UI) | `full` (popup row) | +| --- | --- | --- | +| `vdc_vault` | `Vault` | `Veeam Data Cloud Vault` | +| `vdc_m365` | `M365` | `Microsoft 365 Protection` | +| `vdc_entra_id` | `Entra ID` | `Microsoft Entra ID Protection` | +| `vdc_salesforce` | `Salesforce` | `Salesforce Protection` | +| `vdc_azure_backup` | `Azure Protection` | `Microsoft Azure Protection` | + +The `short` for `vdc_azure_backup` is deliberately `Azure Protection` (not `Azure`) to disambiguate from the `Azure` cloud-provider filter that sits next to it in the header. + +All UI surfaces — multi-select checkbox labels, multi-select button label, and popup rows — read these strings through `getServiceDisplayName(key, type)`. The Playwright test that filters by the Azure service uses the `Azure Protection` label accordingly. + +## Options Considered + +### Option A: Keep the ad-hoc names + +- Pros: No code change; existing tests pass. +- Cons: Vocabulary divergence from official docs; ongoing collision between the `Azure` service short and the `Azure` provider option. + +### Option B: Use only the short brand abbreviations everywhere (`Vault`, `M365`, `Entra`, `SF`, `Azure`) + +- Pros: Most compact UI; no wrapping. +- Cons: `SF` is opaque without context; doesn't match any official Veeam naming; the `Azure` collision is unresolved. + +### Option C: Adopt the official help-center names (chosen) + +- Pros: Direct parity with documentation users already read; the `…Protection` suffix on the Azure service eliminates the provider collision naturally. +- Cons: Long labels — `Microsoft Entra ID Protection` wraps in the popup row on narrow widths. Accepted because the wrapping is benign and the disambiguation gain is worth it. + +## Consequences + +- Any future service added to the YAML schema MUST register a `{ short, full }` entry in `serviceDisplayNames` using the canonical Veeam help-center heading as `full`. If the official docs ever rename a section, this map is the single place to update. +- The `short` label for a new service MUST NOT collide with any existing provider value in `#providerFilter` (today: `Azure`, `AWS`). Where collision is unavoidable, follow the precedent set here and append `Protection` (or the equivalent product noun) to the `short`. +- Documentation surfaces that mention services (info panel “What is this?” copy, `static/llms*.txt`, future help text) should use the canonical `full` names. +- Tests asserting against service labels (e.g. `getByRole('checkbox', { name: 'Azure Protection' })`, ` Azure Protection` regex matches) must be updated in lockstep with any future rename. + +## Links + +- [PR #103](https://github.com/comnam90/veeam-data-cloud-services-map/pull/103) — the redesign that introduced this convention. +- Mission Control Redesign implementation plan: `plans/mission-control-redesign/plan.md` +- Veeam Data Cloud help-center: diff --git a/docs/architecture/ADR-004-mission-control-aesthetic-and-design-tokens.md b/docs/architecture/ADR-004-mission-control-aesthetic-and-design-tokens.md new file mode 100644 index 0000000..4b941ff --- /dev/null +++ b/docs/architecture/ADR-004-mission-control-aesthetic-and-design-tokens.md @@ -0,0 +1,85 @@ +# ADR-004: Paired Mission Control / Workstation Aesthetic with CSS Design Tokens + +**Status**: Accepted + +## Context + +The SPA shipped initially with a generic Tailwind-driven SaaS dashboard look: system fonts, slate-grey palette with a green accent sprinkled evenly across the chrome, circle markers, and a popup that duplicated the Vault row when a region offered multiple editions. A UX review flagged the design as "competent but indistinguishable from every other cloud-service map" and identified specific issues: an Australia-centric default map view, hidden-on-mobile region count, no provider identity in markers, and amber styling collisions between AWS branding and warning callouts. + +A redesign was proposed, brainstormed across two directions ("Mission Control HUD" vs "Cartographic Atlas"), and the Mission Control direction was chosen. To make light mode a first-class peer of dark mode — not a flashlight-inversion of the cockpit aesthetic — a second variant (`Workstation`) was added: same DNA, calmer language. The two themes share the same component anatomy and only swap underlying tokens. + +The redesign is a single-file SPA change (`layouts/index.html`) because the project's architecture deliberately keeps the entire frontend in one Hugo template. Every visible component therefore has to pick its colours, fonts, and spacing values from somewhere — a Tailwind utility class, an inline style, or a custom CSS rule. Without a shared vocabulary the redesign would have continued the original pattern of scattering hex values across hundreds of lines and would have made future theme work hard. + +## Decision Drivers + +- One toggle, one product. Users flipping dark↔light should see the same product wearing different surfaces, not two different apps. +- A small palette that has *meaning*. The accent green was previously decorative everywhere; in the new system it signals "live / available / current selection" specifically. +- Mono-first typography (with a display serif/sans companion) to differentiate from the SaaS-dashboard pack and lean into the data-density of the map. +- Provider identity at the marker level — users shouldn't need to read the legend to know whether a marker is AWS or Azure. +- Theming must work without `.dark` / `.light` selectors on every component. The CSS variable system replaces dozens of paired overrides. + +## Decision + +### Tokenized theme system + +A single `:root` block defines the dark-mode (default) tokens. An `html.light` block re-declares the same token names with light-mode values. Every redesigned component reads colours through `var(--bg)`, `var(--text)`, `var(--accent)`, `var(--azure)`, `var(--aws)`, etc. No component declares hex values directly except where the value is provider-identity (e.g. AWS Squid Ink) and required to render in a context that doesn't inherit CSS variables (e.g. SVG attribute fills are wrapped in `var(--azure)` / `var(--aws)` for the same reason). + +Token domains: +- Surface: `--bg`, `--bg-elev`, `--bg-elev-2` +- Hairlines: `--border`, `--border-strong` +- Ink: `--text`, `--text-mute`, `--text-dim` +- Accent: `--accent`, `--accent-soft`, `--accent-glow` +- Brand: `--azure`, `--aws` + +Dark accent is the fluorescent `#00ff88`; light accent is the muted `#00805a`. The two are intentionally non-identical — bright green on white reads as highlighter, muted green on black has no presence. Both still read as "Veeam green" emotionally. + +### Typography + +- `Space Grotesk` (weights 500, 700) for headlines and product nouns. +- `JetBrains Mono` (weights 400, 500, 700) for body text, UI labels, status strip, popup metadata, and coordinate readouts. + +Loaded once via Google Fonts CDN with `display=swap` and a system-monospace fallback chain. The mono-first body type is the single biggest visual differentiator from the previous Tailwind-default look. + +### Component anatomy + +- Header: brand mark (pulsing dot) + live "status strip" telemetry (`Online · Regions X/Y · Providers 02 · Services 05`) + unified `.ctl` control primitive (search, selects, multi-select, icon buttons share one base class with `.ctl-*` modifiers). +- Map: edge-to-edge with a tokenized `.panel-legend` and tokenized Leaflet zoom/attribution chrome. No rounded corners, no green glow. +- Popup: one row per service (no edition-row duplication); each row carries a distinct monochrome service icon in `--accent` — the icon's presence signals "available", its shape signals which service; edition pills aligned right; vault edition pills hover-disclose commercial limits per [ADR-005](ADR-005-custom-css-tooltip-pattern.md); official Veeam service names per [ADR-003](ADR-003-official-veeam-service-naming.md). +- Markers: `L.divIcon` glyphs in provider brand colour (Azure triangle / AWS cube) replacing the prior `L.circleMarker` SVG paths. Keyboard-navigable; the parent marker carries an accessible `alt`. +- Info panel: same tokens, semantic class names (`.info-section`, `.info-link`, `.info-stat`) — no `bg-slate-*` / `bg-amber-500/10` mix. + +## Options Considered + +### Option A: Polish the existing design + +- Pros: Lowest risk; existing tests stay green; small diffs. +- Cons: Doesn't address the "indistinguishable from every SaaS dashboard" feedback; the AWS amber/warning collision remains; mobile header still consumes ~15% of vertical space. + +### Option B: Cartographic Atlas direction + +- Pros: Genuinely distinctive (serif headlines, parchment palette, atlas vibe); zero competitors in this product space. +- Cons: Stronger departure from Veeam brand voice; the light-mode-first treatment doesn't translate as cleanly to a dark dashboard mode; risk of feeling like a marketing site rather than an operational tool. + +### Option C: Mission Control HUD + Workstation light (chosen) + +- Pros: Reads as a polished operational tool; live status strip + mono numerals lean into the data-density users care about; the dark/light pair shares one design system rather than being two separate aesthetics; CSS tokens scale to future theme work (e.g. a hypothetical high-contrast mode) without requiring `.theme-x` selectors on every component. +- Cons: Bigger up-front diff; Google Fonts adds a render-blocking stylesheet (mitigated by `display=swap` and a system fallback chain); some popup rows can wrap due to the longer official service names. + +## Consequences + +- New UI components MUST read colours/spacing through the existing tokens. Adding a new hardcoded hex value is a smell — either a new token is needed (justify in the PR), or the component should pick the closest existing token. +- Theme variants are added by extending `:root` / `html.light` (and potentially a future `html.high-contrast`, `html.print`, etc.). Components should NOT branch on `html.light` directly except where light mode genuinely needs a different *structure* (e.g. drop-shadow vs glow), and even then prefer adjusting tokens. +- Typography is a brand-level choice. If a future redesign wants different fonts, this ADR is the document to supersede; doing it piecemeal will fragment the system. +- The accent green is reserved for "live / active / available / focus". Don't use it as decoration. The provider blue/orange tokens are reserved for provider identity. Don't repurpose them. +- Each service registered in `serviceDisplayNames` MUST also register a corresponding inline SVG in `serviceIcons` (in `layouts/index.html`). Icons are simple monochrome geometric shapes that render crisply at 14px, using `stroke="currentColor"` so they inherit `--accent`. The shape carries service identity; the colour carries the "available" semantic. A service without an icon falls back to a generic checkmark — acceptable as a defensive default, not as a long-lived state. +- Metadata text tokens (`--text-dim`, `--text-mute`) carry a hard floor: both MUST clear WCAG AA against the popup background (`--bg-elev`) at the 9–10px sizes used in the popup header and coordinate readouts. This UI is regularly shown on projectors and screen-shared over compressed video; a token value that looks crisp on the design machine but fades on lossy displays violates the brief. Tune the values, not the font sizes, if a future contrast regression appears. +- Marker primitive: `L.divIcon` is the contract. Reverting to `L.circleMarker` would re-introduce the accessibility regression (no `keyboard: true`, no `alt`) and break the test selectors that target `.leaflet-marker-icon .map-marker-dot`. +- Cluster icons (`.cluster-small`, `.cluster-medium`, `.cluster-large`) were deliberately left on the old colour system in this redesign — a known follow-up, resolved by [ADR-006](ADR-006-cluster-marker-visual-treatment.md). The resolution re-tokenises clusters to `var(--accent)` only; per-provider colouring at cluster level (`--azure` / `--aws`) was considered and rejected because aggregated provider colouring destroyed the cluster/marker visual hierarchy — see ADR-006 §Options Considered. +- Hugo's HTML minifier strips quotes around single-token attribute values. Smoke tests using `curl | grep` should account for this (`class=hud`, not `class="hud"`). + +## Links + +- [PR #103](https://github.com/comnam90/veeam-data-cloud-services-map/pull/103) — the redesign that introduced this aesthetic. +- Implementation plan: `plans/mission-control-redesign/plan.md` +- [ADR-003](ADR-003-official-veeam-service-naming.md) — service-naming convention used inside the popup component defined by this ADR. +- UX review and mockups (session output): the brainstorming explored Mission Control and Cartographic Atlas directions at desktop + mobile, both themes, before this aesthetic was chosen. diff --git a/docs/architecture/ADR-005-custom-css-tooltip-pattern.md b/docs/architecture/ADR-005-custom-css-tooltip-pattern.md new file mode 100644 index 0000000..3b26fe7 --- /dev/null +++ b/docs/architecture/ADR-005-custom-css-tooltip-pattern.md @@ -0,0 +1,81 @@ +# ADR-005: Custom CSS Tooltip Pattern for In-App Hover Affordances + +**Status**: Accepted + +## Context + +The Mission Control popup ([ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md)) introduced vault tier pills (`Foundation · Core`, `Advanced · Core`, etc.) that buyers visually scan to confirm regional support. A code-review pass on PR #103 surfaced that the pills give no inline disclosure of the commercial limits each edition carries (e.g. Foundation's 20% fair-usage restore cap, Advanced's unlimited restores). Buyers had to leave the page for that context. + +The obvious default — the native HTML `title=` attribute — was explicitly rejected for two reasons: a ~700–1000ms hover delay before the bubble appears, and OS-native styling that clashes with the curated dark/light dashboard aesthetic ADR-004 establishes. The audience and viewing context make both flaws acute: this UI is buyer-facing and is regularly screen-shared over compressed video calls or shown on projectors, where delayed reveals are confusing and OS chrome is jarring. + +The vault tier disclosure is the first in-app tooltip in this UI. Without a documented pattern, future hover affordances (filter chips, provider chips, region-status indicators, future capability badges) will fragment between contributors' instincts. The decision and its trade-offs need to be pinned down once. + +## Decision Drivers + +- Visual consistency with the Mission Control aesthetic — tooltips MUST read as part of the same product, not an OS overlay. +- Zero perceptible latency on hover. The information should appear instantly. +- Accessibility parity with native `title=`. Native ships free keyboard + screen-reader support; a custom replacement MUST replicate both, not silently regress. +- No new runtime dependency. The project ships a single-file SPA; adding a tooltip library (Tippy.js, Floating UI, etc.) for one use case is over-investment. +- The pattern must work inside a Leaflet popup, which adds overflow/z-index constraints absent from a generic page. + +## Decision + +Use a CSS-only tooltip driven by a `data-tooltip` attribute and a `::after` pseudo-element. Every interactive element that needs an in-app tooltip MUST carry all four pieces: + +1. **`data-tooltip=""`** — the visible bubble reads this via `content: attr(data-tooltip)`. +2. **`aria-label=". "`** — replicates the screen-reader announcement that `title=` would have provided. +3. **`tabindex="0"`** — exposes the element to keyboard focus so the same bubble triggers on `:focus-visible`. +4. **CSS triggers on both `:hover` and `:focus-visible`** — mouse AND keyboard users see the disclosure. + +The bubble styles MUST use existing design tokens from [ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md) (`--bg-elev-2` background, `--border-strong` border, `--text` ink, `--bg-elev-2` shadow tint) so the tooltip themes with the rest of the dashboard. + +Reduced-motion users get an instant show/hide: + +```css +@media (prefers-reduced-motion: reduce) { + .popup-svc .pill[data-tooltip]::after { transition: none; } +} +``` + +### Leaflet overflow + stacking + +The `.leaflet-popup-content-wrapper` ships with `overflow: hidden` to clip rounded corners against the inner provider-strip background. A tooltip rendered as a child pseudo-element will be clipped by that wrapper. The fix is a scoped override: + +```css +.leaflet-popup-content-wrapper:has(.popup-card) { overflow: visible; } +``` + +`:has()` keeps the override targeted to our redesigned popup so any other Leaflet popup the project might later add keeps its default clipping. The bubble's `z-index` is set to `1000` to sit cleanly above Leaflet's popup pane (default ≤700). + +## Options Considered + +### Option A: Native HTML `title=` attribute + +- Pros: Zero new CSS/JS; ships full a11y (screen-reader + keyboard) by default. +- Cons: ~700–1000ms hover delay; OS-default styling cannot be themed; clashes with the Mission Control aesthetic; the latency is confusing in the screen-share contexts this UI is used in. + +### Option B: JS-driven tooltip library (Tippy.js, Floating UI, etc.) + +- Pros: Polished out-of-the-box; handles edge positioning, collision avoidance, and multi-line copy. +- Cons: New runtime dependency in a single-file SPA; adds bundle size and build steps for one initial use case; the tooltip needs are simple enough that CSS-only solves them without a library. + +### Option C: CSS-only `data-tooltip` + `::after` (chosen) + +- Pros: Zero dependency; instant on hover; themed via existing CSS tokens; pseudo-element keeps the markup clean; pattern generalises naturally to other hover affordances. +- Cons: Single-line copy is best (multi-line works but needs `white-space: normal` + explicit `max-width`); collision avoidance is manual (positioning above the element is fine for in-popup pills but may need re-evaluation if the trigger is near the top of the viewport); the overflow + z-index gotchas require attention any time the pattern is adopted inside a clipped parent. + +## Consequences + +- New hover affordances in `layouts/index.html` MUST use this pattern. Don't reach for `title=` even for "simple" in-app cases — the latency and styling drift compound across components. + - Exception: `title=` on `` tags pointing to external resources is fine. Those aren't competing with the dashboard aesthetic. +- All four pieces (`data-tooltip`, `aria-label`, `tabindex=0`, hover + focus-visible CSS) are required together. Skipping any one breaks either a11y or keyboard parity. +- When extending the pattern to a new context, audit the parent chain for `overflow: hidden` or `overflow: clip`. Add a scoped `:has()` override (preferred) or restructure the parent. +- Tooltip CSS currently lives scoped under `.popup-svc .pill[data-tooltip]`. The second use of the pattern in another component should trigger a refactor: promote the rules to a generic `[data-tooltip]` selector and keep only component-specific positioning overrides locally. This ADR is the place to record that promotion when it happens. +- Tooltip strings that carry product, commercial, or compliance claims (e.g. fair-usage restore limits) MUST be treated as commercial copy — coordinate edits with product. A code comment marking such strings as commercial disclosures is appropriate where the WHY isn't obvious from the variable name. +- Tests asserting tooltip behaviour SHOULD assert on the attributes (`data-tooltip`, `aria-label`, `tabindex`) rather than the rendered `::after` content, since `::after` is not in the DOM and is awkward to query reliably in Playwright. The `aria-label` is the most stable signal for screen-reader equivalence. + +## Links + +- [PR #103](https://github.com/comnam90/veeam-data-cloud-services-map/pull/103) — the redesign and the popup that introduced this pattern. +- [ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md) — design tokens consumed by the tooltip bubble styling. +- Initial use: `vaultEditionDescriptions` in `layouts/index.html`, rendered by the vault tier pill generator. diff --git a/docs/architecture/ADR-006-cluster-marker-visual-treatment.md b/docs/architecture/ADR-006-cluster-marker-visual-treatment.md new file mode 100644 index 0000000..0f17ccb --- /dev/null +++ b/docs/architecture/ADR-006-cluster-marker-visual-treatment.md @@ -0,0 +1,85 @@ +# ADR-006: Cluster Marker Visual Treatment + +**Status**: Accepted + +## Context + +PR #103 ([ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md)) redesigned the individual region markers from `L.circleMarker` SVG paths to `L.divIcon` glyphs, and a follow-up upgraded them again to "Hybrid POI dots" — 28px white circles with a subtle border, soft drop shadow, and a brand SVG inside. The cluster bubbles, however, were still on the pre-redesign look: green/teal gradient backgrounds with a frosted ring, plus a 4px-tall blue/orange `.cluster-providers` strip beneath each cluster that visualised the AWS/Azure ratio inside. ADR-004 §Consequences explicitly flagged this as a known follow-up. + +That left two problems: clusters and POI dots read as two different design languages on the same map; and the provider mix bars added decorative noise at world/continent zoom where buyers don't make per-provider decisions yet — they zoom in for that. + +A first iteration aligned clusters with the POI dot surface entirely — same white background, subtle border, soft shadow. It shipped (commit `5450ddf`) and was rejected on first eyeball on two grounds: (a) on the Workstation (light) theme the white cluster washed out against the lightened tile palette; (b) cluster and individual marker became visually indistinguishable, destroying the at-a-glance signal that "this is a group, not one region." That iteration produced the principle this ADR records: matching the POI surface is wrong; the cluster must differentiate, in both themes, by design. + +This ADR pins the visual-hierarchy principle and the resulting treatment so future contributors don't collapse the two primitives back together. + +## Decision Drivers + +- A cluster must read as "many" and a POI dot must read as "one" at a glance. They share map space; they cannot share surface. +- Both themes are first-class (per ADR-004). A cluster colour that works on dark tiles but vanishes on light tiles is a bug, not a styling preference. +- The token system (ADR-004) already binds `--accent` to "live / available / current selection". Clusters carry the same semantic — they are the aggregated population of available regions. Re-using `--accent` is consistent, not decorative. +- Provider mix at world/continent zoom is decorative; the per-region provider identity is already encoded in the POI dot's brand SVG and reached by zooming in. +- Cluster count is a real buyer signal — a 30-region cluster should physically read heavier than a 4-region one. Size tiers carry that signal in peripheral vision; the count number alone doesn't. + +## Decision + +### Inverted brand badge + +`.cluster-marker` paints `var(--accent)` background, white count text, and a 2px white ring. The token auto-themes (`#00ff88` dark, `#00805a` light per ADR-004); white text + white ring read against both. Drop shadow is `0 2px 6px rgba(0, 0, 0, 0.35)` at rest, deepening to `0 4px 12px rgba(0, 0, 0, 0.45)` on hover — both stronger than the POI dot's `0 2px 6px rgba(0, 0, 0, 0.25)` so the cluster sits visually above an individual marker on the same tile. + +Token contract follows ADR-004: the colour is read through the token, not declared as a hex. White (`#ffffff`) is the only literal — it is semantic (the "separator against the map" affordance, plus max-contrast count text), not branded. + +A defensive `html.light .cluster-marker` override declares the same accent + white text + white ring explicitly. The override is not strictly required since `var(--accent)` already auto-themes; it exists as a cascade-anchor so a future light-mode adjustment cannot accidentally desaturate the cluster. + +### Size tiers preserved + +Three tiers stay — `.cluster-small` (36×36, 12px), `.cluster-medium` (44×44, 14px), `.cluster-large` (52×52, 16px) — keyed off `CLUSTER_SIZE_THRESHOLDS` (MEDIUM=5, LARGE=20). Three discrete sizes snap to recognisable "small / medium / large" categories in peripheral vision; a continuous size interpolation would be harder to compare across the map at a glance. + +The Leaflet `iconSize` / `iconAnchor` stays at `L.point(52, 52)` / `L.point(26, 26)` uniformly across all tiers. The over-sized click target on small clusters is a deliberate touch-friendliness affordance and keeps the disbanding animation smooth at the breakpoint. + +### Provider mix bars removed + +The `.cluster-providers` / `.azure-bar` / `.aws-bar` CSS rules, the `PROVIDER_BAR` constant, and the per-cluster `markers.forEach(provider counting)` block are deleted together. The cluster icon is now a single accent badge with a count and a tier class — nothing else. + +This is recorded as a deliberate non-feature: the AWS/Azure mix is intentionally NOT surfaced at cluster level. The per-region provider identity is reached by zooming past `CLUSTER_CONFIG.DISABLE_AT_ZOOM` (currently 6), where the POI dots' brand SVGs carry the signal. + +## Options Considered + +### Option A: Keep clusters on the pre-redesign green-gradient look + +- Pros: Zero change; familiar to anyone who'd already seen the prior UI. +- Cons: Two design languages on the same map; provider strips still add noise at world zoom; leaves ADR-004:77's flagged follow-up unresolved. + +### Option B: Match clusters to the POI-dot surface (white + subtle border + soft shadow) + +- Pros: Single visual language; cleanest at first glance on dark tiles. +- Cons: Washes out on the Workstation light theme; collapses the cluster/marker visual hierarchy — a user can no longer tell at a glance whether they're looking at one region or many. Shipped briefly as commit `5450ddf` and rejected on empirical review. + +### Option C: Per-provider cluster colouring using `--azure` + `--aws` + +- Pros: Carries provider identity all the way to cluster level; would have aligned with ADR-004:77's original wording ("should reuse `--accent`, `--azure`, `--aws`"). +- Cons: A two-tone cluster (split / striped / sectored) re-introduces exactly the noise the provider-mix bars added; a single-provider-cluster pure-azure or pure-aws colouring would mislead when the cluster is mixed; clusters are aggregations and the cleanest semantic is "available regions, count of N", not "this many of brand X." + +### Option D: Inverted brand badge — `var(--accent)` background, white text + white ring (chosen) + +- Pros: Re-uses ADR-004's accent semantic ("live / available / aggregated"); the auto-themed token works on both surfaces without per-theme hex variants; colour-inverted vs the white POI dot differentiates the two primitives while staying inside the same palette; removing provider bars simplifies the cluster pipeline (three CSS rules, one constant, one loop deleted). +- Cons: Heavier eye-weight at world zoom with many small clusters — mitigated by the smaller `.cluster-small` size (36px) and the still-soft shadow. If this becomes a problem the response is to tune token values, not to reintroduce per-tier hex variants. + +## Consequences + +- Cluster surfaces MUST be painted from `var(--accent)`. A new contributor reaching for a fresh hex value or for `--azure` / `--aws` at cluster level is a smell — clusters own the accent semantic; per-provider colouring at the aggregated level was considered and rejected (Option C). +- Provider mix at cluster level is a deliberate non-feature. Don't re-add `.cluster-providers` strips, sector arcs, halftone fills, or other "AWS:Azure ratio" treatments to the cluster icon. The provider signal lives on individual POI dots (ADR-004 marker primitive) and is reached by zooming in past `CLUSTER_CONFIG.DISABLE_AT_ZOOM`. +- Size tiers MUST stay discrete, not interpolated. Three sizes keyed off `CLUSTER_SIZE_THRESHOLDS` is the contract. +- The `html.light .cluster-marker` override is a cascade-anchor and should not be removed even though it appears redundant against the auto-themed `var(--accent)`. Removing it makes a future light-mode experiment one bad rule away from desaturating the cluster. +- White (`#ffffff`) on the ring and the count text is the only literal allowed inside `.cluster-marker`. It is semantic (separation from the map; max-contrast count), not a brand colour. Other components needing a "ring against the map" affordance should reach for the same literal rather than introducing a `--ring` token unless multiple components share the need. +- Cluster click-target stays at `L.point(52, 52)` / `L.point(26, 26)` uniformly across tiers. The generous touch target on small clusters is a deliberate touch-friendliness affordance, not an oversight. +- `MarkerCluster.Default.css` (loaded at `index.html:21`) ships green pastel backgrounds for `.marker-cluster` and its inner `div`. The three `background: transparent !important` neutralisers above the cluster styles MUST stay; removing them re-introduces the plugin default behind the accent badge. +- Existing Playwright selectors target `.leaflet-marker-icon .map-marker-dot` only; cluster styling changes don't touch them and shouldn't. Cluster-specific visual assertions are intentionally absent — they'd be more fragile than valuable for a CSS-only contract. If a cluster regression needs a regression test in the future, prefer asserting the DOM contract (`.cluster-marker` exists; `.cluster-small|medium|large` is applied per `CLUSTER_SIZE_THRESHOLDS`) over the rendered pixels. + +## Links + +- [ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md) §Consequences — cluster-icons follow-up flagged at the line referenced from ADR-004 itself; this ADR is the resolution. +- [ADR-005](ADR-005-custom-css-tooltip-pattern.md) — companion ADR for another component-level extension of ADR-004's token system. +- Commits on `feature/mission-control-redesign`: + - `5450ddf` — Option B (white badges) shipped and rejected; the iteration this ADR explicitly supersedes on visual-hierarchy grounds. + - `7ed291c` — provider-mix strips removed from HTML, JS, and CSS together. + - `4fd6dcc` — inverted brand badge (the treatment this ADR records). diff --git a/docs/architecture/ADR-007-initial-map-view-and-tile-wrap.md b/docs/architecture/ADR-007-initial-map-view-and-tile-wrap.md new file mode 100644 index 0000000..b775f21 --- /dev/null +++ b/docs/architecture/ADR-007-initial-map-view-and-tile-wrap.md @@ -0,0 +1,104 @@ +# ADR-007: Responsive Initial Map View via `fitBounds` with Tile Wrap + +**Status**: Accepted + +## Context + +The Leaflet map historically opened Australia-centric with a hard-coded `center: [-25, 140]`, `zoom: 3`, `minZoom: 3.0` — a holdover from the project's origin in Auckland that kept hiding because it framed the dev team's home region. During the mission control redesign ([ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md)) commit `5bafc11` re-centred the initial view to `center: [25, 10]`, `zoom: 2`, `minZoom: 2.0` for a globally-balanced first impression. On a 2K (≈2560 px wide) desktop that re-centred view surfaced two problems together: the world wrapped horizontally 2–3 times (the world at zoom 2 is only 1024 px wide, leaving the wider viewport to be re-filled by adjacent copies), and a large blank vertical band sat above the populated latitudes (markers cluster between roughly −40° and +55° lat but the static view was centred at lat 25 with no padding negotiation). Both static framings — Australia-centric and globally-centred — encoded a single assumed viewport; the wide-display review made clear that *any* hard-coded `center`/`zoom` pair would lose on some screen size. + +The audience is buyer / pre-sales (per [ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md) drivers): the map is presented on whatever screen the demo happens to land on — laptops, 2K external monitors, 4K conference TVs, and incidentally the mobile portraits the Playwright suite (Pixel 7, iPhone 15 Pro) covers as part of the responsive contract. A single solution had to work across all of them without an arbitrary breakpoint forest. + +A first iteration (commits `63415cd`, `0234bde`) replaced the static view with `fitBounds` and added `noWrap: true` to the tile layers to stop the multi-world repeat. It shipped and was rejected on second eyeball: the populated band now framed correctly, but the world clipped to grey edges on wide displays (at zoom 3 the world is 2048 px and a 2K viewport leaves ~256 px of grey on each side). The user feedback — "I'm ok with a little edge wrap to fill this" — produced the principle this ADR records: tile wrap is a free pixel filler at the right zoom and `fitBounds` is the mechanism that keeps the wrap small. + +## Decision Drivers + +- Initial view must adapt across laptop, 2K, 4K, and mobile portrait without per-breakpoint hard-coded zoom values; new screen sizes appear and the design must not need a new conditional each time. +- The thing we want to fit on screen is the *region data*, not the world. Deriving the view from `regions[].coords` is the only honest way to keep this true as regions get added. +- The cluster signal ([ADR-006](ADR-006-cluster-marker-visual-treatment.md)) — at-a-glance "many vs one" via cluster vs POI dot — is part of the map's primary affordance. The initial zoom must not collapse it. On mobile portrait specifically, isolated Azure regions (Australia, NZ, South Africa, Korea) must still read as individual markers; this is empirically asserted by `tests/ui.spec.ts:113` and `:128`. +- The off-canvas space on wide viewports has two valid renderings: grey (tile clip) or adjacent world copy (tile wrap). At a `fitBounds`-chosen zoom the wrap is small enough to feel like "edge fill", not "the map is broken." Grey edges read as broken. +- Initial zoom and interactive zoom are different contracts. The user can drive the map to any zoom in the `[minZoom, maxZoom]` range; the *initial* zoom should never overshoot continent scale on first load. + +## Decision + +### `fitBounds` on `regions[].coords` + +Replaces the static `center`/`zoom` pair. Built once after tile-layer attachment: + +```js +const regionLatLngs = regions + .filter(r => Array.isArray(r.coords) && r.coords.length === 2) + .map(r => r.coords); +map.fitBounds(L.latLngBounds(regionLatLngs), { + padding: [48, 48], + maxZoom: 3, + animate: false +}); +``` + +- `padding: [48, 48]` keeps edge markers off the header and bottom legend chrome without a CSS gap. +- `maxZoom: 3` caps the *initial* zoom so 4K and 5K displays don't overshoot to a continent-fragment view on first load. This ceiling is deliberately lower than the map's `maxZoom: 6` — interactive zoom past 3 is a user choice, not an automatic one. +- `animate: false` — the very first view is not a motion; no users have interacted yet. +- No resize listener. Once `fitBounds` has run on load, the view stays where the user puts it via interaction. + +### `minZoom: 2`, not 1 + +`fitBounds` clamps to `map.minZoom`. On Pixel 7 (412×915) and iPhone 15 Pro (393×852) portraits the natural fit is zoom 1, and at zoom 1 every Azure marker collapses into clusters — including the geographically isolated ones (Australia, NZ, South Africa) that read as "single region per continent" cues on the wider screens. Lowering `minZoom` to 1 also broke `tests/ui.spec.ts:113` (`should filter regions by Azure provider`) on both Mobile Chrome and Mobile Safari, because the assertion `.leaflet-marker-icon .map-marker-dot` count > 0 requires un-clustered individual markers. The test is downstream of the UX regression, not the cause of it. + +Keeping `minZoom: 2` means mobile portrait clamps to the previous default (so the touch UX on phones did not change). Only wide desktops benefit from the new `fitBounds`-chosen zoom of 3. + +### Tile wrap is on (no `noWrap`), `worldCopyJump: true` + +Both tile layers omit the `noWrap` option, so default Leaflet wrap behaviour applies. `worldCopyJump: true` is restored at map level as the partner of wrapped tiles — without it, marker positions can drift relative to the visible world copy when the user pans across the antimeridian. + +At zoom 3 the rendered world is 2048 px wide. On a 2K viewport this leaves ~256 px of edge on each side, which is filled by the adjacent world copy — "a little edge wrap." On a 4K viewport the wrap is closer to half a world per side; still acceptable because the populated continents stay anchored to the canonical copy and the duplicated regions are unpopulated ocean. + +`maxBounds: [[-60, -185], [90, 185]]` is unchanged from the prior code. It governs *interactive* pan limits, not the initial framing, and the ±185° lng range deliberately permits a small pan into the wrap zone. + +## Options Considered + +### Option A: Hard-coded per-breakpoint zoom + +`if (window.innerWidth >= 2200) zoom = 3 else zoom = 2`, optionally with more steps. + +- Pros: Simplest possible code; deterministic. +- Cons: Arbitrary thresholds (every breakpoint is someone's guess); doesn't adapt to ultrawide / 4K / 5K without more conditionals; doesn't adapt as the region distribution changes over time; the breakpoint is not derived from the thing it claims to optimise for. + +### Option B: `fitBounds` + `noWrap: true` + +The first iteration. Shipped briefly in `63415cd` and `0234bde`. + +- Pros: Single world copy, deterministic rendering, no risk of wrap-related marker drift. +- Cons: Clips the world to grey edges on wide displays — at zoom 3 on 2K the rendered world is 2048 px in a 2560 px viewport, so ~256 px of grey on each side; rejected on visual review. Same pattern as ADR-006 Option B: an option that looks cleaner on paper and worse on the screen. + +### Option C: `fitBounds` + lower `minZoom: 1` + +Let mobile portrait pick the natural fit instead of clamping. + +- Pros: All regions visible on initial mobile load without panning. +- Cons: Empirical UX regression — at zoom 1 the cluster signal collapses for isolated regions, and `tests/ui.spec.ts:113`/`:128` fail on both mobile projects (the test is the early warning of a real visual problem, not the problem itself). + +### Option D: `fitBounds` + tile wrap (chosen) + +`noWrap` omitted, `worldCopyJump: true` restored, `maxZoom: 3` ceiling on the fit, `minZoom: 2` clamp. + +- Pros: Responsive across viewport sizes from mobile portrait to 4K with no breakpoint conditional; preserves the cluster signal on mobile; fills wide-display edges with the cheapest available rendering (wrap) instead of grey; all existing Playwright tests pass. +- Cons: `worldCopyJump` and tile wrap are a coupled pair — turning off one requires turning off the other; on very wide displays (4K+) the wrap can occupy close to half the off-canvas width, which a future contributor may misread as the original "world repeats three times" bug. This ADR exists primarily to prevent that misreading. + +## Consequences + +- Initial view MUST be derived from `regions[].coords` via `fitBounds`. A new contributor adding `center: [...]` or `zoom: N` to the `L.map()` options block is a smell; that responsibility moved to Leaflet, computed from the data. +- `noWrap: true` on the tile layer is a defect, not an improvement. The grey-edge clip on wide displays is the failure mode it produces. If wrap-related side effects appear (marker drift, unbounded pan), fix them in `worldCopyJump` and `maxBounds` rather than disabling wrap. +- `worldCopyJump: true` MUST stay enabled while tile wrap is enabled. The pair is a contract: one without the other is a UX regression (either grey edges or marker drift). Removing `worldCopyJump` is acceptable only as part of a deliberate move back to `noWrap: true`, which this ADR rejects. +- `minZoom: 2` is a load-bearing constraint, not an arbitrary floor. Lowering it to 1 collapses isolated regions into clusters on mobile portrait and breaks the cluster affordance ADR-006 records. +- `maxZoom: 3` inside the `fitBounds` call is the *initial-view ceiling*, intentionally lower than the map's `maxZoom: 6`. Bumping it to match the map's max makes 4K displays open at continent-fragment scale on first load. +- No resize listener for the initial view. A debounced resize re-fit would silently undo the user's zoom and pan when the browser is resized — hostile UX. If a viewport-aware re-fit becomes a need, gate it behind a user action (e.g., a "fit to data" control), not on resize. +- The existing Playwright contract — `tests/ui.spec.ts:113`/`:128` (provider filter → un-clustered marker count > 0) and `:479` (un-cluster + click marker) — is part of the contract this ADR records. A change here that breaks either is most likely a regression in this decision, not the test. + +## Links + +- [ADR-004](ADR-004-mission-control-aesthetic-and-design-tokens.md) — mission control aesthetic; the redesign that surfaced the wide-display review and the audience driver this ADR inherits. +- [ADR-006](ADR-006-cluster-marker-visual-treatment.md) — cluster marker treatment; this ADR depends on the cluster/POI visual hierarchy holding at mobile portrait zoom. +- Commits on `feature/mission-control-redesign`: + - `63415cd` — first attempt: `fitBounds` + `noWrap: true`; clipped edges on wide displays. + - `0234bde` — `minZoom` restored to 2 after the mobile cluster-signal regression broke Mobile Chrome / Mobile Safari filter tests. + - `c6a3180` — tile wrap re-enabled, `worldCopyJump` restored (the treatment this ADR records). diff --git a/layouts/index.html b/layouts/index.html index d115620..1deb834 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -4,7 +4,7 @@ - + {{ .Site.Title }} @@ -12,7 +12,10 @@ - + + + + @@ -22,15 +25,48 @@ - +
-
-
-
-
- -
-
-

- Veeam Data Cloud - Service Map -

- -
+
+
+ +
+ VDC // Capability Map +

Veeam Data Cloud Service Map

+
-
- -
-
- - -
- -
- - - -
- - -
+
+
Regions 0/0
+ + +
- + + +
-
+
-
+
- Loading map… + Loading…
- -
+ +

-

Failed to Load Map

-

The map failed to load. Please check your internet connection and try refreshing the page.

+

Failed to Load Map

+

The map failed to load. Please check your internet connection and try refreshing the page.

-
-
- -
-

Community Project

-

This is an unofficial, community-maintained project. Not affiliated with or endorsed by Veeam Software. Data may be incomplete or outdated.

-
+
+ +
+

Community Project

+

This is an unofficial, community-maintained project. Not affiliated with or endorsed by Veeam Software. Data may be incomplete or outdated.

-
-

What is this?

-

- An interactive map visualizing Veeam Data Cloud (VDC) service availability across AWS and Azure regions. Quickly find where services like VDC Vault, M365 Protection, and more are available. -

+
+ +

An interactive map visualizing Veeam Data Cloud (VDC) service availability across AWS and Azure regions. Quickly find where services like Veeam Data Cloud Vault, Microsoft 365 Protection, and more are available.

-
-
-
0
-
Total Regions
+
+
+
0
+
Total Regions
-
-
5
-
Services Tracked
+
+
5
+
Services Tracked
-
-

Maintained By

- -
C
+
-
-

Quick Links

-
- - - View on GitHub - + -
-

Found an Issue?

-
- - - Report Missing Service + -
+
@@ -908,29 +1076,26 @@
+
Providers
+
${markerLogos.azure('legend')}Azure
+
${markerLogos.aws('legend')}AWS
`; return div; }; @@ -1003,7 +1170,7 @@

Array.isArray(r.coords) && r.coords.length === 2) + .map(r => r.coords); + map.fitBounds(L.latLngBounds(regionLatLngs), { + padding: [48, 48], + maxZoom: 3, + animate: false + }); + const systemDarkQuery = window.matchMedia('(prefers-color-scheme: dark)'); const themeModes = ['system', 'dark', 'light']; let currentMode = localStorage.getItem('theme') || 'system'; @@ -1033,7 +1209,7 @@

- ${count} -
- ${azureCount > 0 ? `
` : ''} - ${awsCount > 0 ? `
` : ''} -
-

`, + html: `
${count}
`, className: 'custom-cluster-marker', iconSize: L.point(52, 52), iconAnchor: L.point(26, 26) @@ -1234,6 +1386,8 @@