Construct 3

Changelog

Unreleased
pending

Staged for the next release

Shipped changes awaiting a version bump. Each entry is written by the author of the change; the bump promotes them into a dated release below.

  • FixFixed the map repainting the whole twin on every scrub tick — the old per-date paint path was still wired alongside the new one, so dragging the scrubber reloaded every date-driven tile source many times a second and the workfront re-rendered each time you let go.
v0.0.21
2026-07-31

The Forecast You Can Grab & a Map That Keeps Up

The Forecast report is now something you handle rather than configure: its projected completion is a draggable Complete marker that back-solves the production rate for the day you drop it on. And the map now follows the time scrubber in the frame the date changes, where dragging the timeline used to leave the workfront up to nine seconds behind.

  • NewThe Forecast report's controls are rebuilt around two headline pulldowns — Scope, and an editable Production rate whose presets average the last 1 … 12 buckets or all time — each captioned with what it is reading from: "Source: Aerial" and "Rate: Last Week", or "Rate: Custom" once a number is typed or the Complete handle dragged. That replaces the strip under the chart, which held a slider, a rate readout in two units at once, a "Set custom" link and a Production-rate/Target-date mode toggle: four controls for one number, at the far end of the card from the line they shape. The rate's default window is the narrowest one that actually contains work rather than a fixed week, so a scope quiet for a bucket states a real rate instead of a flat 0% — and the caption names the window it used, worded exactly as the menu row it points at. Changing scope returns the rate to that default, since a number measured on Piers describes nothing about Modules. The value reads to one decimal, is grouped past the hundreds in EA, and no longer commits itself when you focus and leave the field — which used to freeze a live average into a fixed custom rate. The Cumulative/Per-period switch is a mountain and a columns icon. The %/units toggle sits beside it and now always opens in %, where it used to promote itself to EA on any single-element scope — % is the one reading every scope shares, so an axis that changed unit when the scope changed made two scopes incomparable without the reader noticing; choosing EA still carries the rate field and the tooltip with it. And the tooltip names the projection's row "Production Rate" after the control that shapes it, and states that rate rather than the curve's height.
  • NewThe Forecast report's projected completion is a draggable **Complete** marker on the chart — a square orange badge with a grip at its left edge, so it reads as something you pick up rather than as another chart label. Drag it to a day and the report back-solves the production rate that lands there; it cannot be dragged before today, and it latches to today whenever the projection would complete at or before now — including at a rate of zero, so a scope that has paused can still be dragged back into a plan. Dragging it shows the rate it implies, and the projection turns its corner exactly on the marker.
  • NewA scope that is built out now says so: the chart draws a grey **Completed** marker on the day the work actually finished, read from the build data rather than the projection, and the word Completed replaces the rate control. Today stays on the chart, which it previously did not once a scope completed.
  • NewEquipment is on by default now, and it tells you when it has nothing to say. Tracked assets ship visible like any other default layer, so a site's machines are on the map the moment it opens. Scrub back to a day no tag reported and the Equipment group leaves the Layers panel entirely, rather than offering rows that switch on and draw nothing — a day where some tags reported still lists all of them, quiet ones included. On today, where an empty day means "nothing yet" rather than "nothing ever", the layer stays put so you can wait for the first chirp. And while the map is following real time, every asset actually reporting carries a LIVE marker on its row, lit by the same signal as the scrubber's Go live button.
  • NewThe QC dashboard reaches C2 parity: overdue joins the close-out stack, Priority becomes C2's tinted rows (None included), a By Element grid and an Average Time to Close card land, and every widget reports its own empty state. The period selector is now Weekly / Monthly / Quarterly / Entire Project with chevrons to browse back through time, and the header's Export downloads the whole board as CSV.
  • NewRecords → Documents now shows the project's real documents checklist from ClearSky — live sections, uploaded files, and a revisions drawer with working downloads. Upload, replace, and delete stay disabled until the write path lands.
  • NewMap starting views now come from the platform: each project opens on its site centroid at a zoom fitted to the site's actual footprint, replacing the hand-maintained center list and the local SAVE override.
  • NewExport now sits behind the lock on every report, alongside the time selector and the report menu it already gated. A locked report is the read state — look at the numbers — and taking a copy of them away is an act on the report, so the lock is what says you mean to work with it.
  • NewA performance HUD for the map, in any build including production: add `?perf` to a map URL and it appears; remove it and it's gone. Shows the date-change rate, tile loads and re-parses per source, how many features carry state, and frame times — the costs that are invisible through MapLibre's public API and that decide whether scrubbing and zooming feel right. Nothing runs until you ask for it.
  • FixThe map now follows the time scrubber in the frame the date changes — dragging the timeline used to leave the workfront up to nine seconds behind, because every date change rewrote a paint expression and made MapLibre re-parse every tile on screen. Partially-installed rows read better too: the element's colour with dark diagonal gaps taken out of it, so a row in progress carries less ink than a finished one rather than more.
  • FixFixed two data faults in the Forecast report's plan curve. A scope built out early had its series cut off at the last build date, discarding the rest of the plan — so it reported "Planned 58.5%" and an inflated SPI against a plan that in fact reached 100%. And element_progress's date-only values were parsed as UTC midnight before being bucketed in the project's timezone, which dropped the final planned day for any site west of UTC and reported 98.6% for a plan that reaches 100%.
  • FixForecast chart rendering fixes: the mountains stroke their left wall up from zero rather than starting mid-air at the first value, the fill closes exactly on the completion marker instead of a few pixels shy of it, and the axis baseline is a step darker than the gridlines so the plot has a floor.
  • FixSwitching a line layer off (roads, fences, MV/DC feeders, harnesses) now actually stops it working: its invisible click-target twin stayed on, which kept the layer clickable and kept its map tiles loading and re-parsing on every scrubber move.
  • FixPressing play now drops the equipment time bar: playback steps whole days, so a sub-daily position has nothing to say — and it was sitting there frozen rather than following, since the date changes faster than the equipment fetch settles.
  • FixTightened the API proxies: the QC dashboard route now allowlists its upstream endpoints, and dead query parameters inherited from C2 are gone from tile and progress requests.
  • RefactorThe Forecast report no longer carries the Complete / Planned / SPI tiles. The chart is the whole card: the curve already shows where the project stands against its plan, and a bold stat row restating it read as the end of the card rather than as its summary.
v0.0.20
2026-07-29

Live Drone Feeds & the Map’s Own Clock

Live drone feeds arrive in the product: /[account]/live shows one tile per airborne drone across an account’s projects, auto-rotating in pages of nine. And the map now opens on today rather than the latest drone flight, with every layer reporting the date its own data is from.

  • ChoreRetired the `/roadmap` page — the C3 board lives in Linear, and an in-app mirror could only ever be a worse copy of it. The Roadmap tile on `/operations` now links straight out to the C3 Kanban, and the route, its Linear fetcher, and the `LINEAR_API_KEY` it needed are gone.
  • NewSettings → Devices now reads the real drone registry — the FlytBase Site ID and the project's drone list (name + asset id) come from ClearSky instead of placeholder data. Editing still awaits a write path.
  • NewLayers now show the date their data is actually from when it's older than the date you're viewing, and the site panel's "As of" reads that date rather than the scrubber's. Panoramas fall back to the most recent day that has them, the way drone imagery already did.
  • NewThe map now opens on today instead of the latest drone flight, so the twin, the site panel and equipment all read the current state on arrival rather than the last flight's. Drone imagery still shows the most recent flight at or before whatever date you're on.
  • NewThe Operations live feed at `/operations/videowall` — every drone across all accounts on one superadmin-only wall, filterable by account, with tiles labelled from the drone names in Settings → Devices. A simulated-fleet preview at `/preview` makes the tile design workable between flights.
  • NewLive drone video wall at `/[account]/live` — one tile per airborne drone across an account's projects, streamed from FlytBase via Agora, with auto-rotating pages of nine. Tiles carry the project and the drone's number (stable per aircraft), same-project tiles sit together, and only the tiles on screen decode video.
  • FixOrtho imagery now resolves its COG-proxy host per environment (`cogProxyFor`), retiring the hand-set `NEXT_PUBLIC_COG_PROXY` that went stale in the `.services` → `.tech` move and silently broke local ortho tiles.
  • FixFixed the equipment slider and its project-day on sites whose clocks jump at midnight. Chile springs forward from 23:59 straight to 01:00, so that date has no midnight at all, and the day was resolving backwards into the previous evening — the slider's 00:00 sat on the wrong date. Cuba, Paraguay and Lebanon have the same transition. The equipment code now shares the timezone helper the rest of the app uses instead of its own copy.
  • FixThe timeline now appears on projects with no drone flights, which also restores the equipment slider there. Pressing play no longer skips the first flight, and side-by-side compare no longer opens with both panes showing the same imagery.
