`;
tb.appendChild(tr);
}
}
async function call(cmd, params={}){
const url = new URL(location.origin + api);
url.searchParams.set('cmd', cmd);
Object.entries(params).forEach(([k,v])=>url.searchParams.set(k,v));
const res = await fetch(url, { cache:'no-store' });
if(!res.ok) throw new Error('HTTP '+res.status);
const data = await res.json();
if(!data.ok) throw new Error(data.err||'Unknown error');
return data.data;
}
async function loadHosts(){
try{
setStatus('Loading…');
const data = await call('hl');
state.rows = data;
state.statusMap = {};
render();
setStatus('Ready');
}catch(e){ showError('Failed to load hosts: '+e.message); }
}
async function wake(name){
state.statusMap[name] = 'Sent';
render();
setStatus('Waking…');
try{
await call('hw', { host:name });
setStatus('Magic packet sent', 'ok');
}catch(e){
state.statusMap[name] = 'Error';
render();
showError('Wake failed: '+e.message);
}
}
async function ping(name){
state.statusMap[name] = 'Loading';
render();
setStatus('Checking…');
try{
const data = await call('ping', { host:name });
const online = !!data.online;
state.statusMap[name] = online ? 'Online' : 'Offline';
render();
setStatus('Ready', online ? 'ok' : 'err');
}catch(e){
state.statusMap[name] = 'Error';
render();
showError('Ping failed: '+e.message);
}
}
function handleClick(ev){
const btn = ev.target.closest('button[data-act]');
if(!btn) return;
const host = btn.dataset.host;
if(btn.dataset.act==='wake') wake(host);
if(btn.dataset.act==='ping') ping(host);
}
// --- THEME LOGIC ---
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
$('#themeToggle').checked = theme === 'dark';
try { localStorage.setItem('theme', theme); } catch(e){}
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function loadTheme() {
let theme = 'dark';
try {
theme = localStorage.getItem('theme') || getSystemTheme();
} catch(e){
theme = getSystemTheme();
}
applyTheme(theme);
}
function handleThemeToggle() {
const theme = $('#themeToggle').checked ? 'dark' : 'light';
applyTheme(theme);
}
function handleSort(ev){
const th = ev.target.closest('th.sortable');
if(!th) return;
const key = th.dataset.key;
if(key){
if(state.sortKey === key){
state.sortDir = -state.sortDir;
}else{
state.sortKey = key; state.sortDir = 1;
}
render();
}
}
function init(){
$('#grid').addEventListener('click', handleClick);
$('#grid thead').addEventListener('click', handleSort);
$('#refresh').addEventListener('click', loadHosts);
// Theme logic
$('#themeToggle').addEventListener('change', handleThemeToggle);
loadTheme();
// Font size logic
$('#fontPlus').addEventListener('click', handleFontPlus);
$('#fontMinus').addEventListener('click', handleFontMinus);
loadFontSize();
loadHosts();
}
window.addEventListener('DOMContentLoaded', init);
})();
EOF
# ---------------------------
# api.lua (CGI) -- fix ping for all OpenWrt
# ---------------------------
cat > "$EWOL_DIR/api.lua" << 'EOF'
#!/usr/bin/lua
-- eWOL CGI API
local json = require('luci.jsonc')
local nixio = require('nixio')
local sys = require('luci.sys')
local ip = require('luci.ip')
local DEVICES_JSON = '/www/ewol/devices.json'
local LAN_IF = 'br-lan'
local PING_TIMEOUT = 1
local ALLOW_PUBLIC = 0 -- Set 1 to allow WAN/public
local function in_lan(addr)
local a = ip.IPv4(addr)
local dev = ip.IPv4(ip.getaddr(LAN_IF) or '0.0.0.0/0')
return a and dev and a:is4() and dev:contains(a)
end
local function enforce_origin()
if ALLOW_PUBLIC == 1 then return true end
local ra = nixio.getenv('REMOTE_ADDR') or ''
if ra == '' then return false end
if in_lan(ra) then return true end
return false
end
local function wol_send(mac)
return sys.call(string.format("etherwake -D -i %s %q >/dev/null 2>&1", LAN_IF, mac)) == 0
end
-- Use system ping for best compatibility, not luci.sys.ping
local function ping_host(ipaddr)
return os.execute("ping -c 1 -w 1 " .. ipaddr .. " >/dev/null 2>&1") == 0
end
local function load_devices()
local f = io.open(DEVICES_JSON, 'r')
if not f then return nil end
local data = f:read('*a')
f:close()
return json.parse(data)
end
local function json_out(tbl)
io.write('Status: 200 OK\r\n')
io.write('Content-Type: application/json\r\n\r\n')
io.write(json.stringify(tbl))
end
local function main()
local args = require('luci.http').urldecode_params(nixio.getenv('QUERY_STRING') or '')
local ret = { ok=false }
if not enforce_origin() then
ret.err = 'Forbidden: not from LAN';
return json_out(ret)
end
local devices = load_devices()
if not devices then ret.err = 'Missing devices.json'; return json_out(ret) end
local cmd = args.cmd or 'hl'
if cmd == 'hl' then
local rows = {}
for name, info in pairs(devices) do
rows[#rows+1] = { name=name, mac=info.mac, ip=info.ip }
end
ret.ok = true; ret.data = rows
elseif cmd == 'hw' then
local h = devices[args.host]
if not h then ret.err = 'Unknown host'
else
if wol_send(h.mac) then
ret.ok = true; ret.data = { sent=true }
else ret.err = 'WoL command failed' end
end
elseif cmd == 'ping' then
local h = devices[args.host]
if not h or not h.ip then ret.err = 'Unknown host or missing IP'
else
local online = ping_host(h.ip)
ret.ok = true; ret.data = { online = online }
end
else
ret.err = 'Unknown command'
end
return json_out(ret)
end
main()
EOF
# ---------------------------
# devices.json sample file
# ---------------------------
if [ ! -f "$EWOL_DIR/devices.json" ]; then
cat > "$EWOL_DIR/devices.json" << 'EOF'
{
"PC1": {
"mac": "11:11:11:11:11:11",
"ip": "192.168.1.101"
},
"PC2": {
"mac": "22:22:22:22:22:22",
"ip": "192.168.1.102"
},
"PC3": {
"mac": "33:33:33:33:33:33",
"ip": "192.168.1.103"
}
}
EOF
fi
# ---------------------------
# permissions + cgi link
# ---------------------------
chmod 644 "$EWOL_DIR/index.html"
chmod 644 "$EWOL_DIR/script.js"
chmod 644 "$EWOL_DIR/style.css"
chmod 644 "$EWOL_DIR/devices.json"
chmod 755 "$EWOL_DIR/api.lua"
[ -L "$CGI_LINK" ] || ln -s "$EWOL_DIR/api.lua" "$CGI_LINK"
cat </ewol/index.html
Optional settings:
If you attempt to open http:///ewol and it gives 403, set
'option index_page "index.html"'
in /etc/config/uhttpd and restart uhttpd with
/etc/init.d/uhttpd restart
----------------------------------------------------------------------------
Optional security:
Default denies access from WAN. To allow public access, export
EWOL_ALLOW_PUBLIC=1 in uhttpd env or set ALLOW_PUBLIC=1 in api.lua.
----------------------------------------------------------------------------
EOF