chore: initial commit of Server Configs
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
node_modules/
|
||||
.venv/
|
||||
149
httpserver/AUDIT.md
Normal 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 doesn’t 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 Apache’s `/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
@@ -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
@@ -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
@@ -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
@@ -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 (type0–type7) 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
|
After Width: | Height: | Size: 758 B |
BIN
httpserver/data/assets/DOGE.webp
Executable file
|
After Width: | Height: | Size: 730 B |
BIN
httpserver/data/assets/XMR.webp
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
httpserver/data/assets/disabled.mp4.mp4
Normal file
BIN
httpserver/data/assets/disabled_.mp4
Normal file
BIN
httpserver/data/assets/favicon.webp
Executable file
|
After Width: | Height: | Size: 730 B |
BIN
httpserver/data/assets/standby.gif
Normal file
|
After Width: | Height: | Size: 570 KiB |
BIN
httpserver/data/assets/standby.jpg
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
httpserver/data/assets/standby.mp4
Normal file
BIN
httpserver/data/assets/standby.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
httpserver/data/assets/standby.webp
Normal file
|
After Width: | Height: | Size: 210 KiB |
24
httpserver/data/banned.json
Normal 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
@@ -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
@@ -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>
|
||||
BIN
httpserver/data/bgmusic.opus
Normal file
23
httpserver/data/chat-proxy.conf
Normal 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/
|
||||
201
httpserver/data/chat-server.js
Normal 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, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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();
|
||||
});
|
||||
70
httpserver/data/check-status.js
Normal 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();
|
||||
2
httpserver/data/ddate-now
Normal 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
@@ -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
@@ -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
@@ -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>
|
||||
12
httpserver/data/index.html
Normal 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
91
httpserver/data/main.js
Executable 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();
|
||||
});
|
||||
|
||||
26
httpserver/data/mew-neocities/README-neocities.md
Normal 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 client‑side.
|
||||
|
||||
BIN
httpserver/data/mew-neocities/bg.png
Normal file
|
After Width: | Height: | Size: 440 KiB |
450
httpserver/data/mew-neocities/index.html
Normal 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>
|
||||
BIN
httpserver/data/mew-neocities/library/2013-10-04_11-32-51.jpg
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-04_11-34-12.jpg
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-04_11-44-25.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-04_11-54-42.jpg
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-04_21-36-05.jpg
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-05_01-04-33.jpg
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-05_01-05-44.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-05_01-07-04.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-05_18-16-39.jpg
Normal file
|
After Width: | Height: | Size: 473 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-06_12-39-45.jpg
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-06_16-51-58.jpg
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-08_14-03-20.jpg
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-08_14-03-32.jpg
Normal file
|
After Width: | Height: | Size: 474 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-08_14-50-05.jpg
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
httpserver/data/mew-neocities/library/2013-10-13_13-48-50.jpg
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
httpserver/data/mew-neocities/library/2013-11-10_04-22-11.jpg
Normal file
|
After Width: | Height: | Size: 807 KiB |
BIN
httpserver/data/mew-neocities/library/2013-11-16_04-43-24.jpg
Normal file
|
After Width: | Height: | Size: 966 KiB |
BIN
httpserver/data/mew-neocities/library/2013-11-16_04-43-56.jpg
Normal file
|
After Width: | Height: | Size: 932 KiB |
BIN
httpserver/data/mew-neocities/library/2013-11-16_04-45-41.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
httpserver/data/mew-neocities/library/2013-11-16_04-49-32.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
httpserver/data/mew-neocities/library/2013-11-16_04-50-16.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
httpserver/data/mew-neocities/library/2013-11-20_10-03-56.jpg
Normal file
|
After Width: | Height: | Size: 978 KiB |
BIN
httpserver/data/mew-neocities/library/2013-11-20_10-04-22.jpg
Normal file
|
After Width: | Height: | Size: 937 KiB |
BIN
httpserver/data/mew-neocities/library/2013-11-23_21-07-53.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
httpserver/data/mew-neocities/library/2013-12-24_09-52-36.jpg
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
httpserver/data/mew-neocities/library/2014-01-14_23-46-24.jpg
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
httpserver/data/mew-neocities/library/2014-01-15_13-59-21.jpg
Normal file
|
After Width: | Height: | Size: 730 KiB |
BIN
httpserver/data/mew-neocities/library/2014-10-05_17-27-34.jpg
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
httpserver/data/mew-neocities/library/2014-12-04_18-18-14.jpg
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
httpserver/data/mew-neocities/library/2015-08-23_19-05-48.jpg
Normal file
|
After Width: | Height: | Size: 859 KiB |
BIN
httpserver/data/mew-neocities/library/2015-09-19_03-39-04.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
httpserver/data/mew-neocities/library/2015-12-24_11-28-52.jpg
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
httpserver/data/mew-neocities/library/2016-08-13_16-40-49.jpg
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
httpserver/data/mew-neocities/library/2016-08-29_22-02-56.jpg
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
httpserver/data/mew-neocities/library/2016-09-08_21-14-59.jpg
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
httpserver/data/mew-neocities/library/2016-10-23_23-10-23.jpg
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
httpserver/data/mew-neocities/library/2016-10-23_23-34-19.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-09_10-02-08.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-10_21-58-17.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-10_21-58-22.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-10_21-58-33.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-10_22-40-32.jpg
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-11_06-03-13.jpg
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-17_22-42-58.gif
Normal file
|
After Width: | Height: | Size: 887 KiB |
BIN
httpserver/data/mew-neocities/library/2016-12-18_21-03-31.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
httpserver/data/mew-neocities/library/2017-03-08_08-52-13.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
httpserver/data/mew-neocities/library/2017-03-15_16-14-27.jpg
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
httpserver/data/mew-neocities/library/2017-03-31_17-20-19.jpg
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
httpserver/data/mew-neocities/library/2017-04-01_00-14-00.gif
Normal file
|
After Width: | Height: | Size: 816 KiB |
BIN
httpserver/data/mew-neocities/library/2017-04-19_01-50-02.jpg
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
httpserver/data/mew-neocities/library/2017-06-14_09-51-56.jpg
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
httpserver/data/mew-neocities/library/2018-01-21_18-02-00.jpg
Normal file
|
After Width: | Height: | Size: 594 KiB |
BIN
httpserver/data/mew-neocities/library/2018-01-21_18-04-03.jpg
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
httpserver/data/mew-neocities/library/2018-01-21_18-23-41_2.jpg
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
httpserver/data/mew-neocities/library/2018-01-22_02-26-40.jpg
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
httpserver/data/mew-neocities/library/2018-01-22_10-15-08.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
httpserver/data/mew-neocities/library/2018-03-22_12-42-32.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
httpserver/data/mew-neocities/library/2018-06-17_00-26-43.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
httpserver/data/mew-neocities/library/2018-06-19_06-20-25.gif
Normal file
|
After Width: | Height: | Size: 704 KiB |
BIN
httpserver/data/mew-neocities/library/2018-06-19_06-24-35.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
httpserver/data/mew-neocities/library/2019-03-24_13-26-19.jpg
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
httpserver/data/mew-neocities/library/2019-03-24_13-29-40.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
httpserver/data/mew-neocities/library/2019-03-24_13-29-45.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
httpserver/data/mew-neocities/library/2019-04-06_22-09-24.jpg
Normal file
|
After Width: | Height: | Size: 325 KiB |