Auto-detect URL from page origin — works on HTTP, HTTPS, LAN IP, or domain
Removed CONFIG_HA_URL. The dashboard now always uses window.location.origin so it adapts to however you access HA: - https://ha.hideawaygaming.com.au → wss:// WebSocket - http://10.0.0.55:8123 → ws:// WebSocket Same token, no config change needed.
This commit is contained in:
+23
-76
@@ -134,9 +134,6 @@ input[type=time]::-webkit-calendar-picker-indicator{filter:invert(.5)}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// DHCP source: sensor.home_dhcp_leases_lan
|
|
||||||
// attributes.Leases (capital L): [{address, hostname, mac, expires, type}]
|
|
||||||
// Online = expires is in the future
|
|
||||||
const DHCP='sensor.home_dhcp_leases_lan';
|
const DHCP='sensor.home_dhcp_leases_lan';
|
||||||
const CHUNKS=12, CHUNK_SIZE=250;
|
const CHUNKS=12, CHUNK_SIZE=250;
|
||||||
const chunkId=i=>`input_text.parental_config_${i}`;
|
const chunkId=i=>`input_text.parental_config_${i}`;
|
||||||
@@ -147,20 +144,18 @@ let states={},cfg={users:[]},prefs={url:'',token:''};
|
|||||||
let devTargetUser=null,schedOpen={},renderTimer=null,lastSaveTime=0;
|
let devTargetUser=null,schedOpen={},renderTimer=null,lastSaveTime=0;
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// CONFIGURATION — Set these once, works on every device automatically
|
// CONFIGURATION — Set token once, works on every device automatically
|
||||||
// Generate a Long-Lived Access Token in HA: Profile → Security → Long-Lived Access Tokens
|
// Generate a Long-Lived Access Token in HA: Profile → Security → Long-Lived Access Tokens
|
||||||
|
// URL auto-detects from page — works on HTTP, HTTPS, LAN IP, or domain
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
const CONFIG_HA_URL = ''; // e.g. 'https://ha.hideawaygaming.com.au' — leave blank to auto-detect from page URL
|
|
||||||
const CONFIG_HA_TOKEN = ''; // Paste your Long-Lived Access Token here
|
const CONFIG_HA_TOKEN = ''; // Paste your Long-Lived Access Token here
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
function loadPrefs(){
|
function loadPrefs(){
|
||||||
// 1. Use hardcoded config if token is set (works on all devices, no setup needed)
|
|
||||||
if(CONFIG_HA_TOKEN){
|
if(CONFIG_HA_TOKEN){
|
||||||
prefs={url:CONFIG_HA_URL||window.location.origin,token:CONFIG_HA_TOKEN};
|
prefs={url:window.location.origin,token:CONFIG_HA_TOKEN};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// 2. Fallback to localStorage (legacy / standalone use)
|
|
||||||
try{const p=localStorage.getItem('pc2_prefs');if(p){prefs=JSON.parse(p);return true;}}catch(e){}
|
try{const p=localStorage.getItem('pc2_prefs');if(p){prefs=JSON.parse(p);return true;}}catch(e){}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -229,94 +224,59 @@ function onStateChange({entity_id,new_state}){
|
|||||||
if(entity_id===DHCP){updateDHCP();schedRender();return;}
|
if(entity_id===DHCP){updateDHCP();schedRender();return;}
|
||||||
if(entity_id.startsWith('device_tracker.')){schedRender();}
|
if(entity_id.startsWith('device_tracker.')){schedRender();}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCfg(){
|
function parseCfg(){
|
||||||
let combined='';
|
let combined='';
|
||||||
for(let i=0;i<CHUNKS;i++){
|
for(let i=0;i<CHUNKS;i++){const s=states[chunkId(i)];const v=s?.state||'';if(v==='unknown'||v==='unavailable')break;combined+=v;}
|
||||||
const s=states[chunkId(i)];
|
|
||||||
const v=s?.state||'';
|
|
||||||
if(v==='unknown'||v==='unavailable')break;
|
|
||||||
combined+=v;
|
|
||||||
}
|
|
||||||
combined=combined.trim();
|
combined=combined.trim();
|
||||||
if(!combined||combined[0]!=='{'){
|
if(!combined||combined[0]!=='{'){try{combined=localStorage.getItem('pc2_backup')||'';}catch(e){}}
|
||||||
try{combined=localStorage.getItem('pc2_backup')||'';}catch(e){}
|
if(combined&&combined[0]==='{'){try{cfg=JSON.parse(combined);if(!cfg.users)cfg.users=[];}catch(e){cfg={users:[]};}}
|
||||||
}
|
|
||||||
if(combined&&combined[0]==='{'){
|
|
||||||
try{cfg=JSON.parse(combined);if(!cfg.users)cfg.users=[];}catch(e){cfg={users:[]};}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async function saveCfg(){
|
async function saveCfg(){
|
||||||
lastSaveTime=Date.now();
|
lastSaveTime=Date.now();
|
||||||
const json=JSON.stringify(cfg);
|
const json=JSON.stringify(cfg);
|
||||||
try{localStorage.setItem('pc2_backup',json);}catch(e){}
|
try{localStorage.setItem('pc2_backup',json);}catch(e){}
|
||||||
const saves=[];
|
const saves=[];
|
||||||
for(let i=0;i<CHUNKS;i++){
|
for(let i=0;i<CHUNKS;i++){const chunk=json.slice(i*CHUNK_SIZE,(i+1)*CHUNK_SIZE);saves.push(send({type:'call_service',domain:'input_text',service:'set_value',service_data:{entity_id:chunkId(i),value:chunk}}));}
|
||||||
const chunk=json.slice(i*CHUNK_SIZE,(i+1)*CHUNK_SIZE);
|
try{await Promise.all(saves);}catch(e){toast('Save failed','e');console.error('saveCfg',e);}
|
||||||
saves.push(send({type:'call_service',domain:'input_text',service:'set_value',
|
|
||||||
service_data:{entity_id:chunkId(i),value:chunk}}));
|
|
||||||
}
|
|
||||||
try{await Promise.all(saves);}
|
|
||||||
catch(e){toast('Save failed','e');console.error('saveCfg',e);}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLeases(){return states[DHCP]?.attributes?.Leases||[];}
|
function getLeases(){return states[DHCP]?.attributes?.Leases||[];}
|
||||||
function isOnline(lease){if(!lease.expires)return true;return new Date(lease.expires)>new Date();}
|
function isOnline(lease){if(!lease.expires)return true;return new Date(lease.expires)>new Date();}
|
||||||
function updateDHCP(){
|
function updateDHCP(){
|
||||||
const s=states[DHCP];
|
const s=states[DHCP];const dot=document.getElementById('dhcp-dot');const lbl=document.getElementById('dhcp-lbl');
|
||||||
const dot=document.getElementById('dhcp-dot');const lbl=document.getElementById('dhcp-lbl');
|
|
||||||
if(!dot||!lbl)return;
|
if(!dot||!lbl)return;
|
||||||
if(!s||s.state==='unavailable'||s.state==='unknown'){dot.className='sdot err';lbl.textContent='DHCP ✗';}
|
if(!s||s.state==='unavailable'||s.state==='unknown'){dot.className='sdot err';lbl.textContent='DHCP ✗';}
|
||||||
else{dot.className='sdot ok';lbl.textContent=`DHCP (${s.state})`;}
|
else{dot.className='sdot ok';lbl.textContent=`DHCP (${s.state})`;}
|
||||||
}
|
}
|
||||||
function getLease(mac){
|
function getLease(mac){const m=normMac(mac);return getLeases().find(r=>normMac(r.mac||'')===m)||null;}
|
||||||
const m=normMac(mac);
|
|
||||||
return getLeases().find(r=>normMac(r.mac||'')===m)||null;
|
|
||||||
}
|
|
||||||
function deviceInfo(mac){
|
function deviceInfo(mac){
|
||||||
const lease=getLease(mac);
|
const lease=getLease(mac);
|
||||||
if(lease)return{online:isOnline(lease),ip:lease.address||'',label:lease.hostname||mac};
|
if(lease)return{online:isOnline(lease),ip:lease.address||'',label:lease.hostname||mac};
|
||||||
const m=normMac(mac);
|
const m=normMac(mac);
|
||||||
for(const[eid,s]of Object.entries(states)){
|
for(const[eid,s]of Object.entries(states)){
|
||||||
if(!eid.startsWith('device_tracker.'))continue;
|
if(!eid.startsWith('device_tracker.'))continue;const a=s.attributes||{};
|
||||||
const a=s.attributes||{};
|
|
||||||
const sm=normMac(a.mac||a.mac_address||a.macaddress||'');
|
const sm=normMac(a.mac||a.mac_address||a.macaddress||'');
|
||||||
if(sm===m)return{online:s.state==='home',ip:a.ip||a.ip_address||'',label:a.friendly_name||a.hostname||eid.replace('device_tracker.','')};
|
if(sm===m)return{online:s.state==='home',ip:a.ip||a.ip_address||'',label:a.friendly_name||a.hostname||eid.replace('device_tracker.','')};
|
||||||
}
|
}
|
||||||
return{online:false,ip:'',label:''};
|
return{online:false,ip:'',label:''};
|
||||||
}
|
}
|
||||||
function discoveredDevices(){
|
function discoveredDevices(){
|
||||||
const assigned=new Set(cfg.users.flatMap(u=>u.devices.map(d=>normMac(d.mac))));
|
const assigned=new Set(cfg.users.flatMap(u=>u.devices.map(d=>normMac(d.mac))));const out=[];
|
||||||
const out=[];
|
for(const r of getLeases()){const mac=normMac(r.mac||'');if(!mac||assigned.has(mac))continue;out.push({mac,name:r.hostname||mac,ip:r.address||'',online:isOnline(r)});}
|
||||||
for(const r of getLeases()){
|
|
||||||
const mac=normMac(r.mac||'');
|
|
||||||
if(!mac||assigned.has(mac))continue;
|
|
||||||
out.push({mac,name:r.hostname||mac,ip:r.address||'',online:isOnline(r)});
|
|
||||||
}
|
|
||||||
for(const[eid,s]of Object.entries(states)){
|
for(const[eid,s]of Object.entries(states)){
|
||||||
if(!eid.startsWith('device_tracker.'))continue;
|
if(!eid.startsWith('device_tracker.'))continue;const a=s.attributes||{};const mac=normMac(a.mac||a.mac_address||a.macaddress||'');
|
||||||
const a=s.attributes||{};const mac=normMac(a.mac||a.mac_address||a.macaddress||'');
|
if(mac&&!assigned.has(mac)&&!out.find(x=>x.mac===mac))out.push({mac,name:a.friendly_name||a.hostname||eid.replace('device_tracker.',''),ip:a.ip||a.ip_address||'',online:s.state==='home'});
|
||||||
if(mac&&!assigned.has(mac)&&!out.find(x=>x.mac===mac))
|
|
||||||
out.push({mac,name:a.friendly_name||a.hostname||eid.replace('device_tracker.',''),ip:a.ip||a.ip_address||'',online:s.state==='home'});
|
|
||||||
}
|
}
|
||||||
return out.sort((a,b)=>b.online-a.online);
|
return out.sort((a,b)=>b.online-a.online);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callBlock(ip){await send({type:'call_service',domain:'script',service:'parental_block_ip',service_data:{ip}});}
|
async function callBlock(ip){await send({type:'call_service',domain:'script',service:'parental_block_ip',service_data:{ip}});}
|
||||||
async function callUnblock(ip){await send({type:'call_service',domain:'script',service:'parental_unblock_ip',service_data:{ip}});}
|
async function callUnblock(ip){await send({type:'call_service',domain:'script',service:'parental_unblock_ip',service_data:{ip}});}
|
||||||
async function callApply(){try{await send({type:'call_service',domain:'script',service:'parental_apply_firewall',service_data:{}});}catch(e){}}
|
async function callApply(){try{await send({type:'call_service',domain:'script',service:'parental_apply_firewall',service_data:{}});}catch(e){}}
|
||||||
function resolveIp(mac,dev){return deviceInfo(mac).ip||dev.last_ip||'';}
|
function resolveIp(mac,dev){return deviceInfo(mac).ip||dev.last_ip||'';}
|
||||||
|
|
||||||
async function toggleUser(userId){
|
async function toggleUser(userId){
|
||||||
const user=cfg.users.find(u=>u.id===userId);if(!user)return;
|
const user=cfg.users.find(u=>u.id===userId);if(!user)return;
|
||||||
const block=!user.blocked;user.blocked=block;let ok=0,fail=0;
|
const block=!user.blocked;user.blocked=block;let ok=0,fail=0;
|
||||||
for(const dev of user.devices){
|
for(const dev of user.devices){const ip=resolveIp(dev.mac,dev);if(ip){try{block?await callBlock(ip):await callUnblock(ip);dev.blocked=block;dev.last_ip=ip;ok++;}catch(e){fail++;}}else{dev.blocked=block;if(block)toast(`${dev.name}: no IP yet — will block when online`,'w');}}
|
||||||
const ip=resolveIp(dev.mac,dev);
|
await callApply();await saveCfg();render();toast(`${user.name} ${block?'blocked 🔒':'unblocked ✓'}`,block?'w':'s');
|
||||||
if(ip){try{block?await callBlock(ip):await callUnblock(ip);dev.blocked=block;dev.last_ip=ip;ok++;}catch(e){fail++;}}
|
|
||||||
else{dev.blocked=block;if(block)toast(`${dev.name}: no IP yet — will block when online`,'w');}
|
|
||||||
}
|
|
||||||
await callApply();await saveCfg();render();
|
|
||||||
toast(`${user.name} ${block?'blocked 🔒':'unblocked ✓'}`,block?'w':'s');
|
|
||||||
}
|
}
|
||||||
async function toggleDevice(userId,mac){
|
async function toggleDevice(userId,mac){
|
||||||
const user=cfg.users.find(u=>u.id===userId);const dev=user?.devices.find(d=>d.mac===mac);if(!dev)return;
|
const user=cfg.users.find(u=>u.id===userId);const dev=user?.devices.find(d=>d.mac===mac);if(!dev)return;
|
||||||
@@ -325,21 +285,14 @@ async function toggleDevice(userId,mac){
|
|||||||
try{block?await callBlock(ip):await callUnblock(ip);dev.blocked=block;dev.last_ip=ip;await callApply();await saveCfg();render();toast(`${dev.name} ${block?'blocked 🔒':'unblocked ✓'}`,block?'w':'s');}
|
try{block?await callBlock(ip):await callUnblock(ip);dev.blocked=block;dev.last_ip=ip;await callApply();await saveCfg();render();toast(`${dev.name} ${block?'blocked 🔒':'unblocked ✓'}`,block?'w':'s');}
|
||||||
catch(e){toast(`Failed to ${block?'block':'unblock'} ${dev.name}`,'e');}
|
catch(e){toast(`Failed to ${block?'block':'unblock'} ${dev.name}`,'e');}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(runSchedules,60000);
|
setInterval(runSchedules,60000);
|
||||||
async function runSchedules(){
|
async function runSchedules(){
|
||||||
const now=new Date();const hhmm=pad2(now.getHours())+':'+pad2(now.getMinutes());
|
const now=new Date();const hhmm=pad2(now.getHours())+':'+pad2(now.getMinutes());
|
||||||
const wknd=now.getDay()===0||now.getDay()===6;let changed=false;
|
const wknd=now.getDay()===0||now.getDay()===6;let changed=false;
|
||||||
for(const user of cfg.users){
|
for(const user of cfg.users){
|
||||||
if(!user.schedule?.enabled)continue;
|
if(!user.schedule?.enabled)continue;const slot=wknd?user.schedule.weekend:user.schedule.weekday;
|
||||||
const slot=wknd?user.schedule.weekend:user.schedule.weekday;
|
const sb=inRange(hhmm,slot.block_time,slot.unblock_time);if(sb===user.blocked)continue;user.blocked=sb;
|
||||||
const sb=inRange(hhmm,slot.block_time,slot.unblock_time);
|
for(const dev of user.devices){const ip=resolveIp(dev.mac,dev);if(ip){try{sb?await callBlock(ip):await callUnblock(ip);dev.blocked=sb;dev.last_ip=ip;}catch(e){}}else{dev.blocked=sb;}}
|
||||||
if(sb===user.blocked)continue;
|
|
||||||
user.blocked=sb;
|
|
||||||
for(const dev of user.devices){
|
|
||||||
const ip=resolveIp(dev.mac,dev);
|
|
||||||
if(ip){try{sb?await callBlock(ip):await callUnblock(ip);dev.blocked=sb;dev.last_ip=ip;}catch(e){}}else{dev.blocked=sb;}
|
|
||||||
}
|
|
||||||
toast(`${user.name} ${sb?'blocked by schedule 🕐':'unblocked by schedule ✓'}`,sb?'w':'s');changed=true;
|
toast(`${user.name} ${sb?'blocked by schedule 🕐':'unblocked by schedule ✓'}`,sb?'w':'s');changed=true;
|
||||||
}
|
}
|
||||||
if(changed){await callApply();await saveCfg();render();}
|
if(changed){await callApply();await saveCfg();render();}
|
||||||
@@ -348,11 +301,9 @@ function inRange(now,bt,ut){if(!bt||!ut||bt===ut)return false;return bt<ut?(now>
|
|||||||
async function onSchedChange(userId,field,value){
|
async function onSchedChange(userId,field,value){
|
||||||
const user=cfg.users.find(u=>u.id===userId);if(!user)return;
|
const user=cfg.users.find(u=>u.id===userId);if(!user)return;
|
||||||
if(!user.schedule)user.schedule={enabled:false,weekday:{block_time:'21:00',unblock_time:'07:00'},weekend:{block_time:'22:00',unblock_time:'08:00'}};
|
if(!user.schedule)user.schedule={enabled:false,weekday:{block_time:'21:00',unblock_time:'07:00'},weekend:{block_time:'22:00',unblock_time:'08:00'}};
|
||||||
if(field==='enabled')user.schedule.enabled=value;
|
if(field==='enabled')user.schedule.enabled=value;else{const[slot,key]=field.split('.');user.schedule[slot][key]=value;}
|
||||||
else{const[slot,key]=field.split('.');user.schedule[slot][key]=value;}
|
|
||||||
await saveCfg();
|
await saveCfg();
|
||||||
}
|
}
|
||||||
|
|
||||||
function uid(){return 'u'+Date.now().toString(36)+Math.random().toString(36).slice(2,6);}
|
function uid(){return 'u'+Date.now().toString(36)+Math.random().toString(36).slice(2,6);}
|
||||||
function openAddUserModal(){document.getElementById('u-name').value='';buildColorPicker(COLORS[cfg.users.length%COLORS.length]);document.getElementById('add-user-modal').classList.add('open');}
|
function openAddUserModal(){document.getElementById('u-name').value='';buildColorPicker(COLORS[cfg.users.length%COLORS.length]);document.getElementById('add-user-modal').classList.add('open');}
|
||||||
function createUser(){
|
function createUser(){
|
||||||
@@ -367,12 +318,9 @@ async function deleteUser(userId){
|
|||||||
if(user.devices.some(d=>d.blocked))await callApply();
|
if(user.devices.some(d=>d.blocked))await callApply();
|
||||||
cfg.users=cfg.users.filter(u=>u.id!==userId);await saveCfg();render();toast(`${user.name} removed`,'s');
|
cfg.users=cfg.users.filter(u=>u.id!==userId);await saveCfg();render();toast(`${user.name} removed`,'s');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAddDeviceModal(userId){
|
function openAddDeviceModal(userId){
|
||||||
devTargetUser=userId;
|
devTargetUser=userId;document.getElementById('d-name').value='';document.getElementById('d-mac').value='';
|
||||||
document.getElementById('d-name').value='';document.getElementById('d-mac').value='';
|
const disc=discoveredDevices();const list=document.getElementById('disc-list');const note=document.getElementById('dhcp-note');
|
||||||
const disc=discoveredDevices();const list=document.getElementById('disc-list');
|
|
||||||
const note=document.getElementById('dhcp-note');
|
|
||||||
const hasDHCP=!!states[DHCP]&&states[DHCP].state!=='unavailable'&&states[DHCP].state!=='unknown';
|
const hasDHCP=!!states[DHCP]&&states[DHCP].state!=='unavailable'&&states[DHCP].state!=='unknown';
|
||||||
note.innerHTML=hasDHCP?'':`<div class="ibox">DHCP sensor not found. Check <code>sensor.home_dhcp_leases_lan</code> exists in HA. You can still enter a MAC manually.</div>`;
|
note.innerHTML=hasDHCP?'':`<div class="ibox">DHCP sensor not found. Check <code>sensor.home_dhcp_leases_lan</code> exists in HA. You can still enter a MAC manually.</div>`;
|
||||||
if(!disc.length){list.innerHTML=`<div style="padding:10px;text-align:center;font-size:.8rem;color:var(--muted)">${hasDHCP?'All devices already assigned':'No DHCP data available'}</div>`;}
|
if(!disc.length){list.innerHTML=`<div style="padding:10px;text-align:center;font-size:.8rem;color:var(--muted)">${hasDHCP?'All devices already assigned':'No DHCP data available'}</div>`;}
|
||||||
@@ -393,7 +341,6 @@ async function removeDevice(userId,mac){
|
|||||||
if(dev.blocked){const ip=resolveIp(mac,dev);if(ip){try{await callUnblock(ip);await callApply();}catch(e){}}}
|
if(dev.blocked){const ip=resolveIp(mac,dev);if(ip){try{await callUnblock(ip);await callApply();}catch(e){}}}
|
||||||
user.devices=user.devices.filter(d=>d.mac!==mac);await saveCfg();render();toast(`${dev.name} removed`,'s');
|
user.devices=user.devices.filter(d=>d.mac!==mac);await saveCfg();render();toast(`${dev.name} removed`,'s');
|
||||||
}
|
}
|
||||||
|
|
||||||
function schedRender(){clearTimeout(renderTimer);renderTimer=setTimeout(render,400);}
|
function schedRender(){clearTimeout(renderTimer);renderTimer=setTimeout(render,400);}
|
||||||
function render(){
|
function render(){
|
||||||
const grid=document.getElementById('grid');
|
const grid=document.getElementById('grid');
|
||||||
|
|||||||
Reference in New Issue
Block a user