chore: initial commit of Server Configs

This commit is contained in:
sapient
2026-03-22 00:54:28 -07:00
commit 5fc35b2f76
226 changed files with 7906 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.env
.env.*
*.log
node_modules/
.venv/

149
httpserver/AUDIT.md Normal file
View File

@@ -0,0 +1,149 @@
# Code Audit Report — httpserver stack
**Date:** 2026-03-05
**Scope:** `/opt/stacks/httpserver` (compose, data/, chat server, services loader, static assets)
---
## Executive summary
- **Security:** Several issues: sensitive file exposure, client-side XSS risk in chat, missing escaping in services HTML, no chat rate limiting or abuse controls.
- **Correctness:** One protocol mismatch (gs.html chat history never populates).
- **Maintainability:** Minor issues (comment vs script name, dependency pinning).
---
## 1. Security
### 1.1 Sensitive file served as static content (High)
**File:** `data/banned.json`
**Issue:** The entire `data/` directory is mounted as the Apache document root (`./data:/var/www/html`). So `banned.json` is publicly reachable at `/banned.json`. It contains fail2ban-style jail names and IP addresses.
**Impact:** Information disclosure (internal IPs, jail names). Useful for reconnaissance.
**Recommendation:**
- Move `banned.json` outside the web root (e.g. project root or a non-served volume) if it must live in the repo, **or**
- Exclude it from the volume used as document root (e.g. serve only a whitelisted subdirectory), **or**
- Stop serving it over HTTP (e.g. use it only in a backend/script that doesnt expose it).
### 1.2 Chat client uses `innerHTML` for message text (Medium)
**File:** `data/chat.js` (around line 102)
```javascript
// Comment says "use textContent for safety" but code uses innerHTML:
textSpan.innerHTML = msg.text;
```
**Issue:** The server sanitizes only `<` and `>` in `chat-server.js`. Using `innerHTML` with server output is fragile: if sanitization is ever relaxed or bypassed, or if the client receives data from another path, this becomes an XSS sink.
**Recommendation:** Use `textContent` for the message body so the DOM is not parsed as HTML. If you need newlines, use `textContent` and style with `white-space: pre-wrap` (or similar).
### 1.3 Services HTML built from JSON without escaping (Medium)
**File:** `data/services-loader.js`
**Issue:** Service `name`, `url`, `category`, and `meta[].label` / `meta[].url` are interpolated into HTML strings (e.g. `href="${meta.url}"`, `${service.name}`) without encoding. If `services-data.json` is ever edited incorrectly, compromised, or merged with untrusted data, a value containing `"` or `>` could break attributes or inject script.
**Recommendation:** Add a small `escapeHtml` (and optionally `escapeAttr`) helper and use it for every value that is inserted into HTML or attributes. Keep treating `services-data.json` as trusted input, but defense-in-depth avoids mistakes and future data sources.
### 1.4 Chat server: no rate limiting or abuse controls (Medium)
**File:** `data/chat-server.js`
**Issue:** Any client can connect and send unlimited messages. There is no per-IP or per-connection rate limit, no use of `banned.json`, and no max message size beyond the 200-character sanitizer slice.
**Impact:** DoS via message flood; spam; possible memory pressure from a very large `history` if `HISTORY_MAX` is raised.
**Recommendation:** Add rate limiting (e.g. per connection: max N messages per minute). Optionally enforce max message size and consider using `banned.json` (or a similar list) to reject connections from banned IPs if the proxy passes client IP (e.g. via `X-Forwarded-For` and a trusted proxy config).
### 1.5 Chat sanitization is minimal (Low)
**File:** `data/chat-server.js``sanitize()`
**Issue:** Only `<` and `>` are escaped. For plain text in a `<div>` (and if the client uses `textContent` as recommended), this is enough for basic XSS prevention. It does not normalize Unicode or protect against other edge cases (e.g. if the same string were ever used in an attribute).
**Recommendation:** Keep server-side sanitization and switch the client to `textContent`. If you later use the same string in attributes or other contexts, add encoding appropriate to that context (e.g. attribute encoding).
---
## 2. Correctness
### 2.1 Chat history protocol mismatch in gs.html (Bug)
**Files:** `data/chat-server.js` vs `data/gs.html`
**Issue:** The server sends history as:
```javascript
{ type: 'history', messages: history }
```
`gs.html` expects:
```javascript
if (Array.isArray(msg.history)) { ... }
```
So `msg.history` is always undefined and chat history is never shown on the GS page.
**Recommendation:** In `gs.html`, use `msg.messages` when `data.type === 'history'` (and optionally check `Array.isArray(msg.messages)`), consistent with `chat.js`.
---
## 3. Configuration & deployment
### 3.1 Apache proxy config duplication
**File:** `data/chat-proxy.conf`
**Issue:** Both `RewriteRule` and `ProxyPass`/`ProxyPassReverse` are used for `/chat` → WebSocket. This can be redundant or confusing; one consistent mechanism (e.g. mod_rewrite with `[P]` or ProxyPass) is easier to reason about.
**Recommendation:** Prefer one approach (e.g. RewriteRule with `[P,L]` for WebSocket upgrade, and ensure no double proxy). Document which Apache modules are required.
### 3.2 Chat server port exposure
**File:** `compose.yaml`
**Issue:** The chat service publishes `8098:8081`. So the WebSocket server is reachable on host port 8098 without going through Apache. If Apache is the intended single entry point for the site, consider not publishing the chat port on the host (only expose it on `internal-net` so Apache can proxy to it).
**Recommendation:** Remove the `ports:` mapping for the chat service if all access should be via Apaches `/chat`; otherwise document that 8098 is intentionally public.
### 3.3 Dependency pinning
**File:** `data/package.json`
**Issue:** `"ws": "^8.18.0"` allows minor/patch updates. Rebuilds can pull different versions.
**Recommendation:** Use exact versions (e.g. `"ws": "8.18.0"`) or lock with `package-lock.json` committed and `npm ci` in the image for reproducible builds.
---
## 4. Maintainability
### 4.1 Outdated comment / missing script
**File:** `data/services-loader.js` (header comment)
**Issue:** Historical note: the loader previously had an embedded-data workflow and referenced a script for syncing/embedding.
**Recommendation:** Keep `services-data.json` as the single source of truth and load it at runtime (no embed/sync step).
---
## 5. Positive notes
- Chat server sanitizes nickname and message length and strips `<`/`>`.
- `gs.html` uses an `escapeHtml` helper for chat nick and text when building HTML.
- `main.js` respects `prefers-reduced-motion` and limits star count on small viewports.
- Compose resource limits and logging options are set; networks are isolated.
- `.env` in project root is not under the web root, so it is not served.
---
## 6. Summary of recommended actions (all applied)
| Priority | Item | Status |
|----------|------|--------|
| High | Stop serving `banned.json` | Done: Apache `<Files "banned.json"> Require all denied` in `chat-proxy.conf` |
| Medium | In `chat.js`, use `textContent` for message text | Done |
| Medium | In `services-loader.js`, escape all dynamic values | Done: `escapeHtml` / `escapeAttr` added and used |
| Medium | Add rate limiting to chat server | Done: 30 msg/min per connection (configurable via `CHAT_RATE_LIMIT`) |
| Medium | Fix `gs.html` to use `msg.messages` for history | Done |
| Low | Simplify Apache proxy config and document | Done: comment + deny for banned.json |
| Low | Do not publish chat port; use Apache only | Done: port removed; `gs.html` uses `location.host + '/chat'` |
| Low | Pin `ws`; fix `services-loader.js` comment | Done: `"ws": "8.18.0"`; loader now reads `services-data.json` directly (no status/embed script). |

99
httpserver/compose.yaml Normal file
View File

@@ -0,0 +1,99 @@
name: httpserver
x-logging: &a1
driver: json-file
options:
max-size: 10m
max-file: "3"
services:
http1:
image: php:8.2-apache
container_name: http1
restart: unless-stopped
environment:
- TZ=America/Los_Angeles
volumes:
- ./data:/var/www/html
- ./data/gw.html:/var/www/html/index.html
- ./data/chat-proxy.conf:/etc/apache2/conf-enabled/chat-proxy.conf
command: >
bash -c "a2enmod proxy proxy_http proxy_wstunnel rewrite &&
apache2-foreground"
ports:
- 9797:80
healthcheck:
test:
- CMD-SHELL
- curl -fs http://127.0.0.1:80/ || exit 1
interval: 15s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.25"
memory: 128M
reservations:
cpus: "0.05"
memory: 32M
logging: *a1
http2:
image: php:8.2-apache
container_name: http2
restart: unless-stopped
environment:
- TZ=America/Los_Angeles
volumes:
- ./data:/var/www/html
- ./data/ugh.html:/var/www/html/index.html
- ./data/chat-proxy.conf:/etc/apache2/conf-enabled/chat-proxy.conf
command: >
bash -c "a2enmod proxy proxy_http proxy_wstunnel rewrite &&
apache2-foreground"
ports:
- 9798:80
healthcheck:
test:
- CMD-SHELL
- curl -fs http://127.0.0.1:80/ || exit 1
interval: 15s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.25"
memory: 128M
reservations:
cpus: "0.05"
memory: 32M
logging: *a1
chat-server:
image: node:22-alpine
working_dir: /app
restart: unless-stopped
environment:
- CHAT_LOG_PATH=/app/messages.log
volumes:
- ./data:/app
command: sh -c "npm install --omit=dev && node chat-server.js"
healthcheck:
test:
- CMD-SHELL
- curl -fs http://127.0.0.1:80/ || exit 1
interval: 15s
timeout: 5s
retries: 5
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
reservations:
cpus: "0.1"
memory: 64M
networks: {}

21
httpserver/data/.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# OS
.DS_Store
Thumbs.db
# Editor/IDE
*.swp
*.swo
*~
.idea/
.vscode/
*.sublime-*
# Logs
*.log
# Build/temp
node_modules/
dist/
build/
tmp/
.cache/

260
httpserver/data/404.html Normal file
View File

