884 lines
32 KiB
Markdown
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.
|