Compare commits

..

1 commit

Author SHA1 Message Date
Jet
a69f37886f
feat: use webgl shaders instead of wasm 2026-03-27 18:34:15 -07:00
13 changed files with 980 additions and 869 deletions

1
.gitignore vendored
View file

@ -10,7 +10,6 @@
# misc
.DS_Store
*.pem
*.har
# debug
npm-debug.log*

View file

@ -4,8 +4,6 @@ Personal site for Jet Pham.
The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a WebGL2 Conway's Game of Life background.
It also ships as a Nix flake with a reusable NixOS module for serving the static frontend and the Q+A API on a host.
## Features
- ASCII/ANSI-inspired visual style with the IBM VGA font
@ -55,39 +53,9 @@ npm run build
## Structure
```text
api/ Q+A backend
module.nix NixOS module
src/ frontend app
```
## NixOS module
Import the module from the flake and point it at the host-managed secret files you want to use.
```nix
{
inputs.website.url = "github:jetpham/website";
outputs = { self, nixpkgs, website, ... }: {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
website.nixosModules.default
({ config, ... }: {
services.jetpham-website = {
enable = true;
domain = "jetpham.com";
webhookSecretFile = config.age.secrets.webhook-secret.path;
};
})
];
};
};
}
```
Optional Tor support is configured by setting `services.jetpham-website.tor.enable = true;` and providing the three onion key file paths.
## Notes
- The homepage and Q+A page are the intended public pages.

View file

@ -25,10 +25,6 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
let qa_reply_domain =
std::env::var("QA_REPLY_DOMAIN").unwrap_or_else(|_| mail_domain.clone());
let webhook_secret = std::env::var("WEBHOOK_SECRET").expect("WEBHOOK_SECRET must be set");
let listen_address =
std::env::var("QA_LISTEN_ADDRESS").unwrap_or_else(|_| "127.0.0.1".to_string());
let listen_port = std::env::var("QA_LISTEN_PORT").unwrap_or_else(|_| "3003".to_string());
let listen_target = format!("{listen_address}:{listen_port}");
let conn = Connection::open(&db_path)?;
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
@ -62,8 +58,8 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
.layer(CorsLayer::permissive())
.with_state(state);
let listener = tokio::net::TcpListener::bind(&listen_target).await?;
println!("Listening on {listen_target}");
let listener = tokio::net::TcpListener::bind("127.0.0.1:3003").await?;
println!("Listening on 127.0.0.1:3003");
axum::serve(listener, app).await?;
Ok(())
}

2
check_cleanup.txt Normal file
View file

@ -0,0 +1,2 @@
$ bun run lint && tsc --noEmit
$ eslint .

121
flake.lock generated
View file

