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

31 KiB

µ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:

#!/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

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
#!/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

# 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.