From 609056615ebcc55f3c250ebb1c0832c439f8ccf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Mon, 17 Aug 2026 15:31:10 +0200 Subject: [PATCH 1/4] decidim: decidim.hacktivism.ch behind Caddy :9027 --- 2026/2026-08-17--decidim.md | 33 +++++ SECRETS.md | 1 + configs/caddy/Caddyfile | 49 ++++--- configs/decidim/.env.example | 13 ++ configs/decidim/README.md | 25 ++++ configs/decidim/assets/img/favicon.svg | 90 ++++++++++++ configs/decidim/assets/img/logo.svg | 90 ++++++++++++ configs/decidim/bootstrap.rb | 78 +++++++++++ configs/decidim/compose.yml | 132 ++++++++++++++++++ .../container-koopa-decidim-db.service | 21 +++ .../decidim/container-koopa-decidim.service | 21 +++ configs/decidim/entrypoint.sh | 22 +++ configs/decidim/theme/hacktivism-consent.js | 10 ++ configs/decidim/theme/hacktivism.css | 98 +++++++++++++ configs/decidim/theme/hacktivism.rb | 14 ++ configs/ports.md | 3 +- host/overview/LIVE.md | 6 + host/overview/services.md | 3 + scripts/decidim/apply-branding.sh | 22 +++ scripts/decidim/install-systemd.sh | 47 +++++++ scripts/decidim/up.sh | 21 +++ scripts/lemmy/install-systemd.sh | 24 +++- scripts/login-health/README.md | 4 +- scripts/login-health/koopa-login-health.sh | 37 ++++- 24 files changed, 840 insertions(+), 24 deletions(-) create mode 100644 2026/2026-08-17--decidim.md create mode 100644 configs/decidim/.env.example create mode 100644 configs/decidim/README.md create mode 100644 configs/decidim/assets/img/favicon.svg create mode 100644 configs/decidim/assets/img/logo.svg create mode 100644 configs/decidim/bootstrap.rb create mode 100644 configs/decidim/compose.yml create mode 100644 configs/decidim/container-koopa-decidim-db.service create mode 100644 configs/decidim/container-koopa-decidim.service create mode 100755 configs/decidim/entrypoint.sh create mode 100644 configs/decidim/theme/hacktivism-consent.js create mode 100644 configs/decidim/theme/hacktivism.css create mode 100644 configs/decidim/theme/hacktivism.rb create mode 100755 scripts/decidim/apply-branding.sh create mode 100755 scripts/decidim/install-systemd.sh create mode 100755 scripts/decidim/up.sh diff --git a/2026/2026-08-17--decidim.md b/2026/2026-08-17--decidim.md new file mode 100644 index 0000000..688c4e3 --- /dev/null +++ b/2026/2026-08-17--decidim.md @@ -0,0 +1,33 @@ +# Decidim — decidim.hacktivism.ch (2026-08-17) + +Site: **https://decidim.hacktivism.ch/** +Port **9027** → Caddy. Stack: `koopa-decidim` + `koopa-decidim-sidekiq` + `koopa-decidim-db` + `koopa-decidim-redis`. Image `decidim/decidim:0.30.8`. + +Live: `~/koopa-decidim/` (compose mirror `configs/decidim/`). Secrets only on host (`.env`, `users.env`). + +Instance name **hacktivism**. Theme: exchange-dark (gold `#e8a838`, bg `#1a1410`) via `theme/hacktivism.css`. Analog: `configs/lemmy/`. + +Admin users are on the host (`~/koopa-decidim/users.env`), not in this repo. + +Caddy: `sudo /home/hernani/koopa-caddy/apply.sh` +Boot: `scripts/decidim/install-systemd.sh` (linger already yes on koopa). + +## Cookie / privacy + +Stock Decidim still emits `#dc-dialog-wrapper` (Accept all / essential / Settings). This instance has no third-party trackers; privacy is default. + +- CSS: `#dc-dialog-wrapper, .cookies__container { display: none !important; }` +- JS: `/hacktivism-consent.js` sets `decidim-consent={"essential":true}` if missing +- Live 2026-08-17: HTML 200, CSS/JS 200, hide rules in public `/hacktivism.css` + +## Reboot (checked 2026-08-17) + +| Item | Value | +|------|--------| +| linger `hernani` | yes | +| `container-koopa-decidim-db` | enabled + active | +| `container-koopa-decidim` | enabled + active | +| `container-koopa-lemmy-db` / `container-koopa-lemmy` | enabled + active | +| listen | `:9027` `:9026` | + +login-health: probes + containers + linger + these units enabled. Loopback HTTP on :9027 is **301** (app wants HTTPS; TLS is Caddy). Probe allows 301. After reboot: login-health only unless asked. diff --git a/SECRETS.md b/SECRETS.md index 7500471..5b48906 100644 --- a/SECRETS.md +++ b/SECRETS.md @@ -19,6 +19,7 @@ Live passwords/keys live in sibling **`koopa-admin-secrets`**. |------|--------|------| | Castopod | `koopa/home-hernani/koopa-castopod/{.env,users.env}` (+ compose mirror in admin-log `configs/castopod/`) | `~/koopa-castopod/` | | Lemmy | host-only `~/koopa-lemmy/{.env,lemmy.hjson}` (compose mirror in admin-log `configs/lemmy/`) | `~/koopa-lemmy/` | +| Decidim | host-only `~/koopa-decidim/{.env,users.env}` (compose mirror in admin-log `configs/decidim/`) | `~/koopa-decidim/` | | Bonfire (`FEDERATE=true`) | `koopa/home-hernani/koopa-bonfire/{.env,users.env,compose.yml}` | `~/koopa-bonfire/` | | Prime (Jellyfin + qBittorrent) | `koopa/home-hernani/koopa-prime/{.env,users.env}` | `~/koopa-prime/` | | Forgejo (**rootless**, git.hacktivism.ch) | `koopa/home-hernani/koopa-forgejo/{.env,users.env,compose.yml}` | `~/koopa-forgejo/` | diff --git a/configs/caddy/Caddyfile b/configs/caddy/Caddyfile index 58fb8a5..bacff1e 100644 --- a/configs/caddy/Caddyfile +++ b/configs/caddy/Caddyfile @@ -1,7 +1,7 @@ # Internal only (not in the browser URL): # 9010 merchant API | 9011 exchange API | 9012 bank API # 9013 bank landing | 9014 exchange landing | 9015 merchant landing -# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9200 forgejo-ssh +# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9200 forgejo-ssh # 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3 { @@ -39,28 +39,28 @@ taler.hacktivism.ch { # surface + aptdeploy HTML (taler.hacktivism.ch only) handle /taler-monitoring-surface_err { - redir * /taler-monitoring-surface_err/ 302 + redir /taler-monitoring-surface_err/ 302 } handle /taler-monitoring-surface_err* { root * /var/www/monitoring-sites/taler.hacktivism.ch file_server } handle /taler-monitoring-surface { - redir * /taler-monitoring-surface/ 302 + redir /taler-monitoring-surface/ 302 } handle /taler-monitoring-surface* { root * /var/www/monitoring-sites/taler.hacktivism.ch file_server } handle /taler-monitoring-aptdeploy_err { - redir * /taler-monitoring-aptdeploy_err/ 302 + redir /taler-monitoring-aptdeploy_err/ 302 } handle /taler-monitoring-aptdeploy_err* { root * /var/www/monitoring-sites/taler.hacktivism.ch file_server } handle /taler-monitoring-aptdeploy { - redir * /taler-monitoring-aptdeploy/ 302 + redir /taler-monitoring-aptdeploy/ 302 } handle /taler-monitoring-aptdeploy* { root * /var/www/monitoring-sites/taler.hacktivism.ch @@ -68,14 +68,14 @@ taler.hacktivism.ch { } handle /taler-monitoring-mattermost_err { - redir * /taler-monitoring-mattermost_err/ 302 + redir /taler-monitoring-mattermost_err/ 302 } handle /taler-monitoring-mattermost_err* { root * /var/www/monitoring-sites/taler.hacktivism.ch file_server } handle /taler-monitoring-mattermost { - redir * /taler-monitoring-mattermost/ 302 + redir /taler-monitoring-mattermost/ 302 } handle /taler-monitoring-mattermost* { root * /var/www/monitoring-sites/taler.hacktivism.ch @@ -83,14 +83,14 @@ taler.hacktivism.ch { } handle /taler-monitoring-mail_err { - redir * /taler-monitoring-mail_err/ 302 + redir /taler-monitoring-mail_err/ 302 } handle /taler-monitoring-mail_err* { root * /var/www/monitoring-sites/taler.hacktivism.ch file_server } handle /taler-monitoring-mail { - redir * /taler-monitoring-mail/ 302 + redir /taler-monitoring-mail/ 302 } handle /taler-monitoring-mail* { root * /var/www/monitoring-sites/taler.hacktivism.ch @@ -99,14 +99,14 @@ taler.hacktivism.ch { # taler-monitoring console HTML (host static — not in app containers) handle /monitoring_err { - redir * /monitoring_err/ 302 + redir /monitoring_err/ 302 } handle /monitoring_err* { root * /var/www/monitoring-sites/taler.hacktivism.ch file_server } handle /monitoring { - redir * /monitoring/ 302 + redir /monitoring/ 302 } handle /monitoring* { root * /var/www/monitoring-sites/taler.hacktivism.ch @@ -143,14 +143,14 @@ exchange.hacktivism.ch { # taler-monitoring console HTML (host static — not in app containers) handle /monitoring_err { - redir * /monitoring_err/ 302 + redir /monitoring_err/ 302 } handle /monitoring_err* { root * /var/www/monitoring-sites/exchange.hacktivism.ch file_server } handle /monitoring { - redir * /monitoring/ 302 + redir /monitoring/ 302 } handle /monitoring* { root * /var/www/monitoring-sites/exchange.hacktivism.ch @@ -177,14 +177,14 @@ bank.hacktivism.ch { # taler-monitoring console HTML (host static — not in app containers) handle /monitoring_err { - redir * /monitoring_err/ 302 + redir /monitoring_err/ 302 } handle /monitoring_err* { root * /var/www/monitoring-sites/bank.hacktivism.ch file_server } handle /monitoring { - redir * /monitoring/ 302 + redir /monitoring/ 302 } handle /monitoring* { root * /var/www/monitoring-sites/bank.hacktivism.ch @@ -315,7 +315,24 @@ lemmy.hacktivism.ch { } } -http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch { +# 9027 decidim (rails) +decidim.hacktivism.ch { + header Alt-Svc "clear" + reverse_proxy 127.0.0.1:9027 { + header_up Host {host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-Port 443 + flush_interval -1 + transport http { + read_timeout 3600s + write_timeout 3600s + } + } +} + +http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch { handle /.well-known/acme-challenge/* { root * /var/www/acme file_server diff --git a/configs/decidim/.env.example b/configs/decidim/.env.example new file mode 100644 index 0000000..303db55 --- /dev/null +++ b/configs/decidim/.env.example @@ -0,0 +1,13 @@ +POSTGRES_PASSWORD=@inline-secret@ +SECRET_KEY_BASE=@inline-secret@ +DECIDIM_APPLICATION_NAME=hacktivism +DECIDIM_MAILER_SENDER=noreply@hacktivism.ch +DECIDIM_AVAILABLE_LOCALES=en,de,fr +DECIDIM_DEFAULT_LOCALE=en +DECIDIM_HOST=decidim.hacktivism.ch +SMTP_ADDRESS=127.0.0.1 +SMTP_DOMAIN=hacktivism.ch +SMTP_USERNAME=unused +SMTP_PASSWORD=unused +SMTP_PORT=25 +QUEUE_ADAPTER=sidekiq diff --git a/configs/decidim/README.md b/configs/decidim/README.md new file mode 100644 index 0000000..c0433f4 --- /dev/null +++ b/configs/decidim/README.md @@ -0,0 +1,25 @@ +# Decidim — decidim.hacktivism.ch + +| Item | Value | +|------|--------| +| Live | `/home/hernani/koopa-decidim/` | +| Compose | `compose.yml` (mirror, no secrets) | +| Port | **9027** → Caddy | +| Site | https://decidim.hacktivism.ch/ | +| Image | `decidim/decidim:0.30.8` + postgres 16 + redis 7 + sidekiq | + +Secrets: host `~/koopa-decidim/{.env,users.env}` (mode 600). Not in this repo. + +Theme: `theme/hacktivism.css` + `theme/hacktivism.rb` (same exchange-dark palette as Lemmy/Bonfire). Analog stack: `configs/lemmy/`. + +Privacy: no third-party trackers. Stock GDPR banner is hidden (`#dc-dialog-wrapper`); `theme/hacktivism-consent.js` sets essential-only `decidim-consent`. + +```bash +cd ~/koopa-decidim && set -a && source .env && set +a && podman-compose up -d +scripts/decidim/install-systemd.sh +scripts/decidim/apply-branding.sh +``` + +Caddy: `sudo /home/hernani/koopa-caddy/apply.sh` + +Daylog: `2026/2026-08-17--decidim.md`. diff --git a/configs/decidim/assets/img/favicon.svg b/configs/decidim/assets/img/favicon.svg new file mode 100644 index 0000000..e1d3dc3 --- /dev/null +++ b/configs/decidim/assets/img/favicon.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configs/decidim/assets/img/logo.svg b/configs/decidim/assets/img/logo.svg new file mode 100644 index 0000000..e1d3dc3 --- /dev/null +++ b/configs/decidim/assets/img/logo.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configs/decidim/bootstrap.rb b/configs/decidim/bootstrap.rb new file mode 100644 index 0000000..8ec96a1 --- /dev/null +++ b/configs/decidim/bootstrap.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true +# Create system admin + organization + org admin. Idempotent. +# Usage: bundle exec rails runner /code/lib/koopa_bootstrap.rb + +host = ENV.fetch("DECIDIM_HOST", "decidim.hacktivism.ch") +sys_email = ENV.fetch("DECIDIM_SYSTEM_EMAIL") +sys_password = ENV.fetch("DECIDIM_SYSTEM_PASSWORD") +admin_email = ENV.fetch("DECIDIM_ADMIN_EMAIL") +admin_password = ENV.fetch("DECIDIM_ADMIN_PASSWORD") +admin_name = ENV.fetch("DECIDIM_ADMIN_NAME", "hacktivism") +org_name = ENV.fetch("DECIDIM_APPLICATION_NAME", "hacktivism") + +sys = Decidim::System::Admin.find_or_initialize_by(email: sys_email) +sys.password = sys_password +sys.password_confirmation = sys_password +sys.save! +puts "system admin: #{sys.email}" + +org = Decidim::Organization.find_by(host: host) +if org.nil? + attrs = { + host: host, + default_locale: ENV.fetch("DECIDIM_DEFAULT_LOCALE", "en"), + available_locales: ENV.fetch("DECIDIM_AVAILABLE_LOCALES", "en,de,fr").split(","), + reference_prefix: "HCK", + time_zone: "Europe/Zurich", + users_registration_mode: "enabled", + force_users_to_authenticate_before_access_organization: false, + available_authorizations: [], + tos_version: Time.current, + colors: { + "primary" => "#e8a838", + "secondary" => "#3ecfbf", + "tertiary" => "#3d3128", + "success" => "#16a34a", + "warning" => "#f0d090", + "alert" => "#e7131a" + }, + header_snippets: %() + } + name_val = { "en" => org_name, "de" => org_name, "fr" => org_name } + desc_val = { "en" => "hacktivism participatory platform", "de" => "hacktivism Partizipationsplattform", "fr" => "plateforme participative hacktivism" } + begin + org = Decidim::Organization.new(attrs.merge(name: name_val, description: desc_val)) + org.save! + rescue StandardError + org = Decidim::Organization.new(attrs.merge(name: org_name, description: "hacktivism participatory platform")) + org.save! + end + Decidim::System::CreateDefaultPages.call(org) if defined?(Decidim::System::CreateDefaultPages) + Decidim::System::CreateDefaultContentBlocks.call(org) if defined?(Decidim::System::CreateDefaultContentBlocks) + puts "organization created host=#{org.host}" +else + org.colors = { + "primary" => "#e8a838", + "secondary" => "#3ecfbf", + "tertiary" => "#3d3128", + "success" => "#16a34a", + "warning" => "#f0d090", + "alert" => "#e7131a" + } + org.header_snippets = %() + org.save! + puts "organization updated host=#{org.host}" +end + +user = Decidim::User.find_or_initialize_by(email: admin_email, organization: org) +user.name = admin_name +user.nickname = admin_name.downcase.gsub(/[^a-z0-9_]/, "")[0, 20] +user.password = admin_password +user.password_confirmation = admin_password +user.admin = true +user.confirmed_at ||= Time.current +user.tos_agreement = true +user.accepted_tos_version = org.tos_version +user.locale = org.default_locale +user.save! +puts "org admin: #{user.email}" diff --git a/configs/decidim/compose.yml b/configs/decidim/compose.yml new file mode 100644 index 0000000..282e1b3 --- /dev/null +++ b/configs/decidim/compose.yml @@ -0,0 +1,132 @@ +# koopa-decidim — Decidim behind host Caddy on :9027 +# Docs: https://docs.decidim.org/en/develop/install/ +# Image: official pre-generated app (core modules only). No secrets in this file. + +x-logging: &default-logging + driver: "json-file" + options: + max-size: "50m" + max-file: "4" + +x-app-env: &app-env + RAILS_ENV: production + RAILS_SERVE_STATIC_FILES: "true" + RAILS_LOG_TO_STDOUT: "true" + DATABASE_URL: postgres://decidim:${POSTGRES_PASSWORD}@postgres:5432/decidim + REDIS_URL: redis://redis:6379/0 + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + DECIDIM_APPLICATION_NAME: ${DECIDIM_APPLICATION_NAME:-hacktivism} + DECIDIM_MAILER_SENDER: ${DECIDIM_MAILER_SENDER:-noreply@hacktivism.ch} + DECIDIM_AVAILABLE_LOCALES: ${DECIDIM_AVAILABLE_LOCALES:-en,de,fr} + DECIDIM_DEFAULT_LOCALE: ${DECIDIM_DEFAULT_LOCALE:-en} + SMTP_ADDRESS: ${SMTP_ADDRESS:-127.0.0.1} + SMTP_DOMAIN: ${SMTP_DOMAIN:-hacktivism.ch} + SMTP_USERNAME: ${SMTP_USERNAME:-unused} + SMTP_PASSWORD: ${SMTP_PASSWORD:-unused} + SMTP_PORT: ${SMTP_PORT:-25} + SMTP_AUTHENTICATION: ${SMTP_AUTHENTICATION:-plain} + QUEUE_ADAPTER: ${QUEUE_ADAPTER:-sidekiq} + +services: + decidim: + image: docker.io/decidim/decidim:0.30.8 + container_name: koopa-decidim + hostname: decidim + ports: + - "9027:3000" + env_file: + - .env + environment: + <<: *app-env + volumes: + - ./entrypoint.sh:/usr/local/bin/koopa-decidim-entrypoint.sh:ro + - ./theme/hacktivism.rb:/code/config/initializers/hacktivism.rb:ro + - ./theme/hacktivism.css:/code/public/hacktivism.css:ro + - ./theme/hacktivism-consent.js:/code/public/hacktivism-consent.js:ro + - ./bootstrap.rb:/code/lib/koopa_bootstrap.rb:ro + - ./assets/img/logo.svg:/code/public/hacktivism-logo.svg:ro + - ./assets/img/favicon.svg:/code/public/hacktivism-favicon.svg:ro + - decidim-storage:/code/storage + - decidim-uploads:/code/public/uploads + - decidim-tmp:/code/tmp + entrypoint: ["/bin/bash", "/usr/local/bin/koopa-decidim-entrypoint.sh"] + command: ["bundle", "exec", "rails", "s", "-b", "0.0.0.0", "-p", "3000"] + restart: unless-stopped + logging: *default-logging + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + labels: + org.hacktivism.service: decidim + org.hacktivism.host_port: "9027" + org.hacktivism.site: decidim.hacktivism.ch + org.hacktivism.managed_by: koopa-admin + + sidekiq: + image: docker.io/decidim/decidim:0.30.8 + container_name: koopa-decidim-sidekiq + hostname: sidekiq + env_file: + - .env + environment: + <<: *app-env + volumes: + - ./entrypoint.sh:/usr/local/bin/koopa-decidim-entrypoint.sh:ro + - ./theme/hacktivism.rb:/code/config/initializers/hacktivism.rb:ro + - decidim-storage:/code/storage + - decidim-uploads:/code/public/uploads + - decidim-tmp:/code/tmp + entrypoint: ["/bin/bash", "/usr/local/bin/koopa-decidim-entrypoint.sh"] + command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"] + restart: unless-stopped + logging: *default-logging + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + labels: + org.hacktivism.service: decidim + org.hacktivism.managed_by: koopa-admin + + postgres: + image: docker.io/library/postgres:16-alpine + container_name: koopa-decidim-db + hostname: postgres + environment: + - POSTGRES_USER=decidim + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=decidim + volumes: + - decidim-db:/var/lib/postgresql/data + restart: unless-stopped + logging: *default-logging + healthcheck: + test: ["CMD-SHELL", "pg_isready -U decidim -d decidim"] + interval: 5s + timeout: 5s + retries: 12 + labels: + org.hacktivism.service: decidim + org.hacktivism.managed_by: koopa-admin + + redis: + image: docker.io/library/redis:7-alpine + container_name: koopa-decidim-redis + hostname: redis + volumes: + - decidim-redis:/data + restart: unless-stopped + logging: *default-logging + labels: + org.hacktivism.service: decidim + org.hacktivism.managed_by: koopa-admin + +volumes: + decidim-db: + decidim-redis: + decidim-storage: + decidim-uploads: + decidim-tmp: diff --git a/configs/decidim/container-koopa-decidim-db.service b/configs/decidim/container-koopa-decidim-db.service new file mode 100644 index 0000000..c15c8c6 --- /dev/null +++ b/configs/decidim/container-koopa-decidim-db.service @@ -0,0 +1,21 @@ +# user systemd — Decidim postgres + redis +[Unit] +Description=Decidim postgres/redis (koopa-decidim-db) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=%t/containers + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +RestartSec=20 +TimeoutStartSec=300 +TimeoutStopSec=120 +WorkingDirectory=/home/hernani/koopa-decidim +ExecStart=/bin/bash -lc 'set -a && source .env && set +a && /usr/bin/podman-compose up -d postgres redis' +ExecStop=/usr/bin/podman stop -t 30 koopa-decidim-db koopa-decidim-redis +Type=oneshot +RemainAfterExit=yes + +[Install] +WantedBy=default.target diff --git a/configs/decidim/container-koopa-decidim.service b/configs/decidim/container-koopa-decidim.service new file mode 100644 index 0000000..c84ec49 --- /dev/null +++ b/configs/decidim/container-koopa-decidim.service @@ -0,0 +1,21 @@ +# user systemd — Decidim stack (app + sidekiq) +[Unit] +Description=Decidim stack (koopa-decidim) +Wants=network-online.target container-koopa-decidim-db.service +After=network-online.target container-koopa-decidim-db.service +RequiresMountsFor=%t/containers + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +RestartSec=20 +TimeoutStartSec=600 +TimeoutStopSec=120 +WorkingDirectory=/home/hernani/koopa-decidim +ExecStart=/bin/bash -lc 'set -a && source .env && set +a && /usr/bin/podman-compose up -d' +ExecStop=/usr/bin/podman stop -t 30 koopa-decidim koopa-decidim-sidekiq +Type=oneshot +RemainAfterExit=yes + +[Install] +WantedBy=default.target diff --git a/configs/decidim/entrypoint.sh b/configs/decidim/entrypoint.sh new file mode 100755 index 0000000..b7af36d --- /dev/null +++ b/configs/decidim/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Wait for Postgres, migrate, then exec CMD (rails or sidekiq). +set -euo pipefail + +export RAILS_ENV="${RAILS_ENV:-production}" + +echo "koopa-decidim: waiting for postgres..." +i=0 +until bundle exec rails runner 'ActiveRecord::Base.connection.execute("SELECT 1")' >/dev/null 2>&1; do + i=$((i + 1)) + if [ "$i" -ge 60 ]; then + echo "koopa-decidim: postgres not ready after ${i} tries" >&2 + exit 1 + fi + sleep 3 +done + +echo "koopa-decidim: db:prepare" +bundle exec rails db:prepare + +echo "koopa-decidim: exec $*" +exec "$@" diff --git a/configs/decidim/theme/hacktivism-consent.js b/configs/decidim/theme/hacktivism-consent.js new file mode 100644 index 0000000..796f5d7 --- /dev/null +++ b/configs/decidim/theme/hacktivism-consent.js @@ -0,0 +1,10 @@ +/* Essential-only consent; banner hidden via CSS. */ +(function () { + var name = "decidim-consent"; + if (document.cookie.indexOf(name + "=") !== -1) return; + document.cookie = + name + + "=" + + encodeURIComponent(JSON.stringify({ essential: true })) + + "; path=/; max-age=31536000; SameSite=Lax"; +})(); diff --git a/configs/decidim/theme/hacktivism.css b/configs/decidim/theme/hacktivism.css new file mode 100644 index 0000000..f38e96d --- /dev/null +++ b/configs/decidim/theme/hacktivism.css @@ -0,0 +1,98 @@ +/* hacktivism — same palette as bonfire/git/lemmy (exchange-dark) */ +:root, +html { + --primary: #e8a838; + --primary-rgb: 232, 168, 56; + --secondary: #3ecfbf; + --secondary-rgb: 62, 207, 191; + --tertiary: #3d3128; + --success: #16a34a; + --warning: #f0d090; + --alert: #e7131a; + --highlight: #e8a838; +} + +html, +body { + background-color: #1a1410 !important; + background-image: + radial-gradient(ellipse 90% 55% at 50% 108%, rgba(26, 107, 110, 0.35) 0%, transparent 55%), + radial-gradient(circle 420px at 12% 18%, rgba(232, 168, 56, 0.22) 0%, transparent 62%), + linear-gradient(165deg, #2c1e14 0%, #1a1410 38%, #12181a 72%, #0e1c1e 100%) !important; + background-attachment: fixed !important; + color: #fff6e8 !important; +} + +a { + color: #e8a838 !important; +} +a:hover { + color: #f0d090 !important; +} + +.title-bar, +.main-bar, +.navbar, +.topbar, +header.header, +.main-nav, +.footer, +.mini-footer, +.off-canvas, +.sidebar { + background-color: #14110e !important; + color: #fff6e8 !important; + border-color: #2a2018 !important; +} + +.card, +.card__content, +.callout, +.flash, +.dialog, +.reveal, +.accordion-item, +.comment-thread, +.process-header, +.hero, +.home-section { + background-color: #221c16 !important; + color: #fff6e8 !important; + border-color: #2a2018 !important; +} + +.button, +.button.primary, +.button--sc, +input[type="submit"].button { + background-color: #e8a838 !important; + border-color: #e8a838 !important; + color: #1a1410 !important; +} + +.button.secondary { + background-color: #3d3128 !important; + border-color: #3d3128 !important; + color: #fff6e8 !important; +} + +h1, h2, h3, h4, h5, h6, +.title-bar__title, +.card__title { + color: #f0c86a !important; +} + +input, +textarea, +select, +.input-group-field { + background-color: #221c16 !important; + color: #fff6e8 !important; + border-color: #3d3128 !important; +} + +/* No third-party trackers; hide stock GDPR banner (privacy by default). */ +#dc-dialog-wrapper, +.cookies__container { + display: none !important; +} diff --git a/configs/decidim/theme/hacktivism.rb b/configs/decidim/theme/hacktivism.rb new file mode 100644 index 0000000..762e5d8 --- /dev/null +++ b/configs/decidim/theme/hacktivism.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +# hacktivism theme + host allowlist for decidim.hacktivism.ch + +Rails.application.config.hosts << "decidim.hacktivism.ch" +Rails.application.config.hosts << "127.0.0.1" +Rails.application.config.hosts << "localhost" + +if Rails.application.config.respond_to?(:assume_ssl=) + Rails.application.config.assume_ssl = true +end + +Decidim.configure do |config| + config.enable_html_header_snippets = true if config.respond_to?(:enable_html_header_snippets=) +end diff --git a/configs/ports.md b/configs/ports.md index f8bdc8d..872d09c 100644 --- a/configs/ports.md +++ b/configs/ports.md @@ -18,12 +18,13 @@ | **9024** | podman **`koopa-forgejo`** (HTTP) → Caddy **`git.hacktivism.ch`** | | **9025** | podman **`koopa-paivana`** (paivana-httpd) → Caddy **`paivana.hacktivism.ch`** | | **9026** | podman **`koopa-lemmy-proxy`** → Caddy **`lemmy.hacktivism.ch`** | +| **9027** | podman **`koopa-decidim`** → Caddy **`decidim.hacktivism.ch`** | | **9200** | podman **Forgejo git-SSH** (host-direct; not Caddy) | | 9090–9092 | podman `koopa-tops-ng1` … `ng3` → Caddy `tops.ng{1,2,3}.hacktivism.ch` | | 8080 | Tor ORPort | VeciGate: WAN **80→9000**, WAN **443→9001**. -Public apps: Caddy vhosts on **9001** → 127.0.0.1:{9010–9015, 9020–9026, 9090–9092}. +Public apps: Caddy vhosts on **9001** → 127.0.0.1:{9010–9015, 9020–9027, 9090–9092}. Git SSH needs separate NAT/firewall **9200/tcp** if exposed to WAN. | **1789** | podman **`koopa-nym`** mixnet (nym.com nym-node) | diff --git a/host/overview/LIVE.md b/host/overview/LIVE.md index 46ee303..11120be 100644 --- a/host/overview/LIVE.md +++ b/host/overview/LIVE.md @@ -36,6 +36,7 @@ ss -lntp | grep -E '90[0-9]{2}|9200|8080|9080|1789' | `koopa-forgejo` (+ postgres) | `forgejo:11-rootless` | **9024**, **9200** | `git.hacktivism.ch` | | **`koopa-paivana`** (+ upstream) | `localhost/koopa-paivana:latest` | **9025** | `paivana.hacktivism.ch` | | `koopa-lemmy-proxy` (+ lemmy, ui, pictrs, db) | `lemmy:0.19.20` | **9026** | `lemmy.hacktivism.ch` | +| `koopa-decidim` (+ postgres, redis, sidekiq) | `decidim/decidim:0.30.8` | **9027** | `decidim.hacktivism.ch` | | **`koopa-tor-relay`** | `localhost/koopa-tor-relay:latest` | **8080**, **9051** (host net) | Tor OR (non-exit) | | **`koopa-nym`** | `localhost/koopa-nym:latest` | **9080**, **1789**, **1790** (+ opt. WG) | Nym mixnet node (nym.com) | | `koopa-tops-ng1` | `nginx` | **9090** | `tops.ng1.hacktivism.ch` | @@ -61,6 +62,7 @@ Managed from **`/home/hernani/ansible-taler-exchange/`** (see `configs/taler-exc | `git.hacktivism.ch` | 9024 (HTTP); git-SSH **9200** host-direct | | `paivana.hacktivism.ch` | 9025 (paivana-httpd GOA paywall) | | `lemmy.hacktivism.ch` | 9026 | +| `decidim.hacktivism.ch` | 9027 | | `tops.ng1.hacktivism.ch` | 9090 | | `tops.ng2.hacktivism.ch` | 9091 | | `tops.ng3.hacktivism.ch` | 9092 | @@ -74,6 +76,7 @@ Config: `/etc/caddy/Caddyfile` (mirror `configs/caddy/Caddyfile`). | `~/ansible-taler-exchange/` | Ansible + scripts for exchange-ansible container | | `~/koopa-castopod/` | Castopod compose | | `~/koopa-lemmy/` | Lemmy compose | +| `~/koopa-decidim/` | Decidim compose | | `~/koopa-bonfire/` | Bonfire compose + gitbot | | `~/koopa-prime/` | Jellyfin + qBittorrent | | `~/koopa-forgejo/` | Forgejo rootless | @@ -90,6 +93,8 @@ Config: `/etc/caddy/Caddyfile` (mirror `configs/caddy/Caddyfile`). | Taler merchant/bank | root `start_base_services_*` → service user → `/usr/local/bin/start_*.sh` | | Exchange (GOA) | `~/ansible-taler-exchange/run-container-koopa.sh` then `deploy-hacktivism-goa.sh` | | User apps | `cd ~/koopa-* && podman-compose up -d` | +| Lemmy | user units `container-koopa-lemmy-db` + `container-koopa-lemmy` (linger) | +| Decidim | user units `container-koopa-decidim-db` + `container-koopa-decidim` (linger) | | tops (ng1–ng3) | user unit `container-koopa-tops.service` (linger) | ## Config mirrors in this repo (`configs/`) @@ -111,3 +116,4 @@ Config: `/etc/caddy/Caddyfile` (mirror `configs/caddy/Caddyfile`). | Forgejo | `2026/2026-07-10--forgejo-rootless.md` | | Bonfire public feeds | `configs/bonfire/public-feeds.md` | | Castopod content | `2026/2026-07-09--castopod-content.md` | +| Decidim | `2026/2026-08-17--decidim.md` · `configs/decidim/` | diff --git a/host/overview/services.md b/host/overview/services.md index 0c9daf2..6c72548 100644 --- a/host/overview/services.md +++ b/host/overview/services.md @@ -37,6 +37,7 @@ flowchart TB Forgejo["koopa-forgejo ROOTLESS\n:9024 HTTP / :9200 SSH"] Paivana["koopa-paivana\n:9025"] Lemmy["koopa-lemmy-proxy\n:9026"] + Decidim["koopa-decidim\n:9027"] end Tor["tor ORPort :8080"] @@ -63,6 +64,7 @@ flowchart TB CaddyHTTPS -->|git.hacktivism.ch| Forgejo CaddyHTTPS -->|paivana.hacktivism.ch| Paivana CaddyHTTPS -->|lemmy.hacktivism.ch| Lemmy + CaddyHTTPS -->|decidim.hacktivism.ch| Decidim ``` ## Port table @@ -84,6 +86,7 @@ flowchart TB | **9024** | podman | **forgejo** HTTP → `git.hacktivism.ch` | | **9025** | podman | **paivana** → `paivana.hacktivism.ch` | | **9026** | podman | **lemmy** → `lemmy.hacktivism.ch` | +| **9027** | podman | **decidim** → `decidim.hacktivism.ch` | | **9200** | podman | **forgejo** git-SSH (host-direct) | | 9090–9092 | podman | **tops** `koopa-tops-ng1`…`ng3` | | 8080 | tor | ORPort | diff --git a/scripts/decidim/apply-branding.sh b/scripts/decidim/apply-branding.sh new file mode 100755 index 0000000..6327fe3 --- /dev/null +++ b/scripts/decidim/apply-branding.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Apply hacktivism colors + header CSS on the live organization. +set -euo pipefail +cd "${HOME}/koopa-decidim" +set -a +# shellcheck disable=SC1091 +source .env +# shellcheck disable=SC1091 +source users.env +set +a +podman exec \ + -e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \ + -e DECIDIM_SYSTEM_EMAIL \ + -e DECIDIM_SYSTEM_PASSWORD \ + -e DECIDIM_ADMIN_EMAIL \ + -e DECIDIM_ADMIN_PASSWORD \ + -e DECIDIM_ADMIN_NAME \ + -e DECIDIM_APPLICATION_NAME \ + -e DECIDIM_DEFAULT_LOCALE \ + -e DECIDIM_AVAILABLE_LOCALES \ + koopa-decidim bundle exec rails runner /code/lib/koopa_bootstrap.rb +echo "branding applied. public: https://decidim.hacktivism.ch/" diff --git a/scripts/decidim/install-systemd.sh b/scripts/decidim/install-systemd.sh new file mode 100755 index 0000000..01934d3 --- /dev/null +++ b/scripts/decidim/install-systemd.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Install Decidim user systemd units on koopa (boot via linger). +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +LIVE="${HOME}/koopa-decidim" +# admin-log tree (scripts/decidim → ../..) or live ~/koopa-decidim/bin +if [ -f "${HERE}/../../configs/decidim/container-koopa-decidim.service" ]; then + UNIT_SRC="${HERE}/../../configs/decidim" +elif [ -f "${LIVE}/container-koopa-decidim.service" ]; then + UNIT_SRC="${LIVE}" +else + echo "ERROR: decidim unit files not found" >&2 + exit 1 +fi +UNIT_DIR="${HOME}/.config/systemd/user" + +mkdir -p "${UNIT_DIR}" +cp "${UNIT_SRC}/container-koopa-decidim-db.service" "${UNIT_DIR}/" +cp "${UNIT_SRC}/container-koopa-decidim.service" "${UNIT_DIR}/" + +systemctl --user daemon-reload +systemctl --user enable \ + container-koopa-decidim-db.service \ + container-koopa-decidim.service + +if ! podman ps --format '{{.Names}}' | grep -qx koopa-decidim; then + (cd "${HOME}/koopa-decidim" && set -a && source .env && set +a && podman-compose up -d) +fi + +systemctl --user start \ + container-koopa-decidim-db.service \ + container-koopa-decidim.service + +systemctl --user --no-pager --lines=0 status \ + container-koopa-decidim-db.service \ + container-koopa-decidim.service || true + +if command -v loginctl >/dev/null 2>&1; then + if [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)" != "yes" ]; then + echo "NOTE: enable linger so units start after reboot without login:" + echo " sudo loginctl enable-linger $(id -un)" + else + echo "linger already enabled for $(id -un)" + fi +fi + +echo "OK. Public: https://decidim.hacktivism.ch/" diff --git a/scripts/decidim/up.sh b/scripts/decidim/up.sh new file mode 100755 index 0000000..21a6a9d --- /dev/null +++ b/scripts/decidim/up.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail +DIR="${HOME}/koopa-decidim" +cd "${DIR}" +set -a +# shellcheck disable=SC1091 +source .env +set +a +podman-compose up -d +echo "== wait for :9027 ==" +ok=0 +for i in $(seq 1 80); do + code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:9027/" || true) + echo "try $i: $code" + case "$code" in + 2*|3*) ok=1; break ;; + esac + sleep 5 +done +[[ "$ok" -eq 1 ]] || { echo "ERROR: no answer on 9027"; podman-compose ps; exit 1; } +echo "local ok. public: https://decidim.hacktivism.ch/" diff --git a/scripts/lemmy/install-systemd.sh b/scripts/lemmy/install-systemd.sh index 626dc3b..f064f9f 100755 --- a/scripts/lemmy/install-systemd.sh +++ b/scripts/lemmy/install-systemd.sh @@ -1,12 +1,21 @@ #!/usr/bin/env bash # Install Lemmy user systemd units on koopa (boot via linger). set -euo pipefail -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +HERE="$(cd "$(dirname "$0")" && pwd)" +LIVE="${HOME}/koopa-lemmy" +if [ -f "${HERE}/../../configs/lemmy/container-koopa-lemmy.service" ]; then + UNIT_SRC="${HERE}/../../configs/lemmy" +elif [ -f "${LIVE}/container-koopa-lemmy.service" ]; then + UNIT_SRC="${LIVE}" +else + echo "ERROR: lemmy unit files not found" >&2 + exit 1 +fi UNIT_DIR="${HOME}/.config/systemd/user" mkdir -p "${UNIT_DIR}" -cp "${ROOT}/configs/lemmy/container-koopa-lemmy-db.service" "${UNIT_DIR}/" -cp "${ROOT}/configs/lemmy/container-koopa-lemmy.service" "${UNIT_DIR}/" +cp "${UNIT_SRC}/container-koopa-lemmy-db.service" "${UNIT_DIR}/" +cp "${UNIT_SRC}/container-koopa-lemmy.service" "${UNIT_DIR}/" systemctl --user daemon-reload systemctl --user enable \ @@ -25,4 +34,13 @@ systemctl --user --no-pager --lines=0 status \ container-koopa-lemmy-db.service \ container-koopa-lemmy.service || true +if command -v loginctl >/dev/null 2>&1; then + if [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)" != "yes" ]; then + echo "NOTE: enable linger so units start after reboot without login:" + echo " sudo loginctl enable-linger $(id -un)" + else + echo "linger already enabled for $(id -un)" + fi +fi + echo "OK. Public: https://lemmy.hacktivism.ch/" diff --git a/scripts/login-health/README.md b/scripts/login-health/README.md index 04b1a6b..0003217 100644 --- a/scripts/login-health/README.md +++ b/scripts/login-health/README.md @@ -65,7 +65,9 @@ overall: OK ok=42 warn=0 fail=0 | Git SSH | TCP `:9200` | **Critical** (→ overall FAIL): merchant, exchange, bank, forgejo, edge/critical ports. -**Optional** (→ WARN): castopod, bonfire, lemmy, prime, bt, paivana, tops, tor, nym. +**Optional** (→ WARN): castopod, bonfire, lemmy, decidim, prime, bt, paivana, tops, tor, nym. + +Collect also checks **linger** and that Lemmy/Decidim user units are **enabled** (reboot survival). Read-only. No restarts, no secrets. diff --git a/scripts/login-health/koopa-login-health.sh b/scripts/login-health/koopa-login-health.sh index aa9c2db..309a782 100755 --- a/scripts/login-health/koopa-login-health.sh +++ b/scripts/login-health/koopa-login-health.sh @@ -151,6 +151,7 @@ EXPECT_CONTAINERS=( koopa-castopod koopa-bonfire koopa-lemmy-proxy + koopa-decidim koopa-prime-jellyfin koopa-prime-qbittorrent koopa-forgejo @@ -173,6 +174,7 @@ OPTIONAL_CONTAINERS=( koopa-castopod koopa-bonfire koopa-lemmy-proxy + koopa-decidim koopa-paivana ) @@ -185,6 +187,7 @@ PROBES=( "castopod|http://127.0.0.1:9020/|200,302,303,307,308" "bonfire|http://127.0.0.1:9021/|200,302,303,307,308" "lemmy|http://127.0.0.1:9026/|200,302,303,307,308" + "decidim|http://127.0.0.1:9027/|200,301,302,303,307,308" "prime|http://127.0.0.1:9022/|200,302,303,307,308" "bt|http://127.0.0.1:9023/|200,302,303,307,308,401" "paivana|http://127.0.0.1:9025/|200,302,303,307,308" @@ -193,7 +196,7 @@ PROBES=( "tops-ng3|http://127.0.0.1:9092/|200,302,303" ) -OPTIONAL_PROBES=(castopod bonfire lemmy prime bt paivana tops-ng1 tops-ng2 tops-ng3) +OPTIONAL_PROBES=(castopod bonfire lemmy decidim prime bt paivana tops-ng1 tops-ng2 tops-ng3) C_OK=; C_WARN=; C_FAIL=; C_DIM=; C_RST=; C_BOLD= # colors only when writing for a tty later; snapshots are plain + optional ANSI if COLLECT_COLOR=1 @@ -277,6 +280,34 @@ do_collect() { record WARN "systemctl not available" fi + # Reboot survival: linger + enabled user units (public stacks). + if command -v loginctl >/dev/null 2>&1; then + linger_st=$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no) + if [ "${linger_st}" = "yes" ]; then + record OK "linger=yes ($(id -un))" + else + record FAIL "linger=${linger_st} (user units will not start after reboot without login)" + fi + else + record WARN "loginctl not available (cannot check linger)" + fi + if command -v systemctl >/dev/null 2>&1 && [ "$(id -u 2>/dev/null)" != "0" ]; then + local u en + for u in \ + container-koopa-lemmy-db.service \ + container-koopa-lemmy.service \ + container-koopa-decidim-db.service \ + container-koopa-decidim.service + do + en=$(systemctl --user is-enabled "${u}" 2>/dev/null || echo unknown) + if [ "${en}" = "enabled" ]; then + record OK "unit ${u} enabled" + else + record WARN "unit ${u} is ${en} (will not start on reboot)" + fi + done + fi + if command -v ss >/dev/null 2>&1; then listen_blob=$(ss -lnt 2>/dev/null | awk '{print $4}' || true) for port in 9000 9001 9010 9011 9012 9024 9200; do @@ -286,7 +317,7 @@ do_collect() { record FAIL "nothing listening on :${port}" fi done - for port in 9020 9021 9022 9023 9025 9026 9090 9091 9092 8080 9080; do + for port in 9020 9021 9022 9023 9025 9026 9027 9090 9091 9092 8080 9080; do if printf '%s\n' "${listen_blob}" | grep -qE "[:.]${port}\$"; then record OK "listen :${port} (optional)" else @@ -323,7 +354,7 @@ do_collect() { fi fi done - for dep in koopa-forgejo-db koopa-castopod-mariadb koopa-castopod-redis koopa-bonfire-db koopa-lemmy-db; do + for dep in koopa-forgejo-db koopa-castopod-mariadb koopa-castopod-redis koopa-bonfire-db koopa-lemmy-db koopa-decidim-db koopa-decidim-redis; do if printf '%s\n' "${running_names}" | grep -qxF "${dep}"; then record INFO "dep ${dep} running" else From 03e5c0d3ade14e24a8e2f323939815148c39ee13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Mon, 17 Aug 2026 15:38:43 +0200 Subject: [PATCH 2/4] decidim: public example process + isoc participant seed --- 2026/2026-08-17--decidim.md | 8 ++ configs/decidim/.env.example | 4 + configs/decidim/README.md | 3 + configs/decidim/compose.yml | 1 + configs/decidim/seed_example.rb | 139 ++++++++++++++++++++++++++++++++ scripts/decidim/seed-example.sh | 31 +++++++ 6 files changed, 186 insertions(+) create mode 100644 configs/decidim/seed_example.rb create mode 100755 scripts/decidim/seed-example.sh diff --git a/2026/2026-08-17--decidim.md b/2026/2026-08-17--decidim.md index 688c4e3..3e5bac5 100644 --- a/2026/2026-08-17--decidim.md +++ b/2026/2026-08-17--decidim.md @@ -31,3 +31,11 @@ Stock Decidim still emits `#dc-dialog-wrapper` (Accept all / essential / Setting | listen | `:9027` `:9026` | login-health: probes + containers + linger + these units enabled. Loopback HTTP on :9027 is **301** (app wants HTTPS; TLS is Caddy). Probe allows 301. After reboot: login-health only unless asked. + +## Public example + isoc + +- Process **example** (published, not private): `https://decidim.hacktivism.ch/processes/example` +- One published meeting in that process (readable without login). +- Homepage block `highlighted_processes` published. +- Participant `isoc` on the host (`users.env`); password not in this repo. +- Seed: `scripts/decidim/seed-example.sh` / `configs/decidim/seed_example.rb`. diff --git a/configs/decidim/.env.example b/configs/decidim/.env.example index 303db55..9fe9974 100644 --- a/configs/decidim/.env.example +++ b/configs/decidim/.env.example @@ -11,3 +11,7 @@ SMTP_USERNAME=unused SMTP_PASSWORD=unused SMTP_PORT=25 QUEUE_ADAPTER=sidekiq +DECIDIM_ISOC_EMAIL=isoc@hacktivism.ch +DECIDIM_ISOC_PASSWORD=@inline-secret@ +DECIDIM_ISOC_NAME=isoc +DECIDIM_ISOC_NICKNAME=isoc diff --git a/configs/decidim/README.md b/configs/decidim/README.md index c0433f4..76c951e 100644 --- a/configs/decidim/README.md +++ b/configs/decidim/README.md @@ -18,8 +18,11 @@ Privacy: no third-party trackers. Stock GDPR banner is hidden (`#dc-dialog-wrapp cd ~/koopa-decidim && set -a && source .env && set +a && podman-compose up -d scripts/decidim/install-systemd.sh scripts/decidim/apply-branding.sh +scripts/decidim/seed-example.sh # public process /processes/example + optional isoc user ``` +Public example (no account): `https://decidim.hacktivism.ch/processes/example` + Caddy: `sudo /home/hernani/koopa-caddy/apply.sh` Daylog: `2026/2026-08-17--decidim.md`. diff --git a/configs/decidim/compose.yml b/configs/decidim/compose.yml index 282e1b3..5d25d65 100644 --- a/configs/decidim/compose.yml +++ b/configs/decidim/compose.yml @@ -44,6 +44,7 @@ services: - ./theme/hacktivism.css:/code/public/hacktivism.css:ro - ./theme/hacktivism-consent.js:/code/public/hacktivism-consent.js:ro - ./bootstrap.rb:/code/lib/koopa_bootstrap.rb:ro + - ./seed_example.rb:/code/lib/koopa_seed_example.rb:ro - ./assets/img/logo.svg:/code/public/hacktivism-logo.svg:ro - ./assets/img/favicon.svg:/code/public/hacktivism-favicon.svg:ro - decidim-storage:/code/storage diff --git a/configs/decidim/seed_example.rb b/configs/decidim/seed_example.rb new file mode 100644 index 0000000..ada85af --- /dev/null +++ b/configs/decidim/seed_example.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true +# Public example process + meeting (readable without an account). +# Optional participant from env: DECIDIM_ISOC_EMAIL / DECIDIM_ISOC_PASSWORD. +# Usage: bundle exec rails runner /code/lib/koopa_seed_example.rb + +def t(en, de, fr) + { "en" => en, "de" => de, "fr" => fr } +end + +def html(en, de, fr) + t("