@ -1,8 +1,53 @@
{
"nodes": {
"agenix": {
"inputs": {
"darwin": "darwin",
"home-manager": "home-manager",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1770165109,
"narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=",
"owner": "ryantm",
"repo": "agenix",
"rev": "b027ee29d959fda4b60b57566d64c98a202e0feb",
"type": "github"
},
"original": {
"owner": "ryantm",
"repo": "agenix",
"type": "github"
}
},
"darwin": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744478979,
"narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "43975d782b418ebf4969e9ccba82466728c2851b",
"type": "github"
},
"original": {
"owner": "lnl7",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
@ -18,6 +63,27 @@
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1745494811,
"narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1774386573,
@ -34,10 +100,46 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"agenix": "agenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1774581174,
"narHash": "sha256-258qgkMkYPkJ9qpIg63Wk8GoIbVjszkGGPU1wbVHYTk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a313afc75b85fc77ac154bf0e62c36f68361fd0b",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
@ -54,6 +156,21 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -2,38 +2,93 @@
description = "Jet Pham's personal website";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
agenix.url = "github:ryantm/agenix";
agenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{
self,
nixpkgs,
rust-overlay,
flake-utils,
agenix,
}:
(flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
websiteSrc = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./index.html
./package-lock.json
./package.json
./public
./src
./tsconfig.json
./vite-plugin-ansi.ts
./vite.config.ts
];
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
agenixPkg = agenix.packages.${system}.default;
rustToolchain = pkgs.rust-bin.selectLatestNightlyWith (
toolchain:
toolchain.default.override {
extensions = [ "rust-src" ];
targets = [ "wasm32-unknown-unknown" ];
}
);
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
cgol-wasm = rustPlatform.buildRustPackage {
pname = "cgol-wasm";
version = "0.1.0";
src = ./cgol;
cargoLock.lockFile = ./cgol/Cargo.lock;
doCheck = false;
nativeBuildInputs = [
pkgs.wasm-bindgen-cli
pkgs.binaryen
];
buildPhase = ''
runHook preBuild
cargo build --release --frozen --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/cgol.wasm
wasm-opt pkg/cgol_bg.wasm -o pkg/cgol_bg.wasm -O4 \
--enable-bulk-memory --enable-nontrapping-float-to-int \
--enable-sign-ext --low-memory-unused --converge
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out
cp pkg/cgol_bg.wasm $out/
cp pkg/cgol.js $out/
cp pkg/cgol.d.ts $out/
cp pkg/cgol_bg.wasm.d.ts $out/ 2>/dev/null || true
cat > $out/package.json <<'EOF'
{
"name": "cgol",
"type": "module",
"version": "0.1.0",
"files": ["cgol_bg.wasm", "cgol.js", "cgol.d.ts"],
"main": "cgol.js",
"types": "cgol.d.ts",
"sideEffects": ["./snippets/*"]
}
EOF
runHook postInstall
'';
};
# Stage 2: Build the website with npm
website = pkgs.buildNpmPackage {
pname = "jet-website";
version = "0.1.0";
src = websiteSrc;
npmDepsHash = "sha256-UDz4tXNvEa8uiDDGg16K9JbNeQZR3BsVNKtuOgcyurQ=";
src = pkgs.lib.cleanSource ./.;
npmDepsHash = "sha256-O4ZUSYyVWOxP15saIadsaZuRO47Y0AvsL4pwvo5b76U=";
# Inject the Nix-built WASM before npm install resolves file: dep
postPatch = ''
mkdir -p cgol/pkg
cp -r ${cgol-wasm}/* cgol/pkg/
'';
installPhase = ''
runHook preInstall
@ -55,6 +110,7 @@
{
packages = {
default = website;
cgol-wasm = cgol-wasm;
inherit qa-api;
};
devShells.default = pkgs.mkShell {
@ -63,11 +119,12 @@
git
curl
openssl
agenixPkg
typescript-language-server
rust-analyzer
rustc
cargo
pkg-config
wasm-pack
binaryen
rustToolchain
];
};
}

View file

@ -106,24 +106,151 @@
class="pointer-events-none fixed inset-0 z-0 h-screen w-screen"
aria-hidden="true"
></canvas>
<aside id="effect-tuner" class="effect-tuner" aria-label="Effect tuning">
<h2>[EFFECT TUNER]</h2>
<label>
<span>Blur</span>
<input
id="effect-blur"
type="range"
min="0"
max="16"
step="1"
value="1"
/>
<output for="effect-blur" id="effect-blur-value">1</output>
</label>
<label>
<span>Radius</span>
<input
id="effect-radius"
type="range"
min="1"
max="60"
step="1"
value="20"
/>
<output for="effect-radius" id="effect-radius-value">20.0</output>
</label>
<label>
<span>Darkness</span>
<input
id="effect-darkness"
type="range"
min="0"
max="1"
step="0.005"
value="0.20"
/>
<output for="effect-darkness" id="effect-darkness-value">0.20</output>
</label>
<label>
<span>Smallest</span>
<input
id="effect-smallest"
type="range"
min="0.25"
max="32"
step="0.25"
value="1"
/>
<output for="effect-smallest" id="effect-smallest-value">1.00</output>
</label>
<label>
<span>Largest</span>
<input
id="effect-largest"
type="range"
min="1"
max="256"
step="1"
value="20"
/>
<output for="effect-largest" id="effect-largest-value">20</output>
</label>
<label>
<span>Levels</span>
<input
id="effect-levels"
type="range"
min="1"
max="10"
step="1"
value="3"
/>
<output for="effect-levels" id="effect-levels-value">3</output>
</label>
<label>
<span>Detail</span>
<input
id="effect-detail"
type="range"
min="0.001"
max="0.5"
step="0.001"
value="0.04"
/>
<output for="effect-detail" id="effect-detail-value">0.040</output>
</label>
<label>
<span>Dither</span>
<input
id="effect-dither"
type="range"
min="0"
max="2"
step="0.01"
value="0.75"
/>
<output for="effect-dither" id="effect-dither-value">0.75</output>
</label>
<label>
<span>Edge Bias</span>
<input
id="effect-edge"
type="range"
min="0"
max="3"
step="0.05"
value="1.35"
/>
<output for="effect-edge" id="effect-edge-value">1.35</output>
</label>
<label>
<span>Hue Drift</span>
<input
id="effect-hue-drift"
type="range"
min="0"
max="0.25"
step="0.005"
value="0.08"
/>
<output for="effect-hue-drift" id="effect-hue-drift-value"
>0.080</output
>
</label>
<label>
<span>Streak</span>
<input
id="effect-streak"
type="range"
min="0"
max="1"
step="0.01"
value="0.35"
/>
<output for="effect-streak" id="effect-streak-value">0.35</output>
</label>
</aside>
<div class="page-frame relative z-10">
<nav aria-label="Main navigation" class="site-region">
<div class="site-shell site-panel-frame px-[2ch] py-[1ch]">
<div class="site-panel-frost" aria-hidden="true"></div>
<div class="site-panel-border" aria-hidden="true"></div>
<div
class="site-panel-content site-nav-links flex justify-center gap-[2ch]"
>
<a href="/" data-nav-link class="site-nav-link"
><span class="site-nav-marker" aria-hidden="true">&gt;</span
><span>Home</span
><span class="site-nav-marker" aria-hidden="true">&lt;</span></a
>
<a href="/qa" data-nav-link class="site-nav-link"
><span class="site-nav-marker" aria-hidden="true">&gt;</span
><span>Q&amp;A</span
><span class="site-nav-marker" aria-hidden="true">&lt;</span></a
>
<div class="site-panel-content flex justify-center gap-[2ch]">
<a href="/" data-nav-link>[HOME]</a>
<a href="/qa" data-nav-link>[Q&amp;A]</a>
</div>
</div>
</nav>

View file

@ -1,139 +1,27 @@
self:
{
config,
lib,
pkgs,
...
}:
{ config, lib, ... }:
let
cfg = config.services.jetpham-website;
package = cfg.package;
qaApi = cfg.apiPackage;
apiListen = "${cfg.apiListenAddress}:${toString cfg.apiListenPort}";
usingDefaultWebhookSecret = cfg.webhookSecretFile == null;
package = self.packages.x86_64-linux.default;
qaApi = self.packages.x86_64-linux.qa-api;
webhookSecretPath =
if usingDefaultWebhookSecret then config.age.secrets.webhook-secret.path else cfg.webhookSecretFile;
usingDefaultTorSecretKey = cfg.tor.onionSecretKeyFile == null;
usingDefaultTorPublicKey = cfg.tor.onionPublicKeyFile == null;
usingDefaultTorHostname = cfg.tor.onionHostnameFile == null;
torOnionSecretKeyPath =
if usingDefaultTorSecretKey then
config.age.secrets.tor-onion-secret-key.path
if cfg.webhookSecretFile != null then
cfg.webhookSecretFile
else
cfg.tor.onionSecretKeyFile;
torOnionPublicKeyPath =
if usingDefaultTorPublicKey then
config.age.secrets.tor-onion-public-key.path
else
cfg.tor.onionPublicKeyFile;
torOnionHostnamePath =
if usingDefaultTorHostname then
config.age.secrets.tor-onion-hostname.path
else
cfg.tor.onionHostnameFile;
caddyCommonConfig = ''
header Cross-Origin-Opener-Policy "same-origin"
header Cross-Origin-Embedder-Policy "require-corp"
handle /api/* {
reverse_proxy ${apiListen}
}
handle /qa/rss.xml {
reverse_proxy ${apiListen}
}
handle {
root * ${package}
try_files {path} /index.html
file_server
}
${cfg.caddy.extraConfig}
'';
config.age.secrets.webhook-secret.path;
in
{
options.services.jetpham-website = {
enable = lib.mkEnableOption "Jet Pham's personal website";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.default;
defaultText = lib.literalExpression "self.packages.${pkgs.system}.default";
description = "Static site package served by Caddy.";
};
apiPackage = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.qa-api;
defaultText = lib.literalExpression "self.packages.${pkgs.system}.qa-api";
description = "Q&A API package run by systemd.";
};
domain = lib.mkOption {
type = lib.types.str;
default = "jetpham.com";
description = "Domain to serve the website on.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Open HTTP and HTTPS ports when Caddy is enabled.";
};
apiListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address for the local Q&A API listener.";
};
apiListenPort = lib.mkOption {
type = lib.types.port;
default = 3003;
description = "Port for the local Q&A API listener.";
};
caddy.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Serve the static site and reverse proxy the API through Caddy.";
};
caddy.extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra Caddy directives appended inside the virtual host block.";
};
tor = {
enable = lib.mkEnableOption "Tor hidden service for the website";
port = lib.mkOption {
type = lib.types.port;
default = 8888;
description = "Local Caddy port exposed through the onion service.";
};
onionSecretKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to the Tor hidden service secret key file.";
};
onionPublicKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to the Tor hidden service public key file.";
};
onionHostnameFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Path to the Tor hidden service hostname file.";
};
};
tor.enable = lib.mkEnableOption "Tor hidden service for the website";
qaNotifyEmail = lib.mkOption {
type = lib.types.str;
@ -156,53 +44,35 @@ in
webhookSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "File containing the WEBHOOK_SECRET for MTA Hook authentication. Defaults to the module-managed agenix secret when left unset.";
description = "File containing the WEBHOOK_SECRET for MTA Hook authentication.";
};
};
config = lib.mkIf cfg.enable {
age.secrets.webhook-secret = lib.mkIf usingDefaultWebhookSecret {
age.secrets.webhook-secret = lib.mkIf (cfg.webhookSecretFile == null) {
file = "${self}/secrets/webhook-secret.age";
mode = "0400";
};
age.secrets.tor-onion-secret-key = lib.mkIf (cfg.tor.enable && usingDefaultTorSecretKey) {
age.secrets.tor-onion-secret-key = lib.mkIf cfg.tor.enable {
file = "${self}/secrets/tor-onion-secret-key.age";
owner = "tor";
group = "tor";
mode = "0400";
};
age.secrets.tor-onion-public-key = lib.mkIf (cfg.tor.enable && usingDefaultTorPublicKey) {
age.secrets.tor-onion-public-key = lib.mkIf cfg.tor.enable {
file = "${self}/secrets/tor-onion-public-key.age";
owner = "tor";
group = "tor";
mode = "0444";
};
age.secrets.tor-onion-hostname = lib.mkIf (cfg.tor.enable && usingDefaultTorHostname) {
age.secrets.tor-onion-hostname = lib.mkIf cfg.tor.enable {
file = "${self}/secrets/tor-onion-hostname.age";
owner = "tor";
group = "tor";
mode = "0444";
};
assertions = [
{
assertion =
!cfg.tor.enable
|| (torOnionSecretKeyPath != null && torOnionPublicKeyPath != null && torOnionHostnamePath != null);
message = "services.jetpham-website.tor requires onionSecretKeyFile, onionPublicKeyFile, and onionHostnameFile.";
}
];
networking.firewall.allowedTCPPorts = lib.mkIf (cfg.caddy.enable && cfg.openFirewall) [
80
443
];
services.caddy.enable = cfg.caddy.enable;
services.tor = lib.mkIf cfg.tor.enable {
enable = true;
relay.onionServices.jetpham-website = {
@ -211,7 +81,7 @@ in
port = 80;
target = {
addr = "127.0.0.1";
port = cfg.tor.port;
port = 8888;
};
}
];
@ -220,47 +90,36 @@ in
systemd.services.tor-onion-keys = lib.mkIf cfg.tor.enable {
description = "Copy Tor onion keys into place";
after = lib.optional (
usingDefaultTorSecretKey || usingDefaultTorPublicKey || usingDefaultTorHostname
) "agenix.service";
after = [ "agenix.service" ];
before = [ "tor.service" ];
wantedBy = [ "tor.service" ];
serviceConfig.Type = "oneshot";
script = ''
dir="/var/lib/tor/onion/jetpham-website"
mkdir -p "$dir"
cp ${torOnionSecretKeyPath} "$dir/hs_ed25519_secret_key"
cp ${torOnionPublicKeyPath} "$dir/hs_ed25519_public_key"
cp ${torOnionHostnamePath} "$dir/hostname"
cp ${config.age.secrets.tor-onion-secret-key.path} "$dir/hs_ed25519_secret_key"
cp ${config.age.secrets.tor-onion-public-key.path} "$dir/hs_ed25519_public_key"
cp ${config.age.secrets.tor-onion-hostname.path} "$dir/hostname"
chown -R tor:tor "$dir"
chmod 700 "$dir"
chmod 400 "$dir/hs_ed25519_secret_key"
chmod 444 "$dir/hs_ed25519_public_key" "$dir/hostname"
'';
};
# Q&A API systemd service
systemd.services.jetpham-qa-api = {
description = "Jet Pham Q&A API";
after = [ "network-online.target" ] ++ lib.optional usingDefaultWebhookSecret "agenix.service";
wants = [ "network-online.target" ] ++ lib.optional usingDefaultWebhookSecret "agenix.service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "jetpham-qa";
WorkingDirectory = "/var/lib/jetpham-qa";
Environment = [
"QA_DB_PATH=/var/lib/jetpham-qa/qa.db"
"QA_NOTIFY_EMAIL=${cfg.qaNotifyEmail}"
"QA_MAIL_DOMAIN=${cfg.qaMailDomain}"
"QA_REPLY_DOMAIN=${cfg.qaReplyDomain}"
"QA_LISTEN_ADDRESS=${cfg.apiListenAddress}"
"QA_LISTEN_PORT=${toString cfg.apiListenPort}"
];
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
ReadWritePaths = [ "/var/lib/jetpham-qa" ];
Restart = "on-failure";
RestartSec = 5;
LoadCredential = "webhook-secret:${webhookSecretPath}";
@ -276,16 +135,46 @@ in
'';
};
services.caddy.virtualHosts.${cfg.domain} = lib.mkIf cfg.caddy.enable {
extraConfig = caddyCommonConfig;
services.caddy.virtualHosts.${cfg.domain} = {
extraConfig = ''
header Cross-Origin-Opener-Policy "same-origin"
header Cross-Origin-Embedder-Policy "require-corp"
handle /api/* {
reverse_proxy 127.0.0.1:3003
}
handle /qa/rss.xml {
reverse_proxy 127.0.0.1:3003
}
handle {
root * ${package}
try_files {path} /index.html
file_server
}
'';
};
services.caddy.virtualHosts."http://:${toString cfg.tor.port}" =
lib.mkIf (cfg.caddy.enable && cfg.tor.enable)
{
services.caddy.virtualHosts."http://:8888" = lib.mkIf cfg.tor.enable {
extraConfig = ''
bind 127.0.0.1
${caddyCommonConfig}
header Cross-Origin-Opener-Policy "same-origin"
header Cross-Origin-Embedder-Policy "require-corp"
handle /api/* {
reverse_proxy 127.0.0.1:3003
}
handle /qa/rss.xml {
reverse_proxy 127.0.0.1:3003
}
handle {
root * ${package}
try_files {path} /index.html
file_server
}
'';
};
};

View file

@ -32,13 +32,13 @@ export function renderFooter() {
<div class="site-panel-frost" aria-hidden="true"></div>
<div class="site-panel-border" aria-hidden="true"></div>
<div class="site-panel-content site-footer-inner">
<a href="${REPO_URL}">Src</a>
<a href="${REPO_URL}">src</a>
<span aria-hidden="true">|</span>
<a href="/qa/rss.xml" data-native-link>RSS</a>
<a href="/qa/rss.xml" data-native-link>rss</a>
<span aria-hidden="true">|</span>
<a href="/pgp.txt" data-native-link>PGP</a>
<a href="/pgp.txt" data-native-link>pgp</a>
<span aria-hidden="true">|</span>
<a href="/ssh.txt" data-native-link>SSH</a>
<a href="/ssh.txt" data-native-link>ssh</a>
<span aria-hidden="true">|</span>
<a href="${mirror.href}">${mirror.label}</a>
</div>

File diff suppressed because it is too large Load diff

View file

@ -6,20 +6,19 @@ export function homePage(outlet: HTMLElement) {
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start">
${frostedBox(`
<div class="flex flex-col items-center justify-center gap-[1.25ch] md:gap-[2ch] md:flex-row">
<div class="flex flex-col items-center justify-center gap-[2ch] md:flex-row">
<div class="order-1 flex flex-col items-center md:order-2">
<h1 class="sr-only">Jet Pham</h1>
<div aria-hidden="true" data-emitter-ansi>${Jet}</div>
<div aria-hidden="true">${Jet}</div>
<p class="mt-[2ch]">Software Extremist</p>
</div>
<div class="order-2 shrink-0 md:order-1">
<img
data-emitter-image
src="/jet.svg"
alt="A picture of Jet wearing a beanie in purple and blue lighting"
width="250"
height="250"
class="aspect-square w-full max-w-[220px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
class="aspect-square w-full max-w-[250px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
style="background-color: #a80055; color: transparent"
/>
</div>

View file

@ -140,18 +140,12 @@ body[data-background-mode="failed"]::before {
}
.site-shell {
width: 100%;
width: min(100%, 60%);
box-sizing: border-box;
margin: 0 auto;
user-select: text;
}
@media (min-width: 768px) {
.site-shell {
width: min(100%, 60%);
}
}
.site-panel-frame {
position: relative;
overflow: hidden;
@ -159,7 +153,7 @@ body[data-background-mode="failed"]::before {
.site-panel-frost {
position: absolute;
inset: var(--panel-border-inset);
inset: 0;
pointer-events: none;
overflow: hidden;
background:
@ -171,6 +165,40 @@ body[data-background-mode="failed"]::before {
var(--panel-bg);
}
.effect-tuner {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 40;
width: min(26rem, calc(100vw - 2rem));
padding: 1ch 1.5ch;
border: 2px solid var(--white);
background: rgba(0, 0, 0, 0.88);
color: var(--white);
}
.effect-tuner h2 {
margin: 0 0 1ch;
font-size: 1em;
font-weight: normal;
}
.effect-tuner label {
display: grid;
grid-template-columns: 11ch 1fr 5ch;
align-items: center;
gap: 1ch;
margin-top: 0.75ch;
}
.effect-tuner input[type="range"] {
width: 100%;
}
.effect-tuner output {
text-align: right;
}
.site-panel-border {
position: absolute;
inset: var(--panel-border-inset);
@ -221,36 +249,6 @@ a[aria-current="page"] {
text-decoration: underline;
}
.site-nav-link {
display: inline-flex;
align-items: center;
color: var(--light-blue);
text-decoration: none;
}
.site-nav-link:hover,
.site-nav-link:focus-visible {
background-color: transparent;
color: var(--light-blue);
text-decoration: none;
}
.site-nav-link[aria-current="page"] {
color: var(--yellow);
text-decoration: none;
}
.site-nav-marker {
display: inline-block;
width: 1ch;
color: currentColor;
visibility: hidden;
}
.site-nav-link[aria-current="page"] .site-nav-marker {
visibility: visible;
}
.skip-link {
position: absolute;
left: 1rem;

252
tor.har Normal file
View file

@ -0,0 +1,252 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "140.8.0"
},
"browser": {
"name": "Firefox",
"version": "140.8.0"
},
"pages": [
{
"id": "page_1",
"pageTimings": {
"onContentLoad": 1578,
"onLoad": 1578
},
"startedDateTime": "2026-03-26T20:03:41.824-07:00",
"title": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/"
}
],
"entries": [
{
"startedDateTime": "2026-03-26T20:03:41.824-07:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Host",
"value": "jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"
},
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
},
{
"name": "Accept-Language",
"value": "en-US,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br, zstd"
},
{
"name": "Sec-GPC",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Upgrade-Insecure-Requests",
"value": "1"
},
{
"name": "Sec-Fetch-Dest",
"value": "document"
},
{
"name": "Sec-Fetch-Mode",
"value": "navigate"
},
{
"name": "Sec-Fetch-Site",
"value": "none"
},
{
"name": "Priority",
"value": "u=0, i"
},
{
"name": "Pragma",
"value": "no-cache"
},
{
"name": "Cache-Control",
"value": "no-cache"
}
],
"cookies": [],
"queryString": [],
"headersSize": 521
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Server",
"value": "Caddy"
},
{
"name": "Date",
"value": "Fri, 27 Mar 2026 03:03:43 GMT"
},
{
"name": "Content-Length",
"value": "0"
}
],
"cookies": [],
"content": {
"mimeType": "text/plain",
"size": 0,
"text": ""
},
"redirectURL": "",
"headersSize": 90,
"bodySize": 90
},
"cache": {},
"timings": {
"blocked": 894,
"dns": 0,
"connect": 894,
"ssl": 0,
"send": 0,
"wait": 617,
"receive": 0
},
"time": 2405,
"_securityState": "insecure",
"serverIPAddress": "0.0.0.0",
"connection": "80",
"pageref": "page_1"
},
{
"startedDateTime": "2026-03-26T20:03:43.440-07:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/favicon.ico",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Host",
"value": "jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"
},
{
"name": "Accept",
"value": "image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"
},
{
"name": "Accept-Language",
"value": "en-US,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br, zstd"
},
{
"name": "Referer",
"value": "http://jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion/"
},
{
"name": "Sec-GPC",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Sec-Fetch-Dest",
"value": "image"
},
{
"name": "Sec-Fetch-Mode",
"value": "no-cors"
},
{
"name": "Sec-Fetch-Site",
"value": "same-origin"
},
{
"name": "Priority",
"value": "u=6"
},
{
"name": "Pragma",
"value": "no-cache"
},
{
"name": "Cache-Control",
"value": "no-cache"
}
],
"cookies": [],
"queryString": [],
"headersSize": 589
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/1.1",
"headers": [
{
"name": "Server",
"value": "Caddy"
},
{
"name": "Date",
"value": "Fri, 27 Mar 2026 03:03:43 GMT"
},
{
"name": "Content-Length",
"value": "0"
}
],
"cookies": [],
"content": {
"mimeType": "text/plain",
"size": 0,
"text": ""
},
"redirectURL": "",
"headersSize": 90,
"bodySize": 90
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 604,
"receive": 0
},
"time": 604,
"_securityState": "insecure",
"serverIPAddress": "0.0.0.0",
"connection": "80",
"pageref": "page_1"
}
]
}
}