@@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Service Unavailable</title>
<style>
:root {
--glitch-green: #05ffa1;
--glitch-red: #ff0055;
--bg-color: #000;
--text-color: #fff;
--font-mono: 'Cascadia Code', 'Fira Code', 'Courier New', monospace;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
/* Video Background */
.video-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.4;
z-index: -1;
filter: grayscale(100%) brightness(0.5) contrast(1.5);
}
/* Scanlines */
body::before {
content: " ";
display: block;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
z-index: 10;
background-size: 100% 2px, 3px 100%;
pointer-events: none;
}
.container {
text-align: center;
z-index: 20;
padding: 2rem;
background: rgba(0, 0, 0, 0.8);
border: 1px solid var(--glitch-green);
box-shadow: 0 0 20px rgba(5, 255, 161, 0.2);
backdrop-filter: blur(5px);
}
h1 {
font-size: 4rem;
text-transform: uppercase;
letter-spacing: 0.5rem;
margin-bottom: 1rem;
position: relative;
animation: glitch 1s linear infinite;
}
h1::before,
h1::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
h1::before {
left: 2px;
text-shadow: -2px 0 var(--glitch-red);
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim 5s infinite linear alternate-reverse;
}
h1::after {
left: -2px;
text-shadow: -2px 0 var(--glitch-green);
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim2 5s infinite linear alternate-reverse;
}
p {
font-size: 1.2rem;
color: var(--glitch-green);
margin-bottom: 2rem;
text-transform: uppercase;
letter-spacing: 0.2rem;
}
.home-link {
display: inline-block;
padding: 10px 20px;
border: 1px solid var(--glitch-green);
color: var(--glitch-green);
text-decoration: none;
text-transform: uppercase;
transition: all 0.3s ease;
}
.home-link:hover {
background: var(--glitch-green);
color: var(--bg-color);
box-shadow: 0 0 10px var(--glitch-green);
cursor: pointer;
}
@keyframes glitch {
0% {
transform: translate(0);
}
20% {
transform: translate(-2px, 2px);
}
40% {
transform: translate(-2px, -2px);
}
60% {
transform: translate(2px, 2px);
}
80% {
transform: translate(2px, -2px);
}
100% {
transform: translate(0);
}
}
@keyframes glitch-anim {
0% {
clip: rect(31px, 9999px, 94px, 0);
}
20% {
clip: rect(62px, 9999px, 42px, 0);
}
40% {
clip: rect(16px, 9999px, 78px, 0);
}
60% {
clip: rect(43px, 9999px, 11px, 0);
}
80% {
clip: rect(89px, 9999px, 56px, 0);
}
100% {
clip: rect(5px, 9999px, 33px, 0);
}
}
@keyframes glitch-anim2 {
0% {
clip: rect(65px, 9999px, 100px, 0);
}
20% {
clip: rect(12px, 9999px, 55px, 0);
}
40% {
clip: rect(87px, 9999px, 12px, 0);
}
60% {
clip: rect(3px, 9999px, 89px, 0);
}
80% {
clip: rect(45px, 9999px, 23px, 0);
}
100% {
clip: rect(56px, 9999px, 76px, 0);
}
}
/* Terminal Prompt Effect */
.prompt::after {
content: "_";
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
@media (max-width: 600px) {
h1 {
font-size: 2rem;
letter-spacing: 0.2rem;
}
p {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<video class="video-bg" autoplay loop muted playsinline>
<source src="assets/disabled.mp4.mp4" type="video/webp">
</video>
<div class="container">
<h1 data-text="404">404</h1>
<p class="prompt">Service Unavailable</p>
<a href="index.html" class="home-link">Return to Base</a>
</div>
<script>
// Randomly trigger extra glitch effects
setInterval(() => {
const h1 = document.querySelector('h1');
if (Math.random() > 0.95) {
h1.style.transform = `skew(${Math.random() * 20 - 10}deg)`;
setTimeout(() => h1.style.transform = 'skew(0deg)', 50);
}
}, 100);
</script>
</body>
</html>

38
httpserver/data/PAGES.md Normal file
View File

@@ -0,0 +1,38 @@
# Pages
## Root structure
- **index.html** — Redirects to `gw.html`.
- **Active pages** — `gw.html`, `ugh.html`, `gs.html`, `basic.html` (see table below).
- **Shared** — `main.js`, `services-loader.js`, `services-data.json`, `ddate-now`, `assets/`, and per-page CSS/JS. **Chat backend**`chat-server.js`, `package.json` (Node + `ws`) for the real-time chat on **GS**.
## Active
| Page | File | Description |
|--------|------------|-------------|
| **GW** | `gw.html` | Main GravityWell.xYz site (standard). |
| **Ugh**| `ugh.html` | UGH.im branded, high-energy version. |
| **GS** | `gs.html` | GalaxySpin.Space experience; includes embedded real-time chat widget. |
| **Basic** | `basic.html` | Minimal HTML/CSS, no JS; good for low bandwidth and accessibility. |
## Archive
Non-active or alternate versions are under `archive/`:
- **archive/standby.html** — Maintenance / standby page.
- **archive/unavailable.html** — Unavailable notice (with home link).
- **archive/future.html** (+ **archive/future.css**) — “Future” theme; loads `../services-data.json`.
- **archive/extra/** — Alternate layouts: Retro, Shelf, Icons, Bloodlust (ugh-bloodlust); `index.html` redirects to `../../gw.html`.
- **archive/GSS/** — GalaxySpin.Space theme variants (type0type7) and `instructions.txt`.
- **archive/20260208_234428/** — Dated snapshot of GW and Ugh.
- **archive/services.json**, **archive/gs_services.json** — Legacy data (unused by active pages).
## Chat (GS)
**gs.html** includes an embedded real-time chat widget (bottom-right). It connects to a WebSocket server that broadcasts messages to all connected clients (no persistence).
- **Run the chat server:** From the repo root, `npm install` then `node chat-server.js` (or `npm run chat`). Listens on port **8081** by default; set `CHAT_PORT` to override.
- **Docker:** When deployed via the stack at `/opt/stacks/httpsimple`, a second service `chat-server` runs the Node server; the compose file exposes the WebSocket on port **8098**.
- **WebSocket URL (frontend):** Configurable so the same page works locally and behind a reverse proxy.
- **Default:** `ws://<hostname>:8098` (or `wss://` if the page is served over HTTPS). Use when the chat server is reachable on port 8098.
- **Override:** Set `window.CHAT_WS_URL` before the chat script runs, or set `data-ws-url` on `<body>` (e.g. `data-ws-url="wss://galaxyspin.space/chat-ws"` when proxying).

BIN
httpserver/data/assets/BTC.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

BIN
httpserver/data/assets/DOGE.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

BIN
httpserver/data/assets/XMR.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -0,0 +1,24 @@
{
"last_updated": "2026-01-28 21:15:01",
"jails": {
"sshd": [
"182.43.235.218",
"93.71.118.99"
],
"recidive": [
"176.120.22.13",
"209.38.21.233",
"80.94.92.182",
"80.94.92.186"
],
"nginx-env-aggressive": [],
"python-scanner": [
"185.209.196.236",
"4.194.156.15"
],
"nginx-bot-signature": [],
"npm-attacks": [],
"npm-traffic": []
},
"total_ips": 8
}

213
httpserver/data/basic.css Normal file
View File

@@ -0,0 +1,213 @@
/* Basic — minimal, readable, no-JS-friendly */
:root {
--bg: #0f0f0f;
--text: #e0e0e0;
--muted: #888;
--accent: #6a9fb5;
--link: #7cb8d4;
--border: #333;
}
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, Segoe UI, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
max-width: 52rem;
margin: 0 auto;
padding: 1rem 1.5rem;
}
header {
text-align: center;
margin-bottom: 1.5rem;
}
header h1 {
font-size: 1.5rem;
letter-spacing: 0.15em;
margin: 0 0 0.5rem;
}
header p {
margin: 0.25rem 0;
color: var(--muted);
font-size: 0.95rem;
}
nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
justify-content: center;
}
nav a {
color: var(--link);
text-decoration: none;
}
nav a:hover {
text-decoration: underline;
}
.versions {
margin: 0.75rem 0;
padding: 0.5rem 0;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
text-align: center;
}
.versions a {
color: var(--accent);
text-decoration: none;
margin: 0 0.5rem;
}
.versions a:hover {
text-decoration: underline;
}
hr {
border: none;
border-top: 1px solid var(--border);
margin: 1.5rem 0;
}
main section {
margin-bottom: 1rem;
}
h2 {
font-size: 1.15rem;
margin: 1.25rem 0 0.5rem;
color: var(--text);
}
h3 {
font-size: 1rem;
margin: 1rem 0 0.4rem;
color: var(--muted);
}
p {
margin: 0.5rem 0;
}
ul {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
li {
margin: 0.25rem 0;
}
a {
color: var(--link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Maintenance Status */
.status-maintenance {
opacity: 0.6;
pointer-events: none;
}
/* Back-compat: treat "down" the same as prior "maintenance" */
.status-down {
opacity: 0.6;
pointer-events: none;
}
.status-maintenance a {
text-decoration: line-through !important;
color: var(--muted) !important;
}
.status-down a {
text-decoration: line-through !important;
color: var(--muted) !important;
}
.maintenance-badge {
font-size: 0.8em;
color: #e74c3c;
margin-left: 0.5rem;
font-weight: bold;
}
code {
background: #1a1a1a;
padding: 0.15em 0.4em;
border-radius: 3px;
font-size: 0.9em;
word-break: break-all;
}
#guest-info {
background: #1a1a1a;
padding: 0.75rem 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
#guest-info small {
color: var(--muted);
}
footer {
text-align: center;
font-size: 0.9rem;
color: var(--muted);
margin-top: 2rem;
padding-top: 1rem;
}
footer a {
color: var(--accent);
}
.footer-links ul {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
list-style: none;
padding-left: 0;
}
.footer-links li {
margin: 0;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f5f5f5;
--text: #1a1a1a;
--muted: #555;
--accent: #2d6a7a;
--link: #1a5f7a;
--border: #ccc;
}
code {
background: #e8e8e8;
}
#guest-info {
background: #e8e8e8;
}
}

232
httpserver/data/basic.html Normal file
View File

@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GravityWell.xYz (Basic)</title>
<meta name="description" content="Self-hosted services and community. Basic HTML Version.">
<link rel="stylesheet" href="basic.css">
<link rel="icon" type="image/webp" href="assets/favicon.webp">
</head>
<body>
<header>
<h1>GRAVITYWELL.xYz</h1>
<p>Self-Hosting is killing corporate profits!</p>
<p>We left these services open so you can help.</p>
</header>
<hr>
<nav>
<ul>
<li><a href="#about">ABOUT</a></li>
<li><a href="#community">COMMUNITY</a></li>
<li><a href="#contact">CONTACT</a></li>
<li><a href="#services">SERVICES</a></li>
<li><a href="#donate">DONATE</a></li>
</ul>
<p class="versions">Other versions: <a href="gw.html">GW</a> · <a href="ugh.html">Ugh</a> · <a href="archive/extra/ugh-bloodlust.html">Ugh (Bloodlust, archive)</a> · <a href="gs.html">GS</a></p>
</nav>
<hr>
<main>
<section id="about">
<h2>About This Space</h2>
<p>An experiment in self-hosting, data archiving, and community building.</p>
<p>Most services here are open to new users and connected with the Fediverse. All services are running on
home infrastructure and a cheap VPS.</p>
<h3>Recommended Extensions</h3>
<p>The following browser extensions have been known to cause problems for surveillance capitalism and as
such have been banned by Google, but you can still use them!</p>
<ul>
<li><a href="https://gitflic.ru/project/magnolia1234/bypass-paywalls-chrome-clean" target="_blank"
rel="noopener noreferrer">Bypass Paywalls Clean</a></li>
<li><a href="https://adnauseam.io" target="_blank" rel="noopener noreferrer">AdNauseam</a></li>
<li><a href="https://libredirect.github.io" target="_blank" rel="noopener noreferrer">LibRedirect</a>
</li>
<li><a href="https://github.com/ClearURLs/Addon" target="_blank" rel="noopener noreferrer">ClearURLs</a>
</li>
</ul>
</section>
<hr>
<section id="community">
<h2>Community</h2>
<ul>
<li><a href="https://matrix.to/#/#gravitywell:mx.ugh.im" target="_blank" rel="noreferrer">Matrix
Chat</a></li>
<li><a href="https://signal.group/#CjQKIHU8ll31vC-Sb2m-xz3_hCLqbMoxlvRbsUuVKrpKMSgzEhAS7jFO9D_605yFXG8rZfVz"
target="_blank" rel="noreferrer">Signal Group</a></li>
<li><a href="https://lem.ugh.im/c/gravitywellxyz" target="_blank" rel="noreferrer">Lemmy Community</a>
</li>
<li><a href="https://floatilla.gravitywell.xyz/spaces/nostr.gravitywell.xyz" target="_blank"
rel="noreferrer">Nostr Community</a></li>
</ul>
</section>
<hr>
<section id="contact">
<h2>Contact</h2>
<ul>
<li><strong>Matrix:</strong> <a href="https://matrix.to/#/@gravitas:mx.ugh.im" target="_blank"
rel="noreferrer">@gravitas:mx.ugh.im</a></li>
<li><strong>Signal:</strong> <a
href="https://signal.me/#eu/sz35pvMZQ3GjCg6F3bCfYua9Mv2Y1sG4qPjogSLOTHeVFpd6tjBFHKlfaek8RQwh"
target="_blank" rel="noreferrer">Gravitas.75</a></li>
<li><strong>XMPP:</strong> <a href="xmpp:gravitas@xmpp.is" target="_blank"
rel="noreferrer">gravitas@xmpp.is</a></li>
<li><strong>Email:</strong> GravityWell@RiseUp.net</li>
<li><strong>PGP:</strong> <a
href="https://keys.openpgp.org/vks/v1/by-fingerprint/63363203336726B59E981F3FC995CF7689B7546C"
target="_blank" rel="noreferrer">0xC995CF7689B7546C</a></li>
</ul>
</section>
<hr>
<section id="services">
<h2>Services</h2>
<div id="guest-info">
<h3>Guest Access Info</h3>
<p>Some services don't have a sign-up option. Use the guest account to try them out.</p>
<ul>
<li><strong>USER:</strong> gwguest</li>
<li><strong>PASS:</strong> gravitywell.xyz</li>
</ul>
<p><small>* GravityWell.xYz services hosted on home server.<br>* ugh.im services hosted on VPS.</small>
</p>
</div>
<h3>Multimedia</h3>
<ul>
<li><a href="https://jfin.gravitywell.xyz" target="_blank" rel="noreferrer">Jellyfin</a> (<a
href="https://ser77s6g3yhn47auyqfyupbm4odncjmnfatqmxzebiz2fe5tw5emdhyd.onion" target="_blank"
rel="noreferrer">Onion Address</a> | <a href="https://wiz.gravitywell.xyz/j/FCKNFLX"
target="_blank" rel="noreferrer">Register</a>)</li>
<li><a href="https://seerr.gravitywell.xyz" target="_blank" rel="noreferrer">Jellyseerr</a></li>
</ul>
<h3>Audio Streaming</h3>
<ul>
<li><a href="https://navi.gravitywell.xyz" target="_blank" rel="noreferrer">NaviDrome</a></li>
<li><a href="https://feishin.gravitywell.xyz/" target="_blank" rel="noreferrer">Feishin</a></li>
<li><a href="https://funk.gravitywell.xyz" target="_blank" rel="noreferrer">FunkWhale</a></li>
</ul>
<h3>Games & Emulation</h3>
<ul>
<li><a href="https://romm.gravitywell.xyz" target="_blank" rel="noreferrer">Romm</a> (<a
href="https://wiz.gravitywell.xyz/j/NTDOYSUX" target="_blank" rel="noreferrer">Register</a>)
</li>
</ul>
<h3>Fediverse</h3>
<ul>
<li><a href="https://pl.ugh.im" target="_blank" rel="noreferrer">Pleroma</a></li>
<li><a href="https://lem.ugh.im/" target="_blank" rel="noreferrer">Lemmy</a></li>
<li><a href="https://pie.gravitywell.xyz" target="_blank" rel="noreferrer">PieFed</a></li>
<li><a href="https://peertube.gravitywell.xyz" target="_blank" rel="noreferrer">PeerTube</a></li>
</ul>
<h3>Communications</h3>
<ul>
<li><a href="https://talk.gravitywell.xyz/" target="_blank" rel="noreferrer">MiroChat</a></li>
</ul>
<h3>Privacy Front Ends</h3>
<ul>
<li><a href="https://piped.gravitywell.xyz" target="_blank" rel="noreferrer">Piped</a> (<a
href="https://piped.ugh.im" target="_blank" rel="noreferrer">Alt</a>)</li>
<li><a href="https://rimgo.gravitywell.xyz" target="_blank" rel="noreferrer">Rimgo</a> (<a
href="https://rmg.ugh.im" target="_blank" rel="noreferrer">Alt</a>)</li>
<li><a href="https://quetre.gravitywell.xyz" target="_blank" rel="noreferrer">Quetre</a></li>
<li><a href="https://redlib.gravitywell.xyz" target="_blank" rel="noreferrer">Redlib</a> (<a
href="https://rd.ugh.im" target="_blank" rel="noreferrer">Alt</a>)</li>
<li><a href="https://searx.gravitywell.xyz" target="_blank" rel="noreferrer">SearXNG</a> (<a
href="https://searx.ugh.im" target="_blank" rel="noreferrer">Alt</a>)</li>
<li><a href="https://dumb.gravitywell.xyz" target="_blank" rel="noreferrer">Dumb</a></li>
</ul>
<h3>Office & Productivity</h3>
<ul>
<li><a href="https://cloud.gravitywell.xyz" target="_blank" rel="noreferrer">NextCloud</a></li>
<li><a href="https://pb.ugh.im" target="_blank" rel="noreferrer">PrivateBin</a></li>
<li><a href="https://tasks.gravitywell.xyz" target="_blank" rel="noreferrer">Vikunja</a></li>
<li><a href="https://fr.ugh.im" target="_blank" rel="noreferrer">FreshRSS</a></li>
<li><a href="https://hd.ugh.im" target="_blank" rel="noreferrer">HedgeDoc</a></li>
<li><a href="https://lw.gravitywell.xyz" target="_blank" rel="noreferrer">Linkwarden</a></li>
</ul>
<h3>Utilities</h3>
<ul>
<li><a href="https://speedtest.gravitywell.xyz" target="_blank" rel="noreferrer">OpenSpeedTest</a> (<a
href="https://st.ugh.im" target="_blank" rel="noreferrer">Alt</a>)</li>
</ul>
</section>
<hr>
<section id="donate">
<h2>Support this project:</h2>
<h3>Fiat Channels</h3>
<ul>
<li><a href="https://ko-fi.com/L3L1LJRC4" target="_blank" rel="noreferrer">KO-FI</a></li>
<li><a href="https://liberapay.com/GravityWell.XYZ/donate" target="_blank"
rel="noreferrer">LIBERAPAY</a></li>
<li><a href="https://cash.app/$gravitywellxyz" target="_blank" rel="noreferrer">CASH APP</a></li>
</ul>
<h3>Crypto</h3>
<ul>
<li><strong>BTC (Bitcoin):</strong> <code>bc1qxhdlvdc2wpa6ns2xgm5ehv5s3lepd029dwxz5s</code></li>
<li><strong>DOGE (Dogecoin):</strong> <code>D9XJ3ZjG9q9Ern6bKjeugj7v2BEuREWqKG</code></li>
<li><strong>XMR (Monero):</strong>
<code>87mmbb6iLMJ6g5xAMUaP8V5Bus3nCjxPr2v1xzgHNeY2AP4RkYsgcs3cZjXUNwB6tQHJZQxE3PEarUCSJMzZFEDhKRDNo8e</code>
</li>
</ul>
</section>
<hr>
<section class="footer-links">
<h2>Awesome Links!</h2>
<ul>
<li><a href="https://bitwarden.com/" target="_blank" rel="noopener noreferrer">Bitwarden</a></li>
<li><a href="https://www.debian.org/" target="_blank" rel="noopener noreferrer">Debian</a></li>
<li><a href="https://www.defectivebydesign.org/" target="_blank" rel="noopener noreferrer">Defective by
Design</a></li>
<li><a href="https://spyware.neocities.org/" target="_blank" rel="noopener noreferrer">Spyware
Watchdog</a></li>
<li><a href="https://ffmpeg.org/" target="_blank" rel="noopener noreferrer">FFmpeg</a></li>
<li><a href="https://grapheneos.org/" target="_blank" rel="noopener noreferrer">GrapheneOS</a></li>
<li><a href="https://joinfediverse.wiki/" target="_blank" rel="noopener noreferrer">Join Fediverse</a>
</li>
<li><a href="https://neocities.org/" target="_blank" rel="noopener noreferrer">NeoCities</a></li>
<li><a href="https://www.torproject.org/" target="_blank" rel="noopener noreferrer">Tor Project</a></li>
<li><a href="https://stopstalkerware.org" target="_blank" rel="noopener noreferrer">Stop Stalkerware</a>
</li>
<li><a href="https://trash-guides.info" target="_blank" rel="noopener noreferrer">Trash Guides</a></li>
<li><a href="https://deflock.me" target="_blank" rel="noopener noreferrer">Deflock</a></li>
</ul>
</section>
</main>
<hr>
<footer>
<p>GRAVITYWELL.XYZ | <a href="gw.html">GW</a> | <a href="ugh.html">Ugh</a> | <a href="archive/extra/ugh-bloodlust.html">Ugh (Bloodlust, archive)</a> | <a href="gs.html">GS</a> | <a href="archive/extra/retro.html">Retro (archive)</a></p>
</footer>
</body>
</html>

Binary file not shown.

View File

@@ -0,0 +1,23 @@
# WebSocket proxy: /chat -> chat-server:8081
# Requires: proxy, proxy_http, proxy_wstunnel, rewrite
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so
ProxyRequests Off
ProxyPreserveHost On
# Deny direct access to sensitive files (e.g. banned.json) if present in docroot
<Directory /var/www/html>
<Files "banned.json">
Require all denied
</Files>
</Directory>
# Single mechanism: ProxyPass for WebSocket upgrade and initial request
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/chat$ ws://chat-server:8081/ [P,L]
RewriteRule ^/chat ws://chat-server:8081/ [P,L]
ProxyPass /chat ws://chat-server:8081/
ProxyPassReverse /chat ws://chat-server:8081/

View File

@@ -0,0 +1,201 @@
#!/usr/bin/env node
'use strict';
const { WebSocketServer } = require('ws');
const fs = require('fs');
const path = require('path');
const PORT = Number(process.env.CHAT_PORT) || 8081;
const HISTORY_MAX = Math.max(1, Math.min(1000, Number(process.env.CHAT_HISTORY_MAX) || 100));
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
const RATE_LIMIT_MAX_MESSAGES = Number(process.env.CHAT_RATE_LIMIT) || 30;
const DEFAULT_LOG_DIR = __dirname;
const LOG_PATH = process.env.CHAT_LOG_PATH
? path.resolve(process.env.CHAT_LOG_PATH)
: path.join(DEFAULT_LOG_DIR, 'messages.log');
const clients = new Set();
const history = [];
const connectionMessageCount = new WeakMap();
const connectionWindowStart = new WeakMap();
const connectionNickSuffix = new WeakMap();
function getClientIp(req) {
const xff = req && req.headers ? req.headers['x-forwarded-for'] : null;
if (typeof xff === 'string' && xff.trim()) {
// May be a comma-separated list: client, proxy1, proxy2
const first = xff.split(',')[0].trim();
if (first) return first;
}
const realIp = req && req.headers ? req.headers['x-real-ip'] : null;
if (typeof realIp === 'string' && realIp.trim()) return realIp.trim();
const ra = req && req.socket ? req.socket.remoteAddress : null;
return typeof ra === 'string' && ra ? ra : '';
}
function last3DigitsOfIp(ip) {
const digits = String(ip || '').replace(/\D/g, '');
return digits.slice(-3).padStart(3, '0');
}
function sanitizeNickBase(str) {
if (typeof str !== 'string') return '';
// Keep it short; we append 3 digits after.
return str.trim().slice(0, 20).replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function loadHistoryFromLog() {
// Read the log as newline-delimited JSON and keep the last HISTORY_MAX entries.
// Uses a ring buffer so it can handle large logs without loading everything.
try {
if (!fs.existsSync(LOG_PATH)) return;
} catch (_) {
return;
}
const ring = new Array(HISTORY_MAX);
let count = 0;
let buf = '';
try {
const fd = fs.openSync(LOG_PATH, 'r');
try {
const chunk = Buffer.allocUnsafe(64 * 1024);
let bytesRead = 0;
let pos = 0;
// Stream through the file in chunks (simple forward scan).
while ((bytesRead = fs.readSync(fd, chunk, 0, chunk.length, pos)) > 0) {
pos += bytesRead;
buf += chunk.subarray(0, bytesRead).toString('utf8');
let idx;
while ((idx = buf.indexOf('\n')) !== -1) {
const line = buf.slice(0, idx);
buf = buf.slice(idx + 1);
const trimmed = line.trim();
if (!trimmed) continue;
let obj;
try {
obj = JSON.parse(trimmed);
} catch (_) {
continue;
}
if (!obj || typeof obj.nick !== 'string' || typeof obj.text !== 'string') continue;
const entry = {
nick: sanitize(obj.nick || 'Visitor'),
text: sanitize(obj.text || '').trim(),
ts: Number.isFinite(obj.ts) ? obj.ts : Date.now(),
};
if (!entry.text) continue;
ring[count % HISTORY_MAX] = entry;
count++;
}
}
} finally {
fs.closeSync(fd);
}
} catch (err) {
console.error('Failed to read message log for history:', err && err.message ? err.message : err);
return;
}
// Flush any partial final line (no trailing newline).
const last = buf.trim();
if (last) {
try {
const obj = JSON.parse(last);
if (obj && typeof obj.nick === 'string' && typeof obj.text === 'string') {
const entry = {
nick: sanitize(obj.nick || 'Visitor'),
text: sanitize(obj.text || '').trim(),
ts: Number.isFinite(obj.ts) ? obj.ts : Date.now(),
};
if (entry.text) {
ring[count % HISTORY_MAX] = entry;
count++;
}
}
} catch (_) {}
}
const total = Math.min(count, HISTORY_MAX);
const start = count > HISTORY_MAX ? (count % HISTORY_MAX) : 0;
for (let i = 0; i < total; i++) {
history.push(ring[(start + i) % HISTORY_MAX]);
}
}
function sanitize(str) {
if (typeof str !== 'string') return '';
return str.slice(0, 200).replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function checkRateLimit(ws) {
const now = Date.now();
let start = connectionWindowStart.get(ws);
let count = connectionMessageCount.get(ws) || 0;
if (start == null || now - start >= RATE_LIMIT_WINDOW_MS) {
start = now;
count = 0;
}
count++;
connectionWindowStart.set(ws, start);
connectionMessageCount.set(ws, count);
return count <= RATE_LIMIT_MAX_MESSAGES;
}
function safeAppendLogLine(line) {
fs.appendFile(LOG_PATH, line + '\n', { encoding: 'utf8' }, (err) => {
if (err) console.error('Failed to append message log:', err.message);
});
}
const wss = new WebSocketServer({ port: PORT }, () => {
try {
fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
} catch (err) {
console.error('Failed to create log directory:', err && err.message ? err.message : err);
}
loadHistoryFromLog();
console.log('Chat server listening on port', PORT);
console.log('Message log path:', LOG_PATH);
});
wss.on('connection', (ws, req) => {
clients.add(ws);
const ip = getClientIp(req);
const suffix = last3DigitsOfIp(ip);
connectionNickSuffix.set(ws, suffix);
if (history.length > 0) {
const payload = JSON.stringify({ type: 'history', messages: history });
if (ws.readyState === 1) ws.send(payload);
}
ws.on('message', (raw) => {
if (!checkRateLimit(ws)) return;
let msg;
try {
msg = JSON.parse(String(raw));
} catch (_) {
return;
}
const suffix = connectionNickSuffix.get(ws) || '000';
const base = sanitizeNickBase(msg.nick) || 'visitor';
const nick = `${base}${suffix}`;
const text = sanitize(msg.text || '');
if (!text.trim()) return;
const ts = Date.now();
const entry = { nick, text: text.trim(), ts };
history.push(entry);
while (history.length > HISTORY_MAX) history.shift();
safeAppendLogLine(JSON.stringify({ ...entry, iso: new Date(ts).toISOString() }));
const payload = JSON.stringify(entry);
clients.forEach((client) => {
if (client.readyState === 1) client.send(payload);
});
});
ws.on('close', () => clients.delete(ws));
ws.on('error', () => clients.delete(ws));
});

179
httpserver/data/chat.css Normal file
View File

@@ -0,0 +1,179 @@
/* chat.css */
#chat-widget {
position: fixed;
bottom: 20px;
right: 20px;
width: 320px;
max-width: calc(100vw - 40px);
background: rgba(10, 10, 15, 0.95);
border: 2px solid var(--c-accent, #ff00ff);
border-radius: 8px;
box-shadow: 0 0 15px rgba(255, 0, 255, 0.4), inset 0 0 10px rgba(0, 255, 255, 0.1);
display: flex;
flex-direction: column;
z-index: 10000;
font-family: inherit;
color: var(--c-text, #fff);
overflow: hidden;
backdrop-filter: blur(5px);
transition: height 0.3s ease, transform 0.3s ease;
}
#chat-widget.collapsed {
height: 40px !important;
cursor: pointer;
}
#chat-header {
background: linear-gradient(90deg, #2b00ff, #ff00ff);
color: white;
padding: 10px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
#chat-header span {
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
}
#chat-toggle {
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
#chat-body {
height: 350px;
max-height: 50vh;
display: flex;
flex-direction: column;
}
#chat-messages {
flex-grow: 1;
padding: 10px;
overflow-y: auto;
font-size: 0.9rem;
scrollbar-width: thin;
scrollbar-color: var(--c-accent, #ff00ff) rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
gap: 6px;
}
#chat-messages::-webkit-scrollbar {
width: 6px;
}
#chat-messages::-webkit-scrollbar-track {
background: rgba(0,0,0,0.5);
}
#chat-messages::-webkit-scrollbar-thumb {
background-color: var(--c-accent, #ff00ff);
border-radius: 3px;
}
.chat-message {
word-break: break-word;
line-height: 1.3;
}
.chat-ts {
color: #888;
font-size: 0.75rem;
margin-right: 4px;
}
.chat-nick {
color: var(--c-secondary, #00ffff);
font-weight: bold;
margin-right: 4px;
}
.chat-text {
color: #eee;
}
#chat-input-area {
padding: 10px;
border-top: 1px solid rgba(255, 0, 255, 0.3);
background: rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
gap: 5px;
}
.chat-settings {
display: flex;
gap: 5px;
align-items: center;
}
.chat-settings input {
flex-grow: 1;
background: rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.2);
color: #fff;
padding: 4px 6px;
font-size: 0.8rem;
border-radius: 4px;
}
.chat-settings input:focus {
outline: none;
border-color: var(--c-accent, #ff00ff);
}
#chat-controls {
display: flex;
gap: 5px;
}
#chat-input {
flex-grow: 1;
background: rgba(0,0,0,0.6);
border: 1px solid var(--c-accent, #ff00ff);
color: #fff;
padding: 8px;
font-size: 0.9rem;
border-radius: 4px;
}
#chat-input:focus {
outline: none;
box-shadow: 0 0 5px var(--c-accent, #ff00ff);
}
#chat-send {
background: var(--c-accent, #ff00ff);
color: white;
border: none;
padding: 0 12px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
text-transform: uppercase;
font-size: 0.8rem;
}
#chat-send:active {
transform: scale(0.95);
}
#chat-status {
text-align: center;
font-size: 0.75rem;
color: #aaa;
margin-top: 2px;
}
.status-online { color: #0f0 !important; }
.status-offline { color: #f00 !important; }

167
httpserver/data/chat.js Normal file
View File

@@ -0,0 +1,167 @@
/* chat.js */
document.addEventListener('DOMContentLoaded', () => {
const CHAT_WIDGET_HTML = `
<div id="chat-widget" class="collapsed">
<div id="chat-header">
<span><span id="chat-status-dot" class="status-offline">●</span> MESSAGE BOARD</span>
<button id="chat-toggle" aria-label="Toggle message board">▲</button>
</div>
<div id="chat-body" style="display: none;">
<div id="chat-messages"></div>
<div id="chat-input-area">
<div class="chat-settings">
<label for="chat-nick-input" style="font-size:0.8rem; color:#ccc;">Name:</label>
<input type="text" id="chat-nick-input" placeholder="visitor" maxlength="20">
</div>
<div id="chat-controls">
<input type="text" id="chat-input" placeholder="Post a message..." maxlength="200">
<button id="chat-send">Post</button>
</div>
<div id="chat-status">Connecting…</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', CHAT_WIDGET_HTML);
const widget = document.getElementById('chat-widget');
const header = document.getElementById('chat-header');
const toggleBtn = document.getElementById('chat-toggle');
const body = document.getElementById('chat-body');
const messagesContainer = document.getElementById('chat-messages');
const input = document.getElementById('chat-input');
const sendBtn = document.getElementById('chat-send');
const nickInput = document.getElementById('chat-nick-input');
const statusEl = document.getElementById('chat-status');
const statusDot = document.getElementById('chat-status-dot');
let ws = null;
let isCollapsed = true;
// Initialize nickname base (server appends IP suffix)
let savedNick = localStorage.getItem('gw_chat_nick');
if (!savedNick) {
savedNick = 'visitor';
localStorage.setItem('gw_chat_nick', savedNick);
}
nickInput.value = savedNick;
nickInput.addEventListener('change', () => {
let val = nickInput.value.trim();
if (!val) val = 'visitor';
nickInput.value = val;
localStorage.setItem('gw_chat_nick', val);
});
// Toggle chat
function toggleChat() {
isCollapsed = !isCollapsed;
if (isCollapsed) {
widget.classList.add('collapsed');
body.style.display = 'none';
toggleBtn.textContent = '▲';
} else {
widget.classList.remove('collapsed');
body.style.display = 'flex';
toggleBtn.textContent = '▼';
input.focus();
scrollToBottom();
}
}
header.addEventListener('click', (e) => {
if (e.target !== nickInput && e.target !== input) {
toggleChat();
}
});
function formatTime(ts) {
const d = new Date(ts);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
}
function appendMessage(msg) {
const div = document.createElement('div');
div.className = 'chat-message';
const tsSpan = document.createElement('span');
tsSpan.className = 'chat-ts';
tsSpan.textContent = '[' + formatTime(msg.ts) + ']';
const nickSpan = document.createElement('span');
nickSpan.className = 'chat-nick';
nickSpan.textContent = msg.nick + ':';
const textSpan = document.createElement('span');
textSpan.className = 'chat-text';
textSpan.textContent = msg.text;
div.appendChild(tsSpan);
div.appendChild(nickSpan);
div.appendChild(textSpan);
messagesContainer.appendChild(div);
scrollToBottom();
}
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host; // includes port if non-standard
ws = new WebSocket(`${protocol}//${host}/chat`);
ws.onopen = () => {
statusEl.textContent = 'Connected';
statusDot.className = 'status-online';
};
ws.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'history') {
messagesContainer.innerHTML = '';
data.messages.forEach(appendMessage);
} else {
appendMessage(data);
}
} catch (err) {
console.error('Chat MS error:', err);
}
};
ws.onclose = () => {
statusEl.textContent = 'Disconnected. Reconnecting...';
statusDot.className = 'status-offline';
setTimeout(connect, 3000);
};
ws.onerror = () => {
statusEl.textContent = 'Connection Error';
ws.close();
};
}
function sendMessage() {
const text = input.value.trim();
if (!text || ws.readyState !== WebSocket.OPEN) return;
const nick = nickInput.value.trim() || 'visitor';
const msg = { nick, text };
ws.send(JSON.stringify(msg));
input.value = '';
input.focus();
}
sendBtn.addEventListener('click', sendMessage);
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Init connection
connect();
});