#{en}

", "

#{de}

", "

#{fr}

") +end + +def fail_model!(record) + raise "#{record.class}: #{record.errors.full_messages.join("; ")}" +end + +host = ENV.fetch("DECIDIM_HOST", "decidim.hacktivism.ch") +org = Decidim::Organization.find_by!(host: host) +admin = Decidim::User.find_by!(email: ENV.fetch("DECIDIM_ADMIN_EMAIL"), organization: org) + +org.update!(force_users_to_authenticate_before_access_organization: false) + +slug = "example" +process = Decidim::ParticipatoryProcess.find_or_initialize_by(organization: org, slug: slug) +process.assign_attributes( + title: t("Example activity", "Beispielaktivität", "Activité exemple"), + subtitle: t( + "Public demo — no account needed to read", + "Öffentliches Beispiel — Lesen ohne Konto", + "Démo publique — lecture sans compte" + ), + short_description: html( + "A published participatory process so visitors can see Decidim without signing in.", + "Ein veröffentlichter Beteiligungsprozess, den Besucherinnen ohne Anmeldung lesen können.", + "Un processus participatif publié, lisible sans compte." + ), + description: html( + "This is a small public example on hacktivism. Meetings and the process page are visible to anyone. Signing in is only needed to take part.", + "Kleines öffentliches Beispiel auf hacktivism. Prozessseite und Treffen sind für alle sichtbar. Anmelden nur zum Mitmachen.", + "Petit exemple public sur hacktivism. La page du processus et la rencontre sont visibles sans compte. La connexion sert uniquement à participer." + ), + start_date: Date.current, + end_date: Date.current + 180, + published_at: process.published_at || Time.current, + private_space: false, + promoted: true, + scopes_enabled: false, + weight: 1 +) +process.save || fail_model!(process) +puts "process id=#{process.id} slug=#{process.slug} published=#{process.published_at}" + +step = process.steps.find_or_initialize_by(position: 1) +step.assign_attributes( + title: t("Open", "Offen", "Ouvert"), + description: html("Example step.", "Beispielphase.", "Étape exemple."), + start_date: Date.current, + end_date: Date.current + 180, + active: true +) +step.save || fail_model!(step) +puts "step id=#{step.id} active=#{step.active}" + +meetings = Decidim::Component.find_or_initialize_by( + participatory_space: process, + manifest_name: "meetings" +) +meetings.assign_attributes( + name: t("Meetings", "Treffen", "Rencontres"), + published_at: meetings.published_at || Time.current, + visible: true, + weight: 1 +) +meetings.save || fail_model!(meetings) +puts "meetings component id=#{meetings.id} published=#{meetings.published_at}" + +start_at = Time.current.utc.change(hour: 18, min: 0) + 7.days +finish_at = start_at + 2.hours +meeting = Decidim::Meetings::Meeting.find_or_initialize_by( + component: meetings, + author: admin +) +if meeting.new_record? || meeting.title.blank? + meeting.title = t("Open example meeting", "Offenes Beispieltreffen", "Rencontre exemple ouverte") +end +meeting.assign_attributes( + description: html( + "Anyone can open this meeting page without an account. It is a demo activity on decidim.hacktivism.ch.", + "Diese Treffen-Seite ist ohne Konto lesbar. Beispielaktivität auf decidim.hacktivism.ch.", + "Cette page de rencontre est lisible sans compte. Activité exemple sur decidim.hacktivism.ch." + ), + start_time: meeting.start_time || start_at, + end_time: meeting.end_time || finish_at, + address: "hacktivism", + location: t("Online / public", "Online / öffentlich", "En ligne / public"), + location_hints: t("No registration required to read.", "Zum Lesen keine Anmeldung.", "Pas d'inscription pour lire."), + type_of_meeting: "in_person", + registration_type: "registration_disabled", + registrations_enabled: false, + private_meeting: false, + transparent: true, + published_at: meeting.published_at || Time.current, + comments_enabled: true +) +meeting.save || fail_model!(meeting) +puts "meeting id=#{meeting.id} published=#{meeting.published_at} start=#{meeting.start_time}" + +block = Decidim::ContentBlock.find_or_initialize_by( + organization: org, + scope_name: "homepage", + manifest_name: "highlighted_processes" +) +block.weight ||= 45 +block.published_at ||= Time.current +block.save || fail_model!(block) +puts "homepage highlighted_processes published=#{block.published_at}" + +isoc_email = ENV["DECIDIM_ISOC_EMAIL"].to_s.strip +isoc_password = ENV["DECIDIM_ISOC_PASSWORD"].to_s +if isoc_email.empty? || isoc_password.empty? + puts "isoc user skipped (DECIDIM_ISOC_EMAIL / DECIDIM_ISOC_PASSWORD unset)" +else + isoc = Decidim::User.find_or_initialize_by(email: isoc_email, organization: org) + isoc.name = ENV.fetch("DECIDIM_ISOC_NAME", "isoc") + isoc.nickname = ENV.fetch("DECIDIM_ISOC_NICKNAME", "isoc") + isoc.password = isoc_password + isoc.password_confirmation = isoc_password + isoc.admin = false + isoc.confirmed_at ||= Time.current + isoc.tos_agreement = true + isoc.accepted_tos_version = org.tos_version + isoc.locale = org.default_locale + isoc.save || fail_model!(isoc) + puts "isoc user id=#{isoc.id} email=#{isoc.email} nick=#{isoc.nickname} admin=#{isoc.admin} confirmed=#{isoc.confirmed_at}" +end + +puts "public process: https://#{host}/processes/#{process.slug}" +puts "public meeting: https://#{host}/processes/#{process.slug}/f/#{meetings.id}/meetings/#{meeting.id}" diff --git a/scripts/decidim/seed-example.sh b/scripts/decidim/seed-example.sh new file mode 100755 index 0000000..89dbd05 --- /dev/null +++ b/scripts/decidim/seed-example.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Publish public example process/meeting + optional isoc user from users.env. +set -euo pipefail +cd "${HOME}/koopa-decidim" +set -a +# shellcheck disable=SC1091 +source .env +# shellcheck disable=SC1091 +source users.env +set +a + +src="" +if [ -f "${HOME}/src/koopa/koopa-admin-log/configs/decidim/seed_example.rb" ]; then + src="${HOME}/src/koopa/koopa-admin-log/configs/decidim/seed_example.rb" +elif [ -f ./seed_example.rb ]; then + src="./seed_example.rb" +else + echo "ERROR: seed_example.rb not found" >&2 + exit 1 +fi +podman cp "${src}" koopa-decidim:/code/lib/koopa_seed_example.rb + +podman exec \ + -e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \ + -e DECIDIM_ADMIN_EMAIL \ + -e DECIDIM_ISOC_EMAIL \ + -e DECIDIM_ISOC_PASSWORD \ + -e DECIDIM_ISOC_NAME \ + -e DECIDIM_ISOC_NICKNAME \ + koopa-decidim bundle exec rails runner /code/lib/koopa_seed_example.rb +echo "seed done. public: https://decidim.hacktivism.ch/processes/example" From 6a99f7d6bb1b5fba1007dfbddfce225a60d4fb0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Mon, 17 Aug 2026 15:55:23 +0200 Subject: [PATCH 3/4] decidim: public-commons process text + isoc avatar hook --- 2026/2026-08-17--decidim.md | 4 +- configs/decidim/seed_example.rb | 166 ++++++++++++++++++++++++++------ scripts/decidim/seed-example.sh | 8 ++ 3 files changed, 148 insertions(+), 30 deletions(-) diff --git a/2026/2026-08-17--decidim.md b/2026/2026-08-17--decidim.md index 3e5bac5..f2481c1 100644 --- a/2026/2026-08-17--decidim.md +++ b/2026/2026-08-17--decidim.md @@ -35,7 +35,9 @@ login-health: probes + containers + linger + these units enabled. Loopback HTTP ## Public example + isoc - Process **example** (published, not private): `https://decidim.hacktivism.ch/processes/example` -- One published meeting in that process (readable without login). + — title **Öffentliche digitale Allmende** (public-by-default reading). +- Published meeting **Offene Runde: Lesen ohne Konto** + one public proposal + background page. - Homepage block `highlighted_processes` published. - Participant `isoc` on the host (`users.env`); password not in this repo. +- Avatar: gold ISOC globe (same crop as Bonfire `@isoc`); host file `~/koopa-decidim/assets/img/isoc-gold.png`, not in this repo. - Seed: `scripts/decidim/seed-example.sh` / `configs/decidim/seed_example.rb`. diff --git a/configs/decidim/seed_example.rb b/configs/decidim/seed_example.rb index ada85af..e51525c 100644 --- a/configs/decidim/seed_example.rb +++ b/configs/decidim/seed_example.rb @@ -1,16 +1,13 @@ # frozen_string_literal: true -# Public example process + meeting (readable without an account). +# Public process + meeting + page + proposal (readable without an account). # Optional participant from env: DECIDIM_ISOC_EMAIL / DECIDIM_ISOC_PASSWORD. +# Optional avatar: DECIDIM_ISOC_AVATAR (PNG path inside the container). # Usage: bundle exec rails runner /code/lib/koopa_seed_example.rb def t(en, de, fr) { "en" => en, "de" => de, "fr" => fr } end -def html(en, de, fr) - t("

