Inititial commit
This commit is contained in:
commit
aaa2167791
8 files changed
+342
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
state.json
|
||||||
|
config.json
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:alpine
|
||||||
|
|
||||||
|
COPY src/main.py /app/main.py
|
||||||
|
COPY web /app/web
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["python", "/app/main.py", "-c", "/data/config.json", "-s", "/data/state.json", "-w", "/app/web"]
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import ipaddress
|
||||||
|
import threading
|
||||||
|
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Mode:
|
||||||
|
IPv4 = "ipv4"
|
||||||
|
IPv6 = "ipv6"
|
||||||
|
|
||||||
|
def get_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-c", "--config", default="config.json")
|
||||||
|
parser.add_argument("-s", "--state", default="state.json")
|
||||||
|
parser.add_argument("-w", "--web", default="web")
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def get_config(path: str) -> dict:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def resolve(domain: str, mode: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
tuples = socket.getaddrinfo(domain, None, socket.AF_INET6 if mode == Mode.IPv6 else socket.AF_INET)
|
||||||
|
if tuples:
|
||||||
|
return tuples[0][4][0] # type: ignore
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def replace_placeholders(text: str, replace: dict) -> str:
|
||||||
|
for key, value in replace.items():
|
||||||
|
text = text.replace(key, value)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class Updater:
|
||||||
|
def __init__(self, config: dict, state_path: str):
|
||||||
|
self.config = config
|
||||||
|
self.state_path = state_path
|
||||||
|
self.addrs = {}
|
||||||
|
self.history = {}
|
||||||
|
for domain, update_info in config["domains"].items():
|
||||||
|
self.history[domain] = []
|
||||||
|
for mode in update_info.get("modes", [Mode.IPv4]):
|
||||||
|
if not mode in self.addrs:
|
||||||
|
self.addrs[mode] = ""
|
||||||
|
|
||||||
|
def determine_ip(self, mode: str) -> Optional[str]:
|
||||||
|
urls = [*self.config["ip_lookup"][mode]]
|
||||||
|
random.shuffle(urls)
|
||||||
|
while urls:
|
||||||
|
url = urls.pop()
|
||||||
|
log.debug(f"Looking up local {mode} from {url}")
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
response.raise_for_status()
|
||||||
|
addr = response.text.strip()
|
||||||
|
ipaddress.ip_address(addr)
|
||||||
|
log.debug(f"Resolved {mode} to {addr}")
|
||||||
|
return addr
|
||||||
|
except Exception as e:
|
||||||
|
log.error(e)
|
||||||
|
log.debug(f"Unable to determine {mode}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def detect_updates(self):
|
||||||
|
for mode in self.addrs:
|
||||||
|
addr = self.determine_ip(mode)
|
||||||
|
if not addr:
|
||||||
|
continue
|
||||||
|
if self.addrs[mode] != addr:
|
||||||
|
log.info(f"Detected new {mode}: {addr} (old: {self.addrs[mode]})")
|
||||||
|
self.addrs[mode] = addr
|
||||||
|
|
||||||
|
def add_to_history(self, domain: str, addrs: dict, success: bool, response_text: str):
|
||||||
|
if not domain in self.history:
|
||||||
|
self.history[domain] = []
|
||||||
|
self.history[domain].append({
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"addrs": addrs,
|
||||||
|
"success": success,
|
||||||
|
"response_text": response_text
|
||||||
|
})
|
||||||
|
if len(self.history[domain]) > 10:
|
||||||
|
self.history[domain].pop(0)
|
||||||
|
|
||||||
|
def update_domain(self, domain: str, update_info: dict):
|
||||||
|
modes = update_info.get("modes", [Mode.IPv4])
|
||||||
|
update_required = False
|
||||||
|
for mode in modes:
|
||||||
|
log.debug(f"Resolving {domain} ({mode})")
|
||||||
|
resolved = resolve(domain, mode)
|
||||||
|
log.debug(f"Resolved to {resolved}")
|
||||||
|
if resolved != self.addrs[mode]:
|
||||||
|
update_required = True
|
||||||
|
break
|
||||||
|
if not update_required:
|
||||||
|
return
|
||||||
|
log.info(f"Updating {domain}")
|
||||||
|
replacements = {
|
||||||
|
"${DOMAIN}": domain,
|
||||||
|
"${IPV4}": self.addrs.get(Mode.IPv4, ""),
|
||||||
|
"${IPV6}": self.addrs.get(Mode.IPv6, "")
|
||||||
|
}
|
||||||
|
method = update_info.get("method", "GET")
|
||||||
|
url = replace_placeholders(update_info["url"], replacements)
|
||||||
|
params = update_info.get("params", {})
|
||||||
|
params = {k: replace_placeholders(v, replacements) for k, v in params.items()}
|
||||||
|
headers = update_info.get("headers", {})
|
||||||
|
headers = {k: replace_placeholders(v, replacements) for k, v in headers.items()}
|
||||||
|
response = requests.request(method, url, params=params, headers=headers, timeout=10)
|
||||||
|
success = response.status_code == 200
|
||||||
|
response_text = response.text
|
||||||
|
print(response_text)
|
||||||
|
log.info(f"Updated {domain}")
|
||||||
|
self.add_to_history(domain, self.addrs, success, response_text)
|
||||||
|
|
||||||
|
def update_domains(self):
|
||||||
|
for domain, update_info in self.config["domains"].items():
|
||||||
|
self.update_domain(domain, update_info)
|
||||||
|
|
||||||
|
def write_state(self):
|
||||||
|
with open(self.state_path, "w") as f:
|
||||||
|
json.dump({
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"addrs": self.addrs,
|
||||||
|
"domains": self.history
|
||||||
|
}, f, indent=2)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.detect_updates()
|
||||||
|
self.update_domains()
|
||||||
|
self.write_state()
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
time.sleep(10*60)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
assert isinstance(self.server, WebServer)
|
||||||
|
if self.path == '/':
|
||||||
|
path = self.server.args.web + '/index.html'
|
||||||
|
elif self.path == '/state':
|
||||||
|
path = self.server.args.state
|
||||||
|
else:
|
||||||
|
path = self.server.args.web + self.path
|
||||||
|
try:
|
||||||
|
data = open(path).read()
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(bytes(data, 'utf-8'))
|
||||||
|
except:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(bytes("File not found", 'utf-8'))
|
||||||
|
|
||||||
|
class WebServer(ThreadingHTTPServer):
|
||||||
|
def __init__(self, server_address, RequestHandlerClass, args):
|
||||||
|
self.args = args
|
||||||
|
super().__init__(server_address, RequestHandlerClass)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = get_args()
|
||||||
|
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
||||||
|
log.info(f"Loading config from {args.config}")
|
||||||
|
config = get_config(args.config)
|
||||||
|
if args.web:
|
||||||
|
log.info(f"Serving web interface from {args.web}")
|
||||||
|
httpd = WebServer(('', 8080), Handler, args)
|
||||||
|
threading.Thread(target=httpd.serve_forever).start()
|
||||||
|
updater = Updater(config, args.state)
|
||||||
|
updater.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
(()=>{var ee=!1,re=!1,W=[],ne=-1,ie=!1;function Ve(t){Dn(t)}function Ue(){ie=!0}function qe(){ie=!1,We()}function Dn(t){W.includes(t)||W.push(t),We()}function Ke(t){let e=W.indexOf(t);e!==-1&&e>ne&&W.splice(e,1)}function We(){if(!re&&!ee){if(ie)return;ee=!0,queueMicrotask(In)}}function In(){ee=!1,re=!0;for(let t=0;t<W.length;t++)W[t](),ne=t;W.length=0,ne=-1,re=!1}var C,R,j,se,oe=!0;function Ge(t){oe=!1,t(),oe=!0}function Je(t){C=t.reactive,j=t.release,R=e=>t.effect(e,{scheduler:r=>{oe?Ve(r):r()}}),se=t.raw}function ae(t){R=t}function Ye(t){let e=()=>{};return[n=>{let i=R(n);return t._x_effects||(t._x_effects=new Set,t._x_runEffects=()=>{t._x_effects.forEach(o=>o())}),t._x_effects.add(i),e=()=>{i!==void 0&&(t._x_effects.delete(i),j(i))},i},()=>{e()}]}function St(t,e){let r=!0,n,i,o=R(()=>{let s=t(),a=JSON.stringify(s);if(!r&&(typeof s=="object"||s!==n)){let c=typeof n=="object"?JSON.parse(i):n;queueMicrotask(()=>{e(s,c)})}n=s,i=a,r=!1});return()=>j(o)}async function Xe(t){Ue();try{await t(),await Promise.resolve()}finally{qe()}}var Ze=[],Qe=[],tr=[];function er(t){tr.push(t)}function et(t,e){typeof e=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(e)):(e=t,Qe.push(e))}function At(t){Ze.push(t)}function Ot(t,e,r){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[e]||(t._x_attributeCleanups[e]=[]),t._x_attributeCleanups[e].push(r)}function ce(t,e){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([r,n])=>{(e===void 0||e.includes(r))&&(n.forEach(i=>i()),delete t._x_attributeCleanups[r])})}function rr(t){for(t._x_effects?.forEach(Ke);t._x_cleanups?.length;)t._x_cleanups.pop()()}var le=new MutationObserver(pe),ue=!1;function ut(){le.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ue=!0}function fe(){kn(),le.disconnect(),ue=!1}var lt=[];function kn(){let t=le.takeRecords();lt.push(()=>t.length>0&&pe(t));let e=lt.length;queueMicrotask(()=>{if(lt.length===e)for(;lt.length>0;)lt.shift()()})}function m(t){if(!ue)return t();fe();let e=t();return ut(),e}var de=!1,vt=[];function nr(){de=!0}function ir(){de=!1,pe(vt),vt=[]}function pe(t){if(de){vt=vt.concat(t);return}let e=[],r=new Set,n=new Map,i=new Map;for(let o=0;o<t.length;o++)if(!t[o].target._x_ignoreMutationObserver&&(t[o].type==="childList"&&(t[o].removedNodes.forEach(s=>{s.nodeType===1&&s._x_marker&&r.add(s)}),t[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||e.push(s)}})),t[o].type==="attributes")){let s=t[o].target,a=t[o].attributeName,c=t[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ce(s,o)}),n.forEach((o,s)=>{Ze.forEach(a=>a(s,o))});for(let o of r)e.some(s=>s.contains(o))||Qe.forEach(s=>s(o));for(let o of e)o.isConnected&&tr.forEach(s=>s(o));e=null,r=null,n=null,i=null}function Ct(t){return P(F(t))}function N(t,e,r){return t._x_dataStack=[e,...F(r||t)],()=>{t._x_dataStack=t._x_dataStack.filter(n=>n!==e)}}function F(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?F(t.host):t.parentNode?F(t.parentNode):[]}function P(t){return new Proxy({objects:t},$n)}function or(t,e){return t===null||t===Object.prototype?null:Object.prototype.hasOwnProperty.call(t,e)?t:or(Object.getPrototypeOf(t),e)}var $n={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(e=>Object.keys(e))))},has({objects:t},e){return e==Symbol.unscopables?!1:t.some(r=>Object.prototype.hasOwnProperty.call(r,e)||Reflect.has(r,e))},get({objects:t},e,r){return e=="toJSON"?Ln:Reflect.get(t.find(n=>Reflect.has(n,e))||{},e,r)},set({objects:t},e,r,n){let i;for(let s of t)if(i=or(s,e),i)break;i||(i=t[t.length-1]);let o=Object.getOwnPropertyDescriptor(i,e);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,e,r)}};function Ln(){return Reflect.ownKeys(this).reduce((e,r)=>(e[r]=Reflect.get(this,r),e),{})}function rt(t){let e=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(t,c,o):e(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(t)}function Tt(t,e=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return t(this.initialValue,()=>jn(n,i),s=>me(n,i,s),i,o)}};return e(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function jn(t,e){return e.split(".").reduce((r,n)=>r[n],t)}function me(t,e,r){if(typeof e=="string"&&(e=e.split(".")),e.length===1)t[e[0]]=r;else{if(e.length===0)throw error;return t[e[0]]||(t[e[0]]={}),me(t[Line truncated
|
||||||
|
|
||||||
|
${r?'Expression: "'+r+`"
|
||||||
|
|
||||||
|
`:""}`,e),setTimeout(()=>{throw t},0)}var it=!0;function Mt(t){let e=it;it=!1;let r=t();return it=e,r}function T(t,e,r={}){let n;return _(t,e)(i=>n=i,r),n}function _(...t){return ur(...t)}var ur=()=>{};function fr(t){ur=t}var dr;function pr(t){dr=t}function mr(t,e){let r={};H(r,t);let n=[r,...F(t)],i=typeof e=="function"?zn(n,e):Vn(n,e,t);return ar.bind(null,t,e,i)}function zn(t,e){return(r=()=>{},{scope:n={},params:i=[],context:o}={})=>{if(!it){ft(r,e,P([n,...t]),i);return}let s=e.apply(P([n,...t]),i);ft(r,s)}}var _e={};function Hn(t,e){if(_e[t])return _e[t];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${t}`}),s}catch(s){return nt(s,e,t),Promise.resolve()}})();return _e[t]=o,o}function Vn(t,e,r){let n=Hn(e,r);return(i=()=>{},{scope:o={},params:s=[],context:a}={})=>{n.result=void 0,n.finished=!1;let c=P([o,...t]);if(typeof n=="function"){let l=n.call(a,n,c).catch(u=>nt(u,r,e));n.finished?(ft(i,n.result,c,s,r),n.result=void 0):l.then(u=>{ft(i,u,c,s,r)}).catch(u=>nt(u,r,e)).finally(()=>n.result=void 0)}}}function ft(t,e,r,n,i){if(it&&typeof e=="function"){let o=e.apply(r,n);o instanceof Promise?o.then(s=>ft(t,s,r,n)).catch(s=>nt(s,i,e)):t(o)}else typeof e=="object"&&e instanceof Promise?e.then(o=>t(o)):t(e)}function hr(...t){return dr(...t)}function _r(t,e,r={}){let n={};H(n,t);let i=[n,...F(t)],o=P([r.scope??{},...i]),s=r.params??[];if(e.includes("await")){let a=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e;return new a(["scope"],`with (scope) { let __result = ${c}; return __result }`).call(r.context,o)}else{let a=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(()=>{ ${e} })()`:e,l=new Function(["scope"],`with (scope) { let __result = ${a}; return __result }`).call(r.context,o);return typeof l=="function"&&it?l.apply(o,s):l}}var ye="x-";function O(t=""){return ye+t}function gr(t){ye=t}var Rt={};function p(t,e){return Rt[t]=e,{before(r){if(!Rt[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${t}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,t)}}}function xr(t){return Object.keys(Rt).includes(t)}function pt(t,e,r){if(e=Array.from(e),t._x_virtualDirectives){let o=Object.entries(t._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=be(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),e=e.concat(o)}let n={};return e.map(wr((o,s)=>n[o]=s)).filter(Sr).map(qn(n,r)).sort(Kn).map(o=>Un(t,o))}function be(t){return Array.from(t).map(wr()).filter(e=>!Sr(e))}var ge=!1,dt=new Map,yr=Symbol();function br(t){ge=!0;let e=Symbol();yr=e,dt.set(e,[]);let r=()=>{for(;dt.get(e).length;)dt.get(e).shift()();dt.delete(e)},n=()=>{ge=!1,r()};t(r),n()}function he(t){let e=[],r=a=>e.push(a),[n,i]=Ye(t);return e.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:_.bind(_,t),evaluate:T.bind(T,t)},()=>e.forEach(a=>a())]}function Un(t,e){let r=()=>{},n=Rt[e.type]||r,[i,o]=he(t);Ot(t,e.original,o);let s=()=>{t._x_ignore||t._x_ignoreSelf||(n.inline&&n.inline(t,e,i),n=n.bind(n,t,e,i),ge?dt.get(yr).push(n):n())};return s.runCleanups=o,s}var Nt=(t,e)=>({name:r,value:n})=>(r.startsWith(t)&&(r=r.replace(t,e)),{name:r,value:n}),Pt=t=>t;function wr(t=()=>{}){return({name:e,value:r})=>{let{name:n,value:i}=Er.reduce((o,s)=>s(o),{name:e,value:r});return n!==e&&t(n,e),{name:n,value:i}}}var Er=[];function ot(t){Er.push(t)}function Sr({name:t}){return vr().test(t)}var vr=()=>new RegExp(`^${ye}([^:^.]+)\\b`);function qn(t,e){return({name:r,value:n})=>{r===n&&(n="");let i=r.match(vr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=e||t[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var xe="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",xe,"teleport"];function Kn(t,e){let r=G.indexOf(t.type)===-1?xe:t.type,n=G.indexOf(e.type)===-1?xe:e.type;return G.indexOf(r)-G.indexOf(n)}function J(t,e,r={},n={}){return t.dispatchEvent(new CustomEvent(e,{detail:r,bubbles:!0,composed:!0,cancelable:!0,...n}))}function D(t,e){if(typeof ShadowRoot=="function"&&t instanceof ShadowRoot){Array.from(t.children).forEach(i=>D(i,e));return}let r=!1;if(e(t,()=>r=!0),r)return;let n=t.firstElementChild;for(;n;)D(n,e,!1),n=n.nextElementSibling}function E(t,...e){console.warn(`Alpine Warning: ${t}`,...e)}var Ar=!1;function Or(){Ar&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Ar=!0,document.body||E("Unable to initialize. TryinLine truncated
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
:root{--size: 1rem;--font: Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans", "Noto Sans", Arial, sans-serif;--font-icon: "Material Symbols Outlined";--speed1: .1s;--speed2: .2s;--speed3: .3s;--speed4: .4s;--active: rgb(128 128 128 / .192);--overlay: rgb(0 0 0 / .5);--elevate1: 0 .125rem .125rem 0 rgb(0 0 0 / .32);--elevate2: 0 .25rem .5rem 0 rgb(0 0 0 / .4);--elevate3: 0 .375rem .75rem 0 rgb(0 0 0 / .48);--top: env(safe-area-inset-top);--bottom: env(safe-area-inset-bottom);--left: env(safe-area-inset-left);--right: env(safe-area-inset-right);--image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4AWJiYGAQBgAAAP//ZyYa+wAAAAZJREFUAwAAIgAWeX9MsQAAAABJRU5ErkJggg==)}body.dark{--image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4AWL6//+/FAAAAP//qSv5QwAAAAZJREFUAwAJIAMaJWjIvQAAAABJRU5ErkJggg==)}html{font-size:var(--size)}body{color:var(--on-surface);background-color:var(--surface);overflow-x:hidden;font-family:var(--font);font-size:.875rem;line-height:1.5rem;letter-spacing:.0313rem}@media(pointer:fine){body ::-webkit-scrollbar,body ::-webkit-scrollbar-thumb,body ::-webkit-scrollbar-button{background:none;inline-size:.4rem;block-size:.4rem}body :is(:focus,:hover)::-webkit-scrollbar-thumb,body :has(:focus,:hover)::-webkit-scrollbar-thumb{background:var(--outline);border-radius:1rem}}@media(pointer:coarse){body:has(dialog[open],dialog.active),body:has(input[type=range]:focus){overflow:hidden}}@keyframes to-page{0%{opacity:0;transform:var(--_transform)}to{opacity:1;transform:translate(0)}}@keyframes to-linear-progress{0%{margin:0 0 0 -100%}50%{margin:0}to{margin:0 0 0 100%}}@keyframes to-indeterminate-progress{0%{padding:0 100% 0 0}50%{padding:0}to{padding:0 0 0 100%}}@keyframes to-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes to-ripple{to{transform:scale(4);opacity:0}}:root,body.light{--primary: #6750a4;--on-primary: #ffffff;--primary-container: #e9ddff;--on-primary-container: #22005d;--secondary: #625b71;--on-secondary: #ffffff;--secondary-container: #e8def8;--on-secondary-container: #1e192b;--tertiary: #7e5260;--on-tertiary: #ffffff;--tertiary-container: #ffd9e3;--on-tertiary-container: #31101d;--error: #ba1a1a;--on-error: #ffffff;--error-container: #ffdad6;--on-error-container: #410002;--background: #fffbff;--on-background: #1c1b1e;--surface: #fdf8fd;--on-surface: #1c1b1e;--surface-variant: #e7e0eb;--on-surface-variant: #49454e;--outline: #7a757f;--outline-variant: #cac4cf;--shadow: #000000;--scrim: #000000;--inverse-surface: #313033;--inverse-on-surface: #f4eff4;--inverse-primary: #cfbcff;--surface-dim: #ddd8dd;--surface-bright: #fdf8fd;--surface-container-lowest: #ffffff;--surface-container-low: #f7f2f7;--surface-container: #f2ecf1;--surface-container-high: #ece7eb;--surface-container-highest: #e6e1e6}body.dark{--primary: #cfbcff;--on-primary: #381e72;--primary-container: #4f378a;--on-primary-container: #e9ddff;--secondary: #cbc2db;--on-secondary: #332d41;--secondary-container: #4a4458;--on-secondary-container: #e8def8;--tertiary: #efb8c8;--on-tertiary: #4a2532;--tertiary-container: #633b48;--on-tertiary-container: #ffd9e3;--error: #ffb4ab;--on-error: #690005;--error-container: #93000a;--on-error-container: #ffb4ab;--background: #1c1b1e;--on-background: #e6e1e6;--surface: #141316;--on-surface: #e6e1e6;--surface-variant: #49454e;--on-surface-variant: #cac4cf;--outline: #948f99;--outline-variant: #49454e;--shadow: #000000;--scrim: #000000;--inverse-surface: #e6e1e6;--inverse-on-surface: #313033;--inverse-primary: #6750a4;--surface-dim: #141316;--surface-bright: #3a383c;--surface-container-lowest: #0f0e11;--surface-container-low: #1c1b1e;--surface-container: #201f22;--surface-container-high: #2b292d;--surface-container-highest: #363438}@font-face{font-family:Material Symbols Outlined;font-style:normal;font-weight:400;font-display:swap;src:url(material-symbols-outlined.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@4.0.20/dist/cdn/material-symbols-outlined.woff2) format("woff2")}@font-face{font-family:Material Symbols Rounded;font-style:normal;font-weight:400;font-display:swap;src:url(material-symbols-rounded.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@4.0.20/dist/cdn/material-symbols-rounded.woff2) format("woff2")}@font-face{font-family:Material Symbols Sharp;font-style:normal;font-weight:400;font-display:swap;src:url(material-symbols-sharp.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@4.0.20/dist/cdn/material-symbols-sharp.woff2) format("woff2")}@font-face{font-family:Material Symbols Subset;font-style:normal;font-weight:400;font-display:swap;src:url(material-symbols-subset.woff2) format("woff2"),url(https://cdn.jsdelivr.net/npm/beercss@4.0.20/dist/cdn/material-symbols-subset.woff2) format("woff2")}*{-webkit-tap-highlight-color:transparent;position:relative;vertical-align:middle;color:inherit;margin:0;padding:0;border-radius:inherit;box-sizing:border-bLine truncated
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
const Z=[],H=new WeakSet,M=navigator.userAgent.includes("Chrome");navigator.userAgent.includes("Firefox");navigator.userAgent.includes("Safari");navigator.userAgent.includes("Windows");const Q=navigator.userAgent.includes("Macintosh");navigator.userAgent.includes("Linux");navigator.userAgent.includes("Android");const J=/iPad|iPhone|iPod/.test(navigator.userAgent);function kt(){return window==null?void 0:window.matchMedia("(prefers-color-scheme: dark)").matches}async function tt(t){await new Promise(n=>setTimeout(n,t))}function xt(){return"fxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const n=Math.random()*16|0;return(t==="x"?n:n&3|8).toString(16)})}function v(t,n){try{return typeof t=="string"?(n??document).querySelector(t):t}catch{return null}}function c(t,n){try{return typeof t=="string"?(n??document).querySelectorAll(t):t??Z}catch{return Z}}function u(t,n){return(t==null?void 0:t.classList.contains(n))??!1}function g(t,n){var e;return((e=t==null?void 0:t.tagName)==null?void 0:e.toLowerCase())===n}function C(t,n){var e;return((e=t==null?void 0:t.type)==null?void 0:e.toLowerCase())===n}function h(t,n){if(t instanceof NodeList)for(let e=0;e<t.length;e++)t[e].classList.add(n);else t==null||t.classList.add(n)}function p(t,n){if(t instanceof NodeList)for(let e=0;e<t.length;e++)t[e].classList.remove(n);else t==null||t.classList.remove(n)}function T(t,n,e,i=!0){t!=null&&t.addEventListener&&t.addEventListener(n,e,i)}function a(t,n,e,i=!0){Et(t),T(t,n,e,i)}function D(t,n,e,i=!0){t!=null&&t.removeEventListener&&t.removeEventListener(n,e,i)}function mt(t,n){var e;(e=n==null?void 0:n.parentNode)==null||e.insertBefore(t,n)}function q(t){return t==null?void 0:t.previousElementSibling}function O(t){return t==null?void 0:t.nextElementSibling}function E(t){return t==null?void 0:t.parentElement}function At(t){const n=document.createElement("div");for(let e=0,i=Object.keys(t),s=i.length;e<s;e++){const r=i[e],o=t[r];n.setAttribute(r,o)}return n}function $(){var t;(t=document.activeElement)==null||t.blur()}function wt(t){return c('[data-ui="#'+t+'"]')}function Ct(t){return v('[data-ui="#'+t+'"]')}function Tt(t){t.id&&u(t,"page")&&(t=Ct(t.id)??t);const n=E(t);if(!u(n,"tabs")&&!u(n,"tabbed")&&!g(n,"nav"))return;const e=c("a",n);for(let i=0;i<e.length;i++)p(e[i],"active");!g(t,"button")&&!u(t,"button")&&!u(t,"chip")&&h(t,"active")}function Et(t){H.has(t)||H.add(t)}function nt(){const t=getComputedStyle(document.documentElement).getPropertyValue("--size")||"16px";return t.includes("%")?parseInt(t)*16/100:t.includes("em")?parseInt(t)*16:parseInt(t)}function z(t){t.placeholder||(t.placeholder=" ")}function Lt(t){const n=t.currentTarget,e=E(n),i=v("input:not([type=file], [type=checkbox], [type=radio]), select, textarea",e);i&&i.focus()}function N(t){const n=t.currentTarget;L(n)}function R(t){const n=t.currentTarget;L(n)}function St(t){const n=t.currentTarget;K(n)}function It(t){const n=t.currentTarget;W(n)}function Mt(t){const n=t.currentTarget;K(n,t)}function Dt(t){const n=t.currentTarget;W(n,t)}function _t(t){var i;const n=t.currentTarget,e=v("input",E(n));e&&((i=n.textContent)!=null&&i.includes("visibility"))&&(e.type==="password"?(e.type="text",n.textContent="visibility_off"):(e.type="password",n.textContent="visibility"))}function Ft(t){const n=t.currentTarget;et(n)}function Pt(){const t=c(".field > label");for(let n=0;n<t.length;n++)a(t[n],"click",Lt)}function qt(){const t=c(".field > input:not([type=file], [type=color], [type=range])");for(let n=0;n<t.length;n++)a(t[n],"focus",N),a(t[n],"blur",R),L(t[n])}function Ot(){const t=c(".field > select");for(let n=0;n<t.length;n++)a(t[n],"focus",N),a(t[n],"blur",R)}function $t(){const t=c(".field > input[type=file]");for(let n=0;n<t.length;n++)a(t[n],"change",St),K(t[n])}function zt(){const t=c(".field > input[type=color]");for(let n=0;n<t.length;n++)a(t[n],"change",It),W(t[n])}function Nt(){const t=c(".field > textarea");for(let n=0;n<t.length;n++)a(t[n],"focus",N),a(t[n],"blur",R),z(t[n]),!(M&&!Q&&!J)&&(a(t[n],"input",Ft),et(t[n]))}function Rt(){const t=c(".field:has(> input[type=password]) > i, a");for(let n=0;n<t.length;n++)a(t[n],"click",_t)}function L(t){C(t,"number")&&!t.value&&(t.value=""),z(t)}function K(t,n){if((n==null?void 0:n.key)==="Enter"){const i=q(t);if(!C(i,"file"))return;i.click();return}const e=O(t);C(e,"text")&&(e.value=t.files?Array.from(t.files).map(i=>i.name).join(", "):"",e.readOnly=!0,a(e,"keydown",Mt,!1),L(e))}function W(t,n){if((n==null?void 0:n.key)==="Enter"){const i=q(t);if(!C(i,"color"))return;i.click();return}const e=O(t);C(e,"text")&&(e.readOnly=!0,e.value=t.value,a(e,"keydown",Dt,!1),L(e))}function et(t){if(z(t),t.hasAttribute("rows"))return;const n=nt();t.style.blockSize="auto",t.style.blockSize=`${t.scrollHeight-n}px`}function Kt(){Pt(),qt(),Ot(),$t(),zt(),Nt(),Rt()}function X(t){const n=t.target;!g(n,"input")&&!g(n,"select")||(n.type==="range"?(n.focus(),ot(n)):it())}function Wt(t){if(!window.matchMedia("(pointer: coarse)").matches)rLine truncated
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>DynDns Updater</title>
|
||||||
|
<link rel="stylesheet" href="beer.min.css">
|
||||||
|
<script src="beer.min.js" defer></script>
|
||||||
|
<script src="alpine.min.js" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main x-data="data" class="responsive">
|
||||||
|
<h1>DynDns Updater</h1>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>IPv4</td>
|
||||||
|
<td><span x-text="addrs.ipv4"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>IPv6</td>
|
||||||
|
<td><span x-text="addrs.ipv6"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Last Update</td>
|
||||||
|
<td><span x-text="niceTime(lastUpdate)"></span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button class="small" x-on:click="update">Update</button>
|
||||||
|
<template x-for="domain in domains">
|
||||||
|
<article>
|
||||||
|
<h6>
|
||||||
|
<span x-text="domain.success ? '✅' : '❌'"></span>
|
||||||
|
<span x-text="domain.name"></span>
|
||||||
|
</h6>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>IPv4</td>
|
||||||
|
<td><span x-text="domain.addrs.ipv4"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>IPv6</td>
|
||||||
|
<td><span x-text="domain.addrs.ipv6"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Last Update</td>
|
||||||
|
<td><span x-text="niceTime(domain.lastUpdate)"></span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<button class="small">
|
||||||
|
<span>History</span>
|
||||||
|
<i>expand_more</i>
|
||||||
|
</button>
|
||||||
|
</summary>
|
||||||
|
<ul class="list border">
|
||||||
|
<template x-for="update in domain.history">
|
||||||
|
<li>
|
||||||
|
<i>home</i>
|
||||||
|
<div class="max">
|
||||||
|
<h6 class="small" x-text="update.response_text"></h6>
|
||||||
|
<div>
|
||||||
|
<button class="chip">IPv4</button><span x-text="update.addrs.ipv4"></span>
|
||||||
|
<button class="chip">IPv6</button><span x-text="update.addrs.ipv6"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label><span x-text="niceTime(update.timestamp)"></span></label>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('data', () => ({
|
||||||
|
addrs: { ipv4: null, ipv6: null },
|
||||||
|
lastUpdate: null,
|
||||||
|
domains: [],
|
||||||
|
niceTime(time) {
|
||||||
|
if (!time) return '';
|
||||||
|
const diff = new Date() - time;
|
||||||
|
if (diff < 1000) {
|
||||||
|
return 'just now';
|
||||||
|
} else if (diff < 60000) {
|
||||||
|
return `${Math.round(diff / 1000)} seconds ago`;
|
||||||
|
} else if (diff < 3600000) {
|
||||||
|
return `${Math.round(diff / 60000)} minutes ago`;
|
||||||
|
} else {
|
||||||
|
return time.toISOString();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const res = await fetch('http://localhost:8080/state');
|
||||||
|
const data = await res.json();
|
||||||
|
this.addrs = data.addrs;
|
||||||
|
this.lastUpdate = new Date(data.timestamp * 1000);
|
||||||
|
this.domains = Object.entries(data.domains).map(([domain, history]) => ({
|
||||||
|
name: domain,
|
||||||
|
success: history[history.length - 1].success,
|
||||||
|
lastUpdate: new Date(history[history.length - 1].timestamp * 1000),
|
||||||
|
addrs: history[history.length - 1].addrs,
|
||||||
|
history: history.reverse().map(h => ({
|
||||||
|
timestamp: new Date(h.timestamp * 1000),
|
||||||
|
addrs: h.addrs,
|
||||||
|
success: h.success,
|
||||||
|
response_text: h.response_text
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
await this.update();
|
||||||
|
window.setInterval(() => {
|
||||||
|
this.update();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user