View File

@@ -0,0 +1,70 @@
/**
* check-status.js
* Pings all service URLs and updates services-data.json.
* Then re-embeds the data into services-loader.js so file:// works.
*
* Usage: node check-status.js
*/
const fs = require('fs');
const path = require('path');
const DATA_FILE = path.join(__dirname, 'services-data.json');
const LOADER_FILE = path.join(__dirname, 'services-loader.js');
const INLINE_START = '// === INLINE DATA — sync with services-data.json ===';
const INLINE_END = '// === END INLINE DATA ===';
async function checkServices() {
console.log('--- Service Status Check Started ---');
let data;
try {
data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
} catch (err) {
console.error('Error reading data file:', err);
return;
}
for (const service of data.services) {
process.stdout.write(`Checking ${service.name}... `);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(service.url, {
signal: controller.signal,
method: 'HEAD',
headers: { 'User-Agent': 'Status-Checker/1.0' }
});
clearTimeout(timeout);
if (res.status === 200 || res.status === 201) {
console.log('\x1b[32mUP\x1b[0m (' + res.status + ')');
delete service.status;
} else {
console.log('\x1b[33mDOWN\x1b[0m (' + res.status + ')');
service.status = 'maintenance';
}
} catch (err) {
console.log('\x1b[31mOFFLINE\x1b[0m (' + err.message + ')');
service.status = 'maintenance';
}
}
// Write updated services-data.json
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
console.log('\n✓ services-data.json updated\n');
// Sync embedded data into services-loader.js
let loader = fs.readFileSync(LOADER_FILE, 'utf8');
const startIdx = loader.indexOf(INLINE_START);
const endIdx = loader.indexOf(INLINE_END);
if (startIdx === -1 || endIdx === -1) {
console.error('Could not find INLINE DATA markers in services-loader.js');
return;
}
const before = loader.substring(0, startIdx);
const after = loader.substring(endIdx + INLINE_END.length);
const newLine = `${INLINE_START}\nconst SERVICES_EMBEDDED = ${JSON.stringify(data)};\n${INLINE_END}`;
fs.writeFileSync(LOADER_FILE, before + newLine + after, 'utf8');
console.log('✓ services-loader.js inline data synced');
console.log('--- Done ---');
}
checkServices();