#{en}

", "

#{de}

", "

#{fr}

") -end - def fail_model!(record) raise "#{record.class}: #{record.errors.full_messages.join("; ")}" end @@ -24,21 +21,40 @@ org.update!(force_users_to_authenticate_before_access_organization: false) slug = "example" process = Decidim::ParticipatoryProcess.find_or_initialize_by(organization: org, slug: slug) process.assign_attributes( - title: t("Example activity", "Beispielaktivität", "Activité exemple"), + title: t( + "Public digital commons", + "Öffentliche digitale Allmende", + "Communs numériques publics" + ), subtitle: t( - "Public demo — no account needed to read", - "Öffentliches Beispiel — Lesen ohne Konto", - "Démo publique — lecture sans compte" + "Which self-hosted services stay readable without an account?", + "Welche selbst gehosteten Dienste bleiben ohne Konto lesbar?", + "Quels services auto-hébergés restent lisibles sans compte ?" ), - short_description: html( - "A published participatory process so visitors can see Decidim without signing in.", - "Ein veröffentlichter Beteiligungsprozess, den Besucherinnen ohne Anmeldung lesen können.", - "Un processus participatif publié, lisible sans compte." + short_description: t( + "

hacktivism.ch already runs Lemmy, Bonfire, Castopod and Decidim. This process asks what must stay readable without a login — and what may require an account to write.

