From b204c1d133c5f1b145cf05e6e40efe9a804587f8 Mon Sep 17 00:00:00 2001
From: DerGrumpf
Date: Wed, 25 Feb 2026 11:54:11 +0100
Subject: [PATCH] Added: Routes for learnlytics
---
hosts/cyper-pi-1/configuration.nix | 2 +
hosts/cyper-pi-1/learnlytics.nix | 19 +++++
hosts/cyper-pi-1/nginx.nix | 108 +++++++++++++++++++++++++++++
hosts/cyper-pi-1/postgres.nix | 64 ++++++-----------
4 files changed, 150 insertions(+), 43 deletions(-)
create mode 100644 hosts/cyper-pi-1/learnlytics.nix
create mode 100644 hosts/cyper-pi-1/nginx.nix
diff --git a/hosts/cyper-pi-1/configuration.nix b/hosts/cyper-pi-1/configuration.nix
index e56adc2..3c156de 100644
--- a/hosts/cyper-pi-1/configuration.nix
+++ b/hosts/cyper-pi-1/configuration.nix
@@ -8,6 +8,8 @@
./postgres.nix
./postgrest.nix
./swagger.nix
+ ./nginx.nix
+ ./learnlytics.nix
# ./k3s-master.nix
];
diff --git a/hosts/cyper-pi-1/learnlytics.nix b/hosts/cyper-pi-1/learnlytics.nix
new file mode 100644
index 0000000..b400aa2
--- /dev/null
+++ b/hosts/cyper-pi-1/learnlytics.nix
@@ -0,0 +1,19 @@
+{ pkgs, ... }:
+{
+ systemd.services.learnlytics-avatar = {
+ description = "Learnlytics Avatar Upload Server";
+ wantedBy = [ "multi-user.target" ];
+ after = [
+ "network.target"
+ "postgrest.service"
+ ];
+ serviceConfig = {
+ ExecStart = "${pkgs.python3}/bin/python3
+ /etc/learnlytics/avatar_server.py";
+ WorkingDirectory = "/etc/learnlytics";
+ Restart = "on-failure";
+ User = "postgres"; # or whatever user runs postgrest
+ StateDirectory = "learnlytics";
+ };
+ };
+}
diff --git a/hosts/cyper-pi-1/nginx.nix b/hosts/cyper-pi-1/nginx.nix
new file mode 100644
index 0000000..41fd307
--- /dev/null
+++ b/hosts/cyper-pi-1/nginx.nix
@@ -0,0 +1,108 @@
+{
+ pkgs,
+ ...
+}:
+{
+ services.nginx = {
+ enable = true;
+
+ # ── Frontend ────────────────────────────────────────────────────────────
+ virtualHosts."learnlytics-frontend" = {
+ listen = [
+ {
+ addr = "0.0.0.0";
+ port = 80;
+ }
+ ];
+ root = "/var/www/learnlytics";
+ locations."/" = {
+ tryFiles = "$uri $uri/ /index.html";
+ };
+ };
+
+ # ── API + avatars + upload ───────────────────────────────────────────────
+ virtualHosts."learnlytics-api" = {
+ listen = [
+ {
+ addr = "0.0.0.0";
+ port = 3002;
+ }
+ ];
+
+ # PostgREST proxy (default)
+ locations."/" = {
+ proxyPass = "http://127.0.0.1:3001";
+ extraConfig = ''
+ add_header 'Access-Control-Allow-Origin' '*' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Prefer, Accept, Range' always;
+ add_header 'Access-Control-Expose-Headers' 'Content-Range, Content-Location' always;
+
+ if ($request_method = OPTIONS) {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, PUT, DELETE, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Prefer, Accept, Range';
+ add_header 'Content-Length' '0';
+ add_header 'Content-Type' 'text/plain';
+ return 204;
+ }
+
+ 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_intercept_errors off;
+ client_max_body_size 1m;
+ '';
+ };
+
+ # Serve avatar images directly from disk
+ locations."/avatars/" = {
+ alias = "/var/lib/learnlytics/avatars/";
+ extraConfig = ''
+ expires 7d;
+ add_header Cache-Control "public, immutable";
+ add_header 'Access-Control-Allow-Origin' '*';
+ '';
+ };
+
+ # Proxy avatar uploads to the Python upload server on port 3003
+ locations."/upload/" = {
+ proxyPass = "http://127.0.0.1:3003";
+ extraConfig = ''
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ client_max_body_size 3m;
+
+ add_header 'Access-Control-Allow-Origin' '*' always;
+ add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
+
+ if ($request_method = OPTIONS) {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
+ add_header 'Content-Length' '0';
+ add_header 'Content-Type' 'text/plain';
+ return 204;
+ }
+ '';
+ };
+ };
+ };
+
+ # Avatar storage directory
+ systemd.tmpfiles.rules = [
+ "d /var/www/learnlytics 0755 nginx nginx -"
+ "d /var/lib/learnlytics 0755 postgres postgres -"
+ "d /var/lib/learnlytics/avatars 0755 postgres postgres -"
+ ];
+
+ networking.firewall.allowedTCPPorts = [
+ 3000
+ 3001
+ 3002
+ 80
+ ];
+}
diff --git a/hosts/cyper-pi-1/postgres.nix b/hosts/cyper-pi-1/postgres.nix
index b77edaa..4020c62 100644
--- a/hosts/cyper-pi-1/postgres.nix
+++ b/hosts/cyper-pi-1/postgres.nix
@@ -2,84 +2,62 @@
pkgs,
...
}:
-
{
services.postgresql = {
enable = true;
package = pkgs.postgresql_15;
enableTCPIP = true;
- # Initial database setup
- initialScript = pkgs.writeText "backend-init-script" ''
- CREATE USER postgres WITH SUPERUSER PASSWORD 'postgres';
+ extraPlugins = with pkgs.postgresql15Packages; [
+ pgjwt
+ ];
- -- Create web_anon role for PostgREST
- CREATE ROLE web_anon NOLOGIN;
- GRANT USAGE ON SCHEMA public TO web_anon;
- GRANT SELECT ON ALL TABLES IN SCHEMA public TO web_anon;
+ initialScript = /etc/learnlytics/init.sql;
- -- Create example users table
- CREATE TABLE IF NOT EXISTS users (
- id SERIAL PRIMARY KEY,
- name VARCHAR(255),
- email VARCHAR(255),
- created_at TIMESTAMP DEFAULT NOW()
- );
-
- -- Grant permissions
- GRANT SELECT, INSERT, UPDATE, DELETE ON users TO web_anon;
- GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO web_anon;
- '';
-
- # Raspberry Pi 4 optimized settings (2GB RAM assumed)
settings = {
- # Should match firewall
port = 5432;
-
- # Memory settings (RPi 4 has limited RAM)
shared_buffers = "128MB";
effective_cache_size = "512MB";
maintenance_work_mem = "32MB";
work_mem = "2MB";
wal_buffers = "4MB";
-
- # Connection settings
max_connections = 20;
-
- # Performance tuning for ARM/RPi
random_page_cost = 2.0;
effective_io_concurrency = 100;
-
- # WAL settings (conservative for SD card)
wal_level = "replica";
checkpoint_timeout = "15min";
checkpoint_completion_target = 0.7;
min_wal_size = "1GB";
max_wal_size = "4GB";
-
- # Query planning
default_statistics_target = 50;
-
- # Logging
log_min_duration_statement = 1000;
log_duration = false;
-
- # ARM/RPi specific
cpu_index_tuple_cost = 0.1;
cpu_operator_cost = 0.05;
};
authentication = ''
- local all all trust
- host all all 127.0.0.1/32 md5
- host all all ::1/128 md5
- host all all 192.168.2.0/24 md5
+ local all all trust
+ host all all 127.0.0.1/32 md5
+ host all all ::1/128 md5
+ host all all 192.168.2.0/24 md5
'';
};
- # Enable the PostgreSQL service to start on boot
systemd.services.postgresql.wantedBy = [ "multi-user.target" ];
- # Open firewall port for PostgreSQL
+ systemd.services.learnlytics-auth-migration = {
+ description = "Learnlytics auth schema migration";
+ after = [ "postgresql.service" ];
+ wants = [ "postgresql.service" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ User = "postgres";
+ ExecStart = "${pkgs.postgresql_15}/bin/psql -U postgres -f /etc/learnlytics/auth-migration.sql";
+ };
+ };
+
networking.firewall.allowedTCPPorts = [ 5432 ];
}