View File

@@ -0,0 +1,2 @@
Today is Pungenday, the 5th day of Discord in the YOLD 3192
Celebrate Mojoday

272
httpserver/data/gs.html Normal file
View File

@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GalaxySpin.Space</title>
<link rel="stylesheet" href="chat.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
overflow-x: hidden;
}
body {
font-family: 'Courier New', Courier, 'Liberation Mono', 'Consolas', monospace;
background: #0b0d0e;
color: #fff;
min-height: 100vh;
position: relative;
}
/* Starfield */
.starfield {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
}
.star {
position: absolute;
background: #fff;
border-radius: 50%;
animation: twinkle var(--duration) ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
opacity: var(--opacity);
}
50% {
opacity: calc(var(--opacity) * 0.3);
}
}
/* Scanlines overlay */
.scanlines {
position: fixed;
inset: 0;
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 0, 0, 0.1) 2px, rgba(0, 0, 0, 0.1) 4px);
pointer-events: none;
z-index: 100;
}
/* Container */
.container {
position: relative;
z-index: 10;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
}
.parallax-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
max-width: 800px;
}
/* Title */
.title {
font-size: clamp(2rem, 8vw, 5rem);
font-weight: 700;
background: linear-gradient(90deg, #00d4ff, #be3eea);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
font-style: italic;
}
.tagline {
font-size: 1.1rem;
color: #5b77d1;
letter-spacing: 0.3em;
text-transform: uppercase;
margin-bottom: 2rem;
}
/* Notification */
.notification-wrapper {
width: 100%;
margin-bottom: 3rem;
padding: 0;
}
.notification {
background: rgba(190, 62, 234, 0.1);
border: 1px solid #be3eea;
border-radius: 8px;
padding: 1.5rem;
color: #fff;
font-size: 1.1rem;
font-weight: bold;
text-align: center;
line-height: 1.4;
max-width: 700px;
margin: 0 auto;
box-shadow: 0 0 20px rgba(190, 62, 234, 0.15);
}
/* Main links */
.main-link {
width: 200px;
height: 180px;
border: 2px solid #be3eea;
border-radius: 8px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
background: rgba(11, 13, 14, 0.8);
text-decoration: none;
}
.main-link:hover {
border-color: #00d4ff;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
transform: scale(1.02);
}
.links-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
width: 100%;
}
.main-link img,
.main-link svg {
max-width: 120px;
max-height: 120px;
object-fit: contain;
}
/* Attribution */
.attribution {
margin-top: 2rem;
color: #7f8484;
font-size: 0.9rem;
}
.attribution a,
.footer a {
color: #be3eea;
text-decoration: none;
transition: color 0.3s;
}
.attribution a:hover,
.footer a:hover {
color: #00d4ff;
}
/* Footer */
.footer {
padding: 2rem;
text-align: center;
}
.footer a {
color: #5b77d1;
font-size: 0.85rem;
}
/* Responsive */
@media (max-width: 600px) {
.container {
padding: 1rem;
}
.main-link {
width: 160px;
height: 150px;
}
.main-link img {
max-width: 100px;
max-height: 100px;
}
.tagline {
letter-spacing: 0.15em;
font-size: 0.9rem;
}
}
</style>
</head>
<body data-ws-url="">
<div class="starfield" id="starfield"></div>
<div class="scanlines"></div>
<div class="container">
<div class="parallax-content">
<h1 class="title">GalaxySpin.Space</h1>
<p class="tagline">Welcome to the experience!</p>
<div class="notification-wrapper">
<div class="notification">
▶ Notice to all users: temporal fluctuations may occur during extended usage. Please report any
space time distortions or temporal paradoxes to administrators immediately. ◀
</div>
</div>
<div class="links-container">
<a href="https://jfin.galaxyspin.space" class="main-link" target="_blank" rel="noopener" aria-label="Jellyfin"><svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512" width="80" height="80"><linearGradient id="jf-a" x1="97.508" x2="522.069" y1="308.135" y2="63.019" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#aa5cc3"/><stop offset="1" style="stop-color:#00a4dc"/></linearGradient><path d="M256 196.2c-22.4 0-94.8 131.3-83.8 153.4s156.8 21.9 167.7 0-61.3-153.4-83.9-153.4" style="fill:url(#jf-a)"/><linearGradient id="jf-b" x1="94.193" x2="518.754" y1="302.394" y2="57.278" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#aa5cc3"/><stop offset="1" style="stop-color:#00a4dc"/></linearGradient><path d="M256 0C188.3 0-29.8 395.4 3.4 462.2s472.3 66 505.2 0S323.8 0 256 0m165.6 404.3c-21.6 43.2-309.3 43.8-331.1 0S211.7 101.4 256 101.4 443.2 361 421.6 404.3" style="fill:url(#jf-b)"/></svg></a>
<a href="https://seerr.galaxyspin.space" class="main-link" target="_blank" rel="noopener" aria-label="Jellyseerr"><svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 449 449" width="80" height="80"><linearGradient id="js-ja" x1="-2254.016" x2="-2267.51" y1="-2831.433" y2="-2961.618" gradientTransform="matrix(1.75 0 0 -1.75 4099.705 -4631.96)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#502d95"/><stop offset=".1" style="stop-color:#6d37ac"/><stop offset=".57" style="stop-color:#6786d1"/></linearGradient><path d="m170.9 314-27-6s-6.2 39.6-8.6 59.5c-3.8 32.1-8.4 76.5-6.6 110.5 2 37.4 12.2 73.4 15.6 73.4s-1.8-22.9.5-73.3c1.5-33.6 7.1-74 13.8-110.5 3.3-18 12.9-53.4 12.9-53.4h-.6z" style="fill:url(#js-ja)"/><linearGradient id="js-jb" x1="-2175.81" x2="-2189.303" y1="-2839.483" y2="-2969.721" gradientTransform="matrix(1.75 0 0 -1.75 4099.705 -4631.96)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#502d95"/><stop offset=".1" style="stop-color:#6d37ac"/><stop offset=".57" style="stop-color:#6786d1"/></linearGradient><path d="M284.8 311.2h8.3c11.1 41.4 13.2 101.1 10.8 146.1-2.6 49.5-16.2 97.2-20.6 97.2s2.4-30.3-.7-97.1C280.4 412.9 271 371 270 311.2z" style="fill:url(#js-jb)"/><linearGradient id="js-jc" x1="-1831.757" x2="-1831.757" y1="-2576.996" y2="-2695.45" gradientTransform="matrix(2.12 0 0 -2.12 4199.46 -5137.32)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#763dcd"/><stop offset=".22" style="stop-color:#8d61eb"/><stop offset=".37" style="stop-color:#8c86ec"/><stop offset=".64" style="stop-color:#748ce8"/><stop offset=".9" style="stop-color:#6ba1e6"/></linearGradient><path d="M309.8 181.6h13.3c17.8 66.2 26.1 161.9 22.2 234-4.2 79.4-25.9 155.7-33.1 155.7s3.8-48.6-1.1-155.6c-3.3-71.4-23.5-138.4-25.1-234.2z" style="fill:url(#js-jc)"/><linearGradient id="js-jd" x1="-1891.134" x2="-1891.134" y1="-2576.996" y2="-2695.45" gradientTransform="matrix(2.12 0 0 -2.12 4199.46 -5137.32)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#763dcd"/><stop offset=".22" style="stop-color:#8d61eb"/><stop offset=".37" style="stop-color:#8c86ec"/><stop offset=".64" style="stop-color:#748ce8"/><stop offset=".9" style="stop-color:#6ba1e6"/></linearGradient><path d="M196.6 180.1h-13.3c-17.8 66.2-26.1 161.9-22.2 234 4.2 79.4 25.9 155.7 33.1 155.7s-3.8-48.6 1.1-155.6c3.3-71.4 23.5-138.4 25.1-234.2h-23.8z" style="fill:url(#js-jd)"/><linearGradient id="js-je" x1="-1922.004" x2="-1922.004" y1="-2576.996" y2="-2695.45" gradientTransform="matrix(2.12 0 0 -2.12 4199.46 -5137.32)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#763dcd"/><stop offset=".22" style="stop-color:#8d61eb"/><stop offset=".37" style="stop-color:#8c86ec"/><stop offset=".64" style="stop-color:#748ce8"/><stop offset=".9" style="stop-color:#6ba1e6"/></linearGradient><path d="m155.6 150-30.2-10.8s-11.1 70.7-15.3 106.2c-6.7 57.3-20 136.6-16.7 197.3 3.6 66.8 21.8 131.1 27.8 131.1s-3.2-40.9 1-131c2.8-60.1 21.2-117 27.4-197.4 2.5-31.9 7.3-95.5 7.3-95.5z" style="fill:url(#js-je)"/><linearGradient id="js-jf" x1="-1862.421" x2="-1862.421" y1="-2576.996" y2="-2695.45" gradientTransform="matrix(2.12 0 0 -2.12 4199.46 -5137.32)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#763dcd"/><stop offset=".22" style="stop-color:#8d61eb"/><stop offset=".37" style="stop-color:#8c86ec"/><stop offset=".64" style="stop-color:#748ce8"/><stop offset=".9" style="stop-color:#6ba1e6"/></linearGradient><path d="m255.5 181.6-27.3 4.6s3.6 53 3.6 83.1c0 48.9 1.9 98.2 1.8 149.5-.2 58.9 9.7 157.6 14.8 157.6s21.7-127 25.2-203c2.3-50.7-5.2-95.1-6.5-125.4-1.2-27-5-63.4-5-63.4z" style="fill:url(#js-jf)"/><linearGradient id="js-jg" x1="-1735.548" x2="-1586.936" y1="-2673.095" y2="-2838.19" gradientTransform="matrix(1.79 0 0 -1.79 3246.155 -4657.91)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#c395fc"/><stop offset="1" style="stop-color:#4f65f5"/></linearGradient><path d="M405.8 197.7c0 68.8-11.7 73-30.8 102.1-13.8 20.9 14.1 37.1 2.9 42.9-13.3 6.9-9.1-5.6-35.6-12.7-11.5-3-36.5.3-46.6 2.3-10 1.9-40.6-15.1-48.7-17.3-12.1-3.3-41.8 12.5-59.9 12.5s-37-15.8-61.1-9.3c-28.6 7.7-63.1 26.3-68.3 20.2-10-11.7 21.9-20.6 10-41.4-7.5-13.2-33.4-47.9-34.2-83-2.4-112.8 91-208.1 191.1-208.1s181.1 86.8 181.1 183.9" style="fill:url(#js-jg)"/></svg></a>
<a href="https://books.galaxyspin.space" class="main-link" target="_blank" rel="noopener" aria-label="AudioBookShelf"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1235.7 1235.4" width="80" height="80"><linearGradient id="abs-grad" gradientUnits="userSpaceOnUse" x1="617.37" y1="1257.3" x2="617.37" y2="61.44" gradientTransform="matrix(1 0 0 -1 0 1278)"><stop offset="0.32" style="stop-color:#CD9D49"/><stop offset="0.99" style="stop-color:#875D27"/></linearGradient><circle style="fill:url(#abs-grad)" cx="617.4" cy="618.6" r="597.9"/><path style="fill:#FFFFFF" d="M1005.6,574.1c-4.8-4-12.4-10-22.6-17v-79.2c0-201.9-163.7-365.6-365.6-365.6l0,0 c-201.9,0-365.6,163.7-365.6,365.6v79.2c-10.2,7-17.7,13-22.6,17c-4.1,3.4-6.5,8.5-6.5,13.9v94.9c0,5.4,2.4,10.5,6.5,14 c11.3,9.4,37.2,29.1,77.5,49.3v9.2c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45V527.8c0-24.9-16-45-35.8-45l0,0 c-19,0-34.5,18.5-35.8,41.9h-0.1v-46.9c0-171.6,139.1-310.7,310.7-310.7l0,0C789,167.2,928,306.3,928,477.9v46.9H928 c-1.3-23.4-16.8-41.9-35.8-41.9l0,0c-19.8,0-35.8,20.2-35.8,45v227.6c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45v-9.2 c40.3-20.2,66.2-39.9,77.5-49.3c4.2-3.5,6.5-8.6,6.5-14V588C1012.1,582.6,1009.7,577.5,1005.6,574.1z"/><path style="fill:#FFFFFF" d="M489.9,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7 c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L489.9,969.7z M418.2,514.6h98.7v10.3h-98.7V514.6z"/><path style="fill:#FFFFFF" d="M639.7,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3H595c-23.9,0-43.3,19.4-43.3,43.3 v484.8c0,23.9,19.4,43.3,43.3,43.3H639.7z M568,514.6h98.7v10.3H568V514.6z"/><path style="fill:#FFFFFF" d="M789.6,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7 c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L789.6,969.7z M717.9,514.6h98.7v10.3h-98.7V514.6z"/><path style="fill:#FFFFFF" d="M327.1,984.7h580.5c18,0,32.6,14.6,32.6,32.6v0c0,18-14.6,32.6-32.6,32.6H327.1c-18,0-32.6-14.6-32.6-32.6v0 C294.5,999.3,309.1,984.7,327.1,984.7z"/></svg></a>
</div>
<p class="attribution">
brought to you by <a href="https://gravitywell.xyz" target="_blank" rel="noopener">GravityWell Services</a>
</p>
</div>
<footer class="footer">
<a href="https://zombo.com" target="_blank" rel="noopener">You can do anything!</a>
</footer>
</div>
<script>
(function () {
var starfield = document.getElementById('starfield');
if (starfield) {
for (var i = 0; i < 75; i++) {
var star = document.createElement('div');
star.className = 'star';
var size = Math.random() * 2 + 1;
star.style.cssText = 'width: ' + size + 'px; height: ' + size + 'px; left: ' + (Math.random() * 100) + '%; top: ' + (Math.random() * 100) + '%; --duration: ' + (Math.random() * 3 + 2) + 's; --opacity: ' + (Math.random() * 0.7 + 0.3) + ';';
starfield.appendChild(star);
}
}
})();
</script>
<script src="chat.js"></script>
</body>
</html>