", + "

hacktivism.ch betreibt bereits Lemmy, Bonfire, Castopod und Decidim. Dieser Prozess fragt, was ohne Anmeldung lesbar bleiben muss — und wofür ein Konto zum Schreiben reicht.

", + "

hacktivism.ch fait déjà tourner Lemmy, Bonfire, Castopod et Decidim. Ce processus demande ce qui doit rester lisible sans compte — et ce qui peut exiger un compte pour écrire.

" ), - description: html( - "This is a small public example on hacktivism. Meetings and the process page are visible to anyone. Signing in is only needed to take part.", - "Kleines öffentliches Beispiel auf hacktivism. Prozessseite und Treffen sind für alle sichtbar. Anmelden nur zum Mitmachen.", - "Petit exemple public sur hacktivism. La page du processus et la rencontre sont visibles sans compte. La connexion sert uniquement à participer." + description: t( + "

Reading civic material should not depend on creating an account. Writing (proposals, comments, votes) can.

" \ + "

Concrete questions:

    " \ + "
  • Process pages, meetings and last activities stay public.
  • " \ + "
  • No third-party trackers; cookies only what the software needs.
  • " \ + "
  • Which other stacks on hacktivism.ch (Lemmy feeds, Castopod episodes, Bonfire public posts) follow the same rule.
  • " \ + "

