GCS Build Log: Days 0–6
OK so the first post was the architecture — the clean version, layers and types and all that. This one is the messy version: the day-by-day, the decisions, the bugs I chased down completely the wrong hole. Honestly, ito yung mas masaya (this is the more fun one).
I keep it honest on purpose. Most engineering blogs edit out the deviations — the "this didn't work and here's why" parts. Those are the parts actually worth reading.
Quick recap
One console to fly drones, drive ground robots, sail boats, and dive underwater vehicles — all on one screen. Multi-domain from day one, license-clean, works without internet. No cloud, no vendor lock-in. That's the whole pitch.
The screenshots are behind the password wall. The journal below is open — no password needed.
Starting with the boring stuff
First day and I wrote zero features. The whole day was just licenses and project rules. And to be honest, ito yung bahagi na pinaka-hindi ko alam (this is the part I know least about). 25 years writing software, a lot of it in defense, but license discipline and IP record-keeping was always somebody else's job. I plugged in at the engineering level, never the legal-review level. So this is genuinely new to me.
Picked Apache-2.0 over MIT. Both permissive, but Apache has a patent grant clause that matters to defense customers more than people realize. Made a dependency allowlist — MIT, BSD, Apache, ISC only. GPL and AGPL are out, because they'd force a defense customer to expose their own code. CI fails the build if something forbidden sneaks in. Like 50 lines of script. Cheap forever.
The fun part: the Command type won't even compile for an "engage effector" command without an AuthorizationToken. Not a runtime check — a compiler check. A runtime check can be skipped or forgotten in some code path. A compile check stops the build entirely. Different bar. End of day: 36 tests passing, lint clean. Walang feature pero solid yung pundasyon (no features, but the foundation is solid).
Writing the docs nobody reads (until they do)
Here's the thing nobody tells you about defense companies: when they evaluate something, they don't open the code. They open the docs. CONOPS first, then requirements, then technical requirements. If those are vague, tapos na usapan (the conversation's over). So I wrote them.
The CONOPS was the hardest. I've done docs like this before, but mostly on the technical-requirements side — the CONOPS always came down from above. Writing it top-down, as the architect myself, is new. It forced me to actually list who uses this: a pilot, payload operator, mission commander, a supervisor whose only job is approving dangerous stuff (two-person consent — that's literally the law, 10 USC §130i), an RF manager, a maintainer.
The competitive doc was the most fun — head-to-head against Lattice, Auterion, QGroundControl, Skydio, DJI. The trick I learned: list the things you don't compete on. I'm not catching Anduril on autonomy — they have years of GPU clusters and dedicated AI teams. Aminin mo na lang (just admit it). It makes the parts where you do compete more believable.
Making it look right
A tactical console can't look like a SaaS dashboard. But it also can't be so spy-movie that you roll your eyes. White-label theming I've built before — one codebase, many brands. What's new is building the token system itself from scratch.
Built six palettes you swap with the number keys. Carbon is the default — modern defense, parang F-35 cockpit (like an F-35 cockpit). Then Matrix, Woodland, Dossier, Forge, Plum. Every color and spacing lives in one TypeScript object; a small function emits it all as CSS variables, so there's one source driving both sides.
The decision I think matters most: split the codebase into a core plus per-market packages — gov and commercial. A defense build is core + gov, with no commercial code in it. I almost used a runtime flag. The pair-programmer pointed out the problem: with a flag, the code is still in the binary — the audit story becomes "trust the flag." That's hard to defend in a defense review. Package-level partition, you can prove it by reading the dependency list. Mas matibay (stronger). Went with that.
The map goes live
For three days, the center of the console was a striped rectangle that said MAP (TBD). The Common Operating Picture — one map, every domain on it — that's the whole heart of the product. And it was a placeholder. Ngayon, hindi na (today, not anymore).
Built @gcs/map on MapLibre: one marker per asset rotated by heading, a track line trailing each path, fly-to-your-location on load. The air glyph is swept-wing — the shape itself tells you the direction, not an arrow you have to squint at.
Two honest deviations
The air-gap basemap didn't ship. I did slice 1 (OpenStreetMap, license-clean) but not slice 2 (the offline map file), which is the actual reason you can claim "air-gapped." So until it lands, I won't say air-gapped in a demo. That README line isn't true yet.
And I shipped it without tests. The gate's green only because there are no map tests to fail. That's not green — that's untested. Flagging it loud, fixing it first next session.
Making the map testable (and hitting a wall)
The map had zero tests because you can't run WebGL in a headless test runner. The lazy fix is to mock the whole map library — but that's brittle, and you mostly test the mock. The better fix also makes the code better: separate the pure decisions from the imperative shell.
A map component decides (which glyph for this domain? what coordinates for the track?) and it acts (call MapLibre). The deciding is pure logic — no WebGL, no DOM — and that's where the bugs hide. Pulled it into three pure modules. Result: 21 tests, from zero. Gate went 134 → 155.
Also: I put the project under version control for the first time today. Before, it was just saved files. Nakakahiya pero totoo (embarrassing but true). A paper trail starts with actually having one.
The toolchain wall
Then the desktop app refused to launch — ESM crash on Electron boot. I worked it methodically, ruled out the app code, and concluded it's a toolchain interop issue, not a defect. And the real decision: I parked it instead of forcing it. A big dependency bump at the end of a session is how you wake up to a broken license gate. Better to hand off a green tree with a clear diagnosis than a half-finished upgrade. That restraint is a skill, not a cop-out.
Design before code (and a ghost bug)
First, the ghost
The desktop app wouldn't launch for three sessions. And the error changed every time I "fixed" it. When the symptom moves every time you touch it, that's the tell: you're not looking at the real bug. It was one environment variable — ELECTRON_RUN_AS_NODE=1, leaking in from the editor. It tells Electron to run as plain Node, so it never becomes an app. Clear it, launches first try. Tatlong session, isang variable (three sessions, one variable). A lesson I keep relearning: a shifting error means stop fixing and start locating.
Then the real work
One capability into the code, I stopped and wrote a design doc instead. "Control everything" sounds like a feature — it's not. It touches the type model, sim engine, adapters, UI, and how the system behaves with 20 vehicles up at once. Wire it in piece by piece and you get an architecture you have to apologize for.
The shape: only motion is domain-specific — a drone flies, a rover drives, a sub dives. Sensors, payload, mission, comms — all shared. Security as the foundation, not a defense feature — even a commercial survey company or a police department needs anti-spoofing and an audit trail just as much.
Screenshots and deeper technical notes
The console screenshots, command palette walkthrough, and engineering notes on the map architecture and capabilities design.
Or just say hi — I like talking about engineering.