642
httpserver/data/gw.css Executable file
View File

@@ -0,0 +1,642 @@
:root {
--c-bg: #000;
--c-bg-alt: #171717;
--c-bg-dim: rgba(23, 23, 23, 0.5);
--c-border: #262626;
--c-border-mid: #404040;
--c-border-light: #525252;
--c-text: #e5e5e5;
--c-text-dim: #d4d4d4;
--c-text-muted: #a3a3a3;
--c-text-faded: #737373;
--c-white: #fff;
--c-accent: #0a7;
--c-red: #ef4444;
--c-red-dark: #dc2626;
--c-red-bg: #7f1d1d;
--c-gold: #ca8a04;
--c-yellow: #eab308;
--c-blue: #60a5fa;
--c-blue-light: #93c5fd;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box
}
html {
scroll-behavior: smooth
}
body {
background: var(--c-bg);
color: var(--c-text);
font-family: var(--font-mono);
min-height: 100vh
}
::selection {
background: var(--c-white);
color: var(--c-bg)
}
a {
color: inherit;
text-decoration: none
}
ul {
list-style: none
}
li+li {
margin-top: .5rem
}
::-webkit-scrollbar {
width: 12px
}
::-webkit-scrollbar-track {
background: var(--c-bg);
border-left: 1px solid #333
}
::-webkit-scrollbar-thumb {
background: var(--c-white);
border: 2px solid var(--c-bg)
}
::-webkit-scrollbar-thumb:hover {
background: #ccc
}
#warp {
position: fixed;
top: 0;
left: 0;
z-index: -1;
display: none
}
#warp.active {
display: block
}
.warp-trigger {
cursor: pointer;
text-decoration: underline dotted
}
.warp-trigger:hover {
color: var(--c-accent)
}
.container {
max-width: 42rem;
margin: 0 auto;
border-left: 1px solid var(--c-border);
border-right: 1px solid var(--c-border);
min-height: 100vh;
background: var(--c-bg);
box-shadow: 0 0 50px rgba(255, 255, 255, .05)
}
header {
padding: 1rem;
border-bottom: 2px solid var(--c-white);
background: var(--c-bg-alt)
}
h1 {
font-size: 1.875rem;
font-weight: 700;
letter-spacing: -.05em;
color: var(--c-white);
margin-bottom: .5rem
}
h1 .subdomain {
color: var(--c-text-faded)
}
.header-text {
border-left: 4px solid var(--c-border-light);
padding-left: 1rem;
font-size: .875rem;
line-height: 1.25
}
.header-text p:first-child {
color: var(--c-text-dim)
}
.header-text p:last-child {
color: var(--c-text-faded)
}
nav {
position: sticky;
top: 0;
z-index: 40;
background: var(--c-bg);
border-bottom: 1px solid var(--c-border);
overflow-x: auto
}
nav ul {
display: flex;
white-space: nowrap
}
nav li {
flex: 1;
text-align: center
}
nav a {
display: block;
padding: .5rem .75rem;
font-size: .875rem;
font-weight: 700;
border-right: 1px solid var(--c-bg-alt)
}
nav a:hover,
.service-name:hover,
.extension-link:hover,
.donate-link:hover,
.contact-item a:hover,
.contact-item span:hover {
background: var(--c-white);
color: var(--c-bg)
}
main {
padding: .75rem 1.25rem
}
section {
margin-bottom: 2rem;
scroll-margin-top: 5rem
}
p {
margin-bottom: .75rem;
color: var(--c-text-dim)
}
h2 {
font-size: 1.25rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .1em;
margin-bottom: 1rem;
display: inline-block;
border-bottom: 2px solid var(--c-white);
padding-bottom: .25rem
}
h3 {
font-size: .75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .2em;
margin-bottom: .5rem;
color: var(--c-text-faded);
border-bottom: 1px solid var(--c-border);
padding-bottom: .5rem;
transition: color .2s
}
.group:hover h3 {
color: var(--c-white)
}
#ddate {
font-size: .75rem;
color: var(--c-text-faded);
text-transform: uppercase;
letter-spacing: .1em;
border-bottom: 1px solid var(--c-bg-alt);
padding-bottom: .5rem;
margin-bottom: 2rem
}
.grid {
display: grid;
gap: 1.5rem
}
.services-section-title {
grid-column: 1 / -1;
margin-bottom: 0;
border-bottom: 1px solid var(--c-border);
padding-bottom: .5rem
}
.services-section-title .hosts-note {
font-size: .85em;
font-weight: 400;
color: var(--c-text-faded)
}
.services-archived {
grid-column: 1 / -1
}
.services-archived .archived-summary {
text-transform: uppercase;
letter-spacing: .05em;
color: var(--c-text-faded)
}
.services-archived .archived-grid {
margin-top: .75rem
}
.services-intro {
color: var(--c-text-muted);
font-size: .9rem;
margin-bottom: 1rem
}
.group.archived-group h3 {
color: var(--c-text-faded)
}
details {
border: 1px solid var(--c-border-mid);
background: var(--c-bg-dim);
margin-top: 1.5rem;
transition: all .2s
}
details[open] {
background: var(--c-bg-alt);
border-color: var(--c-white)
}
details[open] summary {
border-bottom: 1px solid var(--c-border-mid)
}
summary {
cursor: pointer;
padding: .75rem;
font-weight: 700;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center
}
summary:hover {
background: var(--c-border);
color: var(--c-white)
}
details .content {
padding: .75rem
}
details .content p {
color: var(--c-text-muted);
margin-bottom: .75rem
}
.link-item {
display: flex;
align-items: center;
color: var(--c-text-dim)
}
.link-item:hover {
color: var(--c-white);
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 4px
}
.link-icon {
width: 1rem;
height: 1rem;
margin-right: .5rem;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
background: var(--c-border);
border: 1px solid var(--c-border-light)
}
.extension-link,
.donate-link {
display: block;
padding: .75rem;
border: 1px solid var(--c-border-mid);
transition: all .2s;
text-align: center;
font-weight: 700;
font-size: .875rem
}
.extension-link {
padding: .5rem
}
.donate-link {
border-color: var(--c-border)
}
.extension-link:hover,
.donate-link:hover {
border-color: var(--c-white)
}
.contact-item {
word-break: break-all;
font-size: .875rem
}
.contact-label {
display: block;
color: var(--c-text-faded);
font-size: .75rem;
text-transform: uppercase;
margin-bottom: .25rem
}
.service-link {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: .5rem
}
.service-name {
font-weight: 700;
font-size: 1.125rem;
padding: 0 .25rem;
margin-left: -.25rem
}
.service-meta {
font-size: .75rem;
color: var(--c-text-faded)
}
.service-meta a {
text-decoration: underline dotted
}
.service-meta a:hover {
color: var(--c-white)
}
/* Maintenance Status */
.status-maintenance {
opacity: 0.5;
filter: grayscale(1);
pointer-events: none;
}
/* Back-compat: treat "down" the same as prior "maintenance" */
.status-down {
opacity: 0.5;
filter: grayscale(1);
pointer-events: none;
}
.status-maintenance a {
cursor: not-allowed;
text-decoration: line-through !important;
}
.status-down a {
cursor: not-allowed;
text-decoration: line-through !important;
}
.maintenance-badge {
font-size: 0.65rem;
letter-spacing: 0.05em;
color: var(--c-red);
margin-left: 0.5rem;
vertical-align: middle;
}
.guest-info {
margin-bottom: 1.5rem;
border: 1px solid var(--c-border-mid);
background: rgba(23, 23, 23, .3)
}
.guest-info summary {
padding: .75rem;
font-size: .875rem;
color: var(--c-yellow);
text-transform: uppercase;
letter-spacing: .05em
}
.guest-info .content {
border-top: 1px solid var(--c-border-mid);
font-size: .875rem
}
.guest-credentials {
background: var(--c-bg-alt);
padding: .75rem;
border-left: 4px solid var(--c-white);
font-size: .75rem
}
.guest-credentials p {
margin: 0
}
.guest-credentials .label,
.guest-note {
color: var(--c-text-faded)
}
.guest-note {
font-size: .75rem
}
.crypto-details {
border-color: var(--c-border);
background: transparent
}
.crypto-details summary {
text-transform: uppercase;
letter-spacing: .1em;
font-size: .875rem;
border: 1px solid var(--c-border)
}
.crypto-details summary span:last-child {
transition: transform .2s
}
.crypto-details[open] summary {
background: var(--c-white);
color: var(--c-bg);
border-color: var(--c-white)
}
.crypto-details[open] summary span:last-child {
transform: rotate(180deg)
}
.crypto-content {
margin-top: .5rem;
background: var(--c-bg);
border: 1px solid var(--c-border);
padding: 1rem;
position: relative
}
.crypto-corner {
position: absolute;
top: 0;
right: 0;
width: .5rem;
height: .5rem;
background: var(--c-white)
}
.crypto-item {
margin-bottom: 1rem
}
.crypto-header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 1px solid var(--c-border);
padding-bottom: .25rem;
margin-bottom: .5rem
}
.crypto-name {
font-weight: 700;
color: var(--c-gold)
}
.crypto-label {
font-size: 10px;
color: var(--c-border-light)
}
.crypto-address {
display: block;
font-size: .75rem;
word-break: break-all;
color: var(--c-text-muted);
user-select: all;
background: var(--c-bg-dim);
padding: .5rem;
border-left: 2px solid var(--c-border)
}
.qr-container {
width: 100%;
height: 8rem;
border: 1px solid var(--c-border);
display: flex;
align-items: center;
justify-content: center;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0IiBoZWlnaHQ9IjQiPgo8cmVjdCB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSIjMTExIiAvPgo8L3N2Zz4=');
margin-top: .5rem
}
.qr-placeholder {
background: var(--c-bg);
padding: .5rem;
font-size: 10px;
border: 1px solid var(--c-border);
color: var(--c-text-faded)
}
.footer-links {
padding-top: 1.5rem;
border-top: 1px solid var(--c-border)
}
.footer-links h2 {
text-align: center;
color: var(--c-text-faded);
font-size: .875rem;
margin-bottom: 1.5rem
}
.link-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: .5rem
}
.footer-link {
padding: .25rem .75rem;
font-size: .75rem;
border: 1px solid var(--c-border);
text-transform: uppercase;
font-weight: 700;
transition: transform .2s;
color: var(--c-text-faded)
}
.footer-link:hover {
border-color: var(--c-white);
color: var(--c-white);
background: var(--c-bg-alt);
transform: translateY(-.25rem)
}
.footer-link.danger {
border-color: var(--c-red-bg);
color: var(--c-red)
}
.footer-link.danger:hover {
background: var(--c-red-dark);
color: var(--c-white)
}
footer {
padding: 1rem;
border-top: 1px solid var(--c-border);
text-align: center;
font-size: .75rem;
color: var(--c-border-light)
}
@media(min-width:768px) {
h1 {
font-size: 3rem
}
.header-text {
font-size: 1rem
}
main {
padding: 1rem 1.25rem
}
.grid {
grid-template-columns: repeat(2, 1fr)
}
.grid.services {
column-gap: 2rem;
row-gap: 1.5rem
}
}