Anyone can read this process. Sign in only to take part.

", + "

Öffentliches Beteiligungsmaterial soll ohne Konto lesbar sein. Schreiben (Vorschläge, Kommentare, Stimmen) kann ein Konto brauchen.

" \ + "

Konkrete Fragen:

    " \ + "
  • Prozessseiten, Treffen und letzte Aktivitäten bleiben öffentlich.
  • " \ + "
  • Keine Drittanbieter-Tracker; Cookies nur, was die Software braucht.
  • " \ + "
  • Welche anderen Dienste auf hacktivism.ch (Lemmy, Castopod, Bonfire) derselben Regel folgen.
  • " \ + "

Lesen ohne Anmeldung. Anmelden nur zum Mitmachen.

", + "

Lire du matériel civique ne doit pas exiger un compte. Écrire (propositions, commentaires, votes) peut le faire.

" \ + "

Questions concrètes :

    " \ + "
  • Pages de processus, rencontres et dernières activités restent publiques.
  • " \ + "
  • Pas de traqueurs tiers ; cookies uniquement pour le logiciel.
  • " \ + "
  • Quels autres services sur hacktivism.ch (Lemmy, Castopod, Bonfire) suivent la même règle.
  • " \ + "

Lecture sans compte. Connexion seulement pour participer.

