# µ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 (available: datetime, timedelta, secrets, os, json) source timestamp : python "datetime.now().strftime('%Y-%m-%d %H:%M')" source rand_hex : python "secrets.token_hex(4)" source uptime : python "str(timedelta(seconds=12345))" source hostname : python "os.uname().nodename" # 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 + the `uframe` package 3. Defines runtime helpers (`_shell`, `_read_file`, `_read_json`, etc.) 4. Executes source commands and evaluates conditionals/loops 5. Dynamically builds a `.uf` source string with resolved variables 6. Compiles that source with `uframe.compile()` at runtime 7. Prints the resulting Micron to stdout ```python #!/usr/bin/env python3 # Auto-generated by uFrame # Do not edit — regenerate with: uframe compile .uf import os, sys, json, subprocess, datetime, secrets, shlex from datetime import datetime as _dt_cls, timedelta # ─── Runtime Helpers ───────────────────────────────────────── def _shell(cmd, timeout=5): """Execute shell command, return stdout.""" try: return subprocess.check_output(cmd, shell=True, timeout=timeout).decode().strip() except Exception: return "" def _read_file(path): """Read file contents.""" # ... def _read_json(path): """Read and parse JSON file.""" # ... 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)) def _load_state(path): """Load state from JSON file.""" # ... def _save_state(path, data): """Save state to JSON file.""" # ... def _iter(val): """Make a value iterable for for-loops.""" # handles lists, dicts, newline-delimited strings # ─── µFrame Compile ────────────────────────────────────────── import uframe # ─── Page Logic ────────────────────────────────────────────── _cache_seconds = 0 _uf_source_parts = [] cpu_pct = eval('secrets.randbelow(60) + 20', {'datetime': _dt_cls, ...}) timestamp = eval("datetime.now().strftime('%H:%M:%S')", {'datetime': _dt_cls, ...}) _uf_source_parts.append(f'heading 1 "Resources"') _uf_source_parts.append(f'gauge "CPU" {cpu_pct} 100 28 warn=75.0 crit=90.0') _uf_source_parts.append(f'text "Updated: {timestamp}"') if cpu_pct > 90: _uf_source_parts.append(f'text "ALERT: CPU critical"') # ─── Render & Output ───────────────────────────────────────── _uf_source = f'''page "Live Status" 60 ''' + "\n".join(_uf_source_parts) result = uframe.compile(_uf_source, width=60) if _cache_seconds >= 0: print(f"#!c={_cache_seconds}") print(result.micron) ``` The key insight: the generated script **rebuilds `.uf` source** with live data substituted in, then compiles it with the full µFrame pipeline. This means every layout feature (boxes, gauges, tables, sparklines) works identically in both static and dynamic pages. ### 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 : python "secrets.randbelow(60) + 20" source mem : python "secrets.randbelow(40) + 50" source uptime : python "str(timedelta(seconds=secrets.randbelow(86400)))" source timestamp : python "datetime.now().strftime('%H:%M:%S')" 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 "Uptime" "$uptime" label "Updated" "$timestamp" text "@center{@italic{Press Ctrl+R to refresh}}" ``` Client hits the page → script runs → evaluates sources → renders gauges with live data → client sees it. Ctrl+R re-requests → fresh execution → updated values. On a full Linux node, replace the python sources with shell commands to read real system data: ``` source cpu : shell "grep 'cpu ' /proc/stat | awk '{print int(($2+$4)*100/($2+$4+$5))}'" source mem : shell "free | awk '/Mem/{print int($3/$2*100)}'" source uptime : shell "uptime -p" ``` ### 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.