851 lines
31 KiB
Markdown
851 lines
31 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 (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 <source>.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.
|