feat(frontend): rename inj roles to sys-front-*, add sys-svc-cdn, cache-busting lookup

Introduce sys-svc-cdn (cdn_paths/cdn_urls/cdn_dirs) and ensure CDN directories + latest symlink.

Rename sys-srv-web-inj-* → sys-front-inj-*; update includes/templates; serve shared/per-app CSS & JS via CDN.

Add lookup_plugins/local_mtime_qs.py for mtime-based cache busting; split CSS into default.css/bootstrap.css + optional per-app style.css.

CSP: use style-src-elem; drop unsafe-inline for styles. Services: fix SYS_SERVICE_ALL_ENABLED bool and controlled flush.

BREAKING CHANGE: role names changed; replace includes and references accordingly.

Conversation: https://chatgpt.com/share/68b55494-9ec4-800f-b559-44707029141d
This commit is contained in:
2025-09-01 10:10:23 +02:00
parent 3f8e7c1733
commit 231fd567b3
123 changed files with 1789 additions and 1393 deletions

View File

@@ -0,0 +1,19 @@
# sys-svc-cdn
CDN helper role for building a consistent asset tree, URLs, and on-disk layout.
## Description
Provides compact filters and defaults to define CDN paths, turn them into public URLs, collect required directories, and prepare the filesystem (including a `latest` release link).
## Overview
Defines a per-role CDN structure under `roles/<application_id>/<version>` plus shared and vendor areas. Exposes ready-to-use variables (`cdn`, `cdn_dirs`, `cdn_urls`) and ensures directories exist. Optionally links the current release to `latest`.
## Features
* Jinja filters: `cdn_paths`, `cdn_urls`, `cdn_dirs`
* Variables: `CDN_ROOT`, `CDN_VERSION`, `CDN_BASE_URL`, `cdn`, `cdn_dirs`, `cdn_urls`
* Creates shared/vendor/release directories
* Maintains `roles/<id>/latest` symlink (when version ≠ `latest`)
* Plays nicely with `web-svc-cdn` without circular inclusion

View File

View File

@@ -0,0 +1,17 @@
import os
def cdn_dirs(tree):
out = set()
def walk(v):
if isinstance(v, dict):
for x in v.values(): walk(x)
elif isinstance(v, list):
for x in v: walk(x)
elif isinstance(v, str) and os.path.isabs(v):
out.add(v)
walk(tree)
return sorted(out)
class FilterModule(object):
def filters(self):
return {"cdn_dirs": cdn_dirs}

View File

@@ -0,0 +1,46 @@
import datetime
import os
def cdn_paths(cdn_root, application_id, version):
"""
Build a structured dictionary of all CDN paths for a given application.
Args:
cdn_root (str): Base CDN root, e.g. /var/www/cdn
application_id (str): Role/application identifier
version (str): Release version string (default: current UTC timestamp)
Returns:
dict: Hierarchical CDN path structure
"""
cdn_root = os.path.abspath(cdn_root)
return {
"root": cdn_root,
"shared": {
"root": os.path.join(cdn_root, "_shared"),
"css": os.path.join(cdn_root, "_shared", "css"),
"js": os.path.join(cdn_root, "_shared", "js"),
"img": os.path.join(cdn_root, "_shared", "img"),
"fonts": os.path.join(cdn_root, "_shared", "fonts"),
},
"vendor": os.path.join(cdn_root, "vendor"),
"role": {
"id": application_id,
"root": os.path.join(cdn_root, "roles", application_id),
"version": version,
"release": {
"root": os.path.join(cdn_root, "roles", application_id, version),
"css": os.path.join(cdn_root, "roles", application_id, version, "css"),
"js": os.path.join(cdn_root, "roles", application_id, version, "js"),
"img": os.path.join(cdn_root, "roles", application_id, version, "img"),
"fonts": os.path.join(cdn_root, "roles", application_id, version, "fonts"),
},
},
}
class FilterModule(object):
def filters(self):
return {
"cdn_paths": cdn_paths,
}

View File