" ), start_date: Date.current, end_date: Date.current + 180, @@ -53,8 +69,12 @@ puts "process id=#{process.id} slug=#{process.slug} published=#{process.publishe step = process.steps.find_or_initialize_by(position: 1) step.assign_attributes( - title: t("Open", "Offen", "Ouvert"), - description: html("Example step.", "Beispielphase.", "Étape exemple."), + title: t("Open discussion", "Offene Diskussion", "Discussion ouverte"), + description: t( + "

Collect positions on public-by-default reading. No decision this step.

", + "

Positionen zum Lesen ohne Konto sammeln. In dieser Phase keine Entscheidung.

", + "

Recueillir les positions sur la lecture sans compte. Pas de décision à cette étape.

" + ), start_date: Date.current, end_date: Date.current + 180, active: true @@ -62,6 +82,32 @@ step.assign_attributes( step.save || fail_model!(step) puts "step id=#{step.id} active=#{step.active}" +pages = Decidim::Component.find_or_initialize_by( + participatory_space: process, + manifest_name: "pages" +) +pages.assign_attributes( + name: t("Background", "Hintergrund", "Contexte"), + published_at: pages.published_at || Time.current, + visible: true, + weight: 0 +) +pages.save || fail_model!(pages) +page = Decidim::Pages::Page.find_or_initialize_by(component: pages) +page.body = t( + "

