There is a phase in software development that separates prototypes from products. The features work. The data flows. The pages render. But the edges are rough. A table scrolls off-screen on wide entities. A boolean field shows as a text input instead of a toggle. The sidebar has one color and cannot be customized. Entity definitions can only be created by writing .flin files -- there is no GUI.
Sessions 300 and 301 were the polish sessions. They did not add new pages or new architectural components. They took what existed and made it right. And Session 320, the final production session, eliminated every remaining mock data source, ensuring that the console showed reality and nothing else.
Entity Definition CRUD: phpMyAdmin for Real
Until Session 301, creating a new entity in FLIN required opening a text editor, writing a .flin file with the entity definition, saving it to the entities/ directory, and waiting for the file watcher to detect the change. This worked, but it broke the flow of the admin console. A developer browsing entities in /_flin had to leave the browser to create a new one.
Session 301 added full entity definition management directly in the console: create new entities, edit existing schemas, and delete entity definitions -- all from the GUI.
Creating an Entity
The "New Entity" button opens a modal with a dynamic form:
flin// Entity creation modal
<div class="entity-modal">
<h2>Create New Entity</h2>
<div class="form-group">
<label>Entity Name</label>
<input
type="text"
placeholder="ProductCategory"
validate={is_pascal_case}
/>
<span class="hint">Must start with uppercase (PascalCase)</span>
</div>
<h3>Fields</h3>
<table class="fields-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th></th>
</tr>
</thead>
<tbody>
{for field in fields}
<tr>
<td><input placeholder="fieldName" /></td>
<td>
<select>
<option>text</option>
<option>int</option>
<option>float</option>
<option>bool</option>
<option>file</option>
<option>time</option>
</select>
</td>
<td><input type="checkbox" /></td>
<td><input placeholder="default value" /></td>
<td><button click={remove_field(field)}>X</button></td>
</tr>
{/for}
</tbody>
</table>
<button click={add_field}>+ Add Field</button>
<button class="primary" click={save_entity}>Create Entity</button>
</div>The backend validates the entity definition with strict rules:
rustpub fn validate_entity_name(name: &str) -> Result<(), String> {
if name.is_empty() || name.len() > 64 {
return Err("Entity name must be 1-64 characters".into());
}
if !name.chars().next().unwrap().is_uppercase() {
return Err("Entity name must start with uppercase".into());
}
if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err("Entity name must be alphanumeric".into());
}
Ok(())
}
pub fn validate_field_name(name: &str) -> Result<(), String> {
let reserved = ["id", "created_at", "updated_at", "deleted_at", "version"];
if reserved.contains(&name) {
return Err(format!("'{}' is a reserved field name", name));
}
Ok(())
}When the developer clicks "Create Entity," the API generates a .flin file on disk:
rustpub fn generate_flin_entity_content(
name: &str,
fields: &[FieldDefinition],
) -> String {
let mut content = format!("entity {} {{\n", name);
for field in fields {
content.push_str(&format!(" {}: {}", field.name, field.field_type));
if field.required {
content.push_str(" @required");
}
if let Some(default) = &field.default_value {
content.push_str(&format!(" = {}", default));
}
content.push('\n');
}
content.push_str("}\n");
content
}The generated file is clean FLIN syntax:
flinentity Product {
title: text @required
price: float = 0.0
inStock: bool = true
category: text
}After creation, the entity list refreshes with a 500-millisecond delay to allow the file watcher to detect and register the new entity. The new entity is automatically selected in the entity dropdown.
Editing and Deleting
The "Edit Schema" button pre-fills the modal with the current entity's fields. The entity name input is disabled during editing -- renaming an entity would require a database migration, which is out of scope for a GUI tool.
The "Delete Entity" button opens a confirmation dialog that clearly explains the consequences: the .flin file will be deleted, but existing database records are preserved. Records of deleted entities become accessible as "inline entities" -- they still exist in the database and can be queried, they just no longer have a formal schema definition.
Bug Fixes: The Devil in the Details
Session 301 fixed three bugs that, while small, significantly impacted usability.
Horizontal Scroll for Wide Tables
Entities with many fields (Session 301's test case was a Person entity with 16 fields) pushed the records table off-screen. The Actions column (Edit and Delete buttons) was invisible. The fix was a single CSS property:
rust// The fix: one line of CSS
// #records-container { overflow-x: auto; }But the implication was significant. Without horizontal scroll, the CRUD operations appeared broken for wide entities. A developer encountering this bug would conclude that the entity browser did not support editing or deleting records -- when in reality the buttons were just hidden off the right edge of the viewport.
Sticky Actions Column
Even with horizontal scroll, the Actions column (Edit, Delete) scrolled out of view as the developer scrolled right to see more fields. The solution was CSS position: sticky:
flin// Sticky last column for actions
.records-table th:last-child,
.records-table td:last-child {
position: sticky
right: 0
background: var(--color-bg-primary)
z-index: 2
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.05)
}The shadow on the left edge of the sticky column provides a visual cue that more content exists to the left, preventing the common confusion of "why does this column look disconnected from the rest of the table?"
Background color overrides were needed for hover rows, selected rows, and header cells to prevent transparency artifacts where the sticky column would show content from cells scrolling beneath it.
Boolean Checkbox Fix
The edit and create modals rendered boolean fields as <input type="checkbox" value="true">. But HTML checkboxes use the checked attribute, not the value attribute, to determine their visual state. Every boolean field appeared unchecked regardless of its actual value. Saving a form set all booleans to false.
The fix replaced plain checkboxes with toggle switches:
flin// Toggle switch for boolean fields
fn render_bool_field(name, value) {
is_checked = value == true || value == "true"
<label class="form-toggle">
<input
type="checkbox"
name={name}
checked={is_checked}
/>
<span class="toggle-slider"></span>
<span class="toggle-label">
{if is_checked} "true" {else} "false" {/if}
</span>
</label>
}The toggle switch is visually larger and more obvious than a checkbox, with an animated slider that makes the on/off state immediately clear. The label text updates in real time as the toggle is clicked, showing "true" or "false."
Sidebar Theme Variations
Session 300 added three sidebar color themes, selectable from small circle buttons in the sidebar footer:
| Theme | Background | Active Tint | Character |
|---|---|---|---|
| Default | #f5f0eb (cream) | rgba(201, 149, 44, 0.15) | Warm, professional |
| Accented | Same cream | rgba(201, 149, 44, 0.25) | Bold, high-contrast |
| White | #ffffff (pure white) | rgba(201, 149, 44, 0.12) | Clean, minimal |
flin// Theme application
fn set_sidebar_theme(theme) {
sidebar = document.query("aside.sidebar")
sidebar.set_attribute("data-sidebar-theme", theme)
local_storage.set("flin-console-sidebar-theme", theme)
}
fn apply_sidebar_theme() {
saved = local_storage.get("flin-console-sidebar-theme")
if saved != none {
set_sidebar_theme(saved)
}
}All three themes have dark mode variants that automatically activate when the console is in dark mode. The theme preference persists in localStorage, surviving page refreshes and session restarts.
Session 320: The Production Milestone
Session 320 was the session that removed the word "soon" from the admin console. Seven pages had "Coming Soon" badges indicating that their data was mocked. Session 320 replaced every mock data source with real API endpoints:
| Page | Before Session 320 | After Session 320 |
|---|---|---|
| Users | Mock user table | Real User entity records |
| Logs | Placeholder entries | Real request log buffer |
| Metrics | Static gauges | Real AtomicU64 counters |
| Analytics | Sample chart data | Real route statistics |
| Realtime | Mock connection count | Real zero (honest) |
| AI Gateway | Hardcoded provider list | Real env var detection |
| Settings | Placeholder values | Real flin.config data |
Two new Rust modules were created for this session:
src/server/metrics.rs--AtomicU64request counters, per-route and per-status-code trackingsrc/server/log_buffer.rs-- Ring buffer (VecDeque, max 1,000 entries) for structured request logs
Six new API endpoints were added:
rust// New endpoints in Session 320
GET /_flin/api/logs // Filtered log retrieval
POST /_flin/api/logs/clear // Clear log buffer
GET /_flin/api/metrics // System metrics
GET /_flin/api/analytics // Request analytics
GET /_flin/api/ai-gateway // AI provider stats
GET /_flin/api/settings/general // App configurationThe Realtime page deserves special mention. Rather than showing mock data (which would mislead developers into thinking their app had WebSocket activity), it shows "0 active connections," "0 channels," and "0 messages/min" -- all accurate -- alongside documentation explaining how to enable real-time features. This is the "real data or honest empty state" principle in action.
The Final Tally
After Sessions 300, 301, and 320, the FLIN admin console reached production readiness:
- 19 pages, all showing real data
- 30+ API endpoints, all backed by the FLIN runtime
- Zero mock data across the entire console
- 3 sidebar themes with dark mode support
- Full entity CRUD including schema management from the GUI
- Production authentication with bcrypt, session tokens, and email 2FA
- Real-time monitoring with logs, metrics, and analytics
The console that started as a single dashboard page in Session 259 had grown into a comprehensive management tool that rivals standalone products like PocketBase's admin UI or Supabase's dashboard. The difference: it ships inside the FLIN binary, requires zero configuration, and is available at /_flin the moment the application starts.
This concludes Arc 13 -- the Admin Console arc. Ten articles covering how a programming language gained a built-in management dashboard that makes every other tool in the developer's stack feel incomplete. The next arc continues the journey into FLIN's ecosystem and tooling.
This is Part 145 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO polished an admin console from prototype to production in three focused sessions.
Series Navigation: - [144] Entity History and Temporal Views in Admin - [145] Console UI/UX Final Polish (you are here) - Next arc: FLIN Ecosystem and Tooling