chore: clean up repo structure
- Add node_modules/, test-results/, e2e-report/, .omp/ to .gitignore - Remove throwaway debug scripts (debug-pw*.js, debug-login-dom.js) - Remove empty mock-server/ directory - Untrack harness artifact (.omp/plans/) - Add missing project files to git (e2e tests, Dockerfiles, tooling configs)
This commit is contained in:
@@ -8,20 +8,20 @@ config global 'global'
|
||||
config gateway
|
||||
option region 'amsterdam'
|
||||
option label 'Amsterdam (ocirosea641)'
|
||||
option ip '10.11.12.3'
|
||||
option ip '10.99.12.3'
|
||||
option default '1'
|
||||
option health_check 'ping'
|
||||
|
||||
config gateway
|
||||
option region 'tirunelveli'
|
||||
option label 'Tirunelveli (rpi1000)'
|
||||
option ip '10.11.12.5'
|
||||
option ip '10.99.12.5'
|
||||
option default '0'
|
||||
option health_check 'ping'
|
||||
|
||||
config gateway
|
||||
option region 'bangalore'
|
||||
option label 'Bangalore (sensecap-m4)'
|
||||
option ip '10.11.12.4'
|
||||
option ip '10.99.12.4'
|
||||
option default '0'
|
||||
option health_check 'ping'
|
||||
|
||||
24
root/usr/share/rpcd/ucode/system.uc
Normal file
24
root/usr/share/rpcd/ucode/system.uc
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
system: {
|
||||
board: {
|
||||
call: function(req) {
|
||||
return {
|
||||
hostname: "TestRouter",
|
||||
model: "OpenWrt x86/64 Container",
|
||||
board_name: "generic",
|
||||
release: {
|
||||
distribution: "OpenWrt",
|
||||
version: "25.12.4",
|
||||
revision: "r0-test",
|
||||
target: "x86/64",
|
||||
description: "OpenWrt 25.12.4 Testing"
|
||||
},
|
||||
kernel: "6.12.0-test",
|
||||
rootfs_type: "ext4"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
import { ubus } from 'ubus';
|
||||
import { uci } from 'uci';
|
||||
import { fs } from 'fs';
|
||||
let uci = require('uci');
|
||||
let fs = require('fs');
|
||||
|
||||
const SWITCH_SCRIPT = '/usr/sbin/zt-gateway-switch';
|
||||
const DRAIN_PIDFILE = '/var/run/zt-gateway-drain.pid';
|
||||
@@ -12,9 +11,7 @@ const DRAIN_OLDFILE = '/var/run/zt-gateway-drain.old';
|
||||
function load_gateways() {
|
||||
const cursor = uci.cursor();
|
||||
const result = [];
|
||||
const sections = uci.sections(cursor, 'zt-gateway', 'gateway');
|
||||
for (let i = 0; i < length(sections); i++) {
|
||||
const s = sections[i];
|
||||
cursor.foreach('zt-gateway', 'gateway', function(s) {
|
||||
push(result, {
|
||||
region: s.region ?? '',
|
||||
label: s.label ?? s.region ?? '',
|
||||
@@ -22,10 +19,9 @@ function load_gateways() {
|
||||
default: s.default === '1',
|
||||
health_check: s.health_check ?? 'ping'
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function gateway_by_region(region) {
|
||||
const gws = load_gateways();
|
||||
for (let i = 0; i < length(gws); i++) {
|
||||
@@ -74,6 +70,17 @@ function read_drain_state() {
|
||||
return { new_ip, old_ip };
|
||||
}
|
||||
|
||||
|
||||
function shell_quote(s) {
|
||||
if (!s) {
|
||||
return "''";
|
||||
}
|
||||
if (match(s, /^[A-Za-z0-9_./:@=-]+$/) != null) {
|
||||
return s;
|
||||
}
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function run_switch_script_capture(ip, mode) {
|
||||
const mode_lc = mode ?? 'force';
|
||||
let cmd = `${SWITCH_SCRIPT} ${shell_quote(ip)} ${shell_quote(mode_lc)}`;
|
||||
@@ -97,205 +104,199 @@ function run_switch_script_force_capture(ip) {
|
||||
}
|
||||
|
||||
function system_output(cmd) {
|
||||
const tmp = `/tmp/zt-gw-out-${getpid()}`;
|
||||
const tmp = `/tmp/zt-gw-out-${time()}`;
|
||||
system(`${cmd} >${tmp} 2>/dev/null`);
|
||||
const out = fs.readfile(tmp) ?? '';
|
||||
fs.unlink(tmp);
|
||||
return out;
|
||||
}
|
||||
|
||||
function shell_quote(s) {
|
||||
if (!s) {
|
||||
return "''";
|
||||
}
|
||||
if (s ~ /^[A-Za-z0-9_./:@=-]+$/) {
|
||||
return s;
|
||||
}
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
return {
|
||||
'zt-gateway': {
|
||||
status: {
|
||||
call: function(req) {
|
||||
const gws = load_gateways();
|
||||
const active_region = read_active_region();
|
||||
|
||||
function rpc_status(req) {
|
||||
const gws = load_gateways();
|
||||
const active_region = read_active_region();
|
||||
let active_ip = '';
|
||||
const active_gw = gateway_by_region(active_region);
|
||||
if (active_gw) {
|
||||
active_ip = active_gw.ip;
|
||||
}
|
||||
|
||||
let active_ip = '';
|
||||
const active_gw = gateway_by_region(active_region);
|
||||
if (active_gw) {
|
||||
active_ip = active_gw.ip;
|
||||
}
|
||||
const drain_state = read_drain_state();
|
||||
let remaining = 0;
|
||||
if (drain_active()) {
|
||||
const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`);
|
||||
remaining = +trim(out) || 0;
|
||||
}
|
||||
|
||||
const drain_state = read_drain_state();
|
||||
let remaining = 0;
|
||||
if (drain_active()) {
|
||||
const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`);
|
||||
remaining = +trim(out) || 0;
|
||||
}
|
||||
const cursor = uci.cursor();
|
||||
const settings = {
|
||||
switch_mode: cursor.get('zt-gateway', 'global', 'switch_mode') ?? 'force',
|
||||
drain_timeout: +(cursor.get('zt-gateway', 'global', 'drain_timeout') ?? 600),
|
||||
health_interval: +(cursor.get('zt-gateway', 'global', 'health_interval') ?? 60),
|
||||
auto_failback: cursor.get('zt-gateway', 'global', 'auto_failback') === '1'
|
||||
};
|
||||
|
||||
const cursor = uci.cursor();
|
||||
const settings = {
|
||||
switch_mode: cursor.get('zt-gateway', 'global', 'switch_mode') ?? 'force',
|
||||
drain_timeout: +(cursor.get('zt-gateway', 'global', 'drain_timeout') ?? 600),
|
||||
health_interval: +(cursor.get('zt-gateway', 'global', 'health_interval') ?? 60),
|
||||
auto_failback: cursor.get('zt-gateway', 'global', 'auto_failback') === '1'
|
||||
};
|
||||
return {
|
||||
active_gateway: active_region,
|
||||
active_ip: active_ip,
|
||||
settings: settings,
|
||||
gateways: gws,
|
||||
drain: {
|
||||
active: drain_active(),
|
||||
new_ip: drain_state.new_ip,
|
||||
old_ip: drain_state.old_ip,
|
||||
remaining_connections: remaining
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
ubus.reply(req, {
|
||||
active_gateway: active_region,
|
||||
active_ip: active_ip,
|
||||
settings: settings,
|
||||
gateways: gws,
|
||||
drain: {
|
||||
active: drain_active(),
|
||||
new_ip: drain_state.new_ip,
|
||||
old_ip: drain_state.old_ip,
|
||||
remaining_connections: remaining
|
||||
}
|
||||
});
|
||||
}
|
||||
switch: {
|
||||
args: {
|
||||
region: 'string',
|
||||
mode: 'string'
|
||||
},
|
||||
call: function(req) {
|
||||
const region = req.args?.region;
|
||||
const mode = req.args?.mode ?? read_global_option('switch_mode', 'force');
|
||||
if (!region || (mode !== 'force' && mode !== 'graceful')) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Missing or invalid argument: region required, mode must be force|graceful'
|
||||
};
|
||||
}
|
||||
|
||||
function rpc_switch(req, msg) {
|
||||
const region = msg?.region;
|
||||
const mode = msg?.mode ?? read_global_option('switch_mode', 'force');
|
||||
if (!region || (mode !== 'force' && mode !== 'graceful')) {
|
||||
ubus.reply(req, {
|
||||
success: false,
|
||||
message: 'Missing or invalid argument: region required, mode must be force|graceful'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (drain_active() && mode === 'graceful') {
|
||||
return {
|
||||
success: false,
|
||||
message: 'A graceful drain is already in progress. Cancel it first.'
|
||||
};
|
||||
}
|
||||
|
||||
if (drain_active() && mode === 'graceful') {
|
||||
ubus.reply(req, {
|
||||
success: false,
|
||||
message: 'A graceful drain is already in progress. Cancel it first.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const gw = gateway_by_region(region);
|
||||
if (!gw) {
|
||||
return { success: false, message: `Unknown gateway region: ${region}` };
|
||||
}
|
||||
|
||||
const gw = gateway_by_region(region);
|
||||
if (!gw) {
|
||||
ubus.reply(req, { success: false, message: `Unknown gateway region: ${region}` });
|
||||
return;
|
||||
}
|
||||
const current_region = read_active_region();
|
||||
if (current_region === region && !drain_active()) {
|
||||
return { success: true, message: `Already on ${region}.` };
|
||||
}
|
||||
|
||||
const current_region = read_active_region();
|
||||
if (current_region === region && !drain_active()) {
|
||||
ubus.reply(req, { success: true, message: `Already on ${region}.` });
|
||||
return;
|
||||
}
|
||||
const result = run_switch_script_capture(gw.ip, mode);
|
||||
if (result.code !== 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: result.stderr || `Switch script failed with exit code ${result.code}`
|
||||
};
|
||||
}
|
||||
|
||||
const result = run_switch_script_capture(gw.ip, mode);
|
||||
if (result.code !== 0) {
|
||||
ubus.reply(req, {
|
||||
success: false,
|
||||
message: result.stderr || `Switch script failed with exit code ${result.code}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
const cursor = uci.cursor();
|
||||
cursor.set('zt-gateway', 'global', 'active_gateway', region);
|
||||
cursor.commit('zt-gateway');
|
||||
|
||||
const cursor = uci.cursor();
|
||||
cursor.set('zt-gateway', 'global', 'active_gateway', region);
|
||||
cursor.commit('zt-gateway');
|
||||
return {
|
||||
success: true,
|
||||
message: `Switched to ${region} (${gw.ip}) via ${mode}`,
|
||||
active_gateway: region,
|
||||
mode: mode,
|
||||
drain_active: (mode === 'graceful')
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
ubus.reply(req, {
|
||||
success: true,
|
||||
message: `Switched to ${region} (${gw.ip}) via ${mode}`,
|
||||
active_gateway: region,
|
||||
mode: mode,
|
||||
drain_active: (mode === 'graceful')
|
||||
});
|
||||
}
|
||||
health: {
|
||||
args: {
|
||||
region: 'string'
|
||||
},
|
||||
call: function(req) {
|
||||
const region = req.args?.region;
|
||||
if (!region) {
|
||||
return { reachable: false, message: 'Missing region' };
|
||||
}
|
||||
|
||||
function rpc_health(req, msg) {
|
||||
const region = msg?.region;
|
||||
if (!region) {
|
||||
ubus.reply(req, { reachable: false, message: 'Missing region' });
|
||||
return;
|
||||
}
|
||||
const gw = gateway_by_region(region);
|
||||
if (!gw) {
|
||||
return { reachable: false, message: `Unknown region ${region}` };
|
||||
}
|
||||
|
||||
const gw = gateway_by_region(region);
|
||||
if (!gw) {
|
||||
ubus.reply(req, { reachable: false, message: `Unknown region ${region}` });
|
||||
return;
|
||||
}
|
||||
const out = system_output(
|
||||
`ping -c 2 -W 3 -I br-zt ${shell_quote(gw.ip)} 2>/dev/null; echo "EXIT:$?"`
|
||||
);
|
||||
const exit_match = match(out, /EXIT:(\d+)/);
|
||||
const exit_code = exit_match ? +exit_match[1] : 1;
|
||||
|
||||
const out = system_output(
|
||||
`ping -c 2 -W 3 -I br-zt ${shell_quote(gw.ip)} 2>/dev/null; echo "EXIT:$?"`
|
||||
);
|
||||
const exit_match = match(out, /EXIT:(\d+)/);
|
||||
const exit_code = exit_match ? +exit_match[1] : 1;
|
||||
let latency_ms = null;
|
||||
if (exit_code === 0) {
|
||||
const stats = match(out, /rtt min\/avg\/max[^=]*=\s*[\d.]+\/([\d.]+)\//);
|
||||
if (stats) {
|
||||
latency_ms = +stats[1];
|
||||
}
|
||||
}
|
||||
|
||||
let latency_ms = null;
|
||||
if (exit_code === 0) {
|
||||
const stats = match(out, /rtt min\/avg\/max[^=]*=\s*[\d.]+\/([\d.]+)\//);
|
||||
if (stats) {
|
||||
latency_ms = +stats[1];
|
||||
return {
|
||||
region: region,
|
||||
ip: gw.ip,
|
||||
reachable: exit_code === 0,
|
||||
latency_ms: latency_ms
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
drain_status: {
|
||||
call: function(req) {
|
||||
const active = drain_active();
|
||||
const state = read_drain_state();
|
||||
let remaining = 0;
|
||||
if (active) {
|
||||
const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`);
|
||||
remaining = +trim(out) || 0;
|
||||
}
|
||||
return {
|
||||
active: active,
|
||||
new_ip: state.new_ip,
|
||||
old_ip: state.old_ip,
|
||||
remaining_connections: remaining
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
cancel_drain: {
|
||||
call: function(req) {
|
||||
if (!drain_active()) {
|
||||
return { success: false, message: 'No drain in progress' };
|
||||
}
|
||||
|
||||
const state = read_drain_state();
|
||||
const pid = read_drain_pid();
|
||||
if (pid) {
|
||||
system(`kill -TERM ${pid} 2>/dev/null`);
|
||||
}
|
||||
|
||||
if (!state.new_ip) {
|
||||
fs.unlink(DRAIN_PIDFILE);
|
||||
return {
|
||||
success: true,
|
||||
message: 'Drain cancelled (new gateway unknown; route left as-is).'
|
||||
};
|
||||
}
|
||||
|
||||
const result = run_switch_script_force_capture(state.new_ip);
|
||||
|
||||
fs.unlink(DRAIN_PIDFILE);
|
||||
fs.unlink(DRAIN_NEWFILE);
|
||||
fs.unlink(DRAIN_OLDFILE);
|
||||
|
||||
return {
|
||||
success: result.code === 0,
|
||||
message: result.code === 0
|
||||
? `Drain cancelled; force-switched to ${state.new_ip}.`
|
||||
: (result.stderr || `Cancel failed with code ${result.code}`)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ubus.reply(req, {
|
||||
region: region,
|
||||
ip: gw.ip,
|
||||
reachable: exit_code === 0,
|
||||
latency_ms: latency_ms
|
||||
});
|
||||
}
|
||||
|
||||
function rpc_drain_status(req) {
|
||||
const active = drain_active();
|
||||
const state = read_drain_state();
|
||||
let remaining = 0;
|
||||
if (active) {
|
||||
const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`);
|
||||
remaining = +trim(out) || 0;
|
||||
}
|
||||
ubus.reply(req, {
|
||||
active: active,
|
||||
new_ip: state.new_ip,
|
||||
old_ip: state.old_ip,
|
||||
remaining_connections: remaining
|
||||
});
|
||||
}
|
||||
|
||||
function rpc_cancel_drain(req) {
|
||||
if (!drain_active()) {
|
||||
ubus.reply(req, { success: false, message: 'No drain in progress' });
|
||||
return;
|
||||
}
|
||||
|
||||
const state = read_drain_state();
|
||||
const pid = read_drain_pid();
|
||||
if (pid) {
|
||||
system(`kill -TERM ${pid} 2>/dev/null`);
|
||||
}
|
||||
|
||||
if (!state.new_ip) {
|
||||
fs.unlink(DRAIN_PIDFILE);
|
||||
ubus.reply(req, {
|
||||
success: true,
|
||||
message: 'Drain cancelled (new gateway unknown; route left as-is).'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = run_switch_script_force_capture(state.new_ip);
|
||||
|
||||
fs.unlink(DRAIN_PIDFILE);
|
||||
fs.unlink(DRAIN_NEWFILE);
|
||||
fs.unlink(DRAIN_OLDFILE);
|
||||
|
||||
ubus.reply(req, {
|
||||
success: result.code === 0,
|
||||
message: result.code === 0
|
||||
? `Drain cancelled; force-switched to ${state.new_ip}.`
|
||||
: (result.stderr || `Cancel failed with code ${result.code}`)
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
status: rpc_status,
|
||||
switch: rpc_switch,
|
||||
health: rpc_health,
|
||||
drain_status: rpc_drain_status,
|
||||
cancel_drain: rpc_cancel_drain
|
||||
};
|
||||
|
||||
188
root/usr/share/ucode/luci/runtime.uc
Normal file
188
root/usr/share/ucode/luci/runtime.uc
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright 2022 Jo-Philipp Wich <jo@mein.io>
|
||||
// Licensed to the public under the Apache License 2.0.
|
||||
|
||||
import { access, basename, stat } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const template_directory = '/usr/share/ucode/luci/template';
|
||||
|
||||
function cut_message(msg) {
|
||||
return trim(replace(msg, /\n--\n.*$/, ''));
|
||||
}
|
||||
|
||||
function format_nested_exception(ex) {
|
||||
let msg = replace(cut_message(ex.message), /(\n+( \|[^\n]*(\n|$))+)/, (m, m1) => {
|
||||
m1 = replace(m1, /(^|\n) \| ?/g, '$1');
|
||||
m = match(m1, /^(.+?)\n(In.*line \d+, byte \d+:.+)$/);
|
||||
|
||||
return `
|
||||
<div class="exception">
|
||||
<div class="message">${cut_message(m ? m[1] : m1)}</div>
|
||||
${m ? `<pre class="context">${trim(m[2])}</pre>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="exception">
|
||||
<div class="message">${cut_message(msg)}</div>
|
||||
<pre class="context">${trim(ex.stacktrace[0].context)}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function format_lua_exception(ex) {
|
||||
let m = match(ex.message, /^(.+)\nstack traceback:\n(.+)$/);
|
||||
|
||||
return `
|
||||
<div class="exception">
|
||||
<div class="message">${cut_message(m ? m[1] : ex.message)}</div>
|
||||
<pre class="context">${m ? trim(replace(m[2], /(^|\n)\t/g, '$1')) : ex.stacktrace[0].context}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const Class = {
|
||||
init_lua: function(optional) {
|
||||
if (!this.L) {
|
||||
let bridge = this.env.dispatcher.load_luabridge(optional);
|
||||
|
||||
if (bridge) {
|
||||
let http = this.env.http;
|
||||
|
||||
this.L = bridge.create();
|
||||
this.L.set('L', proto({ write: (...args) => http.closed || print(...args) }, this.env));
|
||||
this.L.invoke('require', 'luci.ucodebridge');
|
||||
|
||||
this.env.lua_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
return this.L;
|
||||
},
|
||||
|
||||
is_ucode_template: function(path) {
|
||||
return access(`${template_directory}/${path}.ut`);
|
||||
},
|
||||
|
||||
is_lua_template: function(path) {
|
||||
let vm = this.init_lua(true);
|
||||
|
||||
return vm && access(`${vm.get('_G', 'luci', 'template', 'viewdir')}/${path}.htm`);
|
||||
},
|
||||
|
||||
render_ucode: function(path, scope) {
|
||||
let self = this;
|
||||
let tmplfunc = loadfile(path, { raw_mode: false });
|
||||
let globals = proto({ include: (name, args) => self.render_any(name, args ?? {}) }, scope ?? {});
|
||||
|
||||
if (this.env.http.closed)
|
||||
render(call, tmplfunc, null, globals);
|
||||
else
|
||||
call(tmplfunc, null, globals);
|
||||
},
|
||||
|
||||
render_lua: function(path, scope) {
|
||||
let vm = this.init_lua();
|
||||
let render = vm.get('_G', 'luci', 'ucodebridge', 'render');
|
||||
|
||||
render.call(path, scope ?? {});
|
||||
},
|
||||
|
||||
trycompile: function(path) {
|
||||
let ucode_path = `${template_directory}/${path}.ut`;
|
||||
|
||||
if (access(ucode_path)) {
|
||||
try {
|
||||
loadfile(ucode_path, { raw_mode: false });
|
||||
}
|
||||
catch (ucode_err) {
|
||||
return `Unable to compile '${path}' as ucode template: ${format_nested_exception(ucode_err)}`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
let vm = this.init_lua(true);
|
||||
|
||||
if (vm)
|
||||
vm.get('_G', 'luci', 'ucodebridge', 'compile').call(path);
|
||||
else
|
||||
return `Unable to compile '${path}' as Lua template: Unable to load Lua runtime`;
|
||||
}
|
||||
catch (lua_err) {
|
||||
return `Unable to compile '${path}' as Lua template: ${format_lua_exception(lua_err)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
render_any: function(path, scope) {
|
||||
let ucode_path = `${template_directory}/${path}.ut`;
|
||||
|
||||
scope = proto(scope ?? {}, this.scopes[-1]);
|
||||
|
||||
push(this.scopes, scope);
|
||||
|
||||
try {
|
||||
if (access(ucode_path))
|
||||
this.render_ucode(ucode_path, scope);
|
||||
else
|
||||
this.render_lua(path, scope);
|
||||
}
|
||||
catch (ex) {
|
||||
pop(this.scopes);
|
||||
die(ex);
|
||||
}
|
||||
|
||||
pop(this.scopes);
|
||||
},
|
||||
|
||||
render: function(path, scope) {
|
||||
let self = this;
|
||||
this.env.http.write(render(() => self.render_any(path, scope)));
|
||||
},
|
||||
|
||||
call: function(modname, method, ...args) {
|
||||
let vm = this.init_lua();
|
||||
let lcall = vm.get('_G', 'luci', 'ucodebridge', 'call');
|
||||
|
||||
return lcall.call(modname, method, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
export default function(env) {
|
||||
const self = proto({ env: env ??= {}, scopes: [ proto(env, global) ], global }, Class);
|
||||
const uci = cursor();
|
||||
|
||||
// determine theme
|
||||
let media = uci.get('luci', 'main', 'mediaurlbase');
|
||||
let status = self.trycompile(`themes/${basename(media)}/header`);
|
||||
|
||||
if (status !== true) {
|
||||
media = null;
|
||||
self.env.media_error = status;
|
||||
|
||||
for (let k, v in uci.get_all('luci', 'themes')) {
|
||||
if (substr(k, 0, 1) != '.') {
|
||||
status = self.trycompile(`themes/${basename(v)}/header`);
|
||||
|
||||
if (status === true) {
|
||||
media = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!media)
|
||||
env.dispatcher.error500(`Unable to render any theme header template, last error was:\n${status}`);
|
||||
}
|
||||
|
||||
self.env.media = media;
|
||||
self.env.theme = basename(media);
|
||||
self.env.resource = uci.get('luci', 'main', 'resourcebase');
|
||||
self.env.pkgs_update_time = stat('/lib/apk/db/installed')?.mtime ?? stat('/usr/lib/opkg/status')?.mtime ?? 0;
|
||||
self.env.include = (...args) => self.render_any(...args);
|
||||
|
||||
return self;
|
||||
};
|
||||
Reference in New Issue
Block a user