Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| a69f37886f |
13 changed files with 980 additions and 869 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,7 +10,6 @@
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
*.har
|
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -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.
|
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
|
## Features
|
||||||
|
|
||||||
- ASCII/ANSI-inspired visual style with the IBM VGA font
|
- ASCII/ANSI-inspired visual style with the IBM VGA font
|
||||||
|
|
@ -55,39 +53,9 @@ npm run build
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
api/ Q+A backend
|
|
||||||
module.nix NixOS module
|
|
||||||
src/ frontend app
|
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
|
## Notes
|
||||||
|
|
||||||
- The homepage and Q+A page are the intended public pages.
|
- The homepage and Q+A page are the intended public pages.
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,6 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let qa_reply_domain =
|
let qa_reply_domain =
|
||||||
std::env::var("QA_REPLY_DOMAIN").unwrap_or_else(|_| mail_domain.clone());
|
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 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)?;
|
let conn = Connection::open(&db_path)?;
|
||||||
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
|
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
|
||||||
|
|
@ -62,8 +58,8 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&listen_target).await?;
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3003").await?;
|
||||||
println!("Listening on {listen_target}");
|
println!("Listening on 127.0.0.1:3003");
|
||||||
axum::serve(listener, app).await?;
|
axum::serve(listener, app).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
check_cleanup.txt
Normal file
2
check_cleanup.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
$ bun run lint && tsc --noEmit
|
||||||
|
$ eslint .
|
||||||
121
flake.lock
generated
121
flake.lock
generated
|
|
@ -1,8 +1,53 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"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": {
|
"flake-utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1731533236,
|
"lastModified": 1731533236,
|
||||||
|
|
@ -18,6 +63,27 @@
|
||||||
"type": "github"
|
"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": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1774386573,
|
"lastModified": 1774386573,
|
||||||
|
|
@ -34,10 +100,46 @@
|
||||||
"type": "github"
|
"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": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"agenix": "agenix",
|
||||||
"flake-utils": "flake-utils",
|
"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": {
|
"systems": {
|
||||||
|
|
@ -54,6 +156,21 @@
|
||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"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",
|
"root": "root",
|
||||||
|
|
|
||||||
95
flake.nix
95
flake.nix
|
|
@ -2,38 +2,93 @@
|
||||||
description = "Jet Pham's personal website";
|
description = "Jet Pham's personal website";
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
agenix.url = "github:ryantm/agenix";
|
||||||
|
agenix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
|
rust-overlay,
|
||||||
flake-utils,
|
flake-utils,
|
||||||
|
agenix,
|
||||||
}:
|
}:
|
||||||
(flake-utils.lib.eachDefaultSystem (
|
(flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
overlays = [ (import rust-overlay) ];
|
||||||
lib = pkgs.lib;
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
websiteSrc = lib.fileset.toSource {
|
agenixPkg = agenix.packages.${system}.default;
|
||||||
root = ./.;
|
rustToolchain = pkgs.rust-bin.selectLatestNightlyWith (
|
||||||
fileset = lib.fileset.unions [
|
toolchain:
|
||||||
./index.html
|
toolchain.default.override {
|
||||||
./package-lock.json
|
extensions = [ "rust-src" ];
|
||||||
./package.json
|
targets = [ "wasm32-unknown-unknown" ];
|
||||||
./public
|
}
|
||||||
./src
|
);
|
||||||
./tsconfig.json
|
rustPlatform = pkgs.makeRustPlatform {
|
||||||
./vite-plugin-ansi.ts
|
cargo = rustToolchain;
|
||||||
./vite.config.ts
|
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 {
|
website = pkgs.buildNpmPackage {
|
||||||
pname = "jet-website";
|
pname = "jet-website";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = websiteSrc;
|
src = pkgs.lib.cleanSource ./.;
|
||||||
npmDepsHash = "sha256-UDz4tXNvEa8uiDDGg16K9JbNeQZR3BsVNKtuOgcyurQ=";
|
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 = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
@ -55,6 +110,7 @@
|
||||||
{
|
{
|
||||||
packages = {
|
packages = {
|
||||||
default = website;
|
default = website;
|
||||||
|
cgol-wasm = cgol-wasm;
|
||||||
inherit qa-api;
|
inherit qa-api;
|
||||||
};
|
};
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
|
|
@ -63,11 +119,12 @@
|
||||||
git
|
git
|
||||||
curl
|
curl
|
||||||
openssl
|
openssl
|
||||||
|
agenixPkg
|
||||||
typescript-language-server
|
typescript-language-server
|
||||||
rust-analyzer
|
|
||||||
rustc
|
|
||||||
cargo
|
|
||||||
pkg-config
|
pkg-config
|
||||||
|
wasm-pack
|
||||||
|
binaryen
|
||||||
|
rustToolchain
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
153
index.html
153
index.html
|
|
@ -106,24 +106,151 @@
|
||||||
class="pointer-events-none fixed inset-0 z-0 h-screen w-screen"
|
class="pointer-events-none fixed inset-0 z-0 h-screen w-screen"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></canvas>
|
></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">
|
<div class="page-frame relative z-10">
|
||||||
<nav aria-label="Main navigation" class="site-region">
|
<nav aria-label="Main navigation" class="site-region">
|
||||||
<div class="site-shell site-panel-frame px-[2ch] py-[1ch]">
|
<div class="site-shell site-panel-frame px-[2ch] py-[1ch]">
|
||||||
<div class="site-panel-frost" aria-hidden="true"></div>
|
<div class="site-panel-frost" aria-hidden="true"></div>
|
||||||
<div class="site-panel-border" aria-hidden="true"></div>
|
<div class="site-panel-border" aria-hidden="true"></div>
|
||||||
<div
|
<div class="site-panel-content flex justify-center gap-[2ch]">
|
||||||
class="site-panel-content site-nav-links flex justify-center gap-[2ch]"
|
<a href="/" data-nav-link>[HOME]</a>
|
||||||
>
|
<a href="/qa" data-nav-link>[Q&A]</a>
|
||||||
<a href="/" data-nav-link class="site-nav-link"
|
|
||||||
><span class="site-nav-marker" aria-hidden="true">></span
|
|
||||||
><span>Home</span
|
|
||||||
><span class="site-nav-marker" aria-hidden="true"><</span></a
|
|
||||||
>
|
|
||||||
<a href="/qa" data-nav-link class="site-nav-link"
|
|
||||||
><span class="site-nav-marker" aria-hidden="true">></span
|
|
||||||
><span>Q&A</span
|
|
||||||
><span class="site-nav-marker" aria-hidden="true"><</span></a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
229
module.nix
229
module.nix
|
|
@ -1,139 +1,27 @@
|
||||||
self:
|
self:
|
||||||
{
|
{ config, lib, ... }:
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.services.jetpham-website;
|
cfg = config.services.jetpham-website;
|
||||||
package = cfg.package;
|
package = self.packages.x86_64-linux.default;
|
||||||
qaApi = cfg.apiPackage;
|
qaApi = self.packages.x86_64-linux.qa-api;
|
||||||
apiListen = "${cfg.apiListenAddress}:${toString cfg.apiListenPort}";
|
|
||||||
usingDefaultWebhookSecret = cfg.webhookSecretFile == null;
|
|
||||||
webhookSecretPath =
|
webhookSecretPath =
|
||||||
if usingDefaultWebhookSecret then config.age.secrets.webhook-secret.path else cfg.webhookSecretFile;
|
if cfg.webhookSecretFile != null then
|
||||||
usingDefaultTorSecretKey = cfg.tor.onionSecretKeyFile == null;
|
cfg.webhookSecretFile
|
||||||
usingDefaultTorPublicKey = cfg.tor.onionPublicKeyFile == null;
|
|
||||||
usingDefaultTorHostname = cfg.tor.onionHostnameFile == null;
|
|
||||||
torOnionSecretKeyPath =
|
|
||||||
if usingDefaultTorSecretKey then
|
|
||||||
config.age.secrets.tor-onion-secret-key.path
|
|
||||||
else
|
else
|
||||||
cfg.tor.onionSecretKeyFile;
|
config.age.secrets.webhook-secret.path;
|
||||||
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}
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.jetpham-website = {
|
options.services.jetpham-website = {
|
||||||
enable = lib.mkEnableOption "Jet Pham's personal 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 {
|
domain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "jetpham.com";
|
default = "jetpham.com";
|
||||||
description = "Domain to serve the website on.";
|
description = "Domain to serve the website on.";
|
||||||
};
|
};
|
||||||
|
|
||||||
openFirewall = lib.mkOption {
|
tor.enable = lib.mkEnableOption "Tor hidden service for the website";
|
||||||
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.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
qaNotifyEmail = lib.mkOption {
|
qaNotifyEmail = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
|
|
@ -156,53 +44,35 @@ in
|
||||||
webhookSecretFile = lib.mkOption {
|
webhookSecretFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
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 {
|
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";
|
file = "${self}/secrets/webhook-secret.age";
|
||||||
mode = "0400";
|
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";
|
file = "${self}/secrets/tor-onion-secret-key.age";
|
||||||
owner = "tor";
|
owner = "tor";
|
||||||
group = "tor";
|
group = "tor";
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
};
|
};
|
||||||
|
age.secrets.tor-onion-public-key = lib.mkIf cfg.tor.enable {
|
||||||
age.secrets.tor-onion-public-key = lib.mkIf (cfg.tor.enable && usingDefaultTorPublicKey) {
|
|
||||||
file = "${self}/secrets/tor-onion-public-key.age";
|
file = "${self}/secrets/tor-onion-public-key.age";
|
||||||
owner = "tor";
|
owner = "tor";
|
||||||
group = "tor";
|
group = "tor";
|
||||||
mode = "0444";
|
mode = "0444";
|
||||||
};
|
};
|
||||||
|
age.secrets.tor-onion-hostname = lib.mkIf cfg.tor.enable {
|
||||||
age.secrets.tor-onion-hostname = lib.mkIf (cfg.tor.enable && usingDefaultTorHostname) {
|
|
||||||
file = "${self}/secrets/tor-onion-hostname.age";
|
file = "${self}/secrets/tor-onion-hostname.age";
|
||||||
owner = "tor";
|
owner = "tor";
|
||||||
group = "tor";
|
group = "tor";
|
||||||
mode = "0444";
|
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 {
|
services.tor = lib.mkIf cfg.tor.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
relay.onionServices.jetpham-website = {
|
relay.onionServices.jetpham-website = {
|
||||||
|
|
@ -211,7 +81,7 @@ in
|
||||||
port = 80;
|
port = 80;
|
||||||
target = {
|
target = {
|
||||||
addr = "127.0.0.1";
|
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 {
|
systemd.services.tor-onion-keys = lib.mkIf cfg.tor.enable {
|
||||||
description = "Copy Tor onion keys into place";
|
description = "Copy Tor onion keys into place";
|
||||||
after = lib.optional (
|
after = [ "agenix.service" ];
|
||||||
usingDefaultTorSecretKey || usingDefaultTorPublicKey || usingDefaultTorHostname
|
|
||||||
) "agenix.service";
|
|
||||||
before = [ "tor.service" ];
|
before = [ "tor.service" ];
|
||||||
wantedBy = [ "tor.service" ];
|
wantedBy = [ "tor.service" ];
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
script = ''
|
script = ''
|
||||||
dir="/var/lib/tor/onion/jetpham-website"
|
dir="/var/lib/tor/onion/jetpham-website"
|
||||||
mkdir -p "$dir"
|
mkdir -p "$dir"
|
||||||
cp ${torOnionSecretKeyPath} "$dir/hs_ed25519_secret_key"
|
cp ${config.age.secrets.tor-onion-secret-key.path} "$dir/hs_ed25519_secret_key"
|
||||||
cp ${torOnionPublicKeyPath} "$dir/hs_ed25519_public_key"
|
cp ${config.age.secrets.tor-onion-public-key.path} "$dir/hs_ed25519_public_key"
|
||||||
cp ${torOnionHostnamePath} "$dir/hostname"
|
cp ${config.age.secrets.tor-onion-hostname.path} "$dir/hostname"
|
||||||
chown -R tor:tor "$dir"
|
chown -R tor:tor "$dir"
|
||||||
chmod 700 "$dir"
|
chmod 700 "$dir"
|
||||||
chmod 400 "$dir/hs_ed25519_secret_key"
|
chmod 400 "$dir/hs_ed25519_secret_key"
|
||||||
chmod 444 "$dir/hs_ed25519_public_key" "$dir/hostname"
|
chmod 444 "$dir/hs_ed25519_public_key" "$dir/hostname"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
# Q&A API systemd service
|
||||||
systemd.services.jetpham-qa-api = {
|
systemd.services.jetpham-qa-api = {
|
||||||
description = "Jet Pham Q&A API";
|
description = "Jet Pham Q&A API";
|
||||||
after = [ "network-online.target" ] ++ lib.optional usingDefaultWebhookSecret "agenix.service";
|
after = [ "network.target" ];
|
||||||
wants = [ "network-online.target" ] ++ lib.optional usingDefaultWebhookSecret "agenix.service";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
DynamicUser = true;
|
DynamicUser = true;
|
||||||
StateDirectory = "jetpham-qa";
|
StateDirectory = "jetpham-qa";
|
||||||
WorkingDirectory = "/var/lib/jetpham-qa";
|
|
||||||
Environment = [
|
Environment = [
|
||||||
"QA_DB_PATH=/var/lib/jetpham-qa/qa.db"
|
"QA_DB_PATH=/var/lib/jetpham-qa/qa.db"
|
||||||
"QA_NOTIFY_EMAIL=${cfg.qaNotifyEmail}"
|
"QA_NOTIFY_EMAIL=${cfg.qaNotifyEmail}"
|
||||||
"QA_MAIL_DOMAIN=${cfg.qaMailDomain}"
|
"QA_MAIL_DOMAIN=${cfg.qaMailDomain}"
|
||||||
"QA_REPLY_DOMAIN=${cfg.qaReplyDomain}"
|
"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";
|
Restart = "on-failure";
|
||||||
RestartSec = 5;
|
RestartSec = 5;
|
||||||
LoadCredential = "webhook-secret:${webhookSecretPath}";
|
LoadCredential = "webhook-secret:${webhookSecretPath}";
|
||||||
|
|
@ -276,17 +135,47 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
services.caddy.virtualHosts.${cfg.domain} = lib.mkIf cfg.caddy.enable {
|
services.caddy.virtualHosts.${cfg.domain} = {
|
||||||
extraConfig = caddyCommonConfig;
|
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}" =
|
services.caddy.virtualHosts."http://:8888" = lib.mkIf cfg.tor.enable {
|
||||||
lib.mkIf (cfg.caddy.enable && cfg.tor.enable)
|
extraConfig = ''
|
||||||
{
|
bind 127.0.0.1
|
||||||
extraConfig = ''
|
header Cross-Origin-Opener-Policy "same-origin"
|
||||||
bind 127.0.0.1
|
header Cross-Origin-Embedder-Policy "require-corp"
|
||||||
${caddyCommonConfig}
|
|
||||||
'';
|
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
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@ export function renderFooter() {
|
||||||
<div class="site-panel-frost" aria-hidden="true"></div>
|
<div class="site-panel-frost" aria-hidden="true"></div>
|
||||||
<div class="site-panel-border" aria-hidden="true"></div>
|
<div class="site-panel-border" aria-hidden="true"></div>
|
||||||
<div class="site-panel-content site-footer-inner">
|
<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>
|
<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>
|
<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>
|
<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>
|
<span aria-hidden="true">|</span>
|
||||||
<a href="${mirror.href}">${mirror.label}</a>
|
<a href="${mirror.href}">${mirror.label}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,20 +6,19 @@ export function homePage(outlet: HTMLElement) {
|
||||||
outlet.innerHTML = `
|
outlet.innerHTML = `
|
||||||
<div class="flex flex-col items-center justify-start">
|
<div class="flex flex-col items-center justify-start">
|
||||||
${frostedBox(`
|
${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">
|
<div class="order-1 flex flex-col items-center md:order-2">
|
||||||
<h1 class="sr-only">Jet Pham</h1>
|
<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>
|
<p class="mt-[2ch]">Software Extremist</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-2 shrink-0 md:order-1">
|
<div class="order-2 shrink-0 md:order-1">
|
||||||
<img
|
<img
|
||||||
data-emitter-image
|
|
||||||
src="/jet.svg"
|
src="/jet.svg"
|
||||||
alt="A picture of Jet wearing a beanie in purple and blue lighting"
|
alt="A picture of Jet wearing a beanie in purple and blue lighting"
|
||||||
width="250"
|
width="250"
|
||||||
height="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"
|
style="background-color: #a80055; color: transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -140,18 +140,12 @@ body[data-background-mode="failed"]::before {
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-shell {
|
.site-shell {
|
||||||
width: 100%;
|
width: min(100%, 60%);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.site-shell {
|
|
||||||
width: min(100%, 60%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-panel-frame {
|
.site-panel-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -159,7 +153,7 @@ body[data-background-mode="failed"]::before {
|
||||||
|
|
||||||
.site-panel-frost {
|
.site-panel-frost {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: var(--panel-border-inset);
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background:
|
background:
|
||||||
|
|
@ -171,6 +165,40 @@ body[data-background-mode="failed"]::before {
|
||||||
var(--panel-bg);
|
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 {
|
.site-panel-border {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: var(--panel-border-inset);
|
inset: var(--panel-border-inset);
|
||||||
|
|
@ -221,36 +249,6 @@ a[aria-current="page"] {
|
||||||
text-decoration: underline;
|
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 {
|
.skip-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
|
|
|
||||||
252
tor.har
Normal file
252
tor.har
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue