dvorakmortonxd.dev/README.md

260 lines
15 KiB
Markdown

# 👾 dvorakmortonxd.dev
> The source code for my personal website, **[dvorakmortonxd.dev](https://dvorakmortonxd.dev)**.
This is not a normal portfolio page. It ships **two shells** and picks which one to show
from the saved theme, all from the same content:
- **`web`** (default) — a light, frosted-glass **website**: a sticky full-height sidebar
(section nav, light/dark switch, desktop switch) beside a column of glass cards — hero with
a socials row, quick tiles, the Freezmo feature panel (a real app icon) above the
Revival Inc. / RustyWolf CSS mock windows, live services,
activity heatmap, projects and hardware (grouped index lists), and friends. Dark by default
with a persisted light mode, and fully responsive, so phones and desktops get the same shell.
- **`skeuo`** — an alternate KDE Plasma / Windows-style **desktop simulation** (desktop
only): draggable snap-to-grid icons, resizable/floating windows, a Start menu, taskbar and
live clock. Reachable from the web theme's sidebar switch.
Both shells surface the same personal content: socials, self-hosted services with **live
up/down health checks**, a hardware collection, coding projects, a **GitDXD contribution
heatmap**, a gym SBD panel and links to friends' sites. The desktop shell additionally embeds
an animated ASCII train background, a clickable easter-egg logo and a working in-browser
terminal (Alacritty) with commands like `help`, `ls`, `cat`, `neofetch`, `emerge`, `ssh` and
more.
## ✨ Features
- **Two themes / shells** — a default frosted-glass **web** shell and an alternate KDE-style
**desktop** shell, toggled from either UI and persisted in `localStorage` (applied pre-paint
to avoid a flash). Phones always get the web shell.
- **Light / dark mode** — the web shell has a light/dark switch (dark by default, remembered
in `localStorage`, applied pre-paint). The desktop shell keeps its own Breeze Dark look.
- **Glassmorphic web layout** — a sticky sidebar with section navigation beside a column of
frosted-glass panels (subtle gradients + `backdrop-filter` blur), reorganised into
hero / quick-tile / section cards.
- **Asset-free feature windows** — the two feature panels (Revival Inc. / RustyWolf) are
hand-built CSS mock windows instead of screenshots: a little necromancy game scene whose
blinking "PRESS START" launches the game, and a little browser window with tabs in the
title bar, a toolbar and a start page.
- **Asset-free index & wallpaper** — Projects and Hardware are grouped repo-style index lists
(category headers, status/language chips, language-tinted dots on project rows, years — no emoji, no
screenshots), the desktop shell's app previews are the same CSS mock
windows (a git-forge page for GitDXD), and its wallpaper is a layered CSS gradient. The
only real images left are the logo, the social SVGs, the Freezmo app icons (`freezmo.png` /
`freezmo_aero.png`), the Frutiger Aero background (`frutigerbackground.png`), the Joker
easter egg and the Gentoo mark.
- **Frutiger Aero easter egg (web shell)** — clicking the Freezmo app icon swaps it to
`freezmo_aero.png` and transforms the web shell into a Frutiger Aero theme:
`frutigerbackground.png` as the full-screen backdrop, floating glossy water/soap bubbles
across the viewport, and crystal-clear 3D bevel-rimmed liquid-glass panels and buttons
with subtle cyan refractive rims. A random track from the *Frutiger Aero* SoundCloud
playlist plays while the mode is on (driven through SoundCloud's widget API), with a
small glass "now playing" chip to pause it. The light/dark switch is hidden while the
mode is active (it's a fixed bright-sky look), and the desktop mode is left untouched.
- **Draggable desktop icons** — free placement that snaps to a measured grid, one icon per
cell, with a follow-the-cursor drag ghost. Icons shrink on cramped viewports so they never
collide.
- **Multi-window manager** — draggable, resizable, minimizable, maximizable, focus/z-order
and animated open/close (with reduced-motion support). The taskbar shrinks its window
buttons as more apps open and scrolls when they still don't fit, so every open app stays
reachable. Windows are clamped to the viewport so they never open off-screen on a small
monitor and can't be dragged off any edge.
- **Working terminal app** — command input with history (`↑`/`↓`, `Ctrl+L` to clear) and a
hidden `ssh` easter egg.
- **Live service status** — each self-hosted service is health-checked through the site's
own API; Minecraft is checked through mcstatus.io.
- **Embedded live services** — opening a frameable self-hosted service (Llama.cpp/Open WebUI,
Jellyfin, Navidrome, GitDXD) from the desktop shows the real site inside the window (an
iframe). A service must not send a restrictive `X-Frame-Options`: Navidrome needs
`ND_HTTPHEADERS_FRAMEOPTIONS=ALLOWALL` and Forgejo needs `[cors] X_FRAME_OPTIONS = unset`.
A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card
instead.
- **Friend sites in windows** — clicking a friend's desktop icon opens their site in a
window. `bukovinafilip.com`, `vlcak.me` and `k0t4tkos-portfolio.com` send no framing headers,
so they load directly; `skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so it loads through `/api/frame`, a
header-stripping proxy that serves the live page from our origin. The `www` button always
opens the real site.
- **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on
Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled
from mcstatus.io through the site's own ping API.
- **Live GitDXD stats** — contribution heatmap + latest commit activity proxied server-side
from the self-hosted Forgejo instance.
- **Extras** — a playable `Revival Inc.` game embedded at `/revival-inc`, `llms.txt`,
`robots.txt` and a `sitemap.xml`.
## 🛠️ Tech Stack
- **Framework:** Next.js 16 (App Router, Turbopack)
- **Language:** TypeScript, React 19 (client components everywhere interactivity lives)
- **Styling:** hand-written CSS — `src/app/web.css` (web theme: frosted glass + light/dark),
`src/app/skeuo.css` (desktop theme), `src/app/globals.css` (base/reset). Tailwind v4 is
imported for the base layer but the UI is custom CSS.
- **Fonts:** the web theme uses the system sans stack; Monocraft (self-hosted in `public/`) is
used by the desktop theme's SMP menu and the in-desktop terminal.
- **Images:** `next/image` — only the logo (`src/app/icon.png`), the social SVGs, the Freezmo
icons (`public/freezmo.png`, `public/freezmo_aero.png`), the Frutiger Aero wallpaper
(`public/frutigerbackground.png`), the Joker easter egg and the Gentoo mark; all cards,
feature/app previews and the desktop wallpaper are CSS.
- **Version:** `2.2.22` (see `package.json`)
- **Dependencies:** `npm audit` reports **0 vulnerabilities**. `next` is kept at a patched
release (`>=16.3.6`) for the `next/og` RCE, and `brace-expansion` is force-upgraded via
`overrides`. `braces` (a transitive ESLint/`fast-glob` dependency) has **no upstream fix**,
so a locally patched copy is vendored at `vendor/braces` (version bumped to `3.0.4` with
depth guards added) and wired in with an `overrides` entry.
## 🚀 Running Locally
Requires **Node.js >= 20.9.0** (Next.js 16).
```bash
git clone https://git.dvorakmortonxd.dev/dvorakmortonxd/dvorakmortonxd.dev
cd dvorakmortonxd.dev
npm install
npm run dev
```
Then open [http://localhost:3000](http://localhost:3000).
### One-shot setup on Debian/Ubuntu
`install-dependencies-deb.sh` installs the system packages, Node.js LTS (>= 20.9.0) and npm,
then runs `npm install` and `npm run build`:
```bash
./install-dependencies-deb.sh # system deps + npm install + build
./install-dependencies-deb.sh --no-build # system deps + npm install only
./install-dependencies-deb.sh --no-npm # system packages only
```
### Scripts
```bash
npm run dev # node scripts/dev-server.mjs (dev + live updates)
npm run dev:next # next dev --turbopack (raw dev server, unprotected)
npm run build # next build --turbopack
npm run start # next start (production)
npm run lint # eslint
```
Handy checks before shipping: `npx tsc --noEmit`, `npx eslint <files>`, `npm run build`.
### Testing from a phone on your LAN
`next.config.ts` sets `allowedDevOrigins` so the dev server can be reached from other devices
on the local network and from the `beta.dvorakmortonxd.dev` tunnel. Without it Next.js
rejects the dev asset requests *and* the HMR WebSocket, which leaves the page un-hydrated
(stacked icons, dead buttons).
A plain `next dev` server is meant for localhost and exposes unauthenticated debug endpoints
(`/__nextjs*` — open-a-file-in-editor, stack frames, source maps, debugger attach, restart)
that you don't want on a tunnel. `npm run dev` therefore runs `scripts/dev-server.mjs`, a
custom server that keeps the app and live updates (HMR / Fast Refresh) but returns `404` for
those endpoints and `*.map` for any request that isn't a direct localhost connection.
Localhost keeps the full dev experience; the LAN and tunnel get a plain, live-reloading
site. To force the raw server instead, use `npm run dev:next` (don't point a tunnel at it).
### Running it as a service (survives reboot)
On this host the dev server runs as a **system** systemd unit, so it starts at boot on
its own — no login or linger needed. The unit lives in the repo at
[`deploy/dvorakmortonxd-dev.system.service`](./deploy/dvorakmortonxd-dev.system.service):
```bash
# installs /etc/systemd/system/dvorakmortonxd-dev.service and enables it
sudo bash deploy/install-system-service.sh # or: doas bash deploy/install-system-service.sh
```
Manage it with `systemctl status|restart|stop dvorakmortonxd-dev` and follow logs with
`journalctl -u dvorakmortonxd-dev -f`. It shares `.next/dev/lock` with a manual
`npm run dev`, so stop the service before starting one by hand.
<details>
<summary>No root? Use the per-user unit instead</summary>
[`deploy/dvorakmortonxd-dev.service`](./deploy/dvorakmortonxd-dev.service) is an equivalent
**user** unit you can install without root:
```bash
mkdir -p ~/.config/systemd/user
cp deploy/dvorakmortonxd-dev.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now dvorakmortonxd-dev.service
loginctl enable-linger "$USER" # once, so it starts at boot
```
Drive it with `systemctl --user …` and `journalctl --user -u …`. Run only one of the two —
they contend for `:3000` and `.next/dev/lock`.
</details>
## 📡 API Routes
| Route | Method | What it does |
| --- | --- | --- |
| `/api/heatmap` | `GET` | Proxies the user's contribution heatmap from `git.dvorakmortonxd.dev`. Returns `502` (and the panel shows a friendly fallback) if the upstream is unreachable. |
| `/api/activities` | `GET` | Fetches the GitDXD activity feed and keeps only the user's own events. |
| `/api/frame?url=…` | `GET` | Header-stripping proxy for a friend's site that refuses framing (`skopec.dev`). **Allow-listed hosts only**; fetches the HTML server-side (so the site's `X-Frame-Options` never reaches the browser), injects a `<base>` for relative assets and a click shim for in-site links. Rejects other hosts, non-http(s) schemes and credentials, and never follows redirects. |
| `/api/ping?url=…` | `GET` | Health-checks an **allow-listed** service (the site's own `*.dvorakmortonxd.dev` hosts and the friends' sites). Handles `minecraft://host:port` via mcstatus.io; treats any non-5xx response as "up". Other hosts and schemes are rejected and redirects are never followed, so it can't be used to probe the LAN (SSRF guard). |
## 📁 Project Structure
```
src/app/
page.tsx → renders <ThemeRoot/>, imports the two theme stylesheets
layout.tsx → html shell + metadata, injects ThemeScript
globals.css → base/reset styles + Tailwind import
web.css → web theme styles (frosted glass, light + dark)
skeuo.css → desktop theme styles (KDE/Plasma shell)
data.ts → socials, codingProjects, techProjects, friends
revival-inc/page.tsx → embeds the bundled game at /public/revival-inc/index.html
api/
heatmap/route.ts → contribution heatmap proxy
activities/route.ts → GitDXD activity feed proxy
ping/route.ts → service health check (HTTP + Minecraft)
components/
ThemeRoot.tsx → picks which shell to mount (web or desktop)
ThemeScript.tsx → pre-paint script: theme + light/dark scheme
useTheme.ts → theme state via useSyncExternalStore
useColorScheme.ts → light/dark state via useSyncExternalStore
WebLayout.tsx → default web shell: sidebar + glass content
AeroMusic.tsx → Frutiger Aero soundtrack (random SoundCloud playlist track)
FeatureWindow.tsx → decorative CSS windows for the two feature panels
WindowsDesktop.tsx → desktop shell: icons, windows, taskbar, Start menu
WinWindow.tsx → one draggable/resizable window
apps.tsx → APPS registry, window contents, in-app terminal
useDesktopIcons.ts → desktop icon grid + drag/drop logic
ServiceButton.tsx → service link + live status dot
ContributionHeatmap.tsx → heatmap + recent commits widget
Clock.tsx → live clock
JokerLogo.tsx → logo with click easter egg
scripts/
dev-server.mjs → custom dev server: live updates without the dev-only debug surface
deploy/
dvorakmortonxd-dev.system.service → systemd unit (system-wide) so the dev server starts at boot
dvorakmortonxd-dev.service → equivalent per-user unit (no root; needs linger)
install-system-service.sh → installs the system unit (run with sudo/doas)
vendor/
braces/ → locally patched braces@3.0.4 (depth guards for CVE-2026-93687; no upstream fix)
public/ → fonts, social SVGs, /revival-inc game, llms.txt, robots.txt, sitemap.xml
```
## 🌍 Deploying?
In order to deploy the website you'd want to set up a reverse proxy and something that runs it
as a service. Me personally, I use cloudflared and pm2. You could also use something like
Vercel, but I prefer self-hosting. Also, if you want to deploy it, you gotta change it enough
so that you don't directly copy my website, and also follow the GPL3.0-only license, meaning
you gotta make your code open source too.
Detailed step-by-step production instructions for **PM2 + npm on Debian and Alpine Linux**
(including the Cloudflare Tunnel / Nginx reverse-proxy setup) are in
[`DEPLOYMENT.md`](./DEPLOYMENT.md).
## 📬 Contact
To see my projects or get in touch, visit the live site or email me at
[dvorak@dvorakmortonxd.dev](mailto:dvorak@dvorakmortonxd.dev).
## 📄 License
GPL-3.0-only — see [`LICENSE`](./LICENSE).