vtimeline added and formatted

This commit is contained in:
2026-10-09 16:59:45 +03:00
parent bc5aad87da
commit 02927a26bb
8 changed files with 140 additions and 46 deletions
+2 -1
View File
@@ -34,7 +34,8 @@ let
# an empty omo.jsonc that drops every agent override — see the journal entry # an empty omo.jsonc that drops every agent override — see the journal entry
# below). # below).
ohMyOpenagentConfig = { ohMyOpenagentConfig = {
"$schema" = "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/omo.schema.json"; "$schema" =
"https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/omo.schema.json";
_migrations = [ _migrations = [
"2026-07-opencode-config-unification" "2026-07-opencode-config-unification"
"2026-08-reasoning-unification" "2026-08-reasoning-unification"
+1 -2
View File
@@ -92,8 +92,7 @@ in
# is the only source — and the container will refuse to start with # is the only source — and the container will refuse to start with
# WEBUI_SECRET_KEY="" (env.py:762 — SystemExit). The error message is # WEBUI_SECRET_KEY="" (env.py:762 — SystemExit). The error message is
# the clear signal that the secret needs to be created. # the clear signal that the secret needs to be created.
environmentFiles = lib.optional (builtins.pathExists ./secrets/open-webui.env) environmentFiles = lib.optional (builtins.pathExists ./secrets/open-webui.env) "/run/secrets/open-webui-env";
"/run/secrets/open-webui-env";
volumes = [ volumes = [
"${panel}/data:/app/backend/data:rw" "${panel}/data:/app/backend/data:rw"
]; ];
+10 -9
View File
@@ -32,7 +32,8 @@ let
# of relying on `@/etc/nix/machines`, which Nix 2.34 parses but does # of relying on `@/etc/nix/machines`, which Nix 2.34 parses but does
# not act on (the daemon's `external-builders` list stays empty and the # not act on (the daemon's `external-builders` list stays empty and the
# client reports "configure remote builders via 'builders'" forever). # client reports "configure remote builders via 'builders'" forever).
formatBuilder = b: formatBuilder =
b:
let let
# Nix 2.34 refuses to dispatch derivations to a builder whose protocol # Nix 2.34 refuses to dispatch derivations to a builder whose protocol
# is `ssh` (the NixOS default): the daemon leaves `external-builders` # is `ssh` (the NixOS default): the daemon leaves `external-builders`
@@ -42,19 +43,19 @@ let
proto = "ssh-ng://"; proto = "ssh-ng://";
user = if b.sshUser != null && b.sshUser != "" then "${b.sshUser}@" else ""; user = if b.sshUser != null && b.sshUser != "" then "${b.sshUser}@" else "";
systems = systems =
if b.system != null then b.system if b.system != null then
else if b.systems != [ ] then lib.concatStringsSep "," b.systems b.system
else "-"; else if b.systems != [ ] then
lib.concatStringsSep "," b.systems
else
"-";
sshKey = if b.sshKey != null && b.sshKey != "" then b.sshKey else "-"; sshKey = if b.sshKey != null && b.sshKey != "" then b.sshKey else "-";
maxJobs = toString b.maxJobs; maxJobs = toString b.maxJobs;
speedFactor = toString b.speedFactor; speedFactor = toString b.speedFactor;
allFeats = b.supportedFeatures ++ b.mandatoryFeatures; allFeats = b.supportedFeatures ++ b.mandatoryFeatures;
supported = supported = if allFeats == [ ] then "-" else lib.concatStringsSep "," allFeats;
if allFeats == [ ] then "-"
else lib.concatStringsSep "," allFeats;
mandatory = mandatory =
if b.mandatoryFeatures == [ ] then "-" if b.mandatoryFeatures == [ ] then "-" else lib.concatStringsSep "," b.mandatoryFeatures;
else lib.concatStringsSep "," b.mandatoryFeatures;
publicKey = if b.publicHostKey != null then b.publicHostKey else "-"; publicKey = if b.publicHostKey != null then b.publicHostKey else "-";
in in
lib.concatStringsSep " " [ lib.concatStringsSep " " [
+1
View File
@@ -32,6 +32,7 @@
./syncthing.nix ./syncthing.nix
./systemd.nix ./systemd.nix
./ttyd.nix ./ttyd.nix
./vtimeline.nix
./uptime-kuma.nix ./uptime-kuma.nix
# ../containers/remnawave.nix # ../containers/remnawave.nix
# ./coturn.nix # ./coturn.nix
+14 -26
View File
@@ -123,23 +123,24 @@ in
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
}; };
# vtimeline.zeroq.su — static site behind Authelia forward-auth. # vtimeline.zeroq.su — Veeam Timeline View. Reverse-proxies the
# Files live under /home/oqyude/External/Git/VeeamTimelineView/public_html, # entire vhost to a local Node.js/Express process (managed by
# which is bind-mounted to /var/lib/vtimeline (see systemd.mounts below) # systemd as `vtimeline-api` — see modules/server/vtimeline.nix),
# because /home/oqyude is mode 700 and the nginx user (uid 60) cannot # which serves both the static frontend (public_html/) and the
# traverse it. Authentication is delegated to Authelia via # /api/uploads JSON-upload CRUD over a single listener on
# auth_request: nginx sub-requests /authelia on every hit, Authelia # 127.0.0.1:8000. Authelia forward-auth is wired on `/` so every
# returns 2xx if the session cookie is valid or 401 (which nginx # hit (static OR /api/*) requires a valid session cookie.
# converts into a 401 to the client; Authelia's response headers #
# carry the redirect target). The login UI itself is served by the # client_max_body_size 6m matches the server.js body limit
# authelia.zeroq.su vhost below — same Authelia container, different # (5 MB hard cap). nginx's default 1m would 413 any upload near
# vhost. # the cap before the request reached the node process.
"vtimeline.zeroq.su" = { "vtimeline.zeroq.su" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
root = "/var/lib/vtimeline";
locations = { locations = {
"/" = { "/" = {
proxyPass = "http://127.0.0.1:8000";
proxyWebsockets = true;
extraConfig = '' extraConfig = ''
auth_request /authelia; auth_request /authelia;
auth_request_set $authelia_user $upstream_http_remote_user; auth_request_set $authelia_user $upstream_http_remote_user;
@@ -160,6 +161,7 @@ in
# Authelia constructs an absolute redirect back to the # Authelia constructs an absolute redirect back to the
# original vhost. # original vhost.
error_page 401 =302 https://authelia.zeroq.su/?rd=$scheme://$host$request_uri; error_page 401 =302 https://authelia.zeroq.su/?rd=$scheme://$host$request_uri;
client_max_body_size 6m;
''; '';
}; };
"= /authelia" = { "= /authelia" = {
@@ -368,20 +370,6 @@ in
443 443
]; ];
# Bind-mount the vtimeline source tree into /var/lib so the nginx user
# (uid 60) doesn't have to traverse /home/oqyude (mode 700). The mount is
# lazy (x-systemd.automount) and nofail, so a missing /home/oqyude/External
# only shows up as a per-request 500/403, never as a hard boot failure.
systemd.mounts = [
(xlib.helpers.mkSystemdBind {
what = "/home/oqyude/External/Git/VeeamTimelineView/public_html";
where = "/var/lib/vtimeline";
})
];
systemd.tmpfiles.rules = [
(xlib.helpers.mkTmpfile "d" "/var/lib/vtimeline" "0755" "nginx" "nginx")
];
# Note: the previous vtimeline-htpasswd sops declaration lived here. It # Note: the previous vtimeline-htpasswd sops declaration lived here. It
# was removed when authelia replaced nginx's auth_basic (see the vtimeline # was removed when authelia replaced nginx's auth_basic (see the vtimeline
# vhost above). The encrypted file modules/server/secrets/vtimeline-htpasswd.yaml # vhost above). The encrypted file modules/server/secrets/vtimeline-htpasswd.yaml
+9 -3
View File
@@ -100,8 +100,10 @@ let
# а в 24.x был путём. `lib.getExe` умеет оба: derivation → bin/<name>, # а в 24.x был путём. `lib.getExe` умеет оба: derivation → bin/<name>,
# string → возвращает как есть. # string → возвращает как есть.
userShellExe = userShellExe =
let shell = config.users.users.${cfg.user}.shell or "/run/current-system/sw/bin/bash"; let
in if builtins.isString shell then shell else lib.getExe shell; shell = config.users.users.${cfg.user}.shell or "/run/current-system/sw/bin/bash";
in
if builtins.isString shell then shell else lib.getExe shell;
in in
{ {
services.ttyd = { services.ttyd = {
@@ -109,7 +111,11 @@ in
port = 7681; port = 7681;
interface = "127.0.0.1"; interface = "127.0.0.1";
user = "oqyude"; user = "oqyude";
entrypoint = [ userShellExe "-i" "-l" ]; entrypoint = [
userShellExe
"-i"
"-l"
];
writeable = true; writeable = true;
checkOrigin = true; checkOrigin = true;
maxClients = 0; maxClients = 0;
+98
View File
@@ -0,0 +1,98 @@
{
config,
lib,
pkgs,
xlib,
...
}:
# Veeam Timeline View — static frontend + REST API, both served by a
# single Node.js/Express process. nginx reverse-proxies the entire
# vtimeline.zeroq.su vhost to the local listener (see the vhost in
# modules/server/nginx.nix — no `root`, no `try_files`).
#
# API surface (server.js):
# GET /api/uploads list uploaded JSONs
# POST /api/uploads upload (raw JSON or {name, content}, ≤ 5 MB)
# GET /api/uploads/:name fetch parsed
# DELETE /api/uploads/:name delete
#
# Auth: delegated to Authelia at the nginx layer (`auth_request
# /authelia` on the vhost). The API itself trusts whoever reaches it —
# the only ingress is the same-origin nginx proxy, so the wide-open
# CORS header in server.js is a no-op in practice.
#
# Storage: /home/oqyude/External/Git/VeeamTimelineView/
# ├── public_html/ static frontend (read-only at runtime)
# ├── server.js Express app
# ├── node_modules/ installed deps (express + transitive)
# └── uploads/ persistent JSON uploads (read+write)
#
# The repo lives on the External ext4 drive (fstab). The service gates
# on home-oqyude-External.mount so it never starts against an empty
# directory after a cold boot without the drive plugged in.
let
user = xlib.device.username;
group = "users";
repo = "/home/oqyude/External/Git/VeeamTimelineView";
port = 8000;
node = pkgs.nodejs_22;
in
{
# systemPackages so /run/current-system/sw/bin/node exists for any
# operator tooling (logs, ad-hoc npm scripts). The systemd unit
# below references the absolute Nix-store path, so this is purely
# for the CLI path.
environment.systemPackages = [ node ];
# Bind-mount public_html into /var/lib. The current vhost proxies
# all traffic to the node listener, so nginx itself does not need
# this — kept for parity with the pre-API setup (so any future
# static-only fallback or external inspection has a stable
# /home-independent path). x-systemd.automount + nofail: a missing
# /home/oqyude/External only surfaces as a per-request 404, never a
# boot failure.
systemd.mounts = [
(xlib.helpers.mkSystemdBind {
what = "${repo}/public_html";
where = "/var/lib/vtimeline";
})
];
systemd.tmpfiles.rules = [
(xlib.helpers.mkTmpfile "d" "/var/lib/vtimeline" "0755" "nginx" "nginx")
];
systemd.services.vtimeline-api = {
description = "Veeam Timeline View API (Node.js + Express)";
wantedBy = [ "multi-user.target" ];
after = [
"network-online.target"
"home-oqyude-External.mount"
];
wants = [ "network-online.target" ];
requires = [ "home-oqyude-External.mount" ];
serviceConfig = {
Type = "simple";
User = user;
Group = group;
WorkingDirectory = repo;
ExecStart = "${node}/bin/node ${repo}/server.js";
Environment = "PORT=${toString port}";
Restart = "on-failure";
RestartSec = "5s";
# Sandbox. Server.js only needs to read public_html/ +
# node_modules/ and to read-write uploads/. ReadWritePaths
# lifts the ProtectHome/ProtectSystem write protection for
# uploads/ only; everywhere else stays read-only.
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = "read-only";
ReadWritePaths = [ "${repo}/uploads" ];
};
};
}