Commit Graph
179 Commits
Author SHA1 Message Date
kevinveenbirkenbach 4e602dbb41 Adjust dashboard frame-ancestors token and update CSP unit test 2026-03-16 23:45:00 +01:00
kevinveenbirkenbach 27133e3292 fix: escape backslashes in ruby filter docstring 2026-03-16 11:55:56 +01:00
kevinveenbirkenbach e1784f88bd chore(python): unify shebangs to env python3 2026-02-23 08:47:40 +01:00
kevinveenbirkenbach b878f5d2a0 refactor(systemd): replace get_service_name filter with unit_name lookup plugin
https://chatgpt.com/share/e/699449d6-580c-8013-8bcc-6a8545c6762f
2026-02-17 11:58:25 +01:00
kevinveenbirkenbach 535ff88189 fix(domains): robust recursive flattening in generate_all_domains and remove json workaround
* replace shallow flatten logic with recursive _add helper to correctly handle nested dict/list structures
* prevent unhashable _AnsibleLazyTemplateList from reaching set(flat)
* remove unnecessary to_json/from_json roundtrip in constructor
* add unittest coverage for mixed str/list/dict domain inputs and deduplication behavior

https://chatgpt.com/share/698e598e-3284-800f-9917-ca1eaa4e6572
2026-02-12 23:51:37 +01:00
kevinveenbirkenbach 6efde55647 refactor(dashboard): rename web-app-desktop to web-app-dashboard and update service toggle
* replace compose.services.desktop with compose.services.dashboard across all roles
* rename role web-app-desktop to web-app-dashboard
* rename sys-front-inj-desktop to sys-front-inj-dashboard
* update resolver mapping to include web-app-dashboard when dashboard is enabled
* adjust CSP filters to reference dashboard for frame-ancestors handling
* update lookup plugins, filter plugins, templates, vars and run_once flags
* migrate all tests to dashboard naming and service key
* update role dependencies and port mappings
* remove legacy desktop role and variables

BREAKING CHANGE:
The service key compose.services.desktop has been renamed to compose.services.dashboard.
All custom configs, inventories, or overrides must be updated accordingly.

https://chatgpt.com/share/698da7ad-1978-800f-82b4-4755b17be026
2026-02-12 11:12:49 +01:00
kevinveenbirkenbach 7a3c44b013 refactor(invokable): derive deployment types from invokable roles via DEFAULT_RULES
- Remove alias-based group name handling from get_deployment_types_from_groups
- Make DEFAULT_RULES the single source of truth for server/workstation/universal
- Determine "universal" strictly as invokable roles not matched by server/workstation
- Update unit tests to reflect invokable-based semantics

This aligns Ansible filters and CLI behavior and avoids inventory-specific heuristics.

https://chatgpt.com/share/698b8125-d2a0-800f-9fbd-f3f9c8bc5023
2026-02-10 20:04:00 +01:00
kevinveenbirkenbach 84782f3b22 refactor(invokable): centralize app discovery, add type grouping, and derive deploy stages from inventory groups
https://chatgpt.com/share/698b0039-c4c4-800f-b962-7033324c07a8
2026-02-10 10:53:53 +01:00
kevinveenbirkenbach 7e6caf7613 Solved wrong parent root bug 2026-02-07 19:53:12 +01:00
kevinveenbirkenbach b78ab29218 Renamed config entry from docker to compose
https://chatgpt.com/share/69870f54-d708-800f-9870-e9ae5a4bff44
2026-02-07 11:09:36 +01:00
kevinveenbirkenbach 53e817fa22 refactor(compose): migrate from docker-compose.yml to compose.yml everywhere
- Rename all docker-compose.yml templates to compose.yml
- Update override and CA override filenames accordingly
- Adjust compose wrapper, lookup plugins, filters and path helpers
- Update roles, tasks, handlers and scripts to use compose CLI
- Rewrite documentation and admin guides to reference compose
- Update unit and integration tests to expect compose.yml
- Keep internal variable keys for backward compatibility where needed

https://chatgpt.com/share/69870272-b168-800f-84c3-6417398e441f
2026-02-07 10:14:21 +01:00
kevinveenbirkenbach 9fa9692d7d refactor(compose): replace docker compose calls with compose wrapper
- Switch all docker compose invocations to the compose wrapper
- Update comments, task names, and log messages accordingly
- Simplify handlers and tasks to rely on wrapper-managed flags and env files
- Align tests and documentation with compose-based workflow

