"Can you add size filtering?" A simple question from a user in Discord. Four words that, on the surface, sound trivial. But when you trace the data flow behind the Smart Assemblies panel, you realize it touches everything: a Docker container reading from a PostgreSQL database indexed from blockchain data, Cloudflare KV snapshots, and a React frontend with Three.js visualization.
We shipped it in 45 minutes. This is the story of how—and why the tooling and documentation investment made it possible.
The Request
EVE Frontier has different sizes of deployable structures. SSUs (Smart Storage Units), Hangars, Shipyards, Printers, and Refineries all come in Small, Medium, and Large variants. The Smart Assemblies panel already let users filter by type (show only SSUs) and status (show only online structures). But there was no way to filter by size.
A user asked: "Can I see only Large SSUs?" Reasonable request. Let's trace what needs to change.
The Architecture (Why This Seems Hard)
Here's the data flow for Smart Assemblies:
- Primordium (pg-indexer) reads blockchain events and writes to PostgreSQL
- Snapshot Exporter (Docker container) queries Postgres, aggregates counts by system/type/status
- Cloudflare KV stores the snapshot JSON (cached globally)
- Worker API serves the snapshot to the frontend
- React Frontend reads the snapshot, renders filter UI, colors stars on the Three.js map
Adding size filtering means:
- Modifying the SQL query in the snapshot exporter
- Adding a new data dimension to the snapshot JSON schema
- Adding TypeScript types for sizes
- Adding UI components for size filter pills
- Wiring up the filtering logic in App.tsx
- Rebuilding and deploying the Docker container
- Deploying the frontend to Cloudflare Pages
Seven distinct changes across four different systems. Sounds like a multi-day project, right?
The 45-Minute Timeline
First question: does the data even exist? Checked the Assembly API response—yes, type_id is present and maps to specific sizes. Created the mapping table.
Added size classification SQL to structure_snapshot_exporter.js. A CASE statement maps type_id to S/M/L.
Extended the snapshot to include sizes[type][size][status] alongside existing counts[type][status]. Parallel structure, no breaking changes.
Added SmartAssemblySize type, constants, labels, and defaults to smartAssembly.ts.
Extended useSmartAssemblyManagement.ts with size state and localStorage persistence.
Added size filter pills to SmartAssembliesPanel.tsx, matching existing type/status pill styling.
Connected size state to panel props, modified smartAssemblyFiltered useMemo to use the new sizes data.
TypeScript compilation passed. Vite build succeeded.
Rebuilt Docker image, restarted container, manually triggered snapshot export. Verified KV now contains sizes field.
Deployed preview to Cloudflare Pages. Tested in browser—size filtering works.
Committed, pushed, deployed to production. User confirmed feature works.
The Type ID Mapping
The key insight: every Smart Assembly variant has a unique type_id in the blockchain data. We created a comprehensive mapping:
| Type | Size | Type ID |
|---|---|---|
| SSU | Large | 77917 |
| SSU | Medium | 88083 |
| SSU | Small | 88082 |
| Hangar | Large | 88094 |
| Hangar | Medium | 88093 |
| Shipyard | Large | 88071 |
| Shipyard | Medium | 88070 |
| Shipyard | Small | 88069 |
| Printer | Large | 87120 |
| Printer | Medium | 88067 |
| Printer | Small | 87119 |
| Refinery | Large | 88064 |
| Refinery | Medium | 88063 |
This mapping became a SQL CASE statement in the snapshot exporter:
const sizeClassificationSql = `
CASE type_id
WHEN 77917 THEN 'L' -- SSU Large
WHEN 88083 THEN 'M' -- SSU Medium
WHEN 88082 THEN 'S' -- SSU Small
WHEN 88094 THEN 'L' -- Hangar Large
WHEN 88093 THEN 'M' -- Hangar Medium
-- ... etc
ELSE 'M' -- Default to Medium for unknown types
END as size_class
`;
The Snapshot Schema Extension
The existing snapshot structure stored counts like this:
{
"systems": {
"30000142": {
"counts": {
"ssu": { "online": 5, "offline": 2 },
"hangar": { "online": 3, "offline": 1 }
}
}
}
}
We added a parallel sizes structure:
{
"systems": {
"30000142": {
"counts": { ... },
"sizes": {
"ssu": {
"L": { "online": 2, "offline": 1 },
"M": { "online": 2, "offline": 1 },
"S": { "online": 1, "offline": 0 }
},
"hangar": {
"L": { "online": 2, "offline": 0 },
"M": { "online": 1, "offline": 1 }
}
}
}
}
}
This parallel structure means:
- No breaking changes: Old code using
countsstill works - Backward compatible: Frontend can fall back if
sizesis missing - Flexible filtering: Can combine type + size + status filters
The Frontend Changes
TypeScript types make refactoring safe. We defined:
export const SMART_ASSEMBLY_SIZES = ['S', 'M', 'L'] as const;
export type SmartAssemblySize = typeof SMART_ASSEMBLY_SIZES[number];
export const SMART_ASSEMBLY_SIZE_LABELS: Record<SmartAssemblySize, string> = {
S: 'Small',
M: 'Medium',
L: 'Large',
};
// Which types support size variants
export const SIZED_ASSEMBLY_TYPES: SmartAssemblyType[] = [
'ssu', 'hangar', 'shipyard', 'smartPrinter', 'refinery'
];
The hook gained new state with localStorage persistence:
const [smartAssemblySizes, setSmartAssemblySizes] = useState<Record<SmartAssemblySize, boolean>>(
() => {
const stored = localStorage.getItem('smart-assembly-sizes');
return stored ? JSON.parse(stored) : SMART_ASSEMBLY_DEFAULT_SIZES;
}
);
// Persist changes
useEffect(() => {
localStorage.setItem('smart-assembly-sizes', JSON.stringify(smartAssemblySizes));
}, [smartAssemblySizes]);
The panel UI added a new row of filter pills, styled identically to the existing type and status filters. We fixed an inconsistency during development—all selected states now use var(--accent) for visual consistency.
Why 45 Minutes Was Possible
This wasn't magic. It was the compound return on months of infrastructure investment: comprehensive documentation, consistent patterns, typed everything, and an AI assistant that understood the codebase because it helped build it.
1. Documentation-First Development
Every component has clear documentation. The vibe coding methodology we use means the AI assistant has access to:
copilot-instructions.mdwith architectural patternsAGENTS.mdwith operational guardrailsdecision-log.mdwith historical context- Type definitions that serve as executable documentation
2. Consistent Patterns
The new size filter follows exactly the same pattern as the existing type and status filters:
- Same state management approach (hook + localStorage)
- Same UI component structure (pill buttons)
- Same data flow (snapshot → hook → panel → App.tsx)
When you establish patterns and stick to them, new features become fill-in-the-blank exercises.
3. The AI Understands the Code
This is the non-obvious part. The AI assistant (GitHub Copilot in agent mode) helped build this codebase over months. When asked to add size filtering, it:
- Knew where the snapshot exporter lives
- Knew the existing snapshot schema
- Knew the TypeScript type patterns we use
- Knew the hook/panel/App.tsx wiring pattern
- Knew our Docker deployment workflow
There was no "let me read through the codebase" phase. The context was already there.
4. End-to-End Ownership
One person (with AI assistance) owns the entire stack. No handoffs. No "waiting for the backend team." No pull request review queues. When you can modify the blockchain indexer, snapshot exporter, Worker API, and React frontend in one session, iteration speed is bounded only by typing and deployment time.
The Bigger Picture
This 45-minute feature represents something important about modern development workflows. The traditional estimate for this feature would be:
- Backend: 1-2 days (schema changes, API updates)
- Frontend: 1-2 days (UI components, state management)
- DevOps: Half day (container rebuild, deployment)
- QA: Half day (testing across environments)
- Total: 3-5 days
We did it in 45 minutes because:
- Documentation compounds. Every hour spent documenting patterns pays back 10x in faster future work.
- Typed languages catch errors at compile time, not in production.
- AI assistants work best on codebases they helped build. The context is implicit.
- Full-stack ownership eliminates coordination overhead. No meetings, no handoffs, no waiting.
- Preview deployments enable confident shipping. Test in isolation, then promote to production.
The Technical Debt We Paid Down
Along the way, we fixed a styling inconsistency. The original panel had:
- Type filters: Blue when selected
- Status filters: Orange when selected
- Size filters (new): Initially purple when selected
Three different colors for essentially the same interaction pattern. We unified everything to use var(--accent)—now all selected filters use the same theme-aware accent color. Small polish, but it's the kind of thing that makes a UI feel cohesive.
Docker Container Versioning
After the feature was live, we followed our container versioning workflow:
- Committed the snapshot exporter changes
- Tagged as
snapshot-exporter-v1.1.0 - Pushed tag to trigger GitHub Actions → GHCR build
- Updated docker-compose to use the new version
This ensures we can roll back to v1.0.0 if anything breaks, and the container image is immutably versioned in GitHub Container Registry.
Lessons for Your Projects
Invest in Documentation Early
It feels slow at first. "I could just write the code." But documentation creates leverage. It lets AI assistants understand your patterns. It lets future-you (or future-AI) work faster.
Establish Patterns and Stick to Them
Every new feature that follows an existing pattern is nearly free. Every feature that invents a new pattern costs the "pattern tax" forever.
Own the Full Stack
Coordination overhead is the hidden killer of velocity. If you can modify everything from database to UI, you can iterate in minutes instead of days.
Trust the AI With Context
AI coding assistants aren't magic. They're pattern matchers. If your patterns are clear, consistent, and documented, the AI becomes a force multiplier. If your code is a mess, the AI will generate more mess.
Try It
The size filtering is live now on EF-Map. Open the Smart Assemblies panel, and you'll see filter pills for Small, Medium, and Large. Combine them with type and status filters to find exactly the structures you're looking for.
45 minutes from user request to production. That's what good tooling, documentation, and AI assistance can achieve.
Related Posts
- Vibe Coding: Building a 124,000-Line Project Without Writing Code - The methodology behind this rapid development
- Smart Assemblies Expansion: Tracking Portable Structures - The original Smart Assemblies feature
- Database Architecture: From Blockchain to Queryable Data - The indexing pipeline that makes this possible
- Reducing Cloud Costs by 93%: A Cloudflare KV Story - The KV optimization that keeps snapshots fast