feat: added a twist
This commit is contained in:
883
docs/dynamic-templates.md
Normal file
883
docs/dynamic-templates.md
Normal file
@@ -0,0 +1,883 @@
|
||||
# µ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.
|
||||
Reference in New Issue
Block a user