v0.0.19
2026-07-26

Equipment Tracking, the Forecast Report & the Site’s Clock

Equipment Tracking is C3’s first live-asset surface: LoRaWAN-tagged machines draw as coloured dots you scrub through the day on the project’s own shift hours. The Forecast report was rebuilt around its chart, and a date in C3 now means a calendar day at the site rather than in UTC or your browser.

  • NewTracker rows now show partially-installed work: each row is solid where its current element (piles, tracker, modules) is fully installed and hatched where only some of it is. Colour still names the element, so existing solid states are unchanged.
  • NewC2 capability parity audit — every capability in the old Construct app scored against C3, with a dashboard at `/operations/parity` that reads the audit markdown at build time. Run `npm run parity:sync` after triaging to regenerate the counts.
  • NewEquipment Tracking arrives in C3: LoRaWAN-tagged machines now show on the map as coloured dots you can scrub through the day, with a per-asset twistdown in the Layers panel, details in the InfoPanel on click, and a read-only sensor roster in Settings → Devices. Turning the layer on lifts the scrubber and reveals a time-of-day slider framed by the project's own shift hours, running on the site's clock rather than yours — and pressing Go live keeps the map tracking real time.
  • NewForecast chart housekeeping: the Cumulative/Per-period and %/units toggles moved down to a footer row beside the legend (they restyle the chart, they don't choose its subject), the "Built this week" tile is gone, the forecast on/off toggle is gone — it's the Forecast report, the projection is always on — and the Aerial/Ground tab strip hides itself on projects that only have aerial data.
  • NewThe Forecast report's projection is now on by default, and the controls that shape it moved out of the "Forecast Installation Plan" modal onto a strip under the chart — what the slope is based on, the resulting rate, and the projected completion date are always visible, and nudging an input moves the line while you watch. Two controls did not make the trip: Overwrite Current Week (a Terabase data-ops affordance, now internal-only) and the custom From/To lookback, which is waiting on its range bar.
  • NewMilestones are back on the Forecast chart. Three things were eating them: the API filtered server-side to binding "guaranteed" milestones only, the chart pinned them to a different series than the one it drew, and the x-axis stopped before the dates that matter. The axis now runs to whichever comes last — projected completion or the final milestone — so you can see what you promised next to when you'll actually finish.
  • NewThe Forecast report's production rate is now a **slider** across lookback windows — 1, 2, 3, 4, 12 periods, or all time — so dragging it re-derives the rate and the projection answers immediately, instead of picking a window from a dropdown. A custom rate sits beside it as "Set custom", seeded from the rate already on screen. This also fixed a quiet lie: the old windows were named durations that stored period counts, so on a Monthly chart "Past Week" actually meant one month.
  • NewForecast is now the first report tab and the one you land on. Its two pulldowns collapsed into a single headline **Scope** picker — the old Scope control had one working option and three disabled ones, so Whole Project moved into the element list — and picking an element with a unit of measure now opens in that unit (EA, LFT) instead of percent. Report titles dropped the redundant word "Report".
  • NewWhether a project offers Ground Data is now read from whether activity tracking has been **configured** for it (the same setting the Settings → Activity Tracking editor shows), rather than guessed by scanning progress data for a non-zero number. A project that's set up but hasn't logged progress yet used to be told it had no ground data at all.
  • NewColour that only needs to tell things apart — drone tags, reference-layer defaults, per-series identity — now comes from a dedicated eight-hue `--id-*` palette rather than being borrowed from the `c3-*` status colours, so an identity can't be misread as a state. Label ink is derived from the fill by `onColor()` (APCA-scored) instead of hand-paired, and the palette has a specimen section in the Component Kit at `/operations/components`.
  • NewA cold portfolio load now paints instantly from the persisted accounts cache — after the active session vouches for its owner — while the live account list refreshes in the background, instead of waiting several seconds on the ClearSky accounts walk.
  • NewThe Forecast chart now separates the two kinds of milestone: binding **guaranteed** dates carry a solid ◆ and their contractual acronym, **floating** ones just a hollow ◇ — the same pair Settings → Milestones prints in its Class column, which doubles as the chart's legend. Neither draws a guide line until you point at it, so a dozen milestones no longer fence off the plot, and the line and name arrive together on the one you asked about.
  • NewSettings → Milestones is real: it was a hardcoded five-row placeholder with a permanently blank Contract Date column, and now reads the project's live schedule with each milestone's class (guaranteed vs floating) and date. Read-only, and the sourceless MV Circuit tab is gone.
  • NewAdded a personal **Profile** page at `/profile` — account details (name, email, home company, role) plus the light/dark appearance toggle, which moved here from the account menu. Reports and Records now default to light, while the portfolio and map stay dark; an explicit preference still applies app-wide.
  • NewRecords has a new Reference Layers tab listing a project's survey and utility overlays with their line and fill styling, laid out like Construct's. Editing a layer opens the properties form, but saving isn't wired up yet.
  • NewThe report Download button is now a labeled **Export** button, and on the Forecast report it actually exports: the plotted series as CSV, one row per period with cumulative plan, actual, and forecast (plus real units when the scope is a single element). Other reports keep the button in the same place while their exporters land.
  • NewHovering a table row now shows a clearly visible blue highlight instead of a tint you could barely see, across every Records table and the Settings and Account section lists.
  • FixThe company picker now tells "we couldn't reach this environment" apart from "this environment is empty." A failed accounts fetch used to render as "No accounts in this environment yet" — reporting a fact we hadn't established. A failed fetch now says so and offers Try again; a genuine environment with zero accounts still reads as before. A list already loaded from cache survives a failed refresh instead of being replaced by the error.
  • FixFixed the Forecast chart's ground/activity-tracking actual curve rendering flat-zero on real projects: `/api/activity-progress` summed the daily actual quantity from `dt_actual_quantity` (a digital-twin-derived field that reads 0 on real data) instead of `eod` ("estimate of day"). This was the same all-zeros trap already fixed in the Activity Tracking path, reintroduced here. The rollup is now a shared, unit-tested helper (`sumActualByDate`) that pins the correct field so it can't silently regress.
  • FixThe Forecast chart's hover tooltip now travels only the middle half of the plot instead of following the pointer anywhere. It stays beside the value you are reading without climbing into the milestone names above the chart or dropping onto the axis below.
  • FixDrone ortho imagery no longer starts invisible at low zoom — it now stays fully opaque from the moment it's in view, so you can see its footprint and rough edges over the satellite before zooming in. It reveals as you zoom (greyscale → true color, dimmer → full brightness) and still fades back to let the twin geometry read through up close, and the satellite basemap was darkened so the ortho reads more clearly against it.
  • Fix[security] Signing in is now the only way to get a ClearSky token. The auth resolver had a dormant fallback that ran any sessionless request as one shared account read from `COGNITO_USER`/`COGNITO_PASS` — unset everywhere today, but setting those vars would have silently collapsed per-user data scoping. Removed: no session now fails honestly instead of borrowing an identity.
  • FixThree browser caches that hold server data — drone-ortho image URLs, plan dates, and each project's last flight date — now carry the environment in their key, so a project that exists in both dev and prod can never be served the other environment's data. The ortho cache mattered most: it holds signed image URLs that grant access on their own for a week, and its old entries are now cleared outright on first load instead of sitting in the browser's storage quota until they expire.
  • FixTwo accuracy fixes on the Forecast report, from a full audit of where its numbers come from. **Planned % and SPI** were biased behind inside the current period — plan was read at the period's *end* while actual was work done *so far*, so on a Tuesday the report compared two days of work against a whole week of plan, and "Behind plan" could be pure calendar artifact. SPI will now read higher mid-week, and that's the correct reading. Separately, when part of a project's weighted scope fails to report, the roll-up used to quietly average over what was left — which *raises* the percentage — and the chart now flags it as **Partial Data**.
  • FixFixed the Forecast plan modal's "Target completion date" picker slipping a day in negative-offset timezones (i.e. all US zones): the field stored the picked date at UTC midnight but rendered and bucketed it in local wall-clock time, so it visibly snapped back a day on selection and the projection targeted the wrong day. The date is now noon-anchored (the repo's date-only convention) and read back in UTC, so it round-trips exactly.
  • FixMap layers now actually fade in and out at their zoom boundaries instead of snapping on. The fade ramp had been placed just below each layer's minimum zoom, where MapLibre doesn't draw the layer at all, so it had never been visible.
  • Fix[security] Closed an SSRF / token-exfiltration hole in `/api/layer-presence`: the upstream API base was taken from a client-supplied `?apiBase=` query param and then called with the caller's ClearSky bearer attached, so a crafted link could make the server send a logged-in user's token to an attacker-chosen host. The base is now always resolved server-side from the caller's own session (`apiBaseForRequest()`), never from the request — matching the rule the `/api/mvt-tile` proxy already documents. No client passed the param, so there's no behavior change for legitimate callers.
  • FixThe map now decides where to send its auth token by checking the request's hostname rather than scanning the whole URL for a matching string, so a URL that merely mentions a ClearSky address in its path or query can't collect the token. No behavior change for real tile requests.
  • FixOn first load with no saved theme preference, the map now renders fully dark — header included — instead of a light header over the dark map. The white-header / dark-map mix is now opt-in: it appears only when you explicitly set the theme to light. Interim behavior until the map surface gets a real light theme.
  • FixTightened which paths the login gate lets through without a session: public route names now have to match a whole path segment, and file extensions no longer grant access anywhere. Required static trees and the one root-level public data file are explicitly allow-listed, and the rule now has tests.
  • FixMilestones on the Forecast chart now sit on their actual day within the period rather than snapping to the period's centre — so a Monday and a Friday deadline in the same week no longer draw on the same line.
  • FixWhen several milestones fall on the same date — a contractual GMC and its Mech Completion, say — the Forecast chart now shows one marker under the binding contractual name instead of two competing for space, where the loser used to degrade into an unexplained dot. Pointing at the marker names everything that collapsed into it.
  • FixA finished project no longer draws a forecast. A completed site was projecting a flat line out to a GCOD months after it was energised; the chart now drops both the projection and the rate controls once the build is over — either because the curve is complete or because ClearSky reports the project in service.
  • FixDev and staging now reach their APIs again. Those environments moved to a new domain and C3 was still calling the old addresses, which had stopped answering — so signing in worked but the project list came back empty. Panorama images and map tiles on those environments were affected the same way.
  • FixDrone imagery now stays dim until zoom 17 before brightening, so it stops competing with the digital twin at the block-scale zooms where the geometry is the subject. Colour still returns on the same curve as before.
  • FixTwo quiet correctness fixes behind the progress charts. The window we request plan data over is now derived from each project's planned finish instead of a hardcoded January 2035 ceiling — a project with dates past it silently lost them. And when the lookup of a project's site timezone fails, we now retry and log it rather than silently falling back to Central time, which could shift late-day installs onto the wrong calendar day.
  • FixA date in C3 now means a calendar day at the **site**, not in UTC or in your browser. Panoramas were fetched over a UTC day, so on a Central-zone project every pano shot after 7pm was missing and the previous evening's showed up instead; report charts bucketed weeks and months in the viewer's timezone, so the same install could land in different weeks for a PM at site and a VP overseas. The same correction now reaches the report defaults — the Production tally's as-of, the Pile report's date range, and the Daily Progress date all mean the site's today — and a lint rule keeps the UTC shortcut from creeping back in. And element dates in the InfoPanel now read in the site's timezone: ClearSky anchors `plan_start` at the project's midnight, so anyone west of the site — a California PM on a Louisiana project — was seeing planned dates a day early.
  • FixReference layers set to a dashed line style now draw dashed on the map instead of solid, and clicking one shows its description alongside the name.
  • FixFixed clicking a reference layer's fill (polygon interior) showing a generic white "Reference layer" info panel instead of the real layer name and color. The fill companion layer (`ref-<uuid>-fill`) was returned by the hit test, but the metadata lookup only matched the canonical outline id (`ref-<uuid>`); a fill click now normalizes to that id and opens the same panel as clicking the outline.
  • FixRow labels now appear at the same zoom as module geometry (18.5) rather than half a zoom later, so you can tell which row you're looking at as soon as modules resolve.
  • Fix[security] Signing out now ends the session at Cognito, not just in the browser. Previously logout only deleted cookies, so the refresh token issued at login stayed valid for its full ~30 days — anyone holding a copy could keep minting access tokens long after the user believed they'd logged out. Sign-out now revokes each environment's refresh token (and every access token descended from it); if Cognito is unreachable the sign-out still completes.
  • FixThe superadmin check now verifies that an access token was really issued by Cognito, instead of trusting the group claim inside it. Previously the two surfaces that never call ClearSky — `/usage` and the login audit log — accepted a token nobody had signed, so a hand-crafted one claiming superadmin was enough to read them. Requires the Cognito user pool ID to be configured; without it, superadmin access is denied rather than assumed.
  • RefactorThe QC issue mappers and the MVT tile retry logic each live in one place now: `mapPriority`, `currentDescription`, and image counting moved into `qcIssueDetail.ts` for the observations route to import rather than duplicate, and both tile proxies share one `fetchTileWithRetry`, so retry, 204 and cache policy can't drift apart. The tile proxies also took the `constructionProjectId` query param the other routes use (was `cid`), and the two same-named `mapStatus` functions became `mapObservationStatus` / `mapInspectionStatus`. Behavior-preserving throughout.
  • RefactorMoved the map's fly-to-a-named-feature logic out of `MapStage` into its own hook, which also made it testable for the first time — 11 new tests now cover the parts that only ever ran in a live browser, like a feature staying findable after its layer is switched off. No change to how the map behaves.
  • RefactorThe inspections list endpoint's upstream shape is now a real type (`RawInspectionRow`) instead of `Record<string, any>`, so a field-name typo is a compile error rather than a blank cell in the Records table. Field names were confirmed against live dev data, which also turned up two things now documented in `docs/API.md`: the list and detail payloads aren't the same shape (the list carries aggregate counters the detail doesn't), and `inspection_number` is a string on one and an int on the other.
  • RefactorThe project-detail document is now a real type (`ProjectDetail`) instead of an untyped record cast to `any` at the point of use. Every field the reports banner, portfolio phase, and twin's `geo_project_uuid` read off it is now checked at compile time — a field-name typo used to compile clean and surface as a silent `null`. Type-only change, no behavior difference.
  • ChoreAgent-facing doc hygiene: `CLAUDE.md` now indexes the four reference docs that existed but were never linked (`ARCHITECTURE.md`, `STRUCTURE.md`, `ADD_LAYER.md`, `AUTH.md`), the network policy and its enforcing hook dropped stale "GeoServer" wording, and two unused one-off scripts plus a handful of dead constants flagged by knip are gone. The chat route's speculative model-id list is now an instruction to verify against the provider's current list before changing `MODEL`. No runtime behavior changes.
  • ChoreCharts are now checked against their spec before drawing, so a malformed chart shows a plain "could not be displayed" message instead of a chart that looks fine but plots the wrong thing — a series misaligned by one point, a bar hidden behind another, or an axis that clips the data flat. Every chart in the app validates today; the check exists because a wrong chart gets read and believed.
  • ChoreThe loader sweep animation was defined three separate times — once each in the portfolio, the panorama viewer, and the shared Loader — and is now a single `animate-scan` utility. Identical timing, so no loader looks different; there's just one definition left to drift.
  • ChoreDropped the unused `@turf/boolean-point-in-polygon` and `@turf/point-on-feature` dependencies — neither was ever imported. The custom site-scale geometry in `measureGeometry.ts`/`geometry.ts` is tested and remains the implementation.
  • ChoreLowered the default zoom floor to 14 for four electrical layers — CBX / LBD, DC Feeders, DC Harnesses, and Inverters — so they now appear at a wider (more zoomed-out) view instead of only at close range.
  • ChoreDeleted the superseded `docs/ROADMAP.md` snapshot and repointed its nineteen inbound references at Linear, and corrected five stale claims the sweep turned up — three dead file paths in `NAMING.md`, a removed QC scope lookup in `API.md`, and the "CI arrives with the repo move" note in `CLAUDE.md`/`NAMING.md`/`TESTING.md` (CI has been running typecheck, lint, test and build on every PR since `72fb5221`).
  • ChoreAdded a test that fails if any page in the app isn't behind an error boundary — the wrapper that turns a crash in one screen into a contained error message instead of a blank white app. Every route is covered today; the check exists so a newly added page can't quietly ship without one.
  • ChoreTwo more hand-rolled date formatters — the InfoPanel's "As of Jun 3rd, 2026" byline and the report header's date — now use the shared formatters, so ordinal-day and month-name logic lives in one place. Dates read exactly as before; a test pins that byte-for-byte across a full year.
v0.0.18
2026-07-22

Reference Layers + Map Cleanup

Reference layers draw again: the platform retired the GeoServer service C3 was still pulling survey and utility geometry from, so every overlay silently rendered nothing. They now stream from ClearSky’s native vector tiles like the rest of the twin — correct at every zoom, with polygon fills. The rest is a cleanup sweep.

  • FixReference layers render on the map again. The platform retired the GeoServer overlay service that C3 was still fetching survey/utility layers from, so toggling any reference layer (ALTA surveys, easements, flood zones, constraint maps) drew nothing. Reference layers now stream from ClearSky’s native vector-tile endpoint like the rest of the digital twin — they draw correctly at every zoom (no more zoom-14-only workaround) and no longer depend on a hand-set project center. Polygon layers (flood zones, constraint maps, wetlands) now render with their fill color and opacity, not just an outline.
  • FixThe timeline no longer jumps to the latest flight on a background refresh — the snap-to-latest is now guarded, so an SWR revalidation returning the same flights won’t yank the scrubber mid-session. A genuinely new flight or a project switch still snaps as intended.
  • RefactorRepo-wide dead-code and duplication sweep — removed a dead `apiBase` code path, a parked tabbed-panel view, and ~20 unused exports, props, and types; folded duplicated tile-math, TTL-caching, and route boilerplate into shared, unit-tested helpers. Behavior-preserving, net −97 lines.
  • RefactorExtracted the measure-tool geometry, the map fly-to matching, and the PileReport chart-axis scaffolding into shared, unit-tested modules — pulling testable logic out of three high-churn files. Behavior-preserving.
  • RefactorMap imagery you just viewed is no longer discarded and re-downloaded — orthos are kept in an identity-keyed cache with a small LRU, so scrubbing back to a date flips its retained layer visible instead of re-fetching every tile. Also adds a map tile-cache budget so panning or zooming away and back stops re-fetching recently-seen tiles.
  • RefactorThe QC dashboard hooks moved onto SWR (request dedup + shared cache), with their data normalization extracted into a unit-tested module. Same dashboard, same behavior.
  • RefactorMoved the loose `src/components/MapCanvas.tsx` into its own `MapCanvas/` folder (`MapCanvas/MapCanvas.tsx`), matching the one-component-one-folder convention every other component already follows — the last “loose component” deviation in NAMING.md §5. Pure structural move: its sibling imports shift `./` → `../`, the single importer (MapHost) updates its path, and the NAMING.md backlog row + CLAUDE.md path pointer are updated. No behavior change.
  • RefactorThe image lightbox now uses the shared `Loader` instead of its own inline copy (the loading caption moves below the pill to match).
  • RefactorInspections report chart axis labels now read `3/15` (was `Mar 15`), matching the other three reports.
  • ChoreChangelog entries are now one file each under `changelog/unreleased/` instead of a shared array, so concurrent PRs can no longer conflict on the changelog. The page reads the directory at build; a release folds the files into a dated entry.
  • ChoreDocs accuracy sweep — fixed reversed layer ids and stale zoom and test-count numbers in CLAUDE.md, corrected component pointers in COMPONENTS.md and TESTING.md, and rewrote ADD_LAYER.md to the current single-manifest model.
  • ChoreTest infra: unshadowed jsdom’s `localStorage`/`sessionStorage` in `vitest.setup.ts`, so storage-touching integration tests don’t silently break on Node builds that ship experimental global web storage.
v0.0.17
2026-07-21

Responsive

C3 goes responsive across laptop, tablet, and phone. On the map, the floating panels become content-height bottom sheets over a blurred scrim, the Legend becomes a popover, and only one panel opens at a time on touch. Desktop-first throughout — the layout tunes down without inverting the interaction model.

  • Fix[security] Server-side project-detail cache partitioned by auth context: the module-level cache in fetchProjectDetail was keyed by cid alone, so on a multi-user deployment one principal’s project detail (including qc_config) could satisfy another caller’s request for the same project, and dev/prod data for the same cid could collide. It now keys on a hash of the resolved apiBase + bearer token + cid (token hashed, never stored raw), sweeps expired entries, and keeps the get→create→set critical section await-free so concurrent callers still collapse to one upstream fetch — pinned by new projectDetail tests.
  • FixMap tiles survive a transient upstream gateway blip: the /api/mvt-tile proxy now retries once (150ms backoff, discarded error body cancelled for prompt socket reuse) on a 502/503/504 before surfacing the failure, so a momentary edge flap no longer reaches MapLibre as an AJAXError + dropped layer. 4xx and 500 are not retried (a 500 is a real fault; retrying just doubles load).
  • Fixdrone_orthos pagination fails fast: each page fetch now carries a 15s per-page timeout (AbortSignal.timeout), so a stuck upstream returns a prompt 500 instead of hanging /api/flights and /api/dem-flights indefinitely. The bound is per page, not a total budget, so a slow-but-progressing multi-page fetch on a large healthy project never trips it.
  • NewResponsive foundation: explicit viewport with viewportFit=cover (iPhone safe-area aware); floating map panels now clamp their seeded position into the viewport on mount, so Layers/Legend/Agent/Info can no longer mount partly off-screen at phone widths; new useMediaQuery/useIsMobile hook (SSR-safe) + a documented breakpoint ladder for the stateful cases. CSS-first via Tailwind max-[Npx]:/lg: variants is the default; the hook is reserved for behavior CSS can’t express. docs/C3Design.md gains a Responsive section.
  • FixLogin hero no longer overflows on phones: the “Construct 3” wordmark + logo clamp floors were sized for desktop (64px/56px), clipping the lockup at ≤~480px. Floors lowered (desktop sizing unchanged; the tablet/phone hero now scales down smoothly), mobile padding trimmed, and the version badge re-anchors centered below the wordmark on ≤640px instead of running off the right edge. Verified 320/375/1280px.
  • NewMap chrome goes mobile. GlobalRail: below the lg breakpoint the section tabs collapse to icons (labels hidden, aria-labels kept) and the project-name switcher folds into a hamburger “left menu” (Portfolio + project list), fixing the nav overlap on phones; desktop is unchanged. InfoPanel: on phones (≤640px) the floating site/element panel becomes a bottom sheet — content-height, growing to a 90dvh cap over a blurred scrim, dismissed by the grab handle or a tap on the scrim (no close button). The Layers and Agent panels use the same sheet; the Legend becomes a small popover anchored beside its button. On touch devices (phones + all iPads) only one floating panel opens at a time; mouse/desktop stacks them freely, and every panel drags via pointer events so repositioning works on iPad, not just mouse. The site panel no longer auto-opens on phone load — the map shows first and the ⓘ button pulls the sheet up — and pressing play closes the panel first so the timelapse runs on a clean map. Desktop keeps the draggable panels + normal scrubber.
  • FixReports/Records tab strip (shared SecondaryNav) now scrolls horizontally when the tabs outrun the viewport instead of clipping the trailing tabs off-screen — on a phone the Records Documents/Exports tabs (and Reports Installation/…) were unreachable. Scrollbar hidden (touch/trackpad swipe), trailing gutter so the last tab clears the edge, and the active tab is scrolled into view on load (deep-linked ?tab= no longer lands with the active tab off-screen); desktop is unchanged.
  • FixSettings is responsive on phones: the fixed 260px sidebar + content grid clipped the section content off-screen below lg. It now goes master-detail under lg — the section list, then tap a section to open it full-width with a “‹ Settings” back link (focus moves with the pane; deep-linked ?section= opens straight to that section) — and restores the two-column rail ≥lg, unchanged on desktop.
  • FixSettings › Permissions matrix stays legible when scrolled on phones: the Company-level Permissions table already scrolled horizontally, but swiping to the Owner/EPC/Supplier toggles pushed the Feature labels off-screen. The chevron + Feature columns are now frozen (position:sticky) so the row label stays pinned while the toggle columns scroll under it; no-op on desktop where the table fits.
  • RefactorGlobalRail: the Terabase logo is now the menu button on phone/tablet (replacing the plain hamburger glyph — it opens Portfolio + the project list) and returns as a Home link on the desktop rail.
  • FixQC observation drawer no longer covers the map on tablet/desktop: opening it insets the map (at sm and up) so the map slides over into the space beside the drawer (MapLibre reflows to keep the selected observation centered), and the drawer goes full-width on phones. The date scrubber is drawer-aware in its collapsed state too — it was clipped under the drawer before — and responsive across all breakpoints.
  • FixPortfolio “Loading projects” state no longer overlaps the phase tabs on short screens (e.g. iPhone SE): the loader and empty-state now center in the area below the tabs instead of the whole viewport.
  • FixRecords table header no longer collides with its pager on phones: the large section title and the pagination (“N of M” + arrows) stack vertically below sm instead of overlapping, and return to side-by-side at sm and up.
  • RefactorLogin version badge moved off the wordmark lockup to a bottom-left CHANGELOG dev tag (alongside the env tags), and the logo + wordmark are now vertically centered. Retires the bespoke version-badge CSS — LoginScreen.module.css is down to two keepers (the autofill override + the stage keyframe) and the repo stylelint-disable count drops to one.
  • FixSettings section list on phones no longer pre-highlights a section in the list-only view — the blue selected state is desktop-only now (where both panes show); the mobile list opens with nothing selected.
  • FixCompare mode on phones: the compare pane zoom controls lift above the right-edge chrome so they no longer sit under the compact date scrubber.
v0.0.16
2026-07-19

Agentic Charts

The reports charting system is rebuilt on a shared substrate that renders declarative chart specs — the foundation for agent-composed charts. The 645-line ProgressChart god-component became a thin container over a pure, tested model, and the Forecast report was redesigned around a single-axis cumulative S-curve.

  • RefactorNew charts/ primitive layer — a shared, spec-rendering chart substrate (scale/SCurve/StatTile/MountainChart + a renderChart dispatcher); collapsed three copied niceTicks tick-math helpers (ProgressChart/InspectionsReport/QcSparklines) onto one, and gave QcSparklines the zero-guard it was missing.
  • RefactorDecomposed the 645-line Reports ProgressChart god-component into ProductionCurves/ — a thin container over a pure, unit-tested model.ts (deriveKpis + buildSCurveSpec) plus extracted Scrubber/Legend, rendering through the charts/ primitives.
  • FixResolved the progressChart/ProgressChart casing-collision: split InfoPanel/progressChart.tsx into progressData.ts (the series engine) + charts/MountainChart (the view); repointed all 6 importers.
  • NewForecast report redesign: a single-axis cumulative S-curve (Planned/Actual/Forecast) with a KPI stat-tile row (Complete/Planned/SPI/built-this-period), replacing the dual-axis bars+lines chart. A %↔units toggle (labeled with the real UOM, e.g. EA) appears only for a single discrete element; sub-tabs and toggle unified on the --select token.
  • RefactorChangelog authorship model: the /changelog page now renders an "Unreleased" section above the versioned releases from a new UNRELEASED bucket. Each author appends their own entry when they finish work (while they have full context); the version bump only promotes UNRELEASED into a dated release — the releaser no longer authors notes for work they did not do. Extracted a shared ChangeList component; CLAUDE.md git rules rewritten to match.
  • FixPer-period chart bars no longer overflow the chart frame: the % axis now always places its top tick at or above the tallest bar (PRD-1087).
v0.0.15
2026-07-18

Activity Tracking + MapCanvas Decomposition

Activity Tracking ships as two read-only surfaces on ClearSky’s Omega progress engine: the Settings config editor for activities and weights, and a Records weekly grid of the work-breakdown tree by day. Category rows roll their children up client-side — deliberately, to push ClearSky to build the rollup server-side.

  • NewActivity Tracking — Settings config editor (Configuration of Activities and Weights) wired read-only to Omega wbs_config via /api/activity-config: the 8-column WBS tree (Activity Code, Unit, EAC Quantity, Start/End dates, plus Group Type / Element Type / Installation Tracking sourced from each activity’s digital_twin_link)
  • NewActivity Tracking — Records weekly grid (Records/Activities/ActivityTracking) via /api/activity-tracking: WBS tree × Mon–Sun days + a Week N total + a right-pinned Total (% complete) column, with a Monday-based week navigator. Daily value = the Omega eod field (daily delta of the manual-entry cumulative; NOT dt_actual_quantity, which is 0 on real data); % complete = cumulative/EAC. Original Plan / Cumulative rows dropped on purpose
  • NewCategory rollups as an explicit client-side demonstration (to push ClearSky to build them server-side): uniform-unit sub-scopes sum to a count, mixed-unit scopes show a weight-averaged %. Depth-tiered row shading (scope / sub-scope / activity), collapse-to-category default, zeros rendered blank, and the Activity cell (not the whole row) as the sole expand toggle so day cells stay free for future data entry
  • RefactorDataTable gained composable, opt-in primitives used by the grid — headerRight slot, rowClassName, dense, thClassName, rowClickExpands, and per-column fixed widths (colgroup) — so the numeric columns stay pixel-stable across week navigation while the Activity column flexes to fit its visible labels; borders are drawn as intent (row rule + chosen seams) rather than a full grid, so no !important overrides
  • RefactorDecomposed the MapCanvas god-orchestrator into cohesive hooks (compare-mode, measure controls, panel z-order) and continued reports/theming polish — retired the vestigial report-chrome ChromeTone (ReportsShell / ReportHeader / SecondaryNav now follow the document .dark)
  • ChoreRemoved the Daily Progress report’s ID column; documented the Omega wbs_config + estimate_of_day + cumulative schemas (fields, semantics, gotchas) in docs/API.md; suite 198 green, typecheck + lint (0/0) clean
v0.0.14
2026-07-17

Dark Reports and Cleanup

Dark mode now spans the whole product: one owner, ThemeController, applies the theme app-wide from a single device preference, so the tone you pick on the portfolio carries into reports, records, and settings. The legacy two-surface --c3l-*/--c3d-* token system was then removed entirely, definitions and consumers alike.

  • NewDark mode spans the whole product: ThemeController (mounted once in GlobalChrome) is the single owner of the shadcn .dark class, applying it on every route + preference change from computeDarkTheme(pathname, pref) in src/lib/theme.ts. The c3.homeTheme device preference now carries across portfolio + project surfaces — dark follows you into reports/records/settings — with the pre-paint script hand-mirroring the same logic for no-flash first paint
  • RefactorPromoted the grey canvas to --background: light --background is now the soft blue-grey #F2F5FA (the default canvas for every light surface), while --card/--popover stay pure white so content pops. The separate --grey-bg token was retired; the white "chip" primitives (tabs active pill, switch thumb, outline button) re-pinned to bg-card so they still read on grey
  • FixSettings sidebars (project + account) now use the Records blue for hover/active selection (c3-blue tint + rail + label, a three-level hierarchy) instead of the muddy grey-on-grey that resulted from the new canvas
  • RefactorAnnihilated the legacy two-surface --c3l-*/--c3d-* token system entirely — all definitions and every consumer. The off-system islands (LoginScreen, ScoreboardMap) and the IS_DEV dev tooling (LayersPanel zoom bar, MapCanvas SAVE chip) now use self-contained literal values, not shared tokens. globals.css fell 457 → 341 lines
  • ChoreRetired the now-dead data-home-theme mechanism (globals block + the HomeTopbar/inline-script writers) and the write-only projectContext tone signal (tone/setTone/inferTone/body.dataset.tone + all call sites) — the shadcn .dark class is the sole theme signal
  • ChoreStruck the /labs route and archived its ThermalScan + EquipmentConsole design studies outside the repo; deleted the throwaway /glass-check route. CSS Modules dropped 5 → 3 (only ScoreboardMap, BnzReport, LoginScreen remain — all intentional islands)
  • ChoreDocs + roadmap updated for the removals (CLAUDE.md theming note, C3Design.md, COMPONENTS.md, NAMING.md, MapVisualization.md); added a computeDarkTheme unit test (suite 161 → 170); typecheck, lint (0/0), and build green
v0.0.13
2026-07-15

Tailwind v4 + shadcn/ui — the Design System, Shipped

C3’s design layer is now Tailwind v4 + shadcn/ui, replacing the hand-rolled two-surface CSS-Modules system. Every product surface moved to plain .tsx with utility classes over shadcn primitives, and CSS Modules dropped from 95 to 5 — only intentionally off-system islands remain.

  • NewAdopted Tailwind v4 (CSS-first) + shadcn/ui as C3’s design layer over a new src/components/ui/ primitive set (Button, Dialog, DropdownMenu, Tabs, Select, Tooltip, Input, Card). Components are now plain .tsx with utility classes; CSS Modules went from 95 → 5 (only off-system islands remain)
  • NewToken model rebuilt on shadcn semantic vars (:root + .dark) + the c3-* brand palette (status/category) + --select (one selection blue) + --grey-bg (the shared reports/records/portfolio canvas). Light/dark is shadcn-native via the .dark class, replacing the old data-home-theme remap
  • RefactorMigrated every surface: the map chrome (LayersPanel, TimeScrubber, InfoPanel, Legend, MeasureToolbar, MapCompare, ZoomControls, AgentPanel), Reports (one uniform grey canvas — the per-dashboard gradients removed), Records, Settings, Portfolio, and the report-adjacent components (PileReport; the QC observation drawer as a solid dark card)
  • NewLogin migrated — the animated glass hero moved to Tailwind + the system glass utility (pills, account dropdown as a solid dark popover, env tags); its module pruned 232 → 59 lines (the autofill override, blinkIn keyframe, and mono version badge kept as CSS-only keepers)
  • RefactorEstablished the glass model: @utility glass = a small light frost for floating utility buttons + login pills only; panels/menus/tooltips/flyouts are solid dark cards (dark bg-popover); context-adaptive components inherit the ancestor theme, only always-dark chrome roots dark
  • RefactorCollapsed the raw <button> elements onto the shared shadcn <Button> (variants + size + asChild) and removed the legacy @/components/Button compat shim. Bespoke chrome uses variant="ghost" + className; underline tabs, the MapCompare drag handle, and IS_DEV dev tooling stay raw by design
  • ChoreBNZ reclassified as an intentional client-custom report island — decoupled from the C3 system onto its own scoped --bnz-* theme, demonstrating per-client custom reporting
  • ChorePruned 88 dead legacy --c3l-*/--c3d-* tokens from globals.css (648 → 458 lines); the survivors are the IS_DEV tooling, the quarantine islands, the self-hosted font tokens, and the --grey-bg chain
  • RefactorConverged the Reports/Records read paths onto SWR (shared keys, request dedup, keepPreviousData): the Portfolio per-project fan-out, the RecordsAvailabilityProvider probes, the paginated Records table engine, and the shared qc-config fetch
  • ChoreDocs rewritten for the new stack (CLAUDE.md, C3Design.md, COMPONENTS.md, NAMING.md); test suite 156 → 161, build green
v0.0.12
2026-07-14

Audit Sweep — Auth Hardening, Shared Server Helpers, Labs & Operations

A pre-handoff sweep closing five open audit findings. Per-user credential isolation was tightened so a multi-user deployment can never serve one user’s token to another, and the API layer’s copy-pasted auth wiring was extracted into shared helpers across some thirty routes. The mock design-study dashboards moved out of Reports into /labs.

  • Fix[security] Per-user credential isolation hardened in the server auth layer: the Cognito token-refresh memo was keyed by environment only, and three media-proxy routes (GeoServer tiles, reference-layer GeoJSON, pano images) cached the first caller’s resolved credential at module scope — on a multi-user deployment either could hand one user’s credential to another for up to ~1h. The refresh memo is now keyed per session (environment + a hash of the refresh token) and the three route caches were removed, so every request resolves against the caller’s own session cookies
  • Chore[security] With the credential-isolation fix above, both server-layer credential-exposure paths surfaced in the pre-handoff audit are now closed — the /api/pano-image SSRF/bearer-exfil path (v0.0.10) and this cross-user cache bleed. A frozen build accepting logins no longer carries either live-credential risk; neither was deferred past the handoff
  • RefactorExtracted shared /api auth wiring into src/lib/authedFetch.ts (clearskyFetch + clearskyContext), removing the per-route getBearerToken() wrapper and hand-built bearer headers across ~30 ClearSky proxy routes; auth semantics unchanged
  • RefactorConsolidated the two ad-hoc route pagination styles into src/lib/paginate.ts (paginateSequential + paginateRemaining), reused by the panos, accounts, and qc-usage routes; added a badRequest() responder to apiError.ts and validated the login route body with zod
  • FixRemoved leftover debug code in the reference-layers route that wrote the raw upstream payload to /tmp on every request
  • RefactorConsolidated the hand-tuned map timing constants (ortho-load debounce, raster hide→reveal fallback, label-rebuild debounce, pano-fetch debounce, timelapse frame interval + prefetch look-ahead) into a single src/lib/mapTiming.ts module; behavior identical, all values unchanged
  • NewAdded a /labs route for design studies, kept out of the product navigation. Moved the Anomalies (Thermal Scan) and Equipment Console dashboards — both mock-data studies — from the project Reports tabs into /labs
  • NewAdded an /operations hub gathering the internal / housekeeping surfaces (Scoreboard, Usage, QC Usage, Changelog, Roadmap), each with a shared “← Operations” back button
  • RefactorRemoved the mock “crews on site” section from the Production report (its tallies were already live) and reduced Reports/data.ts from ~325 lines to shared types only — deleting six dead mock-data constants orphaned when the dashboards moved to live hooks
  • ChoreRemoved the AI Road Trip vision page; eslint now ignores the generated coverage/ and .claude/ worktree directories so the repo lints at a true 0 errors / 0 warnings; test suite 141 → 156
v0.0.11
2026-07-13

Automated Test Suite

The repo gained the automated test layer it never had — Vitest unit tests over the pure business logic plus React Testing Library tests that drive real components, at 141 tests across 19 files. It paid for itself immediately, surfacing a latent crash in the forecast projection.

  • ChoreAutomated test suite: Vitest (unit) + React Testing Library/jsdom (integration) — 141 tests across 19 files over the forecast math, date/timezone bucketing + formatting, routing, layer-visibility composition, geometry, and the security/session helpers, with test/watch/coverage scripts and honest directory-scoped coverage
  • FixFixed a latent crash in the forecast projection (an out-of-range “today” index) that the new tests surfaced
  • FixThe Forecast plan no longer errors when opened for a project with no progress data (empty-series guard + regression test)
  • FixThe sign-in page renders the app’s self-hosted font instead of fetching one from Google
  • RefactorThe layer-visibility rule was lifted into a standalone pure function (composeVisibility) so the invariant behind project switching is directly testable; behavior unchanged
  • RefactorThe media-proxy host allow-list was extracted to a reusable, unit-tested module; JWT group parsing and the sign-out flow gained tests for their security/session contracts
  • ChoreAdded docs/TESTING.md documenting the static/unit/integration layers, setup, coverage philosophy, and conventions
v0.0.10
2026-07-13

Local Linters and Pre-Handoff Hardening

The repo gained the two local linters it never had — ESLint v9 and Stylelint — and was driven from 102 errors and 41 warnings down to a clean 0/0, fixing a real rules-of-hooks violation. Two security findings from the audit were closed, including an SSRF and bearer-exfiltration hole in the pano proxy.

  • ChoreLocal linters stood up: ESLint v9 flat config (typescript-eslint + react-hooks + Next core-web-vitals) and Stylelint (color-no-hex on CSS modules), with lint/lint:js/lint:css/typecheck scripts. The repo went from 102 errors + 41 warnings to 0/0 — rules-of-hooks caught and fixed a real violation (a component calling 9 hooks after an early return), and the effect-dep/img/unused-var rules stay non-blocking so a future violation surfaces without failing the gate
  • Fix[security] /api/pano-image SSRF and bearer-token exfiltration closed: the media proxy now allow-lists only S3 and ClearSky-hosted hosts (refusing cloud-metadata, internal, and loopback addresses plus suffix-confusion hosts) and drops the bearer entirely on the caller-supplied-URL fallback path
  • Fix[security] Removed leftover debug scaffolding in the reference-layer proxy that wrote decoded basic-auth credentials to a per-request temp file
  • RefactorAPI routes share one error responder (src/lib/apiError.ts): a generic 500 that logs server-side instead of echoing raw error text (closing the raw-error leak across ~37 routes), with true gateway errors forwarded only on the path that knows it is upstream
  • Fixelement-progress bucketed every project as US/Central, skewing the portfolio mountain charts by up to a day for non-Central sites — it now threads each project’s real IANA site zone (cached, with an in-flight dedup so the portfolio fan-out shares one round trip)
  • FixThe AI routes (ai/chat, ai/map-agent) now guard body parsing, validate input with a zod schema (400 on malformed), and wrap the stream so an upstream failure returns a generic 500 instead of echoing raw model error text
  • FixThe three remaining unprotected top-level routes (/account and the two settings surfaces) are wrapped in ErrorBoundary — every top-level route now has one
  • FixClient-only initialization in projectContext moved out of the useState initializers into the mount effect, killing a render-phase localStorage read/write and a fresh-deploy hydration mismatch
  • RefactorThe qc_config boundary is now typed (RawQcConfig) instead of Record<string, unknown>, removing five qcConfigOf(...) as any casts and their inert eslint-disable comments
  • RefactorDesign tokens consolidated into a single core palette per surface (--c3{l,d}-red/-orange/-green/-blue/-slate) used directly for both status and category color; zero visible change except the sanctioned scoreboard donut
  • RefactorThe QC layer-id triplet is extracted into one QC_LAYER_IDS constant consumed everywhere; no hardcoded triplets remain
  • ChoreExcised the fake stub role (UserRole, RolePicker, c3.role, setRole and all plumbing) ahead of the real user model — the picker was write-only so behavior is unchanged, and the one real privilege check is untouched
  • ChorePinned the four churny caret dependencies (ai, zod, maplibre-gl, react-map-gl) so a transitive major or minor can’t land silently
  • ChoreDead code removed: the orphaned /api/pier-events route and its unused fetch chain, and set-but-never-read total/truncated state in the Inspections report
v0.0.9
2026-07-05

Design System and React Cleanup

A design-system and stability pass ahead of the handoff. The raw-hex color leak was swept repo-wide down to a small set of intentional exceptions, the Forecast dashboards were fully tokenized, and the standalone pages moved onto one shared full-page scroll shell.

  • ChoreDesign-system handoff prep: repo-wide raw-hex color sweep (~229 → ~64 occurrences, the remainder intentional — bespoke report themes, login glass, media backdrops, the shared built-green)
  • RefactorForecast dashboards fully tokenized into the C3 light system with live filters, and dev-only scaffolding removed
  • NewStandalone pages (changelog, roadmap, usage, QC usage, and a new AI Road Trip vision page) share one full-page scroll shell with consistent error boundaries
  • NewThe roadmap page is now a read-only projection of docs/ROADMAP.md — the board is generated from the doc at build time and always matches it; the public write route was removed
  • FixFaster first paint on refresh and deep links: readiness now keys off the locally-seeded resolved project instead of waiting on two serial network round-trips, so a same-project refresh is instant and the switcher no longer dangles an empty name
  • FixThe ortho/elevation URL cache prunes expired entries and self-heals on a full localStorage quota, so a full cache no longer silently stops every localStorage write across the app
  • FixThe login backdrop reflects your last account instead of flashing a random image on load, and no longer causes a hydration mismatch
  • FixSmoother zoom: zoom values are quantized before propagating, so the layers panel and legend update on real zoom changes instead of every animation frame
  • FixRemoved redundant SWR revalidation bursts when the window regains focus or reconnects
  • RefactorCut needless re-renders: flight and milestone props are now identity-stable and agent-panel name lookups are memoized
  • ChoreDead code removed: the HouseReport component, unused paint-expression builders, stale view-model types, a dead info-panel prop, and a stale teardown-unification TODO
  • RefactorThe scrubber component was renamed from ScrubberShowcase to Scrubber
v0.0.8
2026-07-03

Real URLs and Compare View

Every project surface now lives at its own address — /projects/<name>-<cid>/map, /reports, /records, /settings — so links survive a paste and the back button works. Under the hood the map became a single persistent host that survives project switches, and a new Compare view splits it into two synced panes.

  • NewPath-based URLs: every project surface lives at /projects/<name>-<cid>/{map, reports, records, settings} — addressed by the stable project id behind a readable, dashless name prefix (no owner or account in the path); / redirects to your last-used account’s portfolio, unknown ids 404, and the active project is derived from the URL — navigation is the state change
  • NewPersistent map host: the map mounts once and stays alive across project switches, swapping layers in place instead of tearing down and rebuilding MapLibre on every switch
  • NewCompare view: a utility button splits the map into two synced panes sharing one camera, divided by a draggable seam — left shows the previous flight, right the current date, each with its own compact scrubber
  • NewDate-aware elevation: DEMs now follow the timeline date like aerials do, with an as-of-latest clamp so a date past the last flight resolves to the most recent elevation capture
  • NewPortfolio Scoreboard (/scoreboard): portfolio-wide KPIs — capacity, projects with EPC/owner split, QC issues, inspections, flights, panos — plus donuts, timelines, and a project-locations map, driven by a committed snapshot
  • NewQC usage scan (/qcusage): on-demand sweep of every visible project for QC and inspection activity, with sortable columns, a 60-day recency window, and an active-accounts rollup
  • NewLayer manifest: every layer is now defined once in a single manifest — panel metadata, grouping, map style, zoom scope, and z-order all flow from one entry, making a new layer a one-file change
  • NewForecast report polish: Forecast tab promoted to second position, controls consolidated into one header row, thinner scrubber handles, and a hover tooltip that tracks the cursor
  • NewLegend is now a draggable floating panel like Layers and Agent, with a collapsed button state and aggregate vs. element modes by zoom
  • NewRoadmap tooling: add, edit, and delete roadmap rows directly from /roadmap, persisted to the docs
  • FixFlight dates bucket by each project’s local timezone instead of UTC, so late-day flights no longer land on the wrong calendar day
  • FixBlue rollover highlights one element at a time — the finest element under the cursor wins, and pano/QC pins take priority
  • FixFlight-day markers in the timeline calendar are actually visible now, and the selected day stays solid blue even on a flight day
  • FixCompare mode removes the QC observation layers entirely instead of hiding them; a DEM reload can no longer stack the raster above survey and utility reference lines
  • FixThe portfolio shows a loader instead of a blank page before hydration
  • ChoreFull-repo security and correctness audit (six dimensions) logged to the roadmap with severity-ranked findings; handoff env bundle gitignored; the experimental roadtrip route removed
v0.0.7
2026-06-16

Activity Tracking, Glass, and the Calendar Scrubber

The headline is Activity Tracking — a Daily Progress report that pulls the live daily-report feed into an expandable work-breakdown tree, each line showing where the work stands against plan and its trend. Alongside it, the frosted-glass look became one shared recipe across every panel, and the timeline gained a real calendar date-picker.

  • NewActivity Tracking: a new Daily Progress report on the live daily-report feed — an expandable work-breakdown tree where each activity shows status (Completed / Ahead / Behind / Not Started) and trend, and parents roll up by weight from their children
  • NewGlass design system: the frosted-glass recipe (scrim + blur + shadow) is tokenized and applied consistently across panels, the global rail, the scrubber, the legend, dropdown menus, and portfolio cards
  • NewTimeline calendar: a new date-picker popover with drone-flight days marked by a darker frame; jump straight to any flight date
  • NewScrubbing hides the drone aerial the moment you grab the handle (even when cached) and reveals only the correct new-date image on release — no more flash of the stale aerial
  • NewThe scrubber resizes to clear the QC drawer when it opens and never covers the Info button
  • NewReference layers: clicking one opens an info panel showing its name and color swatch
  • NewFeeder names resolved on the map
  • NewNew shared DropdownMenu, Modal, and SecondaryNav primitives; the account menu’s Sign out is now legible in both tones with a neutral hover
  • NewBottom-of-map chrome (Info button, zoom controls, timeline) aligned on a single baseline; satellite stays put while scrubbing so the map never goes fully blank
  • NewNew default login panorama, higher-resolution pano processing, and a hardened pano-processing script
  • FixQC side panel de-blued: metric labels adopt the site-progress type styling, “People” renamed to “Details”, Scope capitalized, and a divider under each subsection
  • FixMV feeders visible from zoom 12; tightened layers-panel spacing under the view picker; calendar-icon spacing in the scrubber
  • ChoreRoadmap trimmed to active work; added the View-on-Map plan and the collaborative user-access model spec under docs/Projects/
v0.0.6
2026-06-14

Sign-In, Live Projects, and the Map Agent

Construct 3 gets real per-user authentication: you sign in with your own Cognito credentials and the app routes to the matching environment’s data, with sessions that refresh themselves and a true sign-out. Projects are no longer a hardcoded list — they load live and account-wide from ClearSky.

  • NewReal per-user Cognito sign-in with per-environment data routing; refresh-token rotation for long-lived sessions; sign-out is now a true logout; accounts list cached for instant re-login
  • NewThe Cognito session is the single access gate — the separate app-level gate code is retired
  • NewProjects are live-wired account-wide from ClearSky; the static project list is sunset and env + torque-tube gating consolidated
  • NewMap AI agent: floating Agent panel with client-executed tools, project-scoped chat, block disambiguation, and framed fly-to
  • NewViews: native view primitive built on a layerset model
  • NewLayers panel: rows hide when the project has no data for them (Elevation, Observations, Panoramas); reference-layer and observation checkboxes render in the layer’s own color; layer toggle shown as a check
  • NewPersistent light/dark theme toggle shared across home, portfolio, and account
  • NewCompliance record detail page; staged password→account sign-in flow; unified dark subnav across reports and records; top-left info chrome and panel alignment polish
  • NewScrubber timeline derives its horizon from plan data and extends to plan end; mountain plays from project start
  • RefactorMapCanvas decomposed into focused hooks; ClearSky tokens minted in-process (dropped the server-to-server hop); identity-scoped client caches and a same-origin MVT tile proxy
  • FixReference Layers folder toggle no longer desyncs from its child layers after switching projects
  • FixBlock labels unified on one parser (killed a duplicate selected label); empty geo_uuid no longer cached; "Inverter" pluralized; compliance-records self-fetch origin derived from the request
  • ChoreDocs reorganized into reference + Projects/, AUTH.md rewritten, README added; orphaned modules, public assets, and the dead pin-icon system purged
v0.0.5
2026-06-02

Home, Account, Portfolio

The home page loads all projects account-wide and groups them into Construction, Development, and Service grids. Navigation moves to a single top bar, replacing the left-side rail. The QC dashboard is wired to real observation data, the site mountain doubles as a timelapse control, and inspection detail pages are now accessible from records.

  • NewHome: account-wide project loader with phase-grouped grids (Construction, Development, Service); MasTec added to portfolio
  • NewTop navigation bar replaces the left-side rail; GlobalSidebar and HomeTopbar reskinned
  • NewSite mountain: tap to toggle timelapse, hover to reveal play/pause; as-of date label; verified observations shown grey on the map
  • NewQC dashboard: three-status sparklines, priority donut, 12-month close-out bar chart, Last 30 Days panel, granularity picker, all on live observation data
  • NewPile installation report: S-curve progress chart and daily throughput bars, added as a report tab
  • NewInspection detail page: compliance records, sign-off status, full field breakdown
  • NewReport tabs, context menu, AI chat, QC drawer deep-link, chart polish
  • NewInfoPanel: readable titles for all element layer types; default position no longer obscures the sidebar rail
  • NewC3 design: type scale overhaul, white subnav bar, --c3l-status-* and --c3l-priority-* token families, shared ObsStatusBadge / PriorityBadge / SortChevron primitives
  • RefactorComponent model Tracks A and B complete: InfoPanel, LayersPanel, MapCanvas, MapStage decomposed into focused hooks; MapSession context; 7 data hooks migrated to SWR; mapRef split-brain resolved
  • FixError boundaries applied to every surface; 9+ exhaustive-deps violations resolved; latest-ref writes moved from render into useEffect
  • FixRecords: subnav locked to top of viewport; paginator visible during refetch; Inspection chip on observation detail now navigates correctly
  • FixAbortController replaces cancelled flags across fetch hooks; DEM URL route returns gracefully when no DEM exists; QC priority badge hidden when unset
v0.0.4
2026-05-31

Inspection and Observation Records

Observations and inspections become working surfaces, not just lists. Each row opens a detail page with a full activity timeline, inline images, and a share button. Column filters query Construct directly so results are paginated and accurate, and the project QC config is cached so repeated filtering does not refetch it.

  • NewObservation detail page: activity timeline with inline images, thumbnails, dated lightbox, multi-assignee, multi-notifier, share button
  • NewRecords tables: clickable rows, sortable columns, status visuals
  • NewInspection filters push all params upstream (inspection_key, name, group, scope, sub-scope, status, sign-off); single paginated query, real totals
  • NewObservation filters push into the qc_issues search body (ID, QC type, reporter, deficiency, defect, assignee, description, inspection ID, status, priority)
  • NewScope, Sub-Scope, and QC Type filters sourced from the live project QC config; Status, Priority, and Sign-off move into column headers; filter-pill rows removed
  • NewQC Settings: live scope and subscope lookups, Configuration and Inspection Templates pages
  • NewAccount settings screen; role picker on the sign-in screen
  • NewInfoPanel: count / percent toggle on block Production rows
  • RefactorShared project-detail / qc_config cache reduces repeated fetches across QC surfaces; observation-detail fetch shared between map drawer and records detail page
  • FixCross-project data leak on the observation detail surface
  • FixPortfolio: cards held until open-issues count resolves, zero-actual card state, sign-in WebGL context leak
  • FixInfoPanel expand / collapse: no flashing across feature changes, state unified across layouts
  • FixMap: selected date seeded from shared flight cache; map-reuse change that broke home-to-project navigation reverted
v0.0.3
2026-05-30

Portfolio Mountains

The home page becomes a wall of mountain charts. Each project card shows the site progress mountain as of its last aerial flight with Plan, Delta, and Actual at a glance. Projects sort into Construction, Development, and Service tabs, and the company name, tabs, and account controls consolidate into a single top strip.

  • NewPortfolio: mountain chart cards replace the project list; Plan / Delta / Actual as of each project's last flight; whole card navigates into the project
  • NewPhase tabs (Construction / Development / Service) wired to real project status; off-tab cards do not mount
  • NewSingle black top strip: company name, phase tabs, account and settings controls
  • NewSettings section added under General in project Settings
  • RefactorShared progressChart module backs both the site panel and portfolio cards with one series engine and one chart component
  • ChoreDonut archived as DonutSummary for the internal demo; removed from the live panel
v0.0.2
2026-05-30

QC Deep-linking

Selecting a QC observation now scrolls the map so the element is never hidden behind a panel. Renew Energy joins as a new account with four projects.

  • NewQC observation selection: focus ring and viewport offset so the selected element is not occluded by the panel
  • NewRenew Energy account: four projects (Agave, Apollo, Helios, Sol)
  • NewLogin dropdown is scrollable for large account lists
  • RefactorisPlaying lifted to MapCanvas; TimeScrubber and Timelapse share one copy of play state
  • ChoreDead sign-in page removed; version bump tooling simplified
v0.0.1
2026-05-22

Foundation

Initial numbered release. Establishes the core map experience — the digital twin layer stack, the floating layers panel, the time scrubber, ortho imagery — plus the C3 design system across the dark map and light reports surfaces.

  • NewDraggable floating LayersPanel; agent button always visible; map clicks do not collapse panels
  • NewRow-number chips at each tracker head; label extraction works across project call signs
  • NewLayer name click toggles visibility; zoom slider range correct at layer boundary
  • NewSite info panel opens by default at low zoom; InfoPanel extended to feeders with element_progress BOM rows
  • NewReference layers: ALTA surveys, easements, utility lines via GeoJSON proxy to avoid tile coordinate mismatches
  • NewPanorama layer: pano pins on the map, click opens image, pool refreshes each session
  • NewOrtho imagery via TiTiler COG proxy; presigned URL resolved at runtime, not hardcoded
  • NewC3 Light design system tokenized (23 --c3l-* custom properties); reports surface migrated
  • NewQC observation drawer: counts per module, inspection details, open issues badge
  • FixLogin screen reuses one WebGL context for the pano background; previously leaked a context per visit
  • FixOrtho date-match: initial load was picking the page-1 date on multi-page flight lists
  • FixMap camera initializes from the active project, not a hardcoded constant; fixes blank aerial on first load
  • RefactorSingle source of truth for the displayed ortho URL; add-before-remove swap eliminates blank frames during date transitions