full on update with new npm and instructions and maintenance update
This commit is contained in:
parent
dc13b98261
commit
b0bd9cc7b3
10 changed files with 2392 additions and 1281 deletions
489
DEPLOYMENT.md
Normal file
489
DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,489 @@
|
||||||
|
# Deploying with PM2 and npm
|
||||||
|
|
||||||
|
This guide walks through running this Next.js app in production with **npm** and
|
||||||
|
**[PM2](https://pm2.keymetrics.io/)** on **Debian** and **Alpine Linux**. PM2 keeps the
|
||||||
|
process alive, restarts it on crash, and brings it back after a reboot.
|
||||||
|
|
||||||
|
The app is a standard Node.js server (`next start`), so no special runtime is required.
|
||||||
|
It listens on port `3000` by default and **requires Node.js >= 20.9.0** (Next.js 16).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Assumptions
|
||||||
|
|
||||||
|
- You have a server with `sudo`/root access and a way to reach it (SSH).
|
||||||
|
- Your domain's DNS already points at the server.
|
||||||
|
- You will clone the repo somewhere, e.g. `/srv/dvorakmortonxd.dev` (examples below use this path — change it if you like).
|
||||||
|
- You will run the app as a non-root user, e.g. `deploy`.
|
||||||
|
- You already have a reverse proxy (Nginx, Caddy, Traefik, Cloudflare Tunnel, …) terminating TLS. See [step 11](#11-optional-reverse-proxy).
|
||||||
|
|
||||||
|
The instructions for Node.js installation differ per distro, so do **section 1 (Debian)** or
|
||||||
|
**section 2 (Alpine)**, then continue with section 3 onward.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Debian — install Node.js, npm and PM2
|
||||||
|
|
||||||
|
Debian's own `nodejs` package is often too old for Next.js 16. Install a current LTS build.
|
||||||
|
|
||||||
|
### Option A: NodeSource (recommended, system-wide)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y ca-certificates curl gnupg
|
||||||
|
|
||||||
|
# This script adds the NodeSource repo for the current LTS line.
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
|
||||||
|
sudo apt install -y nodejs
|
||||||
|
|
||||||
|
node -v # must print v20.9.0 or newer
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: nvm (per-user, no root)
|
||||||
|
|
||||||
|
Use this if you cannot install system packages, or want a specific version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y curl git build-essential
|
||||||
|
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||||||
|
# Reload your shell, then:
|
||||||
|
source ~/.bashrc
|
||||||
|
nvm install --lts
|
||||||
|
nvm alias default 'lts/*'
|
||||||
|
node -v
|
||||||
|
```
|
||||||
|
|
||||||
|
> With nvm, PM2 must be started from a shell where nvm is loaded, and the generated
|
||||||
|
> systemd unit needs the full node path. Step 7 explains how to handle this.
|
||||||
|
|
||||||
|
### Install PM2 globally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo npm install -g pm2
|
||||||
|
pm2 -v
|
||||||
|
```
|
||||||
|
|
||||||
|
If `sudo npm` is not found with nvm, install PM2 per-user instead (no `sudo`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g pm2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create the deploy user (skip if you already have one)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo useradd --create-home --shell /bin/bash deploy
|
||||||
|
sudo passwd deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Give the deploy user ownership of the app directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /srv/dvorakmortonxd.dev
|
||||||
|
sudo chown -R deploy:deploy /srv/dvorakmortonxd.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Alpine — install Node.js, npm and PM2
|
||||||
|
|
||||||
|
Alpine ships a small Node.js build in the `community` repo. Alpine 3.20+ provides Node 22,
|
||||||
|
which satisfies the `>= 20.9.0` requirement.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable/refresh repos first (community must be enabled, it is by default on standard installs).
|
||||||
|
apk update
|
||||||
|
apk add --no-cache nodejs npm git
|
||||||
|
|
||||||
|
node -v # must print v20.9.0 or newer
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
If `node -v` is too old, either upgrade Alpine (`apk upgrade --available && reboot`) or use
|
||||||
|
the "current" package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apk add --no-cache nodejs-current npm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional build dependencies
|
||||||
|
|
||||||
|
Next.js normally uses prebuilt (musl) binaries, so these are usually **not** needed. Install
|
||||||
|
them only if a dependency has to compile from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apk add --no-cache build-base python3 libc6-compat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install PM2 globally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g pm2
|
||||||
|
pm2 -v
|
||||||
|
```
|
||||||
|
|
||||||
|
On Alpine, PM2 lives under `/usr/lib/node_modules/pm2`; the OpenRC service generated in
|
||||||
|
step 10 will reference that path automatically.
|
||||||
|
|
||||||
|
### Create the deploy user (skip if you already have one)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# -D: no password prompt, -s: usable shell for su/pm2
|
||||||
|
adduser -D -s /bin/ash deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Give the deploy user ownership of the app directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /srv/dvorakmortonxd.dev
|
||||||
|
chown -R deploy:deploy /srv/dvorakmortonxd.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Get the code
|
||||||
|
|
||||||
|
As the `deploy` user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -iu deploy # Debian
|
||||||
|
# or: su - deploy # Alpine
|
||||||
|
|
||||||
|
cd /srv/dvorakmortonxd.dev
|
||||||
|
git clone https://git.dvorakmortonxd.dev/dvorakmortonxd/dvorakmortonxd.dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
If the directory already contains the repo, just `cd` into it and `git pull`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Install dependencies and build
|
||||||
|
|
||||||
|
Always install **all** dependencies (including devDependencies) before building — `next build`
|
||||||
|
needs TypeScript, Tailwind and the Next.js ESLint config.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/dvorakmortonxd.dev
|
||||||
|
|
||||||
|
# Clean, lockfile-exact install (use `npm install` if you edited package.json)
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# Production build (Next.js 16 uses Turbopack)
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Sanity-check it runs before wiring up PM2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
# open http://<server-ip>:3000 , then press Ctrl+C
|
||||||
|
```
|
||||||
|
|
||||||
|
If the build gets killed on a small VPS (512 MB RAM), see
|
||||||
|
[Troubleshooting](#12-troubleshooting).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Create a PM2 ecosystem file
|
||||||
|
|
||||||
|
An ecosystem file keeps the process definition in version control and avoids retyping flags.
|
||||||
|
|
||||||
|
Create `/srv/dvorakmortonxd.dev/ecosystem.config.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "dvorakmortonxd",
|
||||||
|
cwd: "/srv/dvorakmortonxd.dev",
|
||||||
|
|
||||||
|
// Run the npm script: `npm start`
|
||||||
|
script: "npm",
|
||||||
|
args: "start",
|
||||||
|
|
||||||
|
// Single instance (no cluster load-balancing needed for a personal site)
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: "fork",
|
||||||
|
|
||||||
|
// Restart if it uses more than 512 MB (tune to your server)
|
||||||
|
max_memory_restart: "512M",
|
||||||
|
|
||||||
|
// Give the server a moment to shut down cleanly
|
||||||
|
kill_timeout: 5000,
|
||||||
|
|
||||||
|
// Don't watch files in production
|
||||||
|
watch: false,
|
||||||
|
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: 3000,
|
||||||
|
// Bind only to localhost when a reverse proxy runs on the same host.
|
||||||
|
// Remove this line to listen on all interfaces (0.0.0.0).
|
||||||
|
HOSTNAME: "127.0.0.1",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Merge stdout/stderr timestamps
|
||||||
|
time: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Why `script: "npm"`?** PM2 spawns `npm start` for you, exactly like running it by hand.
|
||||||
|
> If you prefer cleaner process signals, point PM2 straight at the Next.js binary instead:
|
||||||
|
> `script: "./node_modules/next/dist/bin/next", args: "start"`. Both work; the npm form is
|
||||||
|
> shown because that is what this project's scripts use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Start it with PM2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/dvorakmortonxd.dev
|
||||||
|
|
||||||
|
# Start using the ecosystem file
|
||||||
|
pm2 start ecosystem.config.js
|
||||||
|
|
||||||
|
# Check it is online
|
||||||
|
pm2 status
|
||||||
|
pm2 logs dvorakmortonxd --lines 50
|
||||||
|
```
|
||||||
|
|
||||||
|
If you'd rather skip the ecosystem file, the equivalent one-liner is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 start npm --name dvorakmortonxd -- start
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful day-to-day commands (the name is `dvorakmortonxd`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 restart dvorakmortonxd # restart
|
||||||
|
pm2 reload dvorakmortonxd # zero-downtime reload
|
||||||
|
pm2 stop dvorakmortonxd
|
||||||
|
pm2 delete dvorakmortonxd
|
||||||
|
pm2 save # persist the current process list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Autostart on boot — Debian (systemd)
|
||||||
|
|
||||||
|
With systemd, let PM2 generate and install the unit. Run this **as the `deploy` user**
|
||||||
|
(do not use `sudo` for the `pm2` command itself):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 startup systemd -u deploy --hp /home/deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
PM2 prints a `sudo env PATH=... pm2 startup systemd ...` command. Copy/paste and run that
|
||||||
|
command (it needs root to write the unit). Then freeze the current process list:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
# Verify the unit exists
|
||||||
|
systemctl status pm2-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it by rebooting and checking `pm2 status` afterwards.
|
||||||
|
|
||||||
|
> **nvm users:** the generated unit hard-codes the node path under `~/.nvm/...`. If you later
|
||||||
|
> upgrade Node with nvm, re-run `pm2 startup systemd -u deploy --hp /home/deploy` and `pm2 save`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Autostart on boot — Alpine (OpenRC)
|
||||||
|
|
||||||
|
Alpine uses OpenRC, which PM2 supports. As the `deploy` user:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 startup openrc -u deploy --hp /home/deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
PM2 writes `/etc/init.d/pm2`, makes it executable, and runs `rc-update add pm2 default`.
|
||||||
|
Then save the process list:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
# Verify and (re)enable at boot
|
||||||
|
rc-update add pm2 default
|
||||||
|
rc-service pm2 start
|
||||||
|
```
|
||||||
|
|
||||||
|
Check after a reboot with `pm2 status`.
|
||||||
|
|
||||||
|
> **Diskless / read-only Alpine (e.g. Raspberry Pi with `lbu`):** files written to `/etc`
|
||||||
|
> may not survive a reboot unless committed. Persist them:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> lbu add /etc/init.d/pm2
|
||||||
|
> lbu add /home/deploy/.pm2
|
||||||
|
> lbu commit -d
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Updating the deployment
|
||||||
|
|
||||||
|
Each time you pull new code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -iu deploy # or: su - deploy
|
||||||
|
cd /srv/dvorakmortonxd.dev
|
||||||
|
|
||||||
|
git pull
|
||||||
|
npm ci # or `npm install` when dependencies changed
|
||||||
|
npm run build
|
||||||
|
pm2 reload dvorakmortonxd
|
||||||
|
pm2 save
|
||||||
|
```
|
||||||
|
|
||||||
|
`pm2 reload` restarts the app with minimal downtime. If the build or startup fails, PM2 keeps
|
||||||
|
the previous process running until the new one is healthy (in `fork` mode this is a quick
|
||||||
|
restart; use `pm2 restart` if you don't need the reload semantics).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Logs and monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 logs dvorakmortonxd # live tail
|
||||||
|
pm2 logs dvorakmortonxd --lines 200
|
||||||
|
pm2 monit # CPU/memory dashboard
|
||||||
|
pm2 status
|
||||||
|
pm2 describe dvorakmortonxd
|
||||||
|
|
||||||
|
# Flush logs
|
||||||
|
pm2 flush dvorakmortonxd
|
||||||
|
```
|
||||||
|
|
||||||
|
Log files live in `~/.pm2/logs/`. To rotate them automatically, install the PM2 logrotate
|
||||||
|
module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 install pm2-logrotate
|
||||||
|
pm2 set pm2-logrotate:max_size 10M
|
||||||
|
pm2 set pm2-logrotate:retain 7
|
||||||
|
```
|
||||||
|
|
||||||
|
To run `next start` on a non-default port, change `PORT` in `ecosystem.config.js` and
|
||||||
|
`pm2 restart dvorakmortonxd --update-env`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Optional reverse proxy
|
||||||
|
|
||||||
|
Keep the app bound to `127.0.0.1:3000` (see `HOSTNAME` in the ecosystem file) and let a proxy
|
||||||
|
handle TLS and the public hostname.
|
||||||
|
|
||||||
|
**Cloudflare Tunnel** (what the site author uses — no open ports needed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian — cloudflared is not in the default repos:
|
||||||
|
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||||
|
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
|
||||||
|
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||||
|
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||||||
|
sudo apt update && sudo apt install -y cloudflared
|
||||||
|
|
||||||
|
# Alpine — available in the community repo:
|
||||||
|
apk add --no-cache cloudflared
|
||||||
|
|
||||||
|
cloudflared tunnel login
|
||||||
|
cloudflared tunnel create dvorakmortonxd
|
||||||
|
cloudflared tunnel route dns dvorakmortonxd dvorakmortonxd.dev
|
||||||
|
|
||||||
|
# ~/.cloudflared/config.yml
|
||||||
|
cat > ~/.cloudflared/config.yml <<'EOF'
|
||||||
|
tunnel: dvorakmortonxd
|
||||||
|
credentials-file: /home/deploy/.cloudflared/<TUNNEL-UUID>.json
|
||||||
|
ingress:
|
||||||
|
- hostname: dvorakmortonxd.dev
|
||||||
|
service: http://127.0.0.1:3000
|
||||||
|
- service: http_status:404
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cloudflared tunnel run dvorakmortonxd
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install it as a service (`sudo cloudflared service install` on Debian/systemd, or create an
|
||||||
|
OpenRC unit on Alpine) so the tunnel also starts at boot.
|
||||||
|
|
||||||
|
**Nginx** on Debian as a simpler alternative:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name dvorakmortonxd.dev;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Upgrade`/`Connection` headers matter if you later use WebSockets or streaming server
|
||||||
|
features.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Troubleshooting
|
||||||
|
|
||||||
|
**Node is too old (`v18.x` etc.)**
|
||||||
|
Next.js 16 refuses to start. Reinstall Node from section 1/2 and confirm `node -v` is
|
||||||
|
`>= 20.9.0`.
|
||||||
|
|
||||||
|
**Build is killed / `Killed` / `JavaScript heap out of memory`**
|
||||||
|
Turbopack builds are memory-hungry. Add swap on small VPSes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian
|
||||||
|
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
|
||||||
|
sudo mkswap /swapfile && sudo swapon /swapfile
|
||||||
|
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||||||
|
|
||||||
|
# Alpine
|
||||||
|
dd if=/dev/zero of=/swapfile bs=1M count=2048
|
||||||
|
chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
|
||||||
|
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also raise the heap: `NODE_OPTIONS=--max-old-space-size=1024 npm run build`.
|
||||||
|
|
||||||
|
**`EADDRINUSE` (port already in use)**
|
||||||
|
Something else is on `3000`. Change `PORT` in `ecosystem.config.js`, or stop the other
|
||||||
|
service: `sudo ss -ltnp | grep 3000` (Debian) / `netstat -ltnp | grep 3000` (Alpine).
|
||||||
|
|
||||||
|
**`EACCES` when running `npm install -g`**
|
||||||
|
On Debian you may need `sudo`; on Alpine run as root or prefix with `doas`. With nvm, never
|
||||||
|
use sudo.
|
||||||
|
|
||||||
|
**PM2 says the app is `errored`**
|
||||||
|
Look at the logs: `pm2 logs dvorakmortonxd --err --lines 100`. The usual cause is starting PM2
|
||||||
|
from the wrong directory (so `npm start` finds no `package.json`) or a missing `.next` build
|
||||||
|
(run `npm run build` first). The `cwd` in the ecosystem file prevents the first case.
|
||||||
|
|
||||||
|
**App doesn't come back after reboot (Alpine)**
|
||||||
|
Confirm the service is enabled (`rc-update show | grep pm2`) and that you ran `pm2 save` after
|
||||||
|
starting the app. On diskless systems, `lbu commit` your changes as in step 8.
|
||||||
|
|
||||||
|
**App doesn't come back after reboot (Debian / nvm)**
|
||||||
|
The systemd unit captured the wrong Node path. Re-run the `pm2 startup` command from step 7
|
||||||
|
and `pm2 save`.
|
||||||
|
|
||||||
|
**`pm2 startup` prints "Init system not found" on Alpine**
|
||||||
|
Specify it explicitly: `pm2 startup openrc -u deploy --hp /home/deploy`.
|
||||||
|
|
@ -30,5 +30,8 @@ you could also use something like vercel but I prefer selfhosting, also please i
|
||||||
you gotta change it enough so that you dont directly copy my website, and also follow the GPL3.0-only license,
|
you gotta change it enough so that you dont directly copy my website, and also follow the GPL3.0-only license,
|
||||||
meaning you gotta make your code opensource too
|
meaning you gotta make your code opensource too
|
||||||
|
|
||||||
|
Detailed step-by-step production instructions for **PM2 + npm on Debian and Alpine Linux** are in
|
||||||
|
[`DEPLOYMENT.md`](./DEPLOYMENT.md).
|
||||||
|
|
||||||
## 📬 Contact
|
## 📬 Contact
|
||||||
To see my projects or get in touch, visit the live site or email me at [dvorak@dvorakmortonxd.dev](mailto:martin@dvorakmortonxd.dev).
|
To see my projects or get in touch, visit the live site or email me at [dvorak@dvorakmortonxd.dev](mailto:martin@dvorakmortonxd.dev).
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,17 @@
|
||||||
import { dirname } from "path";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
import { fileURLToPath } from "url";
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const eslintConfig = defineConfig([
|
||||||
const __dirname = dirname(__filename);
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
const compat = new FlatCompat({
|
globalIgnores([
|
||||||
baseDirectory: __dirname,
|
"node_modules/**",
|
||||||
});
|
".next/**",
|
||||||
|
"out/**",
|
||||||
const eslintConfig = [
|
"build/**",
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
"next-env.d.ts",
|
||||||
{
|
]),
|
||||||
ignores: [
|
]);
|
||||||
"node_modules/**",
|
|
||||||
".next/**",
|
|
||||||
"out/**",
|
|
||||||
"build/**",
|
|
||||||
"next-env.d.ts",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
|
|
||||||
3
next-env.d.ts
vendored
3
next-env.d.ts
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
/// <reference path="./.next/types/routes.d.ts" />
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
import "./.next/dev/types/root-params.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|
|
||||||
2972
package-lock.json
generated
2972
package-lock.json
generated
File diff suppressed because it is too large
Load diff
30
package.json
30
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dvorakmortonxd",
|
"name": "dvorakmortonxd",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
|
@ -9,23 +9,21 @@
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^15.5.18",
|
"next": "^16.3.5",
|
||||||
"nextjs": "^0.0.3",
|
"react": "^19.3.0",
|
||||||
"react": "19.1.0",
|
"react-dom": "^19.3.0"
|
||||||
"react-dom": "19.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^26.5.1",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19.3.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.6.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9.39.5",
|
||||||
"eslint-config-next": "15.5.4",
|
"eslint-config-next": "^16.3.5",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.28",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,24 @@ export default function ServiceButton({ url, checkUrl, className, icon, name, de
|
||||||
checkStatus();
|
checkStatus();
|
||||||
}, [url, checkUrl]);
|
}, [url, checkUrl]);
|
||||||
|
|
||||||
|
const statusColor = status === 'up' ? '#4ade80' : status === 'down' ? '#ef4444' : '#9ca3af';
|
||||||
|
const statusGlow = status === 'up' ? 'rgba(74, 222, 128, 0.9)' : status === 'down' ? 'rgba(239, 68, 68, 0.9)' : 'transparent';
|
||||||
|
const statusLabel = status === 'up' ? 'Active' : status === 'down' ? 'Dead' : 'Checking...';
|
||||||
|
const isFriend = className === 'friend';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer" className={`service-button ${className}`} style={{ position: 'relative' }}>
|
<a
|
||||||
<div style={{
|
href={url}
|
||||||
position: 'absolute',
|
target="_blank"
|
||||||
top: '8px',
|
rel="noopener noreferrer"
|
||||||
right: '8px',
|
className={`service-button ${className}`}
|
||||||
width: '10px',
|
style={{ position: 'relative', '--status-color': statusColor, '--status-glow': statusGlow } as React.CSSProperties}
|
||||||
height: '10px',
|
title={statusLabel}
|
||||||
borderRadius: '50%',
|
>
|
||||||
backgroundColor: status === 'up' ? '#4ade80' : status === 'down' ? '#ef4444' : '#9ca3af',
|
<span className="service-icon-wrap">
|
||||||
boxShadow: status === 'up' ? '0 0 5px #4ade80' : status === 'down' ? '0 0 5px #ef4444' : 'none',
|
{isFriend && <span className="service-status-dot" />}
|
||||||
transition: 'all 0.3s ease'
|
<span className="service-icon">{icon}</span>
|
||||||
}} title={status === 'up' ? 'Active' : status === 'down' ? 'Dead' : 'Checking...'} />
|
</span>
|
||||||
<span className="service-icon">{icon}</span>
|
|
||||||
<span className="service-text">
|
<span className="service-text">
|
||||||
<span className="service-name">
|
<span className="service-name">
|
||||||
{name}
|
{name}
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,10 @@ body {
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.services-buttons.services-main {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.service-button {
|
.service-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -387,12 +391,51 @@ body {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-icon-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.service-icon {
|
.service-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease, filter 0.3s ease;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
text-align: center;
|
height: 28px;
|
||||||
filter: none;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
filter: drop-shadow(0 0 4px var(--status-glow)) drop-shadow(0 0 10px var(--status-glow));
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-button.friend .service-status-dot {
|
||||||
|
z-index: 2;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
transform: none;
|
||||||
|
background: var(--status-color);
|
||||||
|
box-shadow: 0 0 5px var(--status-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-button:hover .service-icon {
|
.service-button:hover .service-icon {
|
||||||
|
|
@ -407,6 +450,7 @@ body {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-icon {
|
.friend-icon {
|
||||||
|
|
@ -436,7 +480,6 @@ body {
|
||||||
.service-button.jellyfin:hover .service-name { color: var(--purple); }
|
.service-button.jellyfin:hover .service-name { color: var(--purple); }
|
||||||
.service-button.pterodactyl:hover .service-name { color: var(--red); }
|
.service-button.pterodactyl:hover .service-name { color: var(--red); }
|
||||||
.service-button.funkwhale:hover .service-name { color: var(--darkpink); }
|
.service-button.funkwhale:hover .service-name { color: var(--darkpink); }
|
||||||
.service-button.mailcow:hover .service-name { color: var(--accent); }
|
|
||||||
.service-button.searxng:hover .service-name { color: var(--aqua); }
|
.service-button.searxng:hover .service-name { color: var(--aqua); }
|
||||||
.service-button.convertx:hover .service-name { color: var(--gray); }
|
.service-button.convertx:hover .service-name { color: var(--gray); }
|
||||||
.service-button.gitdxd:hover .service-name { color: var(--purple); }
|
.service-button.gitdxd:hover .service-name { color: var(--purple); }
|
||||||
|
|
@ -446,7 +489,6 @@ body {
|
||||||
.service-button.jellyfin { border-color: rgba(168, 153, 132, 0.1); }
|
.service-button.jellyfin { border-color: rgba(168, 153, 132, 0.1); }
|
||||||
.service-button.pterodactyl { border-color: rgba(168, 153, 132, 0.1); }
|
.service-button.pterodactyl { border-color: rgba(168, 153, 132, 0.1); }
|
||||||
.service-button.funkwhale { border-color: rgba(255, 0, 67, 0.1); }
|
.service-button.funkwhale { border-color: rgba(255, 0, 67, 0.1); }
|
||||||
.service-button.mailcow { border-color: rgba(168, 153, 132, 0.1); }
|
|
||||||
.service-button.searxng { border-color: rgba(168, 153, 132, 0.1); }
|
.service-button.searxng { border-color: rgba(168, 153, 132, 0.1); }
|
||||||
.service-button.convertx { border-color: rgba(168, 153, 132, 0.1); }
|
.service-button.convertx { border-color: rgba(168, 153, 132, 0.1); }
|
||||||
.service-button.gitdxd { border-color: rgba(255, 0, 67, 0.1); }
|
.service-button.gitdxd { border-color: rgba(255, 0, 67, 0.1); }
|
||||||
|
|
@ -792,6 +834,10 @@ body {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.services-buttons.services-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.friends-buttons {
|
.friends-buttons {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -352,7 +352,7 @@ export default function Home() {
|
||||||
</p>
|
</p>
|
||||||
<div style={{ color: '#a3a3a3' }}>
|
<div style={{ color: '#a3a3a3' }}>
|
||||||
<p>
|
<p>
|
||||||
I'm currently hosting an SMP (dm me on discord). Also I'm pretty much cooking daily, because im bulking through out the non-summer seasons. And me and my pal <span className="text-purple-500"><a href="https://skopec.dev">Pepa</a></span> are working on an education app so we can study more efficiently, It's called <span className="text-purple-500"><a href="https://git.dvorakmortonxd.dev/edu/maturita.c">maturita.c</a></span>!
|
I'm currently hosting an SMP (dm me on discord). Also I'm pretty much cooking daily, because im bulking through out the non-summer seasons. And me and my pal <span className="text-purple-500"><a href="https://skopec.dev">Pepa</a></span> are working on an education app so we can study more efficiently, It's called <span className="text-purple-500"><a href="https://git.dvorakmortonxd.dev/edu/maturita.c">maturita.c</a></span>!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -380,7 +380,7 @@ export default function Home() {
|
||||||
<div className="services-section">
|
<div className="services-section">
|
||||||
<div className="tui-panel" style={panelStyle('var(--accent)')}>
|
<div className="tui-panel" style={panelStyle('var(--accent)')}>
|
||||||
<span className="tui-panel-label" style={{ color: 'var(--accent)', borderColor: 'var(--accent)' }}>SERVICES.HTML</span>
|
<span className="tui-panel-label" style={{ color: 'var(--accent)', borderColor: 'var(--accent)' }}>SERVICES.HTML</span>
|
||||||
<div className="services-buttons">
|
<div className="services-buttons services-main">
|
||||||
<ServiceButton
|
<ServiceButton
|
||||||
url="https://proxmox.dvorakmortonxd.dev/"
|
url="https://proxmox.dvorakmortonxd.dev/"
|
||||||
className="proxmox"
|
className="proxmox"
|
||||||
|
|
@ -421,14 +421,6 @@ export default function Home() {
|
||||||
desc="Music Streaming"
|
desc="Music Streaming"
|
||||||
access="private"
|
access="private"
|
||||||
/>
|
/>
|
||||||
<ServiceButton
|
|
||||||
url="https://mailcow.dvorakmortonxd.dev/"
|
|
||||||
className="mailcow"
|
|
||||||
icon="💌"
|
|
||||||
name="Mailcow"
|
|
||||||
desc="Email Server"
|
|
||||||
access="private"
|
|
||||||
/>
|
|
||||||
<ServiceButton
|
<ServiceButton
|
||||||
url="https://search.dvorakmortonxd.dev/"
|
url="https://search.dvorakmortonxd.dev/"
|
||||||
className="searxng"
|
className="searxng"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
@ -11,7 +15,7 @@
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|
@ -19,9 +23,19 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue