Files
micronomicon/docs/dynamic-templates.md
2026-04-01 00:53:55 +02:00

884 lines
32 KiB
Markdown

# µ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 (`\`<field\`placeholder>`, `\`<^|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.