diff --git a/src/models.py b/src/models.py index b1af68f..ad8d12c 100644 --- a/src/models.py +++ b/src/models.py @@ -38,7 +38,6 @@ class Site(Model): async with in_transaction() as tx: return await self.run_health_check(using_db=tx) - start = time.perf_counter() online = False latency = 0 status_code = 0 @@ -59,15 +58,15 @@ class Site(Model): status_code = response.status_code latency = round(response.elapsed.microseconds / 1000) online = response.is_success - except httpx.HTTPError: + except httpx.HTTPError as e: status_code = 0 latency = (time.perf_counter() - start) * 1000 online = False sleep_time = 2**retry_count + random.uniform(0, 1) sleep_time = min(60, sleep_time) log.warning( - "Retry %d on %r failed - sleeping %.2f seconds and trying again", - retry_count, self, sleep_time, exc_info=True + "Retry %d on %r failed - sleeping %.2f seconds and trying again (%r)", + retry_count, self, sleep_time, e ) await asyncio.sleep(sleep_time) else: @@ -117,24 +116,31 @@ class HealthCheckRunner(asyncio.Queue): async def run(self): while True: + self.log.info("Waiting for a heathcheck job.") site = await self.get() - self.log.info("Got a HealthCheck job: %r", site) + self.log.debug("HealthCheck job: %r", site) + self.log.info("HealthCheck job: %s", site) await site.run_health_check() self.task_done() - self.log.info("Finisdelay=hed health check job: %r", site) + self.log.debug("Finished health check job: %r", site) + self.log.debug("Finished health check job: %s", site) async def add_site(self, site: Site): await self.put(site) - self.log.info("Added a HealthCheck job: %r", site) + self.log.debug("Added a HealthCheck job: %r", site) return site async def add_expired_sites(self): # Sites that have not had a health check in the last hour - expired = ( + expired = list( await Site.filter(health_checks__checked_at__lt=datetime.datetime.now() - datetime.timedelta(hours=1)) .order_by("health_checks__checked_at") .distinct() ) + # Sites that have not had a health check at all + never_checked = await Site.filter(health_checks__isnull=True) + if never_checked: + expired.extend(list(never_checked)) for site in expired: await self.add_site(site) @@ -146,3 +152,6 @@ class HealthCheckRunner(asyncio.Queue): except asyncio.CancelledError: self.log.info("HealthCheckRunner loop cancelled.") return + + +RUNNER = HealthCheckRunner() diff --git a/src/server.py b/src/server.py index c60e81b..fb696bc 100644 --- a/src/server.py +++ b/src/server.py @@ -1,17 +1,23 @@ import asyncio -import os import logging -from tortoise.contrib.fastapi import RegisterTortoise +import os from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import Body, Depends, HTTPException, Query, Request, BackgroundTasks from fastapi import FastAPI -from .models import HealthCheckRunner -from fastapi import HTTPException, Query, Depends, Body +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse from fastapi.security import APIKeyHeader +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from httpx import AsyncClient from tortoise import generate_config +from tortoise.contrib.fastapi import RegisterTortoise from tortoise.queryset import Q from tortoise.transactions import in_transaction -from httpx import AsyncClient -from .models import Site, HealthCheck, SitesModel, HealthChecksModel + +from .models import HealthCheck, HealthChecksModel, RUNNER, Site, SitesModel if not os.environ.get("API_KEY"): @@ -26,6 +32,8 @@ async def lifespan(_app): handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) logging.getLogger("uvicorn.error").handlers = [handler] logging.getLogger("uvicorn.access").handlers = [handler] + logging.getLogger("uvicorn").handlers = [handler] + logging.info("Starting") async with RegisterTortoise( _app, config=generate_config( @@ -39,11 +47,15 @@ async def lifespan(_app): use_tz=True, _create_db=True, ): - _app.state.health_check_runner = HealthCheckRunner() - _app.state.hc_runner_task = asyncio.create_task(_app.state.health_check_runner.run()) - _app.state.hc_runner_loop = asyncio.create_task(_app.state.health_check_runner.loop()) + default = ["nexy7574.co.uk", "i-am.nexus", "transgender.ing", "example.invalid"] + logging.info("Generating default values") + for d in default: + await Site.get_or_create(domain=d, name=d, contact="https://matrix.to/#/@nex:nexy7574.co.uk") + logging.info("Starting health check runners") + _app.state.hc_runner_task = asyncio.create_task(RUNNER.run()) + logging.info("Starting API") yield - _app.state.hc_runner_loop.cancel() + _app.state.hc_runner_task.cancel() app = FastAPI( @@ -52,6 +64,14 @@ app = FastAPI( debug=os.environ.get("DEBUG") in ["0", "false", "no", None], lifespan=lifespan ) +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["GET"], + allow_credentials=False +) +app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static") API_KEY = APIKeyHeader(name="X-API-Key") @@ -71,6 +91,16 @@ async def get_http_client() -> AsyncClient: yield http_client +@app.get("/", include_in_schema=False) +async def home(req: Request): + sites = await SitesModel.from_queryset(Site.all()) + return templates.TemplateResponse( + request=req, + name="index.html", + context={"request": req, "sites": sites} + ) + + @app.get("/sites") async def get_sites( limit: int = Query(50, ge=1, le=100), @@ -81,7 +111,7 @@ async def get_sites( @app.get("/sites/{identifier}") -async def get_site(identifier: str) -> SitesModel: +async def get_site(identifier: str, tasks: BackgroundTasks) -> SitesModel: """ Returns a single site. @@ -91,6 +121,7 @@ async def get_site(identifier: str) -> SitesModel: result = await Site.filter(Q(guid=identifier) | Q(domain=identifier)).first() if not result: raise HTTPException(status_code=404, detail="Site not found.") + tasks.add_task(result.run_health_check) return await SitesModel.from_tortoise_orm(result) @@ -116,8 +147,10 @@ async def get_site_health( @app.get("/previous") async def get_previous_domain( + tasks: BackgroundTasks, domain: str = Query(..., min_length=4, max_length=1024, alias="d"), allow_offline: bool = Query(False), + redirect: bool = Query(True) ) -> SitesModel: """ Returns the most recent site added with the given domain. @@ -131,28 +164,34 @@ async def get_previous_domain( # Then, get the previous domain if allow_offline is False: - previous = Site.filter(online=allow_offline) + previous = Site.filter(online=True) else: previous = Site previous = await previous.filter(added_at__lt=site.added_at).order_by("-added_at").first() if not previous: # Fetch the latest site, there is none before us. - previous = await Site.order_by("-added_at").first() + previous = await Site.all().order_by("-added_at").first() if previous.domain == domain: raise HTTPException(500, "Could not find previous nor next domain.") + tasks.add_task(previous.run_health_check) + if redirect: + return RedirectResponse(url=f"https://{previous.domain}", status_code=308) return await SitesModel.from_tortoise_orm(previous) @app.get("/next") async def get_next_domain( + tasks: BackgroundTasks, domain: str = Query(..., min_length=4, max_length=1024, alias="d"), allow_offline: bool = Query(False), + redirect: bool = Query(True) ) -> SitesModel: """ Returns the next site added after the given domain. Raises 404 if the domain is not found. + If redirect is True (the default), a 308 Permanent Redirect is returned. """ # First, get the current domain site = await Site.get_or_none(domain=domain) @@ -161,16 +200,20 @@ async def get_next_domain( # Then, get the next domain if allow_offline is False: - next_site = Site.filter(online=allow_offline) + next_site = Site.filter(online=True) else: next_site = Site + next_site = await next_site.filter(added_at__gt=site.added_at).order_by("added_at").first() if not next_site: # Fetch the first site, there is none after us. - next_site = await Site.order_by("added_at").first() - if next_site.domain == domain: + next_site = await Site.all().order_by("added_at").filter(domain__not=domain).first() + if not next_site or next_site.domain == domain: raise HTTPException(500, "Could not find previous nor next domain.") + tasks.add_task(site.run_health_check) + if redirect: + return RedirectResponse(url=f"https://{next_site.domain}", status_code=308) return await SitesModel.from_tortoise_orm(next_site) @@ -191,25 +234,6 @@ async def add_new_site( return await SitesModel.from_tortoise_orm(site) -@app.post("/sites/{identifier}/health", dependencies=[Depends(get_api_key)], status_code=201) -async def start_health_check(identifier: str) -> dict[str, str | int | SitesModel]: - """ - Adds a health check to the queue for the site. - - The identifier can be the GUID of the site or the domain name. - Raises 404 if the site is not found. - """ - site = await Site.filter(Q(guid=identifier) | Q(domain=identifier)).first() - if not site: - raise HTTPException(status_code=404, detail="Site not found.") - await app.state.health_check_runner.put(site) - return { - "message": "Health check added to queue.", - "site": await SitesModel.from_tortoise_orm(site), - "queue.length": app.state.health_check_runner.qsize(), - } - - @app.delete("/sites/{identifier}", dependencies=[Depends(get_api_key)]) async def delete_site(identifier: str): """ diff --git a/src/static/prism.css b/src/static/prism.css new file mode 100644 index 0000000..a89f76a --- /dev/null +++ b/src/static/prism.css @@ -0,0 +1,3 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */ +code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} diff --git a/src/static/prism.js b/src/static/prism.js new file mode 100644 index 0000000..9b0cb8d --- /dev/null +++ b/src/static/prism.js @@ -0,0 +1,7 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; +!function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; diff --git a/src/static/script.js b/src/static/script.js new file mode 100644 index 0000000..d8b55a8 --- /dev/null +++ b/src/static/script.js @@ -0,0 +1,13 @@ +function runHealthCheck(target) { + async function inner() { + try { + const response = await fetch("/sites/" + target).then((r) => r.json()); + location.reload(); + } + catch (error) { + console.error("Error:", error); + } + } + return inner; +} + diff --git a/src/static/style.css b/src/static/style.css index e69de29..f730f76 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -0,0 +1,82 @@ +:root { + --background-primary: #1F242C; + --background-secondary: #272C34; + --text-primary: #abb2bf; + --text-link: #56b6c2; + --text-link-visited: #c678dd +} + +@media screen and (prefers-color-scheme: light) { + :root { + --background-primary: #f5f5f5; + --background-secondary: #fff; + --text-primary: #333; + } +} + +html { + font-family: sans-serif; + padding: 0; + margin: 0; + height: 100%; + width: 100%; + background-color: var(--background-primary); + color: var(--text-primary); +} + +body { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + padding: 0; + margin: 0; + text-align: center; +} + +table { + width: 100%; + border-collapse: collapse; + background-color: var(--background-secondary); + color: var(--text-primary); +} + +a { + color: var(--text-link); + text-decoration: none; +} + +a:visited { + color: var(--text-link-visited); +} + +table th, +table td { + padding: 0.5em; + border: 1px solid #444; +} + +table th { + background-color: #222; + font-weight: bold; +} + +table tr:nth-child(even) { + background-color: #333; +} + +table tr:hover { + background-color: #555; +} + + +.online { + color: #89ca78; + background-color: #89ca78; +} + +.offline { + color: #ef596f; + background-color: #ef596f; +} diff --git a/src/templates/about.html b/src/templates/about.html new file mode 100644 index 0000000..7c71fc8 --- /dev/null +++ b/src/templates/about.html @@ -0,0 +1,39 @@ + + + + + + + +
+ + +
+

WebbedRing

+

Lorem ipsum

+ +

Code to use ring

+
<div>
+    <a href="https://webbed-ring.example/previous?d=my-domain.example" rel="noopener noreferrer">Previous</a>
+    <a href="https://webbed-ring.example/" rel="noopener noreferrer">WebbedRingName</a>
+    <a href="https://webbed-ring.example/next?d=my-domain.example" rel="noopener noreferrer">Next</a>
+</div>
+
+ +
+ + \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html index 566549b..48f1456 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -2,9 +2,34 @@ - Title + WebbedRing + - +
+ {% include "about.html" %} + + + + + + + + + + {% for site in sites %} + + + + {% if site.online %} + + {% else %} + + {% endif %} + + {% endfor %} + +
NameDomainOnline
{{ site.name }}{{ site.domain }}YesNo
+
\ No newline at end of file