229
httpserver/data/gw.html Normal file
View File

@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GravityWell.xYz</title>
<meta name="description" content="Self-hosted services and community.">
<link rel="stylesheet" href="gw.css">
<link rel="stylesheet" href="chat.css">
<link rel="icon" type="image/webp" href="assets/favicon.webp">
</head>
<body>
<canvas id="warp"></canvas>
<div class="container">
<header>
<h1>GRAVITYWELL<span class="subdomain">.xYz</span></h1>
<div class="header-text">
<p>Self-Hosting is killing corporate profits!</p>
<p>We left these services open so you can help.</p>
</div>
</header>
<nav>
<ul>
<li><a href="#about">ABOUT</a></li>
<li><a href="#community">COMMUNITY</a></li>
<li><a href="#contact">CONTACT</a></li>
<li><a href="#services">SERVICES</a></li>
<li><a href="#donate">DONATE</a></li>
</ul>
</nav>
<main>
<div id="ddate">Loading date...</div>
<section id="about">
<h2>About This <span class="warp-trigger" title="Click to activate warp speed!">Space</span></h2>
<p>An experiment in self-hosting, data archiving, and community building.</p>
<p>Most services here are open to new users and connected with the Fediverse. All services are running on home
infrastructure and a cheap VPS.</p>
<p> Self-hosting is the practice of hosting and managing applications on your own server(s)
instead of consuming from
<a href="https://www.gnu.org/philosophy/who-does-that-server-really-serve.html" target="_blank">
SaaSS</a> providers.</p>
<details>
<summary><span>[+] TRY THESE BANNED EXTENSIONS</span></summary>
<div class="content">
<p>The following browser extensions have been known to cause problems for surveillance capitalism and as
such have been banned by Google, but you can still use them!</p>
<ul>
<li><a href="https://gitflic.ru/project/magnolia1234/bypass-paywalls-chrome-clean" target="_blank"
rel="noopener noreferrer" class="extension-link">→ Bypass Paywalls Clean</a></li>
<li><a href="https://adnauseam.io" target="_blank" rel="noopener noreferrer" class="extension-link">
AdNauseam</a></li>
<li><a href="https://libredirect.github.io" target="_blank" rel="noopener noreferrer"
class="extension-link">→ LibRedirect</a></li>
<li><a href="https://github.com/ClearURLs/Addon" target="_blank" rel="noopener noreferrer"
class="extension-link">→ ClearURLs</a></li>
</ul>
</div>
</details>
</section>
<div class="grid">
<section id="community">
<h2>Community</h2>
<ul>
<li><a href="https://matrix.to/#/#gravitywell:mx.ugh.im" target="_blank" rel="noreferrer"
class="link-item"><span class="link-icon">M</span>Matrix Chat</a></li>
<li><a href="https://signal.group/#CjQKIHU8ll31vC-Sb2m-xz3_hCLqbMoxlvRbsUuVKrpKMSgzEhAS7jFO9D_605yFXG8rZfVz"
target="_blank" rel="noreferrer" class="link-item"><span class="link-icon">S</span>Signal Group</a></li>
<li><a href="https://lem.ugh.im/c/gravitywellxyz" target="_blank" rel="noreferrer" class="link-item"><span
class="link-icon">L</span>Lemmy Community</a></li>
<li><a href="https://floatilla.gravitywell.xyz/spaces/nostr.gravitywell.xyz" target="_blank"
rel="noreferrer" class="link-item"><span class="link-icon">N</span>Nostr Community</a></li>
</ul>
</section>
<section id="contact">
<h2>Contact</h2>
<ul>
<li class="contact-item"><span class="contact-label">Matrix</span><a
href="https://matrix.to/#/@gravitas:mx.ugh.im" target="_blank" rel="noreferrer">@gravitas:mx.ugh.im</a>
</li>
<li class="contact-item"><span class="contact-label">Signal</span><a
href="https://signal.me/#eu/sz35pvMZQ3GjCg6F3bCfYua9Mv2Y1sG4qPjogSLOTHeVFpd6tjBFHKlfaek8RQwh"
target="_blank" rel="noreferrer">Gravitas.75</a></li>
<li class="contact-item"><span class="contact-label">XMPP</span><a href="xmpp:gravitas@xmpp.is"
target="_blank" rel="noreferrer">gravitas@xmpp.is</a></li>
<li class="contact-item"><span class="contact-label">Email</span><span>GravityWell@RiseUp.net</span></li>
<li class="contact-item"><span class="contact-label">PGP</span><a
href="https://keys.openpgp.org/vks/v1/by-fingerprint/63363203336726B59E981F3FC995CF7689B7546C"
target="_blank" rel="noreferrer">0xC995CF7689B7546C</a></li>
</ul>
</section>
</div>
<section id="services">
<h2>Services</h2>
<details class="guest-info">
<summary>[!] Read: Guest Access Info</summary>
<div class="content">
<p>Some services don't have a sign-up option. Use the guest account to try them out.</p>
<div class="guest-credentials">
<p><span class="label">USER:</span> gwguest</p>
<p><span class="label">PASS:</span> gravitywell.xyz</p>
</div>
<p class="guest-note">* GravityWell.xYz services hosted on home server.<br>* ugh.im services hosted on VPS.
</p>
</div>
</details>
<div id="services-container" class="grid services">
<!-- Services will be loaded dynamically from services-data.json -->
</div>
</section>
<section id="donate">
<h2>Support this project:</h2>
<div class="grid">
<div>
<h3>Fiat Channels</h3>
<ul>
<li><a href="https://ko-fi.com/L3L1LJRC4" target="_blank" rel="noreferrer" class="donate-link">KO-FI</a>
</li>
<li><a href="https://liberapay.com/GravityWell.XYZ/donate" target="_blank" rel="noreferrer"
class="donate-link">LIBERAPAY</a></li>
<li><a href="https://cash.app/$gravitywellxyz" target="_blank" rel="noreferrer" class="donate-link">CASH
APP</a></li>
</ul>
</div>
<div>
<h3>Crypto</h3>
<details class="crypto-details">
<summary>
<span>View Crypto Addresses</span>
<span></span>
</summary>
<div class="crypto-content">
<div class="crypto-corner"></div>
<div class="crypto-item">
<div class="crypto-header">
<span class="crypto-name">BTC</span>
<span class="crypto-label">BITCOIN</span>
</div>
<code class="crypto-address">bc1qxhdlvdc2wpa6ns2xgm5ehv5s3lepd029dwxz5s</code>
<div class="qr-container">
<span class="qr-placeholder"><img src="./assets/BTC.webp" alt="BTC"></span>
</div>
</div>
<div class="crypto-item">
<div class="crypto-header">
<span class="crypto-name">DOGE</span>
<span class="crypto-label">DOGECOIN</span>
</div>
<code class="crypto-address">D9XJ3ZjG9q9Ern6bKjeugj7v2BEuREWqKG</code>
<div class="qr-container">
<span class="qr-placeholder"><img src="./assets/DOGE.webp" alt="DOGE"></span>
</div>
</div>
<div class="crypto-item">
<div class="crypto-header">
<span class="crypto-name">XMR</span>
<span class="crypto-label">MONERO</span>
</div>
<code
class="crypto-address">87mmbb6iLMJ6g5xAMUaP8V5Bus3nCjxPr2v1xzgHNeY2AP4RkYsgcs3cZjXUNwB6tQHJZQxE3PEarUCSJMzZFEDhKRDNo8e</code>
<div class="qr-container">
<span class="qr-placeholder"><img src="./assets/XMR.webp" alt="XMR"></span>
</div>
</div>
</div>
</details>
</div>
</div>
</section>
<section class="footer-links">
<h2>Awesome Links!</h2>
<div class="link-grid">
<a href="https://bitwarden.com/" target="_blank" rel="noopener noreferrer" class="footer-link">Bitwarden</a>
<a href="https://www.debian.org/" target="_blank" rel="noopener noreferrer" class="footer-link">Debian</a>
<a href="https://www.defectivebydesign.org/" target="_blank" rel="noopener noreferrer"
class="footer-link">Defective by Design</a>
<a href="https://spyware.neocities.org/" target="_blank" rel="noopener noreferrer" class="footer-link">Spyware
Watchdog</a>
<a href="https://ffmpeg.org/" target="_blank" rel="noopener noreferrer" class="footer-link">FFmpeg</a>
<a href="https://grapheneos.org/" target="_blank" rel="noopener noreferrer" class="footer-link">GrapheneOS</a>
<a href="https://joinfediverse.wiki/" target="_blank" rel="noopener noreferrer" class="footer-link">Join
Fediverse</a>
<a href="https://neocities.org/" target="_blank" rel="noopener noreferrer" class="footer-link">NeoCities</a>
<a href="https://www.torproject.org/" target="_blank" rel="noopener noreferrer" class="footer-link">Tor
Project</a>
<a href="https://stopstalkerware.org" target="_blank" rel="noopener noreferrer" class="footer-link">Stop
Stalkerware</a>
<a href="https://trash-guides.info" target="_blank" rel="noopener noreferrer" class="footer-link">Trash
Guides</a>
<a href="https://deflock.me" target="_blank" rel="noopener noreferrer" class="footer-link">Deflock</a>
</div>
</section>
</main>
<footer>
<p>GRAVITYWELL.XYZ | <a href="archive/extra/retro.html"
style="color: var(--c-text-faded); text-decoration: underline;">Retro</a></p>
</footer>
</div>
<script src="services-loader.js"></script>
<script src="main.js"></script>
<script src="chat.js"></script>
<script>
// Load services dynamically when page loads
document.addEventListener('DOMContentLoaded', function () {
renderServicesForGW('services-container');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0;url=gw.html">
<title>GravityWell.xYz</title>
<link rel="canonical" href="gw.html">
</head>
<body>
<p><a href="gw.html">Go to GravityWell.xYz</a></p>
</body>
</html>

BIN
httpserver/data/intro.opus Normal file

Binary file not shown.

91
httpserver/data/main.js Executable file
View File

@@ -0,0 +1,91 @@
fetch('./ddate-now')
.then(r => r.text())
.then(d => { document.getElementById('ddate').textContent = d.trim(); })
.catch(() => { document.getElementById('ddate').textContent = ''; });
const canvas = document.getElementById('warp');
const ctx = canvas.getContext('2d');
let w, h, cx, cy, stars = [], animId = null, active = false;
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const isMobile = window.innerWidth < 768 || window.innerHeight < 600 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const starCount = isMobile ? 200 : 400;
const speed = prefersReduced ? 0.01 : 0.02;
function resize() {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
cx = w / 2;
cy = h / 2;
}
class Star {
constructor() { this.reset(); }
reset() {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * Math.max(w, h);
this.x = Math.cos(angle) * radius;
this.y = Math.sin(angle) * radius;
this.z = Math.random() * w;
this.pz = this.z;
}
update() {
this.pz = this.z;
this.z -= speed * this.z;
if (this.z < 1) this.reset();
}
draw() {
const sz = 1 / this.z, spz = 1 / this.pz;
const sx = this.x * sz * w + cx, sy = this.y * sz * h + cy;
const px = this.x * spz * w + cx, py = this.y * spz * h + cy;
const r = Math.max(0, (1 - this.z / w) * 2);
ctx.beginPath();
ctx.strokeStyle = `rgba(255, 255, 255, ${r})`;
ctx.lineWidth = r * 2;
ctx.moveTo(px, py);
ctx.lineTo(sx, sy);
ctx.stroke();
}
}
function init() {
resize();
stars = [];
for (let i = 0; i < starCount; i++) stars.push(new Star());
}
function animate() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, w, h);
for (let i = 0; i < stars.length; i++) {
stars[i].update();
stars[i].draw();
}
animId = requestAnimationFrame(animate);
}
function startWarp() {
if (active) return;
active = true;
canvas.classList.add('active');
init();
animate();
}
function stopWarp() {
if (!active) return;
active = false;
canvas.classList.remove('active');
if (animId) cancelAnimationFrame(animId);
}
document.addEventListener('click', (e) => {
if (e.target.classList.contains('warp-trigger')) {
e.preventDefault();
active ? stopWarp() : startWarp();
}
});
window.addEventListener('resize', () => {
if (active) resize();
});

