# Public Enelix Documentation Publisher ## Context and decision The license portal already serves public static files and exposes its effective catalog at `GET /api/catalog`. Documentation is therefore built as static HTML; the existing portal process, authentication, database and payment code are not modified. Generic Utils runtime code is not coupled to the documentation service. Source ownership: - `docs/public/*.md` in Enelix-EMS: installation, operator guides, FAQ and pricing explanation. - `docs/public/images/*.png`: explicitly reviewed real Symcon screenshots, allowlisted in `images.mjs`. - Explicit README/interface allowlists in `config.mjs`: original EMS and Utils references. - Backend catalog: every monetary value, product name, SKU, term and effective VAT rate. - This directory: rendering, navigation, publication and tests. The default source branch is `develop`, visibly identified as Testing. The publisher reads separate shallow bare mirrors; it never changes existing developer checkouts. All sources are pinned to the commits captured at the start of a successful run. Neither unpublished worktrees nor internal handoffs, open issues, credentials, operational notes or arbitrary new files are published. Alternative considered: adding server-side documentation routes. Not selected: the current static mount enables this rollout without a portal restart or a new public service. A separately maintained copy of prices was rejected because it would drift from checkout. The browser refreshes the same catalog endpoint every 30 seconds while visible, on reappearance and on manual refresh. ## Public routes - `/docs/index.html` - `/docs/erste-schritte.html` - `/docs/installation.html` - `/docs/manager.html` - `/docs/verbraucher.html` - `/docs/utils.html` - `/docs/faq.html` - `/docs/preise.html` - `/docs/ems/*.html` and `/docs/utils/*.html` - `/docs/manifest.json`: published source commits and file mappings. - `/docs-status.json`: last attempted/successful synchronization, no internal errors. The portal's anonymous login screen links to documentation, FAQ and prices. Authenticated sidebar navigation additionally links to documentation and prices. Within the EMS references, Manager remains a direct link; consumer modules and technical interfaces are nested in native, keyboard-accessible disclosure groups. The current page's group opens on the server, also without JavaScript. Search expands matching groups and restores their previous state when cleared. All page URLs remain unchanged; new references without a group stay accessible. `assets/portal-links.css` contains only the added navigation styling. `portal-navigation.patch` records the exact additive portal HTML change; check it against the current portal before applying it and copy the stylesheet to `public/docs-portal.css`. Never replace the full portal HTML from another checkout. ## Install and publish Deploy the reviewed publisher files to `/home/agent/services/public-docs` as the unprivileged `agent` user. Do not copy `node_modules`, state, preview, QA or backups into a repository or the public directory. Install locked dependencies: ```sh npm ci --ignore-scripts npm test ``` For rendering/rollback checks without changing public pages: ```sh node preview.mjs node verify.mjs node browser-test.mjs ``` Publication uses the existing public mount at `/home/agent/services/license/public`. The portal container's UID must be able to read generated directories and files. No credentials are included in the publisher: Git uses the server's preconfigured access. Never put credentials in repository URLs or public output. Install the two supplied units through `systemctl --user link`, reload the user manager, start `enelix-public-docs.service` once and enable `enelix-public-docs.timer`. Only these user units are affected. The timer checks Git five minutes after completion, with at most fifteen seconds of jitter. The successful first run can take longer while fetching both repositories. `DOCS_BRANCH` accepts `develop`, `beta` or `main`; reading a source branch does not merge or modify it. `DOCS_PUBLIC_DIR` and `DOCS_STATE_DIR` support isolated tests. Keep the chosen channel explicit in the service configuration. ## Git-only operation and historical bootstrap The production service does not enable `DOCS_ALLOW_DRAFTS`. All approved guides and screenshots must exist in the selected Git branch. A missing source aborts publication and preserves the last good release; deployment copies cannot silently replace deleted Git content. The initial 2026-10-06 deployment used explicitly labelled drafts while Git publication awaited authorization. Daniel approved commit and push on the same day. The bootstrap fallback remains available only through the explicit `DOCS_ALLOW_DRAFTS=true` setting, including isolated previews. It reads missing sources from the deployment-only `guides/` directory and labels them as drafts. Do not treat these bootstrap copies as a second long-term editing location. When completing a bootstrap deployment, push the approved sources first, verify that publication uses their Git commits, then install the production unit without the draft setting and reload the user service manager. ## Security and resilience Markdown is rendered with pinned Marked and sanitized with sanitize-html. Raw HTML and remote images are disabled; link schemes and local mappings are restricted. Relative links to unpublished files are plain text. The existing portal CSP remains active; there is no inline JavaScript or browser-side Git credential. Every page is readable without JavaScript except the live price table, which shows an explicit notice and a catalog link in that case. Only the exact local PNG paths in `images.mjs` render as screenshots. The publisher reads binary files at the same EMS commit as the guides, checks regular file type, PNG signature and dimensions, and publishes content-hashed filenames. Image hashes, dimensions and source revisions are recorded in the manifest. Missing or invalid images fail publication and preserve the last good release. Draft bootstrap images follow the same explicit opt-in as the draft guides. Screenshot refresh is intentionally reviewed, not a live camera into Symcon. Capture through the local-only web console in a separate browser session, without saving configuration or invoking regulation. Select useful crops, inspect them for license codes, installation IDs, accounts and private plant details, then replace the approved PNGs in Git and update the dated captions. Never publish unreviewed captures automatically. Current screenshots show the demo object tree and the manager basic settings in Symcon 8.0 on 2026-10-06. Test values are not recommended installation settings; reference READMEs may evolve faster than the dated screenshots. Publication is a complete immutable release followed by an atomic `docs` symlink switch. Failed fetches or rendering leave the previous release intact. Public status reports failed or overdue checks. Stale prices are removed when the catalog cannot be obtained or validated; no hardcoded fallback is used. The sync lock prevents overlapping manual and timer runs. If the process is forcibly terminated, inspect process state before removing its stale lock from the publisher state directory. Generated releases are retained to permit rollback; disk usage should be monitored and old releases pruned only after checking that they are not current or needed for rollback. ## Verification and rollback `npm test` covers relative and cross-repository links, HTML sanitization, allowlist isolation, headings/tables, price validation, VAT and periods. `node verify.mjs` validates all published links and deliberately exercises a source failure against an isolated mirror, proving last-good preservation. `node browser-test.mjs` checks desktop, 390px and 320px layouts, search, automatic catalog refresh, newly returned products, VAT toggle, API failure/recovery and JavaScript-free documentation. Mocks affect only a fresh browser context, never the production catalog. Screenshots remain private in `qa/`. `node verify.mjs live` and `node browser-test.mjs --live` perform anonymous checks on the actual domain. The tests must not log in or mutate live application data. For rollback, first stop and disable only the documentation timer. Restore the previous documentation symlink atomically. For full removal, revert only the added portal navigation and stylesheet link after checking for concurrent edits; do not overwrite the portal with an older full backup if other edits occurred. No database, Nginx or IP-Symcon rollback is required because those were unchanged. ## Initial deployment evidence 2026-10-06: 29 pages, 1,116 valid local links, 12 unit tests passed, isolated failure recovery passed, desktop/mobile browser tests passed both in preview and against the public domain. The initial sources were EMS `58f91e431936fedca3e59178037b939f420ce3e3` and Utils `82860c0eb938173841088f935a306729b6ce9a1c`. These are dated evidence, not permanent source pins. The price API initially returned 15 products and was accessed without login. Portal navigation backup: `/home/agent/services/public-docs/backups/2026-10-06T12-58-13-558Z/index.html`. The backup is private and must never be copied into `public/`. ## Pull request summary Add public documentation and FAQ from reviewed Git-backed sources, plus a live backend-driven price table. Preserve existing portal authentication and data; publish static releases atomically with a five-minute user timer. No schema or module migration and no beta/main changes. Include the end-user introduction, guided first steps, reviewed Symcon screenshots and grouped module navigation. Production publication requires versioned guides and screenshots, without draft fallback.