https://chatgpt.com/share/69866ede-be2c-800f-94b0-bffcf8fed260
2026-02-06 23:44:39 +01:00
kevinveenbirkenbach 51239968fd refactor: rename docker-compose roles to sys-svc-compose and update paths
Renamed docker-compose and docker-compose-ca roles to sys-svc-compose and sys-svc-compose-ca, updated template includes, handlers, lookups, tests, and variables, and moved compose instances to /opt/compose.

https://chatgpt.com/share/6985ab2c-8344-800f-9e9a-768f562d138d
2026-02-06 09:49:42 +01:00
kevinveenbirkenbach 24616b2d35 refactor: extract get_docker_paths to module_utils and fix affected tests
- Move get_docker_paths() implementation from filter_plugins to module_utils
  to make it reusable by filters and lookup plugins (single source of truth)
- Adapt filter_plugins/get_docker_paths.py to re-export the shared util
- Add new lookup plugin "database" resolving DB settings from applications config
- Fix unit test patching: patch module_utils.docker_paths_utils.get_entity_name
  instead of filter module, because imports are now bound at util level
- Add comprehensive unit tests for database lookup covering all helper-variable cases

https://chatgpt.com/share/6983b526-00dc-800f-b3a0-b71e4104d0fc
2026-02-04 22:07:42 +01:00
kevinveenbirkenbach 8a81630391 Added newline at file and POSIX compatible 2026-02-04 19:49:56 +01:00
kevinveenbirkenbach 76c614541f refactor(funkwhale): drop get_docker_image filter and switch to service-based image config
- Remove deprecated get_docker_image filter plugin and its unit tests
- Restructure Funkwhale docker config to use docker.services.<name>.image/version
- Introduce explicit FUNKWHALE_*_IMAGE and *_VERSION vars for clarity
- Update docker-compose template to use explicit image:version variables
- Align Funkwhale role with unified service-based Docker config model
2026-02-02 00:58:43 +01:00
kevinveenbirkenbach c24b1044ae fix(domains): respect AUTO_BUILD_ALIASES when generating redirect mappings
- Do not auto-generate default alias (<entity>.<primary>) when AUTO_BUILD_ALIASES is false
- Only use explicitly defined aliases unless auto-build is enabled
- Treat empty server.domains as opt-out when auto-build is disabled
- Update unit tests to reflect strict alias semantics
- Clarify behavior with inline documentation and comments

https://chatgpt.com/share/697d4994-7690-800f-8987-296ce7215559
2026-01-31 01:15:11 +01:00
kevinveenbirkenbach 79d50ae9b8 fix(pixelfed): ensure writable .env for www-data and use managed env path
- set ownership and permissions on Pixelfed .env for www-data (uid/gid 33)
- mount managed env file path instead of local ./env
- remove forced APP_KEY generation flag and quote MAIL_FROM_NAME
- prevent premature handler flush during stack load

https://chatgpt.com/share/697a4a0e-8ebc-800f-85f8-e8e21c23fe9c
2026-01-28 18:40:19 +01:00
kevinveenbirkenbach 5ec5b64dae - Add ruby_dq filter to correctly escape values for Ruby double-quoted strings
- Apply dotenv_quote to GITLAB_OMNIBUS_CONFIG to prevent .env parsing errors
- Fix docker compose failures with passwords containing ', $, € or newlines

https://chatgpt.com/share/697a48af-ba40-800f-8de6-930adda782b0
2026-01-28 18:34:35 +01:00
kevinveenbirkenbach 5ca6ecd3d7 fix(dotenv): escape $ for docker compose env interpolation
Ensure dotenv_quote escapes `$` as `$$` so secrets are not
expanded by docker compose when rendered into .env files.

Add unit tests covering $, quotes, backslashes, unicode and
edge cases to prevent regressions.

https://chatgpt.com/share/6977825f-d8f8-800f-bafb-aae3b3704945
2026-01-26 16:03:36 +01:00
kevinveenbirkenbach 3b1032895a feat(security): introduce sed_escape filter and enforce safe sed replacements
- add sed_escape Jinja filter for sed replacement safety
  - escapes \, &, and the active sed delimiter
  - designed explicitly for sed s/// replacement context