ISOC-style public-interest internet: people should inspect how a civic tool works without first handing over an identity.

" \ + "

On this instance the stock cookie banner is hidden; only essential consent is set. The same idea applies to participation spaces: the text of a process is not a privilege of registered users.

" \ + "

This page is part of that argument. It is published on purpose.

", + "

Internet im öffentlichen Interesse: man soll sehen können, wie ein Beteiligungswerkzeug arbeitet, ohne zuerst eine Identität abzugeben.

" \ + "

Auf dieser Instanz ist der Vorrats-Cookie-Banner ausgeblendet; es gilt nur essential. Dieselbe Idee gilt für Beteiligungsräume: der Text eines Prozesses ist kein Privileg registrierter Konten.

" \ + "

Diese Seite ist absichtlich öffentlich.

", + "

Internet d'intérêt public : on doit pouvoir voir comment un outil civique fonctionne sans d'abord céder une identité.

" \ + "

Ici, la bannière cookies par défaut est masquée ; seul l'essentiel est posé. Même idée pour les espaces de participation : le texte d'un processus n'est pas un privilège des comptes.

" \ + "

Cette page est publique volontairement.

" +) +page.save || fail_model!(page) +puts "pages component id=#{pages.id} page=#{page.id}" + meetings = Decidim::Component.find_or_initialize_by( participatory_space: process, manifest_name: "meetings" @@ -81,20 +127,38 @@ meeting = Decidim::Meetings::Meeting.find_or_initialize_by( component: meetings, author: admin ) -if meeting.new_record? || meeting.title.blank? - meeting.title = t("Open example meeting", "Offenes Beispieltreffen", "Rencontre exemple ouverte") -end +meeting.title = t( + "Open session: reading without an account", + "Offene Runde: Lesen ohne Konto", + "Séance ouverte : lire sans compte" +) meeting.assign_attributes( - description: html( - "Anyone can open this meeting page without an account. It is a demo activity on decidim.hacktivism.ch.", - "Diese Treffen-Seite ist ohne Konto lesbar. Beispielaktivität auf decidim.hacktivism.ch.", - "Cette page de rencontre est lisible sans compte. Activité exemple sur decidim.hacktivism.ch." + description: t( + "