View File

@@ -0,0 +1,26 @@
## Mew Timeline — Neocities Version
This folder is a static copy of the original `mew` timeline site, prepared for hosting on Neocities.
### How to deploy
1. **Create the site on Neocities**
- Log in to Neocities and create (or open) the site you want to use.
2. **Upload files**
- Upload `index.html` to the root of your Neocities site (or to a subfolder if you want the timeline at a path like `/mew/`).
- Create a `library/` folder on Neocities.
- Upload all of the media files listed in the `MEDIA` array inside `index.html` into that `library/` folder.
3. **Paths / structure**
- The page expects files at `./library/<filename>` relative to `index.html`.
- If you keep that structure, you do **not** need to change any code.
4. **Using a subdirectory (optional)**
- If you put `index.html` into a folder such as `/mew/`, also put the `library/` folder inside that same folder.
- For example:
- `/mew/index.html`
- `/mew/library/2013-10-04_11-32-51.webp`
Once the files are uploaded, visit your Neocities URL and the slideshow + timeline should work entirely clientside.

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

View File

@@ -0,0 +1,450 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timeline of Mew</title>
<style>
:root {
--bg: #0b0510;
--surface: #1a1025;
--accent: #ffcc66;
--text: #f7f2ff;
--text-dim: #b8afcd;
--line: #4a3b69;
--window-border: 2px outset #5e4a8a;
--window-border-inner: 1px inset #3a2a5a;
}
body {
background: var(--bg) url('bg.png') repeat;
background-size: 128px;
color: var(--text);
font-family: 'Verdana', 'Tahoma', sans-serif;
margin: 0;
padding: 0;
line-height: 1.5;
}
header {
padding: 4rem 1rem;
text-align: center;
border-bottom: 2px solid var(--accent);
background: rgba(0, 0, 0, 0.5);
}
h1 {
font-family: 'Times New Roman', Times, serif;
font-size: 3.5rem;
color: var(--accent);
text-shadow: 2px 2px 0px #000;
margin: 0;
}
.tagline {
font-family: 'Times New Roman', Times, serif;
font-size: 1.2rem;
font-style: italic;
color: var(--text-dim);
margin-top: 0.5rem;
}
#title-screen {
max-width: 800px;
margin: 0 auto;
text-align: center;
padding: 2rem 1rem;
}
#title-screen img {
width: 100%;
max-height: 500px;
object-fit: contain;
border: var(--window-border);
background: #000;
padding: 4px;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 2rem 1rem;
}
.year-section {
margin-bottom: 4rem;
}
.year-header {
font-family: 'Courier New', Courier, monospace;
font-size: 1.1rem;
font-weight: bold;
color: var(--accent);
border-bottom: 1px dashed var(--line);
padding-bottom: 0.5rem;
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.photo-card {
background: var(--surface);
border: var(--window-border);
padding: 4px;
cursor: pointer;
position: relative;
transition: transform 0.1s;
}
.photo-card:hover {
transform: scale(1.02);
box-shadow: 5px 5px 0px rgba(0, 0, 0, 0.5);
}
.photo-card img,
.photo-card video {
width: 100%;
height: 120px;
object-fit: cover;
display: block;
}
.photo-card .label {
font-size: 10px;
color: var(--text-dim);
padding-top: 4px;
text-align: center;
}
#lightbox {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.95);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
flex-direction: column;
cursor: pointer;
}
#lightbox img,
#lightbox video {
max-width: 90vw;
max-height: 80vh;
border: var(--window-border);
}
#lightbox .lb-caption {
margin-top: 1rem;
color: var(--accent);
font-family: 'Courier New', Courier, monospace;
font-size: 0.9rem;
font-weight: bold;
}
footer {
text-align: center;
padding: 4rem 1rem;
background: #0d0616;
border-top: 4px double var(--line);
}
.webring-title {
font-family: 'Courier New', Courier, monospace;
font-size: 0.8rem;
font-weight: bold;
color: #ff9af2;
margin-bottom: 1rem;
}
.stamps-grid {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.stamp {
width: 88px;
height: 31px;
background: #201135;
border: 1px outset #5e4a8a;
color: var(--accent);
font-family: monospace;
font-size: 9px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-weight: bold;
}
.stamp:hover {
border-style: inset;
filter: brightness(1.2);
}
</style>
</head>
<body>
<header>
<h1>Timeline of Mew</h1>
<p class="tagline">Brightest Smile, Bestest Buddy (2013—2026)</p>
</header>
<section id="title-screen">
<img id="featured-image" src="./library/2013-11-16_04-43-24.jpg" alt="Mew">
</section>
<div class="container" id="timeline">
</div>
<div id="lightbox" onclick="this.style.display='none'">
<div id="lb-content"></div>
<div class="lb-caption" id="lb-caption"></div>
</div>
<footer>
<div class="webring-title">~*~ MOAR CATS ~*~</div>
<div class="stamps-grid">
<a href="https://bigmomo.neocities.org/" target="_blank" class="stamp">BIG MOMO</a>
<a href="https://allthesecatstho.neocities.org/" target="_blank" class="stamp"
style="background: #351120;">KEVIN</a>
<a href="https://graceries.neocities.org/" target="_blank" class="stamp"
style="background: #112035;">GRACERIES</a>
</div>
<p style="font-size: 11px; margin-top: 2rem; color: var(--text-dim); opacity: 0.6;">
Forever Loved
</p>
</footer>
<script>
const MEDIA = [
{ file: "2013-10-04_11-32-51.jpg", type: "img" },
{ file: "2013-10-04_11-34-12.jpg", type: "img" },
{ file: "2013-10-04_11-44-25.jpg", type: "img" },
{ file: "2013-10-04_11-54-42.jpg", type: "img" },
{ file: "2013-10-04_21-36-05.jpg", type: "img" },
{ file: "2013-10-05_01-04-33.jpg", type: "img" },
{ file: "2013-10-05_01-05-44.jpg", type: "img" },
{ file: "2013-10-05_01-07-04.jpg", type: "img" },
{ file: "2013-10-05_18-16-39.jpg", type: "img" },
{ file: "2013-10-06_12-39-45.jpg", type: "img" },
{ file: "2013-10-06_16-51-58.jpg", type: "img" },
{ file: "2013-10-08_14-03-20.jpg", type: "img" },
{ file: "2013-10-08_14-03-32.jpg", type: "img" },
{ file: "2013-10-08_14-50-05.jpg", type: "img" },
{ file: "2013-10-13_13-48-50.jpg", type: "img" },
{ file: "2013-11-10_04-22-11.jpg", type: "img" },
{ file: "2013-11-16_04-43-24.jpg", type: "img" },
{ file: "2013-11-16_04-43-56.jpg", type: "img" },
{ file: "2013-11-16_04-45-41.jpg", type: "img" },
{ file: "2013-11-16_04-49-32.jpg", type: "img" },
{ file: "2013-11-16_04-50-16.jpg", type: "img" },
{ file: "2013-11-20_10-03-56.jpg", type: "img" },
{ file: "2013-11-20_10-04-22.jpg", type: "img" },
{ file: "2013-11-23_21-07-53.jpg", type: "img" },
{ file: "2013-12-24_09-52-36.jpg", type: "img" },
{ file: "2014-01-14_23-46-24.jpg", type: "img" },
{ file: "2014-01-15_13-59-21.jpg", type: "img" },
{ file: "2014-10-05_17-27-34.jpg", type: "img" },
{ file: "2014-12-04_18-18-14.jpg", type: "img" },
{ file: "2015-08-23_19-05-48.jpg", type: "img" },
{ file: "2015-09-19_03-39-04.jpg", type: "img" },
{ file: "2015-12-24_11-28-52.jpg", type: "img" },
{ file: "2016-08-13_16-40-49.jpg", type: "img" },
{ file: "2016-08-29_22-02-56.jpg", type: "img" },
{ file: "2016-09-08_21-14-59.jpg", type: "img" },
{ file: "2016-10-23_23-10-23.jpg", type: "img" },
{ file: "2016-10-23_23-34-19.jpg", type: "img" },
{ file: "2016-12-09_10-02-08.jpg", type: "img" },
{ file: "2016-12-10_21-58-17.jpg", type: "img" },
{ file: "2016-12-10_21-58-22.jpg", type: "img" },
{ file: "2016-12-10_21-58-33.jpg", type: "img" },
{ file: "2016-12-10_22-40-32.jpg", type: "img" },
{ file: "2016-12-11_06-03-13.jpg", type: "img" },
{ file: "2016-12-17_22-42-58.gif", type: "img" },
{ file: "2016-12-18_21-03-31.jpg", type: "img" },
{ file: "2017-03-08_08-52-13.jpg", type: "img" },
{ file: "2017-03-15_16-14-27.jpg", type: "img" },
{ file: "2017-03-31_17-20-19.jpg", type: "img" },
{ file: "2017-04-01_00-14-00.gif", type: "img" },
{ file: "2017-04-19_01-50-02.jpg", type: "img" },
{ file: "2017-06-14_09-51-56.jpg", type: "img" },
{ file: "2018-01-21_18-02-00.jpg", type: "img" },
{ file: "2018-01-21_18-04-03.jpg", type: "img" },
{ file: "2018-01-21_18-23-41_2.jpg", type: "img" },
{ file: "2018-01-22_02-26-40.jpg", type: "img" },
{ file: "2018-01-22_10-15-08.jpg", type: "img" },
{ file: "2018-03-22_12-42-32.jpg", type: "img" },
{ file: "2018-06-17_00-26-43.jpg", type: "img" },
{ file: "2018-06-19_06-20-25.gif", type: "img" },
{ file: "2018-06-19_06-24-35.jpg", type: "img" },
{ file: "2019-03-24_13-26-19.jpg", type: "img" },
{ file: "2019-03-24_13-29-40.jpg", type: "img" },
{ file: "2019-03-24_13-29-45.jpg", type: "img" },
{ file: "2019-04-06_22-09-24.jpg", type: "img" },
{ file: "2019-06-26_16-42-51.jpg", type: "img" },
{ file: "2019-06-26_16-43-14.jpg", type: "img" },
{ file: "2019-06-26_16-43-26.jpg", type: "img" },
{ file: "2019-06-26_16-44-49.jpg", type: "img" },
{ file: "2019-06-26_16-45-05.jpg", type: "img" },
{ file: "2019-06-26_16-45-15.jpg", type: "img" },
{ file: "2019-06-26_16-45-30.jpg", type: "img" },
{ file: "2019-08-13_22-25-52.jpg", type: "img" },
{ file: "2019-08-13_22-25-54.jpg", type: "img" },
{ file: "2019-08-13_22-26-16.jpg", type: "img" },
{ file: "2019-08-30_00-30-01.jpg", type: "img" },
{ file: "2019-09-02_23-22-36.jpg", type: "img" },
{ file: "2019-09-04_14-44-02.jpg", type: "img" },
{ file: "2019-09-09_13-43-30.jpg", type: "img" },
{ file: "2019-09-09_13-43-34.jpg", type: "img" },
{ file: "2019-09-09_13-43-45.jpg", type: "img" },
{ file: "2019-09-10_03-28-24.jpg", type: "img" },
{ file: "2019-10-21_19-40-51.jpg", type: "img" },
{ file: "2019-10-21_19-40-55.jpg", type: "img" },
{ file: "2019-11-29_10-37-37.jpg", type: "img" },
{ file: "2019-11-29_10-37-46.jpg", type: "img" },
{ file: "2019-11-29_10-38-17.jpg", type: "img" },
{ file: "2019-11-29_10-38-30.jpg", type: "img" },
{ file: "2019-11-29_10-38-33.jpg", type: "img" },
{ file: "2020-03-01_20-38-41.jpg", type: "img" },
{ file: "2020-03-01_20-38-43.jpg", type: "img" },
{ file: "2020-04-04_21-31-48.jpg", type: "img" },
{ file: "2020-04-13_07-47-36.jpg", type: "img" },
{ file: "2020-10-22_18-02-17.jpg", type: "img" },
{ file: "2022-02-09_10-09-03.jpg", type: "img" },
{ file: "2022-02-09_10-09-26.jpg", type: "img" },
{ file: "2022-02-09_10-09-53.jpg", type: "img" },
{ file: "2022-02-09_10-10-09.jpg", type: "img" },
{ file: "2022-02-09_10-10-29.jpg", type: "img" },
{ file: "2022-06-27_02-30-51.jpg", type: "img" },
{ file: "2022-07-17_13-51-33.jpg", type: "img" },
{ file: "2022-09-20_17-01-50.jpg", type: "img" },
{ file: "2024-09-27_14-19-30.jpg", type: "img" },
{ file: "2024-12-26_00-42-51.jpg", type: "img" },
{ file: "2025-03-19_11-19-46.jpg", type: "img" },
{ file: "2025-03-19_11-19-46_1.jpg", type: "img" },
{ file: "2025-03-19_11-19-52.jpg", type: "img" },
{ file: "2025-03-19_12-07-08.jpg", type: "img" },
{ file: "2025-03-19_12-07-08_1.jpg", type: "img" },
{ file: "2025-03-19_12-07-08_4.jpg", type: "img" },
{ file: "2025-03-19_12-07-08_6.jpg", type: "img" },
{ file: "2025-03-19_12-07-10.jpg", type: "img" },
{ file: "2025-03-19_12-07-10_1.jpg", type: "img" },
{ file: "2025-03-19_12-07-10_3.jpg", type: "img" },
{ file: "2025-03-19_12-07-10_5.jpg", type: "img" },
{ file: "2025-03-19_12-07-10_7.jpg", type: "img" },
{ file: "2025-03-19_12-07-12.jpg", type: "img" },
{ file: "2025-03-19_12-07-14_1.jpg", type: "img" },
{ file: "2025-03-21_03-25-56.jpg", type: "img" },
{ file: "2025-03-21_03-26-06.jpg", type: "img" },
{ file: "2025-03-21_03-26-22.jpg", type: "img" },
{ file: "2025-03-25_20-37-12.jpg", type: "img" },
{ file: "2025-04-09_01-28-26.jpg", type: "img" },
{ file: "2025-04-09_01-28-32.jpg", type: "img" },
{ file: "2025-04-27_22-31-02.jpg", type: "img" },
{ file: "2025-04-27_22-31-02_1.jpg", type: "img" },
{ file: "2025-06-22_05-33-20.jpg", type: "img" },
{ file: "2025-06-22_05-33-42.jpg", type: "img" },
{ file: "2025-09-03_01-42-43.jpg", type: "img" },
{ file: "2025-09-03_04-33-34.jpg", type: "img" },
{ file: "2025-09-03_08-42-26.jpg", type: "img" },
{ file: "2025-10-16_16-07-12.jpg", type: "img" },
{ file: "2025-11-02_05-22-30.jpg", type: "img" },
{ file: "2025-11-20_19-10-28.jpg", type: "img" },
{ file: "2025-11-29_23-05-00.jpg", type: "img" },
{ file: "2025-11-29_23-05-02.jpg", type: "img" },
{ file: "2025-11-29_23-05-04.jpg", type: "img" },
{ file: "2025-12-01_05-01-50.jpg", type: "img" },
{ file: "2025-12-01_05-02-00.jpg", type: "img" },
{ file: "2025-12-01_05-02-08.jpg", type: "img" },
{ file: "2025-12-01_05-02-10.jpg", type: "img" },
{ file: "2025-12-08_03-52-02.jpg", type: "img" },
{ file: "2025-12-08_03-52-12.jpg", type: "img" },
{ file: "2025-12-15_18-54-22.jpg", type: "img" },
{ file: "2025-12-15_18-54-28.jpg", type: "img" },
{ file: "2025-12-19_02-17-50.jpg", type: "img" },
{ file: "2025-12-23_10-36-00.jpg", type: "img" },
{ file: "2025-12-23_10-36-14.jpg", type: "img" },
{ file: "2025-12-23_10-36-16.jpg", type: "img" },
{ file: "2025-12-28_01-29-12.jpg", type: "img" },
{ file: "2026-01-03_11-49-00.jpg", type: "img" },
{ file: "2026-01-17_16-17-34.jpg", type: "img" },
{ file: "2026-02-13_14-55-30.jpg", type: "img" },
{ file: "2026-02-13_14-55-48.jpg", type: "img" },
{ file: "2026-02-20_11-14-05.jpg", type: "img" },
{ file: "2026-02-20_11-14-15.jpg", type: "img" },
{ file: "2026-02-20_11-14-18.jpg", type: "img" },
{ file: "2026-02-20_11-14-23.jpg", type: "img" },
{ file: "2026-02-20_11-14-26.jpg", type: "img" },
{ file: "2026-02-20_13-11-03.jpg", type: "img" },
{ file: "2026-02-20_14-29-46.jpg", type: "img" },
{ file: "2026-02-22_08-01-22.jpg", type: "img" },
{ file: "2026-02-22_08-47-23.jpg", type: "img" },
];
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const timeline = document.getElementById('timeline');
const lb = document.getElementById('lightbox');
const lbContent = document.getElementById('lb-content');
const lbCaption = document.getElementById('lb-caption');
function parseDate(file) {
const m = file.match(/^(\d{4})-(\d{2})-(\d{2})/);
return m ? { year: m[1], disp: `${MONTHS[parseInt(m[2]) - 1]} ${parseInt(m[3])}, ${m[1]}` } : { year: '?', disp: file };
}
function init() {
let currentYear = null;
let grid = null;
MEDIA.forEach(item => {
const date = parseDate(item.file);
if (date.year !== currentYear) {
currentYear = date.year;
const section = document.createElement('div');
section.className = 'year-section';
section.innerHTML = `<div class="year-header"><span>${currentYear}</span></div>`;
grid = document.createElement('div');
grid.className = 'grid';
section.appendChild(grid);
timeline.appendChild(section);
}
const card = document.createElement('div');
card.className = 'photo-card';
const src = `./library/${item.file}`;
if (item.type === 'video') {
card.innerHTML = `<video src="${src}" muted loop onmouseenter="this.play()" onmouseleave="this.pause();this.currentTime=0;"></video><div class="label">${date.disp}</div>`;
} else {
card.innerHTML = `<img src="${src}" alt="${date.disp}" loading="lazy"><div class="label">${date.disp}</div>`;
}
card.onclick = (e) => {
e.stopPropagation();
lbContent.innerHTML = item.type === 'video' ? `<video src="${src}" controls autoplay loop></video>` : `<img src="${src}">`;
lbCaption.innerText = date.disp;
lb.style.display = 'flex';
};
grid.appendChild(card);
});
}
init();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Some files were not shown because too many files have changed in this diff Show More