# µFrame Dynamic Templates — Making Rich UIs Live on NomadNet ## Addendum to the µFrame Design Document (v3) --- ## 1. The NomadNet Dynamic Page Model Understanding how NomadNet serves dynamic pages is essential to understanding how µFrame templates become live applications. ### How it works NomadNet has a simple but powerful execution model, analogous to CGI on the early web: ``` Client Node Server ────── ─────────── 1. Browse to /page/dashboard.mu ───────────▶ 2. Is dashboard.mu executable? YES → run it NO → send contents as-is 3. Execute: #!/usr/bin/env python3 Script prints Micron to stdout 4. Capture stdout → send to client ◀─────────── 5. Render Micron in terminal ``` Key mechanics: - A `.mu` file without the execute bit is **static** — NomadNet sends its contents directly to the browsing client - A `.mu` file with the execute bit set is **dynamic** — NomadNet runs it as a subprocess and serves whatever it prints to stdout - The shebang line (`#!/usr/bin/env python3`) determines the interpreter — Python, Bash, Lua, Rust, anything that runs - The script must **terminate** — it cannot wait for input or run indefinitely - Cache behavior is controlled via a header line: `#!c=0` means never cache (always re-execute), `#!c=300` means cache for 5 min ### Form data flow Micron form fields (`\``, `\`<^|group|val\`label>`, etc.) collect user input. When the user clicks a link on a page that contains form fields, the field data is submitted along with the link request: ``` Page A (has form fields + a submit link) ┌───────────────────────────────────────────────┐ │ Name: `<24|name`Enter name...> │ │ Role: `<^|role|admin`Admin> `<^|role|user`User> │ │ │ │ `[Submit`:/page/handle.mu] │ └───────────────────────────────────────────────┘ User fills in "Alice", selects "Admin", clicks Submit │ ▼ Page B (handle.mu) — receives field data via environment variables, generates a response page with Micron output ``` The submitted field data is passed to the executable script through environment variables in the format: ``` FIELD_name=Alice FIELD_role=admin ``` Or via stdin as a URL-encoded or structured data payload (implementation varies by NomadNet version). The executable script reads these values and uses them to generate its output. --- ## 2. µFrame's Dynamic Compilation Model Here is the key insight: **µFrame doesn't just emit static Micron text — it can compile `.uf` templates into executable Python scripts that generate Micron at request time.** This gives us three output modes: ``` .uf source ──▶ Parser ──▶ IR ──┬──▶ Plain ASCII (static preview) ├──▶ Static .mu (static page) └──▶ Dynamic .mu (executable script) │ ▼ #!/usr/bin/env python3 # Auto-generated by µFrame # from: dashboard.uf import os, sys, json, subprocess ... print(rendered_micron) ``` The dynamic output is a self-contained Python script with: - The µFrame rendering engine embedded (or imported) - Data-fetching hooks that run at request time - Form field processing from environment variables - Conditional rendering based on submitted data - The full ASCII art + Micron generation pipeline ### The three modes compared ``` ┌──────────────────────────────────────────────────────────────┐ │ µFrame Output Modes │ ├────────────────┬───────────────┬─────────────────────────────┤ │ Plain ASCII │ Static .mu │ Dynamic .mu │ ├────────────────┼───────────────┼─────────────────────────────┤ │ Box-drawing │ Box-drawing │ Box-drawing │ │ Block chars │ Block chars │ Block chars │ │ Braille │ Braille │ Braille │ │ │ + Color │ + Color │ │ │ + Bold/italic │ + Bold/italic │ │ │ + Links │ + Links │ │ │ + Form fields │ + LIVE form fields │ │ │ │ + Server-side data binding │ │ │ │ + Conditional rendering │ │ │ │ + Form submission handling │ │ │ │ + System data at render time │ │ │ │ + State persistence │ ├────────────────┼───────────────┼─────────────────────────────┤ │ Local terminal │ NomadNet page │ NomadNet live application │ │ preview │ (cached) │ (re-executed per request) │ └────────────────┴───────────────┴─────────────────────────────┘ ``` --- ## 3. DSL Extensions for Dynamic Behavior ### 3.1 Data Sources — `source` blocks A `source` block declares where live data comes from. At render time, the generated script executes the source and binds the result to a variable. ``` # Shell command — output captured as string source cpu_pct : shell "grep 'cpu ' /proc/stat | awk '{print int(($2+$4)*100/($2+$4+$5))}'" source mem_pct : shell "free | awk '/Mem/{print int($3/$2*100)}'" source uptime : shell "uptime -p" source peers : shell "rnstatus -j | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d.get(\"peers\",[]))); '" # File read — contents loaded as string or parsed as JSON source motd : file "/etc/motd" source config : json "/home/node/.nomadnetwork/config.json" # Python expression — evaluated inline source timestamp : python "datetime.now().strftime('%Y-%m-%d %H:%M')" source rand_hex : python "secrets.token_hex(4)" # RNS/Reticulum API — direct integration source peer_list : rns "peers" source node_info : rns "identity" ``` Sources are resolved **at page render time** — every time a client requests the page, the commands run fresh. Usage in templates: ``` gauge "CPU" $cpu_pct 100 28 warn=75 crit=90 gauge "MEM" $mem_pct 100 28 warn=80 crit=95 label "Uptime" "$uptime" label "Peers" "$peers active" text "Last updated: $timestamp" ``` ### 3.2 Form Handling — `on_submit` blocks An `on_submit` block defines what happens when a form is submitted. It receives field values and controls what the page renders in response. ``` page "Search" 64 form "search" field "query" 30 "Enter search term..." radio "scope" "Local" | "Network" | "All" button "Search" "/page/search.mu" on_submit "search" # $query and $scope are now populated from submitted form data source results : shell "search_index.py '$query' --scope '$scope'" heading 2 "Results for: $query" if $results text "$results" else text "No results found." ``` The generated Python script handles this as: ```python #!/usr/bin/env python3 #!c=0 import os, subprocess # Read submitted form data query = os.environ.get("FIELD_query", "") scope = os.environ.get("FIELD_scope", "Local") if query: # Form was submitted — render results results = subprocess.check_output( ["search_index.py", query, "--scope", scope] ).decode().strip() # ... render results template with Micron ... else: # No submission — render the form # ... render form template with Micron ... ``` ### 3.3 Conditional Rendering — `if` / `else` / `elif` ``` source disk_pct : shell "df / | awk 'NR==2{print int($5)}'" if $disk_pct > 90 box heavy "DISK CRITICAL" color f00 gauge "Disk" $disk_pct 100 40 crit=90 text "@bold{@color{f00}{Immediate action required!}}" elif $disk_pct > 75 box light "Disk Warning" color ff0 gauge "Disk" $disk_pct 100 40 warn=75 else gauge "Disk" $disk_pct 100 40 ``` ### 3.4 Iteration — `for` loops ``` source peer_json : shell "rnstatus --json-peers" heading 2 "Active Peers" for peer in $peer_json row 2 col 30 text "$peer.name" col 10 status "$peer.name" $peer.state col 10 text "$peer.latency" ``` The `for` construct works with JSON arrays or newline-delimited text from shell commands. ### 3.5 State Persistence — `state` blocks Since each page request is a fresh script execution, state must be stored externally. µFrame provides a simple key-value store backed by a JSON file on the node: ``` state "counter" "/tmp/uframe_counter.json" # Read let visits = $counter.visits || 0 # Write (increments on each page load) set counter.visits = $visits + 1 text "This page has been viewed $counter.visits times." ``` For form-driven state (e.g., a guestbook): ``` state "guestbook" "/var/nomadnet/guestbook.json" form "sign" field "name" 20 "Your name..." field "message" 40 "Your message..." button "Sign" "/page/guestbook.mu" on_submit "sign" append guestbook.entries { name: $name, message: $message, time: $timestamp } text "@color{0f0}{Thanks, $name! Your message has been saved.}" heading 2 "Guestbook ($guestbook.entries.length entries)" for entry in $guestbook.entries box light text "@bold{$entry.name} — @italic{$entry.time}" text "$entry.message" spacer ``` ### 3.6 Page Navigation with Data — `link` with parameters Links can pass data to the target page via query-style encoding: ``` # Simple navigation link "Home" "/page/index.mu" # Navigation with parameters link "View Peer $peer.name" "/page/peer_detail.mu?hash=$peer.hash" # In the target page, access with: source peer_hash : param "hash" ``` ### 3.7 Cache Control ``` page "Dashboard" 64 cache 0 # never cache — always re-execute # cache 60 # cache for 60 seconds # cache none # alias for 0 source cpu : shell "..." ... ``` Translates to the Micron header `#!c=0` in the first line of output. --- ## 4. Compilation Pipeline — .uf to Executable .mu ### 4.1 What the compiler generates A `.uf` file with dynamic features compiles into a Python script that: 1. Sets the shebang and cache header 2. Imports required modules 3. Reads environment variables (form data) 4. Executes source commands (shell, file, python, rns) 5. Evaluates conditionals and loops 6. Renders the IR tree into a CharGrid 7. Emits the CharGrid as Micron with style tags 8. Prints to stdout ```python #!/usr/bin/env python3 #!c=0 # Auto-generated by µFrame from dashboard.uf # Do not edit — regenerate with: uframe compile dashboard.uf import os, sys, json, subprocess, datetime, secrets # ─── µFrame Runtime (embedded) ─────────────────────────────── class CharGrid: """2D character grid with style annotations.""" def __init__(self, width, height): self.w = width self.h = height self.chars = [[' ']*width for _ in range(height)] self.styles = [[None]*width for _ in range(height)] def put(self, x, y, ch, style=None): if 0 <= x < self.w and 0 <= y < self.h: self.chars[y][x] = ch self.styles[y][x] = style def box(self, x, y, w, h, weight='light', title=None, title_style=None): """Draw a box with automatic border characters.""" # ... border drawing logic ... def gauge(self, x, y, w, value, max_val, label=None, warn=None, crit=None): """Render a horizontal gauge bar with threshold colors.""" pct = min(value / max_val, 1.0) filled = int(w * pct) for i in range(w): ch = '█' if i < filled else '░' fg = None if crit and value >= crit: fg = 'f00' elif warn and value >= warn: fg = 'ff0' elif i < filled: fg = '0f0' else: fg = '555' self.put(x + i, y, ch, {'fg': fg}) # ... label and percentage ... def sparkline(self, x, y, w, values): """Render braille sparkline from value array.""" # ... braille pattern generation ... def emit_micron(self): """Scan grid and emit Micron with style tags.""" lines = [] for row_idx in range(self.h): line = [] cur_style = None for col_idx in range(self.w): ch = self.chars[row_idx][col_idx] st = self.styles[row_idx][col_idx] if st != cur_style: # Close previous style tags if cur_style: if cur_style.get('fg'): line.append('`f') if cur_style.get('bold'): line.append('`!') # Open new style tags if st: if st.get('bold'): line.append('`!') if st.get('fg'): line.append(f'`F{st["fg"]}') cur_style = st line.append(ch) # Close final style if cur_style: if cur_style.get('fg'): line.append('`f') if cur_style.get('bold'): line.append('`!') lines.append(''.join(line).rstrip()) return '\n'.join(lines) # ─── Form Data ─────────────────────────────────────────────── def get_field(name, default=''): """Read submitted form field from environment.""" return os.environ.get(f'FIELD_{name}', default) def get_param(name, default=''): """Read URL parameter.""" return os.environ.get(f'PARAM_{name}', os.environ.get(f'var_{name}', default)) # ─── Data Sources ──────────────────────────────────────────── def shell(cmd): """Execute shell command, return stdout.""" try: return subprocess.check_output( cmd, shell=True, timeout=5 ).decode().strip() except Exception: return '' # ─── Resolve Sources ───────────────────────────────────────── cpu_pct = int(shell( "grep 'cpu ' /proc/stat | awk '{print int(($2+$4)*100/($2+$4+$5))}'" ) or 0) mem_pct = int(shell( "free | awk '/Mem/{print int($3/$2*100)}'" ) or 0) uptime_str = shell("uptime -p") peer_count = shell("rnstatus -j 2>/dev/null | python3 -c " "'import sys,json; print(len(json.load(sys.stdin).get(\"peers\",[])))'") timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') # ─── Build Grid & Render ──────────────────────────────────── grid = CharGrid(66, 40) # ... all the box(), gauge(), sparkline(), text() calls # ... exactly as the layout engine would produce them ... # ─── Output ────────────────────────────────────────────────── print('#!c=0') # cache header: never cache print(grid.emit_micron()) ``` ### 4.2 CLI usage ```bash # Compile to dynamic executable .mu uframe compile dashboard.uf --out dashboard.mu chmod +x dashboard.mu # Compile and deploy in one step uframe deploy dashboard.uf # → renders, sets +x, copies to ~/.nomadnetwork/storage/pages/ # Compile with embedded vs. imported runtime uframe compile dashboard.uf --embed # single self-contained file uframe compile dashboard.uf --import # requires uframe_runtime.py on node ``` ### 4.3 Compilation modes ``` ┌────────────────────────────────────────────────────────────────┐ │ µFrame Compilation Modes │ ├────────────────┬───────────────┬───────────────────────────────┤ │ uframe render │ uframe render │ uframe compile │ │ --ascii │ --micron │ │ ├────────────────┼───────────────┼───────────────────────────────┤ │ Static ASCII │ Static .mu │ Executable .mu (Python) │ │ to stdout │ file │ file with +x │ ├────────────────┼───────────────┼───────────────────────────────┤ │ All values │ All values │ source{} values fetched │ │ resolved at │ resolved at │ at request time │ │ render time │ render time │ │ │ │ │ Form fields become live │ │ │ │ on_submit{} blocks active │ │ │ │ if/for evaluated per request │ │ │ │ State persists across visits │ └────────────────┴───────────────┴───────────────────────────────┘ ``` --- ## 5. Complete Dynamic Example ### 5.1 Source — `search_node.uf` ``` page "Node Search" 64 cache 0 source timestamp : python "datetime.now().strftime('%H:%M:%S')" source peer_count : shell "rnstatus 2>/dev/null | grep -c 'Peer'" state "history" "/var/nomadnet/search_history.json" box double "Node Search" align center text "Find peers and pages on the Reticulum mesh" text "@italic{$peer_count peers reachable · updated $timestamp}" spacer form "search" field "query" 30 "Search term..." radio "type" "Nodes" | "Pages" | "Files" checkbox "cache" "Include cached results" button "Search" "/page/search_node.mu" on_submit "search" # Log the search append history.queries { q: $query, type: $type, time: $timestamp } source results : shell "mesh_search.py '$query' --type '$type'" source result_count : python "len('''$results'''.strip().splitlines())" divider light heading 2 "Results for \"$query\" ($result_count found)" if $result_count > 0 for line in $results source parts : python "'''$line'''.split('|')" row 1 col 28 link "$parts.0" "/page/detail.mu?hash=$parts.1" col 8 text "$parts.2" col 10 status "$parts.0" $parts.3 else spacer text "@center{@color{ff0}{No results found for \"$query\"}}" spacer divider light heading 3 "Recent Searches" for entry in $history.queries[-5:] text " $entry.time $entry.q ($entry.type)" divider heavy text "@center{@italic{Relay Alpha-7 · $timestamp}}" ``` ### 5.2 What the client sees **Before submission** (form is empty): ``` ╔══ Node Search ═══════════════════════════════════════════════╗ ║ Find peers and pages on the Reticulum mesh ║ ║ 7 peers reachable · updated 14:32:07 ║ ╚══════════════════════════════════════════════════════════════╝ Search: [ Search term...___________________ ] Type: (•) Nodes ( ) Pages ( ) Files [ ] Include cached results `[`!Search`!`:/page/search_node.mu] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Relay Alpha-7 · 14:32:07 ``` **After submitting "relay"** (script re-executes with FIELD_query=relay): ``` ╔══ Node Search ═══════════════════════════════════════════════╗ ║ Find peers and pages on the Reticulum mesh ║ ║ 7 peers reachable · updated 14:32:15 ║ ╚══════════════════════════════════════════════════════════════╝ Search: [ relay__________________________ ] Type: (•) Nodes ( ) Pages ( ) Files [ ] Include cached results `[`!Search`!`:/page/search_node.mu] ────────────────────────────────────────────────────────────── >> Results for "relay" (3 found) `F0cfRelay-East`f 2 hops `F0f0●`f online `F0cfRelay-South`f 4 hops `F0f0●`f online `F0cfRelay-Backup`f 6 hops `Fff0◐`f degraded ────────────────────────────────────────────────────────────── >>> Recent Searches 14:32:15 relay (Nodes) 14:28:44 bridge (Pages) 14:25:01 firmware (Files) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Relay Alpha-7 · 14:32:15 ``` The form fields retain submitted values, results appear below, and the search history persists across visits via the state file. Every piece of box-drawing, every colored indicator, every braille sparkline renders identically in both ASCII preview and live Micron. --- ## 6. Dynamic Patterns — A Cookbook ### 6.1 Live Dashboard (auto-refresh via cache=0) ``` page "Status" 64 cache 0 source cpu : shell "cat /proc/loadavg | awk '{print int($1*100/$(nproc))}'" source mem : shell "free | awk '/Mem/{print int($3/$2*100)}'" source net_in : shell "net_traffic.sh in" source net_out : shell "net_traffic.sh out" source net_history_in : shell "net_spark.sh in 20" source net_history_out : shell "net_spark.sh out 20" box heavy "System Status" row 2 gauge "CPU" $cpu 100 28 warn=75 crit=90 gauge "MEM" $mem 100 28 warn=80 crit=95 spacer label "IN" "$net_in KB/s" sparkline "IN" $net_history_in 28 label "OUT" "$net_out KB/s" sparkline "OUT" $net_history_out 28 text "@center{@italic{Press Ctrl+R to refresh}}" ``` Client hits the page → script runs → reads `/proc` → renders gauges and sparklines with real data → client sees it. Ctrl+R re-requests → fresh execution → updated values. ### 6.2 Guestbook with Persistent State ``` page "Guestbook" 64 cache 0 state "gb" "/var/nomadnet/guestbook.json" source timestamp : python "datetime.now().strftime('%Y-%m-%d %H:%M')" heading 1 "Guestbook" form "sign" field "name" 20 "Your name" field "msg" 40 "Leave a message..." button "Sign" "/page/guestbook.mu" on_submit "sign" if $name && $msg prepend gb.entries { name: $name, msg: $msg, time: $timestamp } text "@color{0f0}{✓ Thanks, $name!}" divider light for entry in $gb.entries[:20] box rounded text "@bold{$entry.name} @italic{@color{888}{$entry.time}}" text "$entry.msg" spacer ``` ### 6.3 Multi-Page Wizard with Navigation ``` # Page 1: setup.mu page "Setup Wizard — Step 1" 64 cache 0 heading 1 "Network Configuration" form "net" field "interface" 20 "eth0" radio "mode" "Auto" | "Manual" | "Mesh Only" button "Next →" "/page/setup_2.mu" # Page 2: setup_2.mu page "Setup Wizard — Step 2" 64 cache 0 source iface : param "interface" # or field from previous page source mode : param "mode" heading 1 "Confirm Settings" label "Interface" "$iface" label "Mode" "$mode" form "confirm" checkbox "apply_now" "Apply immediately" button "← Back" "/page/setup.mu" button "Finish ✓" "/page/setup_done.mu?interface=$iface&mode=$mode" ``` ### 6.4 Chat Room (community pattern) ``` page "Chat" 64 cache 0 state "chat" "/var/nomadnet/chatlog.json" source timestamp : python "datetime.now().strftime('%H:%M')" heading 1 "Node Chat" # Display last 15 messages for msg in $chat.messages[-15:] text "@bold{@color{$msg.color}{$msg.nick}} @color{888}{$msg.time}" text " $msg.text" divider light form "send" field "nick" 12 "Nickname" field "text" 40 "Type message..." button "Send" "/page/chat.mu" on_submit "send" if $nick && $text source color : python "format(hash('$nick')%4095,'03x')" append chat.messages { nick: $nick, text: $text, time: $timestamp, color: $color } text "@center{@italic{@color{888}{Ctrl+R to refresh · $chat.messages.length messages}}}" ``` ### 6.5 Interactive Data Explorer ``` page "Peer Explorer" 64 cache 0 source peers_json : shell "rnstatus --json 2>/dev/null" source selected : param "hash" heading 1 "Peer Explorer" table "Peers" columns "Name" 24 | "Hops" 6 | "RTT" 8 | "Status" 10 for peer in $peers_json.peers row "$peer.name" | "$peer.hops" | "$peer.rtt" | "$peer.status" if $selected divider heavy source detail : shell "rnstatus --peer $selected --json 2>/dev/null" box double "Peer Detail: $detail.name" label "Hash" "$detail.hash" label "Address" "$detail.address" label "Hops" "$detail.hops" label "Latency" "$detail.rtt" label "Last Seen" "$detail.last_seen" label "Transport" "$detail.transport" sparkline "Latency (24h)" $detail.latency_history 40 row 2 link "Ping" "/page/action.mu?cmd=ping&hash=$detail.hash" link "Trace" "/page/action.mu?cmd=trace&hash=$detail.hash" link "Browse" "$detail.hash:/page/index.mu" ``` --- ## 7. Security Considerations Dynamic pages execute code on the node server. µFrame enforces: - **Shell command sanitization**: all `$variable` values interpolated into shell commands are escaped with `shlex.quote()` to prevent injection - **State file isolation**: state files are restricted to a configurable directory (default: `/var/nomadnet/uframe/`) - **Execution timeout**: all shell sources have a default 5-second timeout, configurable per source - **No network egress by default**: source commands run in the node's local context — they can read local system data but µFrame does not add network capabilities beyond what the scripts themselves invoke - **Input validation**: field values are length-limited and sanitized before use in sources or state operations ``` # In the DSL, explicit sanitization: source result : shell "search.py" --arg $query --sanitize timeout 10 max_length 256 ``` --- ## 8. Architecture Summary ``` ┌──────────────────────────────┐ │ .uf Source │ │ (layout + sources + forms │ │ + conditionals + state) │ └──────────────┬───────────────┘ │ ┌────▼────┐ │ Parse │ └────┬────┘ │ ┌────▼────┐ │ IR │ │ Tree │ └──┬───┬──┘ │ │ ┌──────────────┘ └────────────────┐ │ │ ┌──────▼──────┐ ┌───────▼───────┐ │ render mode │ │ compile mode │ │ (immediate) │ │ (codegen) │ └──┬───────┬──┘ └───────┬───────┘ │ │ │ ┌──────▼┐ ┌───▼─────┐ ┌───────▼────────┐ │ ASCII │ │ Static │ │ Executable .mu │ │ stdout│ │ .mu file│ │ Python script │ └───────┘ └─────────┘ │ with embedded │ │ runtime + │ │ data sources + │ │ form handling │ └───────┬────────┘ │ chmod +x deploy to │ ┌─────────▼─────────┐ │ NomadNet Node │ │ ~/.nomadnetwork/ │ │ storage/pages/ │ │ │ │ Client request → │ │ Execute script → │ │ Stdout = Micron │ │ with live data, │ │ rich ASCII art, │ │ color + forms │ └───────────────────┘ ``` The dynamic model turns µFrame from a static template engine into a **full application framework for NomadNet** — where the same DSL that defines the visual layout also defines the data flow, user interaction, and server-side logic. The ASCII art isn't decoration — it's the UI of a live, interactive, decentralized application running over encrypted mesh networks.