Public agenda (no registration to read):

    " \ + "
  1. What is already public on hacktivism.ch (Lemmy, Decidim, Castopod).
  2. " \ + "
  3. Privacy by default — no tracker banner as a gate.
  4. " \ + "
  5. Where login is justified (writing, admin).
  6. " \ + "

Online / public. Signing in is optional.

", + "

Öffentliche Traktanden (Lesen ohne Anmeldung):

    " \ + "
  1. Was auf hacktivism.ch schon öffentlich ist (Lemmy, Decidim, Castopod).
  2. " \ + "
  3. Privatsphäre als Vorgabe — kein Tracker-Banner als Tür.
  4. " \ + "
  5. Wo ein Login gerechtfertigt ist (Schreiben, Admin).
  6. " \ + "

Online / öffentlich. Anmelden ist freiwillig.

", + "

Ordre du jour public (lecture sans inscription) :

    " \ + "
  1. Ce qui est déjà public sur hacktivism.ch (Lemmy, Decidim, Castopod).
  2. " \ + "
  3. Vie privée par défaut — pas de bannière traqueur comme porte.
  4. " \ + "
  5. Où un compte se justifie (écriture, admin).
  6. " \ + "

En ligne / public. Connexion facultative.

" ), start_time: meeting.start_time || start_at, end_time: meeting.end_time || finish_at, - address: "hacktivism", - location: t("Online / public", "Online / öffentlich", "En ligne / public"), - location_hints: t("No registration required to read.", "Zum Lesen keine Anmeldung.", "Pas d'inscription pour lire."), + address: "hacktivism.ch", + location: t("Online, public stream notes", "Online, öffentliche Notizen", "En ligne, notes publiques"), + location_hints: t( + "No ticket. Page stays readable if you do not attend.", + "Kein Ticket. Die Seite bleibt lesbar, wenn man nicht teilnimmt.", + "Pas de billet. La page reste lisible sans y assister." + ), type_of_meeting: "in_person", registration_type: "registration_disabled", registrations_enabled: false, @@ -106,6 +170,36 @@ meeting.assign_attributes( meeting.save || fail_model!(meeting) puts "meeting id=#{meeting.id} published=#{meeting.published_at} start=#{meeting.start_time}" +proposals = Decidim::Component.find_or_initialize_by( + participatory_space: process, + manifest_name: "proposals" +) +proposals.assign_attributes( + name: t("Proposals", "Vorschläge", "Propositions"), + published_at: proposals.published_at || Time.current, + visible: true, + weight: 2 +) +proposals.save || fail_model!(proposals) + +proposal = Decidim::Proposals::Proposal.find_or_initialize_by(component: proposals) +if proposal.new_record? || proposal.title.blank? + proposal.title = t( + "Keep public processes readable without login", + "Öffentliche Prozesse ohne Anmeldung lesbar halten", + "Garder les processus publics lisibles sans connexion" + ) + proposal.body = t( + "

Decide as a standing rule: published process pages, meetings and this proposal list stay readable without an account. Login is only for creating or endorsing proposals, commenting, and administration.

", + "

Als Regel festhalten: veröffentlichte Prozessseiten, Treffen und diese Vorschlagsliste bleiben ohne Konto lesbar. Login nur zum Erstellen oder Unterstützen von Vorschlägen, Kommentieren und für die Administration.

", + "

En faire une règle : les pages de processus publiées, les rencontres et cette liste de propositions restent lisibles sans compte. Connexion seulement pour créer ou soutenir, commenter, et pour l'administration.

" + ) + proposal.add_coauthor(admin) + proposal.published_at = Time.current +end +proposal.save || fail_model!(proposal) +puts "proposal id=#{proposal.id} published=#{proposal.published_at}" + block = Decidim::ContentBlock.find_or_initialize_by( organization: org, scope_name: "homepage", @@ -132,8 +226,22 @@ else isoc.accepted_tos_version = org.tos_version isoc.locale = org.default_locale isoc.save || fail_model!(isoc) + avatar_path = ENV["DECIDIM_ISOC_AVATAR"].to_s + if !avatar_path.empty? && File.file?(avatar_path) + isoc.avatar.purge if isoc.avatar.attached? + isoc.avatar.attach( + io: File.open(avatar_path, "rb"), + filename: "isoc-gold.png", + content_type: "image/png" + ) + isoc.save || fail_model!(isoc) + puts "isoc avatar attached from #{avatar_path} blob=#{isoc.avatar.blob&.byte_size}" + else + puts "isoc avatar skipped (DECIDIM_ISOC_AVATAR missing)" + end puts "isoc user id=#{isoc.id} email=#{isoc.email} nick=#{isoc.nickname} admin=#{isoc.admin} confirmed=#{isoc.confirmed_at}" end puts "public process: https://#{host}/processes/#{process.slug}" puts "public meeting: https://#{host}/processes/#{process.slug}/f/#{meetings.id}/meetings/#{meeting.id}" +puts "public proposal: https://#{host}/processes/#{process.slug}/f/#{proposals.id}/proposals/#{proposal.id}" diff --git a/scripts/decidim/seed-example.sh b/scripts/decidim/seed-example.sh index 89dbd05..51f1dba 100755 --- a/scripts/decidim/seed-example.sh +++ b/scripts/decidim/seed-example.sh @@ -20,6 +20,13 @@ else fi podman cp "${src}" koopa-decidim:/code/lib/koopa_seed_example.rb +avatar_host="${DECIDIM_ISOC_AVATAR:-${HOME}/koopa-decidim/assets/img/isoc-gold.png}" +avatar_ctr="" +if [ -f "${avatar_host}" ]; then + avatar_ctr="/tmp/isoc-gold.png" + podman cp "${avatar_host}" "koopa-decidim:${avatar_ctr}" +fi + podman exec \ -e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \ -e DECIDIM_ADMIN_EMAIL \ @@ -27,5 +34,6 @@ podman exec \ -e DECIDIM_ISOC_PASSWORD \ -e DECIDIM_ISOC_NAME \ -e DECIDIM_ISOC_NICKNAME \ + -e DECIDIM_ISOC_AVATAR="${avatar_ctr}" \ koopa-decidim bundle exec rails runner /code/lib/koopa_seed_example.rb echo "seed done. public: https://decidim.hacktivism.ch/processes/example" From 50fc9a35c0e72cb593e624e581a9a9094a9dac71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sat, 5 Sep 2026 03:57:08 +0200 Subject: [PATCH 4/4] koopa: hernani user ollama loopback + host-power sampling --- 2026/2026-09-05--hernani-ollama-user+power.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 2026/2026-09-05--hernani-ollama-user+power.md diff --git a/2026/2026-09-05--hernani-ollama-user+power.md b/2026/2026-09-05--hernani-ollama-user+power.md new file mode 100644 index 0000000..b6a6e22 --- /dev/null +++ b/2026/2026-09-05--hernani-ollama-user+power.md @@ -0,0 +1,16 @@ +# koopa: hernani user Ollama + power sampling (2026-09-05) + +Freigabe: operator — install Ollama under hernani (no root package); loopback only; laptop tunnels. + +## Ollama + +- Binary: `~hernani/.local/bin/ollama` (v0.33.3 from GitHub `ollama-linux-amd64.tar.zst`) +- Unit: `~/.config/systemd/user/ollama.service` · `OLLAMA_HOST=127.0.0.1:11434` · enabled +- Access: SSH as **hernani** from clementine/magikoopa → LocalForward (helper `koopa-ollama-tunnel` → laptop `:11438`) +- Models: small + exotic pulls (see pull log `/tmp/ollama-koopa-pull-20260905.log`) — prefer ≤3B + +## Power / energy + +- Deployed `host-power.py` to `~hernani/.local/bin/host-power` +- Laptop: `remote-power koopa` / `koopa-power` → JSON watts; combo derives Wh +- RAPL `energy_uj` root-only → quality **estimated** until Freigabe for `enable-host-power-access.sh --apply`