@@ -0,0 +1,60 @@
# filter_plugins/cdn_urls.py
import os
def _to_url_tree(obj, cdn_root, base_url):
"""
Recursively walk a nested dict and replace any string paths under cdn_root
with URLs based on base_url. Non-path strings (e.g. role.id, role.version)
are left untouched.
"""
if isinstance(obj, dict):
return {k: _to_url_tree(v, cdn_root, base_url) for k, v in obj.items()}
if isinstance(obj, list):
return [_to_url_tree(v, cdn_root, base_url) for v in obj]
if isinstance(obj, str):
# Normalize inputs
norm_root = os.path.abspath(cdn_root)
norm_val = os.path.abspath(obj)
if norm_val.startswith(norm_root):
# Compute path relative to CDN root and map to URL
rel = os.path.relpath(norm_val, norm_root)
# Handle root itself ('.') → empty path
if rel == ".":
rel = ""
# Always forward slashes for URLs
rel_url = rel.replace(os.sep, "/")
base = base_url.rstrip("/")
return f"{base}/{rel_url}" if rel_url else f"{base}/"
# Non-CDN string → leave as-is (e.g., role.id / role.version)
return obj
# Any other type → return as-is
return obj
def cdn_urls(cdn_dict, base_url):
"""
Create a URL-structured dict from a CDN path dict.
Args:
cdn_dict (dict): output of cdn_paths(...), containing absolute paths
base_url (str): CDN base URL, e.g. https://cdn.example.com
Returns:
dict: same shape as cdn_dict, but with URLs instead of filesystem paths
for any strings pointing under cdn_dict['root'].
Keys like role.id and role.version remain strings as-is.
"""
if not isinstance(cdn_dict, dict) or "root" not in cdn_dict:
raise ValueError("cdn_urls expects a dict from cdn_paths with a 'root' key")
return _to_url_tree(cdn_dict, cdn_dict["root"], base_url)
class FilterModule(object):
def filters(self):
return {
"cdn_urls": cdn_urls,
}

View File

@@ -0,0 +1,24 @@
---
galaxy_info:
author: "Kevin Veen-Birkenbach"
description: "Prepares and manages the CDN folder structure with shared, vendor, and per-role release directories."
license: "Infinito.Nexus NonCommercial License"
license_url: "https://s.infinito.nexus/license"
company: |
Kevin Veen-Birkenbach
Consulting & Coaching Solutions
https://www.veen.world
min_ansible_version: "2.9"
platforms:
- name: Any
versions:
- all
galaxy_tags:
- cdn
- nginx
- assets
- roles
- versioning
repository: "https://s.infinito.nexus/code"
issue_tracker_url: "https://s.infinito.nexus/issues"
documentation: "https://s.infinito.nexus/code/tree/main/roles/sys-svc-cdn"

View File

@@ -0,0 +1,41 @@
---
- block:
- name: "Load CDN for '{{ domain }}'"
include_role:
name: web-svc-cdn
public: false
when:
#- inj_enabled.logout
#- inj_enabled.desktop
- application_id != 'web-svc-cdn'
- run_once_web_svc_cdn is not defined
- name: Overwritte CDN handlers with neutral handlers
ansible.builtin.include_tasks: "{{ [ playbook_dir, 'tasks/utils/load_handlers.yml'] | path_join }}"
loop:
- svc-prx-openresty
- docker-compose
loop_control:
label: "{{ item }}"
vars:
handler_role_name: "{{ item }}"
- include_tasks: utils/run_once.yml
when:
- run_once_sys_svc_cdn is not defined
- name: Ensure CDN directories exist
file:
path: "{{ item }}"
state: directory
owner: "{{ NGINX.USER }}"
group: "{{ NGINX.USER }}"
mode: "0755"
loop: "{{ cdn_dirs }}"
- name: Ensure 'latest' symlink points to current release
file:
src: "{{ cdn.role.release.root }}"
dest: "{{ [cdn.role.root, 'latest'] | path_join }}"
state: link
force: true
when: CDN_VERSION != 'latest'

View File

@@ -0,0 +1,19 @@
# Base CDN root (shared across all roles)
CDN_ROOT: "{{ NGINX.DIRECTORIES.DATA.CDN }}"
# Default version identifier: UTC timestamp
CDN_VERSION: "latest" # Latest is used atm because timestamp based would just lead to an unneccessary overhead
# Base Url to deliver the files
CDN_BASE_URL: "{{ domains | get_url('web-svc-cdn', WEB_PROTOCOL) }}"
# Role specific CDN paths
## Build CDN path structure (via filter)
cdn: "{{ CDN_ROOT | cdn_paths(application_id, CDN_VERSION) }}"
## Flatten CDN dict to list of all string paths
cdn_dirs: "{{ cdn | cdn_dirs }}"
# Dictionary with all urls
cdn_urls: "{{ cdn | cdn_urls(CDN_BASE_URL) }}"