summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 16:30:47 +0300
committerAhmed <git@gumx.cc>2026-06-14 16:30:47 +0300
commit307ff4912bac1095ebf382d70241f19409b2f8b8 (patch)
treea2c3d36634fa86705e48db4fc797437ba816e5e0
parentfa568e13d04c0aacdb29ca252b783f1dcdb6bf23 (diff)
add: templating
-rw-r--r--.gitignore1
-rw-r--r--_shared/build.py94
-rw-r--r--_shared/footer.html12
-rw-r--r--_shared/style.css22
-rw-r--r--build.sh2
-rw-r--r--demo.gumx.cc/demos.json9
-rw-r--r--demo.gumx.cc/meta2
-rw-r--r--files.gumx.cc/body.html6
-rw-r--r--files.gumx.cc/meta2
-rw-r--r--hooks/post-receive19
-rw-r--r--irc.gumx.cc/body.html9
-rw-r--r--irc.gumx.cc/index.html16
-rw-r--r--irc.gumx.cc/meta2
-rw-r--r--mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml22
-rw-r--r--mail.gumx.cc/body.html12
-rw-r--r--mail.gumx.cc/index.html12
-rw-r--r--mail.gumx.cc/meta2
-rw-r--r--pgp.gumx.cc/body.html7
-rw-r--r--pgp.gumx.cc/index.html14
-rw-r--r--pgp.gumx.cc/meta2
-rw-r--r--twt.gumx.cc/body.html49
-rw-r--r--twt.gumx.cc/extra.css4
-rw-r--r--twt.gumx.cc/meta2
-rw-r--r--vpn.gumx.cc/body.html19
-rw-r--r--vpn.gumx.cc/index.html15
-rw-r--r--vpn.gumx.cc/meta2
-rw-r--r--wk.fo/body.html19
-rw-r--r--wk.fo/index.html16
-rw-r--r--wk.fo/meta2
29 files changed, 383 insertions, 12 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..52cdfdb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*/index.html
diff --git a/_shared/build.py b/_shared/build.py
new file mode 100644
index 0000000..935bf5c
--- /dev/null
+++ b/_shared/build.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+"""Build sites from body.html + meta into index.html using shared templates."""
+import json
+import os
+import sys
+
+FAVICON = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+"
+
+
+def render(title, breadcrumb, style, extra_css, body, footer):
+ css = style + ("\n" + extra_css if extra_css.strip() else "")
+ return f"""<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<link rel="icon" type="image/svg+xml" href="{FAVICON}">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>{title}</title>
+<style>
+{css}
+</style>
+</head>
+<body>
+<header>
+<h1><a href="https://gumx.cc">gumx</a> / {breadcrumb}</h1>
+</header>
+<main>
+{body}
+</main>
+{footer}
+</body>
+</html>
+"""
+
+
+def build_demo_body(demos_file):
+ demos = json.load(open(demos_file))
+ parts = []
+ for d in demos:
+ url = d.get("url", "#")
+ title = d.get("title", d.get("name", ""))
+ desc = d.get("description", "")
+ src = d.get("source", "")
+ src_link = f' / <a href="{src}">source</a>' if src else ""
+ parts.append(f'<p><a href="{url}">{title}</a>{src_link}</p>\n<p>{desc}</p>')
+ return "\n".join(parts)
+
+
+def build(sites_dir):
+ shared = os.path.join(sites_dir, "_shared")
+ style = open(os.path.join(shared, "style.css")).read()
+ footer = open(os.path.join(shared, "footer.html")).read()
+
+ for site in sorted(os.listdir(sites_dir)):
+ if site.startswith("_") or site == "fonts" or site == "hooks":
+ continue
+ site_dir = os.path.join(sites_dir, site)
+ if not os.path.isdir(site_dir):
+ continue
+
+ body_file = os.path.join(site_dir, "body.html")
+ demos_file = os.path.join(site_dir, "demos.json")
+
+ if site == "demo.gumx.cc" and os.path.exists(demos_file):
+ body = build_demo_body(demos_file)
+ elif os.path.exists(body_file):
+ body = open(body_file).read()
+ else:
+ continue
+
+ title = site
+ breadcrumb = site
+ meta_file = os.path.join(site_dir, "meta")
+ if os.path.exists(meta_file):
+ for line in open(meta_file):
+ k, _, v = line.strip().partition("=")
+ if k == "TITLE":
+ title = v.strip('"')
+ elif k == "BREADCRUMB":
+ breadcrumb = v.strip('"')
+
+ extra_css = ""
+ extra_file = os.path.join(site_dir, "extra.css")
+ if os.path.exists(extra_file):
+ extra_css = open(extra_file).read()
+
+ out = render(title, breadcrumb, style, extra_css, body, footer)
+ with open(os.path.join(site_dir, "index.html"), "w") as f:
+ f.write(out)
+ print(f"built: {site}")
+
+
+if __name__ == "__main__":
+ build(sys.argv[1] if len(sys.argv) > 1 else ".")
diff --git a/_shared/footer.html b/_shared/footer.html
new file mode 100644
index 0000000..3aedfc6
--- /dev/null
+++ b/_shared/footer.html
@@ -0,0 +1,12 @@
+<footer>
+<hr>
+<a href="https://twt.gumx.cc">twt</a> /
+<a href="https://git.gumx.cc">git</a> /
+<a href="https://mail.gumx.cc">mail</a> /
+<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
+<a href="https://vpn.gumx.cc">vpn</a> /
+<a href="https://pgp.gumx.cc">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
+<a href="https://wk.fo">wk.fo</a>
+</footer>
diff --git a/_shared/style.css b/_shared/style.css
new file mode 100644
index 0000000..0f7e7cf
--- /dev/null
+++ b/_shared/style.css
@@ -0,0 +1,22 @@
+@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Regular.woff2); font-weight: normal; }
+@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Bold.woff2); font-weight: bold; }
+* { unicode-bidi: plaintext; box-sizing: border-box; }
+html { color: black; background-color: white; }
+body { font-family: "Kawkab Mono"; font-size: 16px; line-height: 1.4; margin: 0; padding: 4rem 0; min-height: 100%; overflow-wrap: break-word; }
+main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; }
+h1, header, footer { text-align: center; }
+main { text-align: justify; }
+p, h2, h3, h4 { margin: 1em 0 0 0; }
+ol { margin: 0.5em 0 0 0; }
+table { margin: auto; border-collapse: collapse; }
+th, td { border: 1px solid; padding: 0.3em 0.8em; }
+pre { margin: 1em 0; }
+pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
+code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
+header { margin-bottom: 1em; }
+footer { margin-top: 3em; }
+a { color: inherit; }
+@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
+@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } }
+@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..6de6968
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+python3 "$(dirname "$0")/_shared/build.py" "$(dirname "$0")"
diff --git a/demo.gumx.cc/demos.json b/demo.gumx.cc/demos.json
new file mode 100644
index 0000000..ae6d7df
--- /dev/null
+++ b/demo.gumx.cc/demos.json
@@ -0,0 +1,9 @@
+[
+ {
+ "name": "no-style-please",
+ "title": "zola-no-style-please",
+ "url": "/no-style-please/",
+ "description": "a Zola theme with no style, or rather, just a little style.",
+ "source": "https://git.gumx.cc/zola-no-style-please"
+ }
+]
diff --git a/demo.gumx.cc/meta b/demo.gumx.cc/meta
new file mode 100644
index 0000000..496fc7e
--- /dev/null
+++ b/demo.gumx.cc/meta
@@ -0,0 +1,2 @@
+TITLE="demo.gumx.cc"
+BREADCRUMB="demo"
diff --git a/files.gumx.cc/body.html b/files.gumx.cc/body.html
new file mode 100644
index 0000000..3c34353
--- /dev/null
+++ b/files.gumx.cc/body.html
@@ -0,0 +1,6 @@
+<p>File hosting via <a href="https://github.com/mia-0/0x0">0x0</a>. Uploads require a token.</p>
+<h2>upload</h2>
+<pre><code>curl -F "file=@photo.jpg" -H "Authorization: YOUR_TOKEN" https://files.gumx.cc/</code></pre>
+<p>Files expire after 24 hours by default. Max 256 MB.</p>
+<h2>access</h2>
+<p>Contact <a href="mailto:hi@gumx.cc">hi@gumx.cc</a> to request a token.</p>
diff --git a/files.gumx.cc/meta b/files.gumx.cc/meta
new file mode 100644
index 0000000..2c19518
--- /dev/null
+++ b/files.gumx.cc/meta
@@ -0,0 +1,2 @@
+TITLE="files.gumx.cc"
+BREADCRUMB="files"
diff --git a/hooks/post-receive b/hooks/post-receive
new file mode 100644
index 0000000..aa142ed
--- /dev/null
+++ b/hooks/post-receive
@@ -0,0 +1,19 @@
+#!/bin/sh
+set -e
+
+WORK=/home/git/build/sites
+
+cd "$WORK"
+python3 _shared/build.py .
+
+for SITE in irc.gumx.cc vpn.gumx.cc mail.gumx.cc pgp.gumx.cc wk.fo twt.gumx.cc files.gumx.cc demo.gumx.cc; do
+ if [ -d "$SITE" ]; then
+ WEBROOT="/var/www/$SITE"
+ mkdir -p "$WEBROOT"
+ rsync -rlptD --delete --exclude="/fonts" "$SITE/" "$WEBROOT/"
+ mkdir -p "$WEBROOT/fonts"
+ rsync -rlptD fonts/ "$WEBROOT/fonts/"
+ fi
+done
+
+echo "sites deployed"
diff --git a/irc.gumx.cc/body.html b/irc.gumx.cc/body.html
new file mode 100644
index 0000000..ee66fec
--- /dev/null
+++ b/irc.gumx.cc/body.html
@@ -0,0 +1,9 @@
+<p>Personal IRC server running <a href="https://ngircd.barton.de/">ngircd</a>, fronted by a <a href="https://soju.im/">Soju</a> bouncer. Access is by invitation.</p>
+<h2>connect</h2>
+<table>
+<tr><th>server</th><td>irc.gumx.cc:6697 (TLS)</td></tr>
+</table>
+<h2>request access</h2>
+<p>Send a message to <a href="mailto:hi@gumx.cc">hi@gumx.cc</a> with your nick and preferred IRC client.</p>
+<h2>bots</h2>
+<p><a href="/bots">irc bots</a></p>
diff --git a/irc.gumx.cc/index.html b/irc.gumx.cc/index.html
index a18a309..efb2b69 100644
--- a/irc.gumx.cc/index.html
+++ b/irc.gumx.cc/index.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
+<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>irc.gumx.cc</title>
<style>
@@ -14,15 +15,20 @@ main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; }
h1, header, footer { text-align: center; }
main { text-align: justify; }
p, h2, h3, h4 { margin: 1em 0 0 0; }
+ol { margin: 0.5em 0 0 0; }
table { margin: auto; border-collapse: collapse; }
th, td { border: 1px solid; padding: 0.3em 0.8em; }
+pre { margin: 1em 0; }
+pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
header { margin-bottom: 1em; }
footer { margin-top: 3em; }
a { color: inherit; }
@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } }
@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }
+
</style>
</head>
<body>
@@ -33,20 +39,26 @@ a { color: inherit; }
<p>Personal IRC server running <a href="https://ngircd.barton.de/">ngircd</a>, fronted by a <a href="https://soju.im/">Soju</a> bouncer. Access is by invitation.</p>
<h2>connect</h2>
<table>
-<tr><th>server</th><td>wk.fo:6697 (TLS)</td></tr>
+<tr><th>server</th><td>irc.gumx.cc:6697 (TLS)</td></tr>
</table>
<h2>request access</h2>
<p>Send a message to <a href="mailto:hi@gumx.cc">hi@gumx.cc</a> with your nick and preferred IRC client.</p>
+<h2>bots</h2>
+<p><a href="/bots">irc bots</a></p>
+
</main>
<footer>
<hr>
-<a href="https://gumx.cc">gumx.cc</a> /
+<a href="https://twt.gumx.cc">twt</a> /
<a href="https://git.gumx.cc">git</a> /
<a href="https://mail.gumx.cc">mail</a> /
<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
<a href="https://vpn.gumx.cc">vpn</a> /
<a href="https://pgp.gumx.cc">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
<a href="https://wk.fo">wk.fo</a>
</footer>
+
</body>
</html>
diff --git a/irc.gumx.cc/meta b/irc.gumx.cc/meta
new file mode 100644
index 0000000..fdf1428
--- /dev/null
+++ b/irc.gumx.cc/meta
@@ -0,0 +1,2 @@
+TITLE="irc.gumx.cc"
+BREADCRUMB="irc"
diff --git a/mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml b/mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml
new file mode 100644
index 0000000..8098148
--- /dev/null
+++ b/mail.gumx.cc/.well-known/autoconfig/mail/config-v1.1.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<clientConfig version="1.1">
+ <emailProvider id="gumx.cc">
+ <domain>gumx.cc</domain>
+ <displayName>gumx mail</displayName>
+ <displayShortName>gumx</displayShortName>
+ <incomingServer type="imap">
+ <hostname>mail.gumx.cc</hostname>
+ <port>993</port>
+ <socketType>SSL</socketType>
+ <authentication>password-cleartext</authentication>
+ <username>%EMAILADDRESS%</username>
+ </incomingServer>
+ <outgoingServer type="smtp">
+ <hostname>mail.gumx.cc</hostname>
+ <port>587</port>
+ <socketType>STARTTLS</socketType>
+ <authentication>password-cleartext</authentication>
+ <username>%EMAILADDRESS%</username>
+ </outgoingServer>
+ </emailProvider>
+</clientConfig>
diff --git a/mail.gumx.cc/body.html b/mail.gumx.cc/body.html
new file mode 100644
index 0000000..7a3e9fe
--- /dev/null
+++ b/mail.gumx.cc/body.html
@@ -0,0 +1,12 @@
+<p>Self-hosted mail running <a href="http://www.postfix.org/">Postfix</a> and <a href="https://www.dovecot.org/">Dovecot</a>.</p>
+<h2>contact</h2>
+<p><a href="mailto:hi@gumx.cc">hi@gumx.cc</a></p>
+<h2>IMAP / SMTP</h2>
+<table>
+<tr><th>IMAP</th><td>mail.gumx.cc:993 (TLS)</td></tr>
+<tr><th>SMTP</th><td>mail.gumx.cc:587 (STARTTLS)</td></tr>
+</table>
+<h2>CalDAV / CardDAV</h2>
+<p>Available at <code><a href="https://mail.gumx.cc/dav/">/dav/</a></code> via <a href="https://www.xandikos.org/">Xandikos</a>. Credentials on request.</p>
+<h2>mailing list</h2>
+<p>Site updates and discussion: <a href="/list">gumx.cc mailing list</a>.</p>
diff --git a/mail.gumx.cc/index.html b/mail.gumx.cc/index.html
index 9e5597c..e22b67a 100644
--- a/mail.gumx.cc/index.html
+++ b/mail.gumx.cc/index.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
+<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>mail.gumx.cc</title>
<style>
@@ -14,15 +15,20 @@ main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; }
h1, header, footer { text-align: center; }
main { text-align: justify; }
p, h2, h3, h4 { margin: 1em 0 0 0; }
+ol { margin: 0.5em 0 0 0; }
table { margin: auto; border-collapse: collapse; }
th, td { border: 1px solid; padding: 0.3em 0.8em; }
+pre { margin: 1em 0; }
+pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
header { margin-bottom: 1em; }
footer { margin-top: 3em; }
a { color: inherit; }
@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } }
@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }
+
</style>
</head>
<body>
@@ -42,16 +48,20 @@ a { color: inherit; }
<p>Available at <code><a href="https://mail.gumx.cc/dav/">/dav/</a></code> via <a href="https://www.xandikos.org/">Xandikos</a>. Credentials on request.</p>
<h2>mailing list</h2>
<p>Site updates and discussion: <a href="/list">gumx.cc mailing list</a>.</p>
+
</main>
<footer>
<hr>
-<a href="https://gumx.cc">gumx.cc</a> /
+<a href="https://twt.gumx.cc">twt</a> /
<a href="https://git.gumx.cc">git</a> /
<a href="https://mail.gumx.cc">mail</a> /
<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
<a href="https://vpn.gumx.cc">vpn</a> /
<a href="https://pgp.gumx.cc">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
<a href="https://wk.fo">wk.fo</a>
</footer>
+
</body>
</html>
diff --git a/mail.gumx.cc/meta b/mail.gumx.cc/meta
new file mode 100644
index 0000000..50375b1
--- /dev/null
+++ b/mail.gumx.cc/meta
@@ -0,0 +1,2 @@
+TITLE="mail.gumx.cc"
+BREADCRUMB="mail"
diff --git a/pgp.gumx.cc/body.html b/pgp.gumx.cc/body.html
new file mode 100644
index 0000000..df5b04d
--- /dev/null
+++ b/pgp.gumx.cc/body.html
@@ -0,0 +1,7 @@
+<p>Curated HKP keyserver. Keys are added after in-person verification only.</p>
+<h2>lookup</h2>
+<pre><code>gpg --keyserver hkps://pgp.gumx.cc --recv-keys &lt;fingerprint&gt;</code></pre>
+<p>Or via HTTPS:</p>
+<pre><code>curl "https://pgp.gumx.cc/pks/lookup?op=get&amp;search=0x&lt;fingerprint&gt;"</code></pre>
+<h2>submit</h2>
+<p>Keys are not accepted without prior arrangement. Contact <a href="mailto:hi@gumx.cc">hi@gumx.cc</a>.</p>
diff --git a/pgp.gumx.cc/index.html b/pgp.gumx.cc/index.html
index 7c3db3b..96e7623 100644
--- a/pgp.gumx.cc/index.html
+++ b/pgp.gumx.cc/index.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
+<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>pgp.gumx.cc</title>
<style>
@@ -14,15 +15,20 @@ main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; }
h1, header, footer { text-align: center; }
main { text-align: justify; }
p, h2, h3, h4 { margin: 1em 0 0 0; }
+ol { margin: 0.5em 0 0 0; }
+table { margin: auto; border-collapse: collapse; }
+th, td { border: 1px solid; padding: 0.3em 0.8em; }
pre { margin: 1em 0; }
pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
header { margin-bottom: 1em; }
footer { margin-top: 3em; }
a { color: inherit; }
@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } }
@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }
+
</style>
</head>
<body>
@@ -34,19 +40,23 @@ a { color: inherit; }
<h2>lookup</h2>
<pre><code>gpg --keyserver hkps://pgp.gumx.cc --recv-keys &lt;fingerprint&gt;</code></pre>
<p>Or via HTTPS:</p>
-<pre><code>curl "https://pgp.gumx.cc/pks/lookup?op=get&search=0x&lt;fingerprint&gt;"</code></pre>
+<pre><code>curl "https://pgp.gumx.cc/pks/lookup?op=get&amp;search=0x&lt;fingerprint&gt;"</code></pre>
<h2>submit</h2>
<p>Keys are not accepted without prior arrangement. Contact <a href="mailto:hi@gumx.cc">hi@gumx.cc</a>.</p>
+
</main>
<footer>
<hr>
-<a href="https://gumx.cc">gumx.cc</a> /
+<a href="https://twt.gumx.cc">twt</a> /
<a href="https://git.gumx.cc">git</a> /
<a href="https://mail.gumx.cc">mail</a> /
<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
<a href="https://vpn.gumx.cc">vpn</a> /
<a href="https://pgp.gumx.cc">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
<a href="https://wk.fo">wk.fo</a>
</footer>
+
</body>
</html>
diff --git a/pgp.gumx.cc/meta b/pgp.gumx.cc/meta
new file mode 100644
index 0000000..6cbe97d
--- /dev/null
+++ b/pgp.gumx.cc/meta
@@ -0,0 +1,2 @@
+TITLE="pgp.gumx.cc"
+BREADCRUMB="pgp"
diff --git a/twt.gumx.cc/body.html b/twt.gumx.cc/body.html
new file mode 100644
index 0000000..7f3adc3
--- /dev/null
+++ b/twt.gumx.cc/body.html
@@ -0,0 +1,49 @@
+<p>
+<a href="/twtxt.txt">twtxt.txt</a> /
+follow: <code>twtxt follow gumx https://twt.gumx.cc/twtxt.txt</code>
+</p>
+<div id="feed"></div>
+
+<script>
+async function loadFeed() {
+ const feed = document.getElementById('feed');
+ try {
+ const res = await fetch('/twtxt.txt');
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ const text = await res.text();
+ const lines = text.split('\n')
+ .filter(l => l && !l.startsWith('#'))
+ .map(l => {
+ const tab = l.indexOf('\t');
+ if (tab === -1) return null;
+ return { time: l.slice(0, tab), text: l.slice(tab + 1) };
+ })
+ .filter(Boolean)
+ .reverse();
+
+ if (lines.length === 0) {
+ feed.innerHTML = '<p id="error">no twts yet.</p>';
+ return;
+ }
+
+ feed.innerHTML = lines.map(t => {
+ const d = new Date(t.time);
+ const dateStr = d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
+ const timeStr = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
+ return `<div class="twt">
+<hr>
+<span class="twt-time">${dateStr} ${timeStr}</span>
+<span class="twt-text">${escapeHtml(t.text)}</span>
+</div>`;
+ }).join('');
+ } catch (e) {
+ feed.innerHTML = '<p id="error">could not load feed.</p>';
+ }
+}
+
+function escapeHtml(s) {
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+}
+
+loadFeed();
+</script>
diff --git a/twt.gumx.cc/extra.css b/twt.gumx.cc/extra.css
new file mode 100644
index 0000000..39c27d7
--- /dev/null
+++ b/twt.gumx.cc/extra.css
@@ -0,0 +1,4 @@
+.twt { margin: 1.5em 0 0 0; }
+.twt-time { font-size: 0.85em; opacity: 0.6; display: block; }
+.twt-text { display: block; margin-top: 0.25em; }
+#error { opacity: 0.5; }
diff --git a/twt.gumx.cc/meta b/twt.gumx.cc/meta
new file mode 100644
index 0000000..1721f95
--- /dev/null
+++ b/twt.gumx.cc/meta
@@ -0,0 +1,2 @@
+TITLE="twt.gumx.cc"
+BREADCRUMB="twt"
diff --git a/vpn.gumx.cc/body.html b/vpn.gumx.cc/body.html
new file mode 100644
index 0000000..49fb589
--- /dev/null
+++ b/vpn.gumx.cc/body.html
@@ -0,0 +1,19 @@
+<p><a href="https://www.wireguard.com/">WireGuard</a> VPN. Endpoint: <code>vpn.gumx.cc:51820</code>. Access is by invitation.</p>
+<h2>setup</h2>
+<ol>
+<li>Generate a keypair: <code>wg genkey | tee private.key | wg pubkey &gt; public.key</code></li>
+<li>Send your public key to <a href="mailto:hi@gumx.cc">hi@gumx.cc</a></li>
+<li>Receive your assigned IP (<code>10.0.0.x/32</code>) and the server public key</li>
+<li>Create <code>/etc/wireguard/wg0.conf</code> and bring it up with <code>wg-quick up wg0</code></li>
+</ol>
+<h2>example client config</h2>
+<pre><code>[Interface]
+PrivateKey = &lt;your private key&gt;
+Address = 10.0.0.x/32
+DNS = 1.1.1.1
+
+[Peer]
+PublicKey = &lt;server public key&gt;
+Endpoint = vpn.gumx.cc:51820
+AllowedIPs = 0.0.0.0/0
+PersistentKeepalive = 25</code></pre>
diff --git a/vpn.gumx.cc/index.html b/vpn.gumx.cc/index.html
index c2a3ce3..0d33729 100644
--- a/vpn.gumx.cc/index.html
+++ b/vpn.gumx.cc/index.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
+<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>vpn.gumx.cc</title>
<style>
@@ -15,15 +16,19 @@ h1, header, footer { text-align: center; }
main { text-align: justify; }
p, h2, h3, h4 { margin: 1em 0 0 0; }
ol { margin: 0.5em 0 0 0; }
+table { margin: auto; border-collapse: collapse; }
+th, td { border: 1px solid; padding: 0.3em 0.8em; }
pre { margin: 1em 0; }
pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
header { margin-bottom: 1em; }
footer { margin-top: 3em; }
a { color: inherit; }
@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } }
@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }
+
</style>
</head>
<body>
@@ -31,7 +36,7 @@ a { color: inherit; }
<h1><a href="https://gumx.cc">gumx</a> / vpn</h1>
</header>
<main>
-<p><a href="https://www.wireguard.com/">WireGuard</a> VPN. Endpoint: <code>wk.fo:51820</code>. Access is by invitation.</p>
+<p><a href="https://www.wireguard.com/">WireGuard</a> VPN. Endpoint: <code>vpn.gumx.cc:51820</code>. Access is by invitation.</p>
<h2>setup</h2>
<ol>
<li>Generate a keypair: <code>wg genkey | tee private.key | wg pubkey &gt; public.key</code></li>
@@ -47,19 +52,23 @@ DNS = 1.1.1.1
[Peer]
PublicKey = &lt;server public key&gt;
-Endpoint = wk.fo:51820
+Endpoint = vpn.gumx.cc:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25</code></pre>
+
</main>
<footer>
<hr>
-<a href="https://gumx.cc">gumx.cc</a> /
+<a href="https://twt.gumx.cc">twt</a> /
<a href="https://git.gumx.cc">git</a> /
<a href="https://mail.gumx.cc">mail</a> /
<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
<a href="https://vpn.gumx.cc">vpn</a> /
<a href="https://pgp.gumx.cc">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
<a href="https://wk.fo">wk.fo</a>
</footer>
+
</body>
</html>
diff --git a/vpn.gumx.cc/meta b/vpn.gumx.cc/meta
new file mode 100644
index 0000000..3c1ebbc
--- /dev/null
+++ b/vpn.gumx.cc/meta
@@ -0,0 +1,2 @@
+TITLE="vpn.gumx.cc"
+BREADCRUMB="vpn"
diff --git a/wk.fo/body.html b/wk.fo/body.html
new file mode 100644
index 0000000..40ac3f3
--- /dev/null
+++ b/wk.fo/body.html
@@ -0,0 +1,19 @@
+<p>File sharing, IRC bouncer, and VPN for friends. All services require an invitation.</p>
+<h2>file sharing</h2>
+<p>Upload via HTTPS (token required):</p>
+<pre><code>curl -F "file=@photo.jpg" -H "Authorization: YOUR_TOKEN" https://wk.fo/</code></pre>
+<p>Files expire after 24 hours by default (max 48h with <code>expires</code> param). Max 256 MB.</p>
+<h2>irc</h2>
+<table>
+<tr><th>server</th><td>wk.fo:6697 (TLS)</td></tr>
+<tr><th>type</th><td>Soju bouncer</td></tr>
+</table>
+<p>Use the credentials provided to you on invite. Works with any IRC client that supports SASL PLAIN.</p>
+<h2>vpn</h2>
+<table>
+<tr><th>server</th><td>wk.fo:51820 (UDP)</td></tr>
+<tr><th>type</th><td>WireGuard</td></tr>
+</table>
+<p>A config file is provided to you on invite. Import it into any WireGuard client.</p>
+<h2>access</h2>
+<p>Contact <a href="mailto:hi@gumx.cc">hi@gumx.cc</a>.</p>
diff --git a/wk.fo/index.html b/wk.fo/index.html
index c79d706..fbb74cb 100644
--- a/wk.fo/index.html
+++ b/wk.fo/index.html
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
+<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1IDUiPjxyZWN0IHg9IjEiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIxIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMCIgeT0iMiIgd2lkdGg9IjEiIGhlaWdodD0iMSIvPjxyZWN0IHg9IjEiIHk9IjIiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PC9zdmc+">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>wk.fo</title>
<style>
@@ -14,22 +15,25 @@ main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; }
h1, header, footer { text-align: center; }
main { text-align: justify; }
p, h2, h3, h4 { margin: 1em 0 0 0; }
-pre { margin: 1em 0; }
-pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
+ol { margin: 0.5em 0 0 0; }
table { margin: auto; border-collapse: collapse; }
th, td { border: 1px solid; padding: 0.3em 0.8em; }
+pre { margin: 1em 0; }
+pre code { border: thin solid; padding: 1em; display: block; text-align: start; overflow-x: scroll; }
code { font-size: 85%; }
+hr { border: none; border-top: thin solid; margin: 1.25rem 0; }
header { margin-bottom: 1em; }
footer { margin-top: 3em; }
a { color: inherit; }
@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } }
@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } }
@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }
+
</style>
</head>
<body>
<header>
-<h1>wk.fo</h1>
+<h1><a href="https://gumx.cc">gumx</a> / wk.fo</h1>
</header>
<main>
<p>File sharing, IRC bouncer, and VPN for friends. All services require an invitation.</p>
@@ -51,16 +55,20 @@ a { color: inherit; }
<p>A config file is provided to you on invite. Import it into any WireGuard client.</p>
<h2>access</h2>
<p>Contact <a href="mailto:hi@gumx.cc">hi@gumx.cc</a>.</p>
+
</main>
<footer>
<hr>
-<a href="https://gumx.cc">gumx.cc</a> /
+<a href="https://twt.gumx.cc">twt</a> /
<a href="https://git.gumx.cc">git</a> /
<a href="https://mail.gumx.cc">mail</a> /
<a href="https://irc.gumx.cc">irc</a> /
+<a href="https://files.gumx.cc">files</a> /
<a href="https://vpn.gumx.cc">vpn</a> /
<a href="https://pgp.gumx.cc">pgp</a> /
+<a href="https://demo.gumx.cc">demo</a> /
<a href="https://wk.fo">wk.fo</a>
</footer>
+
</body>
</html>
diff --git a/wk.fo/meta b/wk.fo/meta
new file mode 100644
index 0000000..e068cc2
--- /dev/null
+++ b/wk.fo/meta
@@ -0,0 +1,2 @@
+TITLE="wk.fo"
+BREADCRUMB="wk.fo"