feat: composer improvements
This commit is contained in:
@@ -158,9 +158,11 @@ source peers : shell "rnstatus -j | python3 -c 'import sys,json; d=json.load(s
|
||||
source motd : file "/etc/motd"
|
||||
source config : json "/home/node/.nomadnetwork/config.json"
|
||||
|
||||
# Python expression — evaluated inline
|
||||
# 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"
|
||||
@@ -346,135 +348,95 @@ 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
|
||||
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
|
||||
#!c=0
|
||||
# Auto-generated by µFrame from dashboard.uf
|
||||
# Do not edit — regenerate with: uframe compile dashboard.uf
|
||||
# Auto-generated by uFrame
|
||||
# Do not edit — regenerate with: uframe compile <source>.uf
|
||||
|
||||
import os, sys, json, subprocess, datetime, secrets
|
||||
import os, sys, json, subprocess, datetime, secrets, shlex
|
||||
from datetime import datetime as _dt_cls, timedelta
|
||||
|
||||
# ─── µFrame Runtime (embedded) ───────────────────────────────
|
||||
# ─── Runtime Helpers ─────────────────────────────────────────
|
||||
|
||||
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):
|
||||
def _shell(cmd, timeout=5):
|
||||
"""Execute shell command, return stdout."""
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
cmd, shell=True, timeout=5
|
||||
).decode().strip()
|
||||
return subprocess.check_output(cmd, shell=True, timeout=timeout).decode().strip()
|
||||
except Exception:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
# ─── Resolve Sources ─────────────────────────────────────────
|
||||
def _read_file(path):
|
||||
"""Read file contents."""
|
||||
# ...
|
||||
|
||||
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')
|
||||
def _read_json(path):
|
||||
"""Read and parse JSON file."""
|
||||
# ...
|
||||
|
||||
# ─── Build Grid & Render ────────────────────────────────────
|
||||
def _get_field(name, default=""):
|
||||
"""Read submitted form field from environment."""
|
||||
return os.environ.get(f"FIELD_{name}", default)
|
||||
|
||||
grid = CharGrid(66, 40)
|
||||
def _get_param(name, default=""):
|
||||
"""Read URL parameter."""
|
||||
return os.environ.get(f"PARAM_{name}",
|
||||
os.environ.get(f"var_{name}", default))
|
||||
|
||||
# ... all the box(), gauge(), sparkline(), text() calls
|
||||
# ... exactly as the layout engine would produce them ...
|
||||
def _load_state(path):
|
||||
"""Load state from JSON file."""
|
||||
# ...
|
||||
|
||||
# ─── Output ──────────────────────────────────────────────────
|
||||
def _save_state(path, data):
|
||||
"""Save state to JSON file."""
|
||||
# ...
|
||||
|
||||
print('#!c=0') # cache header: never cache
|
||||
print(grid.emit_micron())
|
||||
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
|
||||
@@ -644,30 +606,35 @@ sparkline renders identically in both ASCII preview and live Micron.
|
||||
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"
|
||||
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 "IN" "$net_in KB/s"
|
||||
sparkline "IN" $net_history_in 28
|
||||
label "OUT" "$net_out KB/s"
|
||||
sparkline "OUT" $net_history_out 28
|
||||
label "Uptime" "$uptime"
|
||||
label "Updated" "$timestamp"
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user