- patch all sed-based config updates across roles
  - espocrm, friendica, gitea, matomo, moodle, wordpress
  - ensure every Jinja value in sed replacements uses sed_escape
  - use correct delimiter-specific escaping (/, |, #)

- add robust integration test to enforce sed_escape usage
  - scans all roles/*/tasks/**/*.yml
  - detects sed substitutions with Jinja in replacement part
  - ignores delimiters inside {{ ... }} blocks
  - fails CI if sed_escape is missing

- harden repository against sed injection, delimiter breakage,
  and credential corruption in config patching tasks

https://chatgpt.com/share/6974fb45-1ee8-800f-80bd-c71122e4ac02
2026-01-24 18:02:37 +01:00
kevinveenbirkenbach aa7d0c642c feat(dotenv): add dotenv_quote filter and enforce quoted password vars in env.j2
https://chatgpt.com/share/6974cbac-023c-800f-8198-fd85df2aa7bc
2026-01-24 15:22:19 +01:00
kevinveenbirkenbach 35c1d29a88 fix(docker-compose): remove has_domain filter dependency from compose_f_args
Make has_domain filter self-contained (no module_utils/sys.path hacks) and let compose_f_args detect domains internally to avoid missing-filter failures. Adjust unit tests accordingly and increase Mailu user creation retry delay.

https://chatgpt.com/share/6973e02e-f22c-800f-aae3-1f57637a746c
2026-01-23 21:54:55 +01:00
kevinveenbirkenbach 204c028d6a feat(domains): add has_domain filter and guard self-signed CA handling
- introduce has_domain filter reusing get_domain logic
- prevent self-signed CA role execution if no domain is defined
- add unit tests for has_domain filter

https://chatgpt.com/share/6973cdf7-f7fc-800f-901b-4de186a3637c
2026-01-23 20:37:22 +01:00
kevinveenbirkenbach 4dbbcec106 feat(docker-compose): add dynamic CA trust injection for self-signed TLS
Introduce docker-compose-ca role with CA wrapper and inject script, add CA-aware compose override handling, and refactor compose pull/run logic to support multi-file projects. Update handlers, lookups, paths, and unit tests to consistently resolve TLS-driven CA injection without implicit defaults.

https://chatgpt.com/share/6973c00c-139c-800f-8ddb-e7d0cf5043fc
2026-01-23 19:36:55 +01:00
kevinveenbirkenbach 0b9f71463c refactor(tls): replace legacy tls filters with strict tls_resolve lookup
- Remove deprecated filter_plugins/tls.py and all dependent filters
- Introduce strict lookup plugin lookup_plugins/tls_resolve.py as single TLS authority
- Migrate all roles, templates, vars, and tasks to use tls_resolve (protocols, ports, URLs, cert paths)
- Enforce explicit TLS context (domains, applications, TLS_ENABLED, TLS_MODE)
- Support letsencrypt and self_signed with SAN handling and per-app overrides
- Update TLS provider roles to assert resolver state instead of setting facts
- Add comprehensive unit tests for tls_resolve lookup

https://chatgpt.com/share/6972b49b-b074-800f-a323-dd9fe69d02eb
2026-01-23 00:36:46 +01:00
kevinveenbirkenbach 54ad6a501b refactor(tls): split tls layer into orchestrator + providers
https://chatgpt.com/share/697286d6-d400-800f-a67b-425981fa5781
2026-01-22 21:21:34 +01:00
kevinveenbirkenbach 9fdb0cddd0 Replace global WEB_PROTOCOL / WEB_PORT / WEBSOCKET_PROTOCOL logic
with explicit per-application TLS resolution.

Key changes:
- Introduce sys-front-tls-layer as the single TLS abstraction
- Centralize certificate paths via TLS_CERT_FILE / TLS_KEY_FILE / TLS_CA_FILE
- Replace sys-svc-letsencrypt SSL templates with generic TLS layer includes
- Add tls_* Jinja filters (enabled, mode, protocol, port, websocket)
- Resolve TLS mode per application (letsencrypt | self_signed | off)
- Remove implicit defaults and WEB_* globals to fail fast on misconfiguration
- Support self-signed certificates with SAN awareness
- Make TLS behavior explicit and consistent across all roles

This decouples application logic from certificate providers and
allows switching TLS flavor without touching app roles.

https://chatgpt.com/share/69727c61-8e68-800f-a267-473a7e627978
2026-01-22 20:36:49 +01:00
kevinveenbirkenbach fae7b82243 feat(security): introduce strong_password filter backed by ValueGenerator
- add strong_password Jinja2 filter as thin wrapper around ValueGenerator
- enforce password policy (upper, lower, digit, special, min length)
- replace insecure ascii-only password lookup with strong_password filter
- update CLI setup users and unit tests accordingly
- harden inventory sample password example

https://chatgpt.com/share/69722c7e-ebe0-800f-a9ab-a3e21b43269c
2026-01-22 14:56:05 +01:00
kevinveenbirkenbach 66aec3b577 refactor(docker-compose): generate volumes via compose_volumes filter
Replace legacy volumes*.yml.j2 includes with the new compose_volumes filter across roles.
Add YAML-safe serialization for Ansible/Jinja proxy types to avoid PyYAML representer errors.
Remove obsolete volumes templates and extend unit tests to enforce required database_volume behavior.

https://chatgpt.com/share/696c2722-fd30-800f-bf01-dfcd3cb7ba72
2026-01-18 01:19:31 +01:00
kevinveenbirkenbach 8059f51e33 feat(filter): add compose_volumes helper to replace volumes.yml.j2 logic
- Add compose_volumes filter to generate docker-compose volumes section dynamically
- Preserve exact logic from roles/docker-compose/templates/volumes.yml.j2
  - database volume only when database service enabled and not shared
  - redis volume when redis service enabled or oauth2 enabled
  - no fallback names, database_volume is required when database is needed
- Support manual volume injection via extra_volumes (YAML merge equivalent)
- Add strict input validation for applications and application_id
- Add full unit test coverage for all conditions and edge cases

https://chatgpt.com/share/696c1710-04c4-800f-a73f-de3d5ebd2902
2026-01-18 00:11:07 +01:00
kevinveenbirkenbach 854f5501f7 test(filter): remove invalid applications type guard test for service_load_enabled
The applications parameter is guaranteed to always be a dict in the runtime
environment. The defensive test for non-mapping input was therefore removed
to avoid unrealistic failure scenarios and false negatives.

This keeps the unit tests aligned with the actual inventory and schema model.

https://chatgpt.com/share/6968551e-7b74-800f-8a5e-a2380c950d06
2026-01-15 03:46:47 +01:00
kevinveenbirkenbach c9a3c811c6 feat(config): migrate docker.service flags to docker.services
* Switch feature flag paths from docker.service.<name>.enabled to docker.services.<name>.enabled
* Update CSP builder and smart-default toggles and unit tests
* Update desktop cards iframe flag and desktop domain validation
* Update logout domain filter and tests
* Update recaptcha enable flags in Keycloak and EspoCRM
* Fix sys-ctl-cln-docker meta description formatting
* Follow-up: reorder imports in web_health_expectations so module_utils path injection runs before importing get_app_conf

https://chatgpt.com/share/6963f484-4cb4-800f-a9f4-8d70a66e4d32
2026-01-11 22:27:36 +01:00
kevinveenbirkenbach f584a61f1f refactor(csp, inj): switch to docker.service.*.enabled flags and align unit tests
- Update CSP smart-default toggles to use docker.service.<feature>.enabled
  (matomo, simpleicons, recaptcha, hcaptcha, logout, desktop)
- Refactor inj_enabled filter to resolve docker.service.<feature>.enabled only
- Migrate inj_enabled unit tests to the new flag layout
- Normalize and reformat CSP test fixtures for readability and consistency

https://chatgpt.com/share/6963f484-4cb4-800f-a9f4-8d70a66e4d32
2026-01-11 20:50:30 +01:00
kevinveenbirkenbach 158c6569dc test(csp): migrate feature toggles to docker.service.*.enabled and split unit tests
- Switch CSP smart-default toggles to docker.service.<name>.enabled flags
  (matomo, simpleicons, recaptcha, hcaptcha, logout, desktop)
- Update CSP header logic docs/comments to match new flag layout
- Split large CSP filter unit test into focused test modules for clarity
- Add coverage for service toggle behavior, determinism, union rules, and hash guards

https://chatgpt.com/share/6963f484-4cb4-800f-a9f4-8d70a66e4d32
2026-01-11 20:05:33 +01:00
kevinveenbirkenbach 04c9490745 Solved unit and integration tests 2026-01-10 13:07:12 +01:00
kevinveenbirkenbach bf4af7c823 Renamed domain variables 2025-12-29 20:37:47 +01:00
kevinveenbirkenbach 7123841fb4 style: apply ruff autoformat 2025-12-17 16:37:21 +01:00
kevinveenbirkenbach c549febe8a Autofix ruff F401 2025-12-17 14:41:42 +01:00
kevinveenbirkenbach 176c8799ce lint(ruff): fix E402 and related import issues
- Fix E402 by removing sys.path hacks and ensuring imports are at module top level
- Replace ad-hoc imports with proper package imports (filter_plugins, lookup_plugins, module_utils, cli)
- Make lookup plugins importable via package paths (e.g. lookup_plugins.application_gid)
- Clean up test imports to rely on installed package layout instead of sys.path manipulation
- Partially fix F821 caused by missing/invalid imports

This establishes a consistent, ruff-compliant import architecture across CLI, filters, plugins, and tests.
2025-12-17 14:36:29 +01:00
kevinveenbirkenbach ca8af3393e Solved errors in unit tests appearing through ruff autofix 2025-12-17 13:20:11 +01:00
kevinveenbirkenbach 9db66f6f98 Ruff autofix 2025-12-17 12:56:47 +01:00
kevinveenbirkenbach 26dfab147d Implement reserved username handling for users, LDAP and Keycloak
Add end-to-end support for reserved usernames and tighten CAPTCHA / Keycloak logic.

Changes:

- Makefile: rename EXTRA_USERS → RESERVED_USERNAMES and pass it as --reserved-usernames to the users defaults generator.

- cli/build/defaults/users.py: propagate  flag into generated users, add --reserved-usernames CLI option and mark listed accounts as reserved.

- Add reserved_users filter plugin with  and  helpers for Ansible templates and tasks.

- Add unit tests for reserved_users filters and the new reserved-usernames behaviour in the users defaults generator.

- group_vars/all/00_general.yml: harden RECAPTCHA_ENABLED / HCAPTCHA_ENABLED checks with default('') and explicit > 0 length checks.

- svc-db-openldap: introduce OPENLDAP_PROVISION_* flags, add OPENLDAP_PROVISION_RESERVED and OPERNLDAP_USERS to optionally exclude reserved users from provisioning.

- svc-db-openldap templates/tasks: switch role/group LDIF and user import loops to use OPERNLDAP_USERS instead of the full users dict.

- networks: assign dedicated subnet for web-app-roulette-wheel.

- web-app-keycloak vars: compute KEYCLOAK_RESERVED_USERNAMES_LIST and KEYCLOAK_RESERVED_USERNAMES_REGEX from users | reserved_usernames.

- web-app-keycloak user profile template: inject reserved-username regex into username validation pattern and improve error message, fix SSH public key attribute usage and add component name field.

- web-app-keycloak update/_update.yml: strip subComponents from component payloads before update and disable async/poll for easier debugging.

- web-app-keycloak tasks/main.yml: guard cleanup include with MODE_CLEANUP and keep reCAPTCHA update behind KEYCLOAK_RECAPTCHA_ENABLED.

- user/users defaults: mark system/service accounts (root, daemon, mail, admin, webmaster, etc.) as reserved so they cannot be chosen as login names.

- svc-prx-openresty vars: simplify OPENRESTY_CONTAINER lookup by dropping unused default parameter.

- sys-ctl-rpr-btrfs-balancer: simplify main.yml by removing the extra block wrapper.

- sys-daemon handlers: quote handler name for consistency.

Context: change set discussed and refined in ChatGPT on 2025-11-29 (Infinito.Nexus reserved usernames & Keycloak user profile flow). See conversation: https://chatgpt.com/share/692b21f5-5d98-800f-8e15-1ded49deddc9
2025-11-29 17:40:45 +01:00
kevinveenbirkenbach 7fa6b2d770 Make CSP header token ordering deterministic
Sort CSP directive tokens lexicographically (keeping 'self' first) to ensure
stable, reproducible Content-Security-Policy headers and avoid spurious
Ansible changes caused by ordering-only differences. Add unit tests to verify
sorted connect-src tokens and deterministic output for unsorted whitelists.

Reference: https://chatgpt.com/share/692af53f-c3a4-800f-bd28-cc7e0b6ff7ce
2025-11-29 14:30:19 +01:00
kevinveenbirkenbach c2a181edd4 Add generic hCaptcha CSP support and tests (ref: ChatGPT conversation https://chatgpt.com/share/6929f2ba-cedc-800f-9c4c-2049810cea94) 2025-11-28 20:06:58 +01:00
kevinveenbirkenbach da8dc3b53a CSP: Add logout-specific 'unsafe-inline' injection for script-src-attr/elem and extend unit test coverage
Implements automatic addition of 'unsafe-inline' when logout feature is enabled, ensuring inline handlers work for iframe logout bridge. Preserves strict base script-src when explicitly disabled. Includes full test suite for logout behaviour, propagation rules, and base override interactions.

Context: https://chatgpt.com/share/6927075c-4de0-800f-bcee-b1f5193e4a99
2025-11-26 14:58:17 +01:00
kevinveenbirkenbach a312f353fb Refactor JVM memory filters, add Redis sizing and Docker cleanup service
- Replace jvm_filters with unified memory_filters (JVM + Redis helpers)
- Add redis_maxmemory_mb filter and unit tests
- Introduce sys-ctl-cln-docker role (systemd-based Docker prune + anon volumes)
- Refactor disk space health check to Python script and wire SIZE_PERCENT_CLEANUP_DISC_SPACE
- Adjust schedules and services for Docker cleanup and disk space health

See discussion: https://chatgpt.com/share/6925c1c5-ee38-800f-84b6-da29ccfa7537
2025-11-25 15:50:27 +01:00
kevinveenbirkenbach 36f9573fdf feat(filters): enforce safe Node.js heap sizing via reusable filter
- Add node_autosize filter (node_max_old_space_size) using get_app_conf
- Raise error when mem_limit < min_mb to prevent OOM-kill misconfigurations
- Wire Whiteboard NODE_OPTIONS and increase mem_limit to 1g; set cpus=1
- Refactor PeerTube to use the same filter; simplify vars
- Add unit tests; keep integration filters usage green

Context: https://chatgpt.com/share/690e0499-6a94-800f-b8ed-2c5124690103
2025-11-07 15:39:54 +01:00
kevinveenbirkenbach 57d5269b07 CSP (Safari-safe): merge -elem/-attr into base; respect explicit disables; no mirror-back; header only for documents/workers
- Add CSP3 support for style/script: include -elem and -attr directives
- Base (style-src, script-src) now unions elem/attr (CSP2/Safari fallback)
- Respect explicit base disables (e.g. style-src.unsafe-inline: false)
- Hashes only when 'unsafe-inline' absent in the final base tokens
- Nginx: set CSP only for HTML/worker via header_filter_by_lua_block; drop for subresources
- Remove per-location header_filter; keep body_filter only
- Update app role flags to *-attr where appropriate; extend desktop CSS sources
- Add comprehensive unit tests for union/explicit-disable/no-mirror-back

Ref: https://chatgpt.com/share/68f87a0a-cebc-800f-bb3e-8c8ab4dee8ee
2025-10-22 13:53:06 +02:00
kevinveenbirkenbach aa19a97ed6 CORS/CSP hardening & centralization
- Add reusable Nginx include: roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2
  (dynamic ACAO/credentials/methods/headers via role vars)
- Set global 'Vary: Origin' in nginx.conf.j2 to prevent cache poisoning
- CSP: allow Simple Icons via connect-src when feature is enabled
- Front proxy: rename vars to lowercase + flush handlers after config deploy
- Desktop: gate & load Simple Icons role; inject brand logos when enabled
- Bluesky + Logout: replace inline CORS with centralized include
- Simpleicons: public CORS (ACAO='*', no credentials), keep GET/OPTIONS, allow headers
- Taiga: adjust canonical domain to taiga.kanban.{{ PRIMARY_DOMAIN }}
- LibreTranslate: remove unused images/versions keys

Fixes: https://open.project.infinito.nexus/projects/cymais/work_packages/342/activity
Discussion: https://chatgpt.com/share/68da5e27-ffd4-800f-91a3-0ef103058d44
2025-09-29 12:23:58 +02:00