From 26a4ecbe9d7d2cf5c9d86077ad96bc70dfe42ee9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:52:02 +0000 Subject: [PATCH 01/11] docs: restore getting-started guidance for stable LibreSign Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- versioned/base/user_manual/start.rst | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 versioned/base/user_manual/start.rst diff --git a/versioned/base/user_manual/start.rst b/versioned/base/user_manual/start.rst new file mode 100644 index 00000000..40f58281 --- /dev/null +++ b/versioned/base/user_manual/start.rst @@ -0,0 +1,39 @@ +Getting started +=============== + +Choose the path that matches your role. An administrator must install and +configure LibreSign before users can request signatures. + +I want to request signatures +---------------------------- + +#. Sign in to the Nextcloud instance where LibreSign is enabled. +#. Open **LibreSign** and find **Request Signatures** or the request action + in the **Files** screen. +#. Select a PDF using **Upload**, **Choose from Files**, or **Upload from URL** + when those options are available to your account. +#. Follow :ref:`add_signature` to add recipients and send the request. + +If the request controls are missing, ask your administrator whether your +account is permitted to create requests. + +I received a signing invitation +------------------------------- + +#. Open the invitation link from the requester and check the document and + requested identity details before proceeding. +#. Follow the on-screen signing steps. Depending on the configured signing + method, LibreSign may ask you to define a signature, provide a password, + upload a certificate, or complete identity verification. +#. Review the confirmation before submitting. See :ref:`sign_invitation`. + +External email signers can use the invitation workflow without a Nextcloud +account when that identification method is enabled by the administrator. +Do not forward a personal invitation link to another person. + +I want to verify a signed document +---------------------------------- + +Use :ref:`validate_document` to check a signed PDF or a document UUID. The +validation result must be reviewed; an image of a handwritten signature is not +by itself evidence of a valid digital signature. From 99c6ab010efc8861d98b5ff4641c9b6fb71e7d0e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:52:30 +0000 Subject: [PATCH 02/11] docs: restore stable user signing, validation, and help guides Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- versioned/base/user_manual/faq.rst | 39 ++++++++++ versioned/base/user_manual/functionality.rst | 77 ++++++++++++++++++++ versioned/base/user_manual/index.rst | 32 +++++--- versioned/base/user_manual/support.rst | 19 +++++ 4 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 versioned/base/user_manual/faq.rst create mode 100644 versioned/base/user_manual/functionality.rst create mode 100644 versioned/base/user_manual/support.rst diff --git a/versioned/base/user_manual/faq.rst b/versioned/base/user_manual/faq.rst new file mode 100644 index 00000000..07803c4f --- /dev/null +++ b/versioned/base/user_manual/faq.rst @@ -0,0 +1,39 @@ +Frequently asked questions +========================== + +Do I need a Nextcloud account to sign? +------------------------------------ + +Not always. A requester can invite an external email signer without creating +an account when the administrator enables that identification method. Creating +signature requests from the Nextcloud interface does require appropriate +permissions. See :doc:`start`. + +Must I upload my own digital certificate? +----------------------------------------- + +That depends on the signing method required for the request and the server's +certificate configuration. If a personal certificate is required, LibreSign +will prompt for it. A drawn or typed appearance is not the same thing as the +cryptographic signature embedded in a signed PDF. + +How do I know whether everyone has signed? +----------------------------------------- + +Check the request details and each signer's progress in :ref:`see_signed_file`. +One completed signer does not prove a multi-signer document is finished. + +Does validation prove a signature is legally valid everywhere? +------------------------------------------------------------- + +No. :ref:`validate_document` checks technical evidence. Applicable laws, +identity-assurance requirements, retention duties, and acceptance by a +recipient depend on the context and jurisdiction. LibreSign is not a blanket +GDPR/LGPD or legal-compliance certification. + +What if my invitation does not work? +----------------------------------- + +Check that you opened the intended invitation, then ask the requester to +verify its status, recipient, and configured identification method. Do not +send your password or signing credential to support staff. See :doc:`support`. diff --git a/versioned/base/user_manual/functionality.rst b/versioned/base/user_manual/functionality.rst new file mode 100644 index 00000000..daa819b0 --- /dev/null +++ b/versioned/base/user_manual/functionality.rst @@ -0,0 +1,77 @@ +Documents, signatures, and validation +===================================== + +.. _main_screen: + +Find your documents +------------------- + +Open **LibreSign** in Nextcloud and choose **Files** to see documents and +signature requests available to you. Select a file to inspect its details +and signers. An individual signer can be **pending** or **signed**; do not +assume the entire request is complete merely because one person has signed. + +.. _add_signature: + +Request signatures +------------------ + +#. Choose **Request Signatures**. Use **Upload** for a PDF on your device, + **Choose from Files** for an existing Nextcloud file, or **Upload from + URL** if your administrator allows it. +#. Review the selected file before asking another person to sign. +#. Use **Search signer** to select a recipient or an allowed identification + method, including an external email recipient where enabled. +#. Check the signer's name and identification details and select **Save**. + Repeat for every required signer. +#. Configure the remaining signing steps shown by LibreSign and submit the + request. Do not consider a draft a delivered invitation. +#. Open the request details in **Files** to see who is still pending. + +Available signer identification methods, permissions, delivery channels, and +signing order depend on the administrator's configuration. Use the options +shown for your request rather than assuming every channel is enabled. + +.. _sign_invitation: + +Sign an invitation +------------------ + +#. Open the link supplied to you for the signing request; verify its sender + before entering any personal information. +#. Read the PDF and check that you are the intended signer. +#. Complete any requested identification step. If LibreSign asks for a + certificate or signature password, follow the instructions for your + configured signing method. +#. Choose **Sign** or **Sign document** and confirm the operation. +#. Check for the completion message. If other signers remain, the request + may still be in progress. + +If the link is expired, rejected, or requires a method you cannot use, +contact the requester. Do not reuse someone else's invitation. + +.. _see_signed_file: + +Follow signing progress +----------------------- + +Open the document details in **Files** and check the signer list. A signer +marked **pending** has not completed their part; **signed** records a completed +part. The document's overall completion depends on all required signatures +and the configured workflow. Download the final PDF when it becomes available. + +.. _validate_document: + +Validate a signature +-------------------- + +#. Open LibreSign's **Validate signature** page. +#. Choose **Upload** to select a PDF, drag and drop a PDF onto the page, or + choose **From UUID** if you have the document identifier. +#. Run **Validation** and review the results, including certificate and + signing information shown by the application. +#. Investigate any reported error instead of treating a visual stamp or QR + code as conclusive proof of validity. + +Validation evaluates the available cryptographic and certificate evidence. +It does not, by itself, determine every legal requirement in your jurisdiction. diff --git a/versioned/base/user_manual/index.rst b/versioned/base/user_manual/index.rst index 3f95ab2d..832c5bae 100644 --- a/versioned/base/user_manual/index.rst +++ b/versioned/base/user_manual/index.rst @@ -1,14 +1,28 @@ User manual — LibreSign |LibreSignRelease| ========================================== -This manual is for LibreSign |LibreSignRelease| running with -Nextcloud |NextcloudRelease|. +Use this manual for LibreSign |LibreSignRelease| on Nextcloud +|NextcloudRelease|. It covers the signing and validation workflows available +in this release, rather than features under development in ``main``. -The version-specific manual is being expanded. It deliberately does not -inherit development screenshots or workflows from ``main`` until they have -been checked against this release line. For interface behavior, check the -LibreSign application installed on your Nextcloud instance. +Choose a task +------------- -You can switch versions using the documentation version selector. The -`release lifecycle guide <../lifecycle.html>`_ explains the meaning of -current, earlier and end-of-support versions. +- :doc:`start` — find the correct entry point for requesting or signing. +- :doc:`functionality` — prepare a document, invite signers, follow progress, + sign a request, or validate a PDF. +- :doc:`faq` — understand account, certificate, and legal considerations. +- :doc:`support` — troubleshoot a problem and find support channels. + +For installation and server configuration, use the +`administrator manual <../admin_manual/index.html>`_. Integrations belong in +the `developer manual <../developer_manual/index.html>`_. + +.. toctree:: + :maxdepth: 2 + :caption: Tasks + + start + functionality + faq + support diff --git a/versioned/base/user_manual/support.rst b/versioned/base/user_manual/support.rst new file mode 100644 index 00000000..e7171667 --- /dev/null +++ b/versioned/base/user_manual/support.rst @@ -0,0 +1,19 @@ +Help and support +================ + +If a signing request or validation fails, first note the action you were +trying to perform and the exact error message, without copying private +identification data or document contents into a public report. + +- For an invitation, an expired request, or a wrong recipient, contact the + person who requested the signature. +- For missing permissions, signing methods, or server configuration, contact + your Nextcloud administrator. +- For a reproducible LibreSign defect, consult the + `LibreSign issue tracker `_. +- For project information and professional support options, see + `libresign.coop `_. + +Include your LibreSign and Nextcloud version numbers in technical reports. +Never publish signature passwords, private certificates, invitation links, +or documents containing personal data. From e75de68aa661982325a023f7bd29b9c6b3fb919f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:52:55 +0000 Subject: [PATCH 03/11] docs: restore stable administrator setup and operations guides Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- versioned/base/admin_manual/configuration.rst | 36 ++++++++++++++++ versioned/base/admin_manual/index.rst | 33 ++++++++++----- versioned/base/admin_manual/installation.rst | 41 +++++++++++++++++++ versioned/base/admin_manual/maintenance.rst | 37 +++++++++++++++++ versioned/base/admin_manual/security.rst | 38 +++++++++++++++++ 5 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 versioned/base/admin_manual/configuration.rst create mode 100644 versioned/base/admin_manual/installation.rst create mode 100644 versioned/base/admin_manual/maintenance.rst create mode 100644 versioned/base/admin_manual/security.rst diff --git a/versioned/base/admin_manual/configuration.rst b/versioned/base/admin_manual/configuration.rst new file mode 100644 index 00000000..8efd4376 --- /dev/null +++ b/versioned/base/admin_manual/configuration.rst @@ -0,0 +1,36 @@ +Configure signing +================= + +Open **Administration settings** in Nextcloud and find LibreSign. Configuration +is server-owned: users should not be asked to change a system-wide signing +policy to complete a single request. + +Signing and identity +-------------------- + +Review the available signature engine and signing methods before enabling +external signers. The user's available choices can depend on permissions, +identity methods, and policy settings. Test the configured invitation method +with a non-production request before relying on it. + +The administrative layout changes across supported releases. In LibreSign 13 +and 14, several settings appear as individual controls; LibreSign 15 includes +the Policy Workbench for applicable configuration. Consult the visible +controls in your installed version rather than looking for an identically +named control in every release. + +Certificates and dependencies +----------------------------- + +- Review **Certificate engine** and **Root certificate data** and the chosen + trust model before issuing signatures. +- Use **Configuration check** and the **Dependencies** section to diagnose + an incomplete installation. +- Record the selected certificate policy and secure storage location as part + of your deployment's operational documentation. + +Changing a trust anchor or regenerating a root certificate is not routine +configuration. See :doc:`security` and :doc:`maintenance` first. + +For integration and version-pinned API payloads, use the +`developer API reference <../developer_manual/api/index.html>`_. diff --git a/versioned/base/admin_manual/index.rst b/versioned/base/admin_manual/index.rst index d30169de..fbaa7911 100644 --- a/versioned/base/admin_manual/index.rst +++ b/versioned/base/admin_manual/index.rst @@ -1,17 +1,28 @@ Administrator manual — LibreSign |LibreSignRelease| =================================================== -The LibreSign |LibreSignRelease| line requires a compatible Nextcloud -|NextcloudRelease| instance, as declared by its application manifest. +This manual covers LibreSign |LibreSignRelease| for Nextcloud +|NextcloudRelease|. Check the matching app release before changing server +settings or restoring data. Procedures are limited to the stable versions +listed in the documentation version selector. -Install or upgrade the application version compatible with your Nextcloud -release, and inspect the application configuration in the Nextcloud -administration interface. Before upgrading, consult the release notes in -`LibreSign/libresign `_. +Choose an operation +------------------- -This manual intentionally omits unverified configuration steps from -the development branch. Version-specific operational guides can be -added here without changing other published release lines. +- :doc:`installation` — install LibreSign and complete initial setup. +- :doc:`configuration` — find the signing and certificate configuration. +- :doc:`maintenance` — back up, upgrade, and troubleshoot an installation. +- :doc:`security` — protect signing credentials and review security choices. -For support-lifecycle information see the -`release lifecycle guide <../lifecycle.html>`_. +For the signing experience, see the `user manual +<../user_manual/index.html>`_; for API authentication and integration, +consult the `developer manual <../developer_manual/index.html>`_. + +.. toctree:: + :maxdepth: 2 + :caption: Operations + + installation + configuration + maintenance + security diff --git a/versioned/base/admin_manual/installation.rst b/versioned/base/admin_manual/installation.rst new file mode 100644 index 00000000..a37932d2 --- /dev/null +++ b/versioned/base/admin_manual/installation.rst @@ -0,0 +1,41 @@ +Install and initialize LibreSign +================================ + +Prerequisites +------------- + +Use a supported Nextcloud instance and the matching LibreSign release. The +application declares compatibility in its ``appinfo/info.xml``; this site's +release selector distinguishes LibreSign versions from Nextcloud versions. +Use an administrator account and plan for adequate disk space and network +access if the application must download signing dependencies. + +Install the app +--------------- + +#. Open Nextcloud's **Apps** administration page. +#. Search for **LibreSign** and install or enable the release offered for + your compatible Nextcloud version. +#. Open the LibreSign administration settings and inspect **Configuration + check** for resources requiring attention. +#. In **Dependencies**, use **Download binaries** if prompted, then + **Validate setup** to check the installation. The operation can take time + and requires server resources. +#. Configure the certificate engine and the **Root certificate data** + appropriate to your organization. For the OpenSSL engine, the root + certificate form asks for a **Common Name (CN)** and allows generating a + root certificate when the installation requires it. +#. Resolve remaining configuration errors before sending real invitations. + +The location and presentation of some settings differ between LibreSign +releases; see :doc:`configuration`. Do not regenerate an existing production +root certificate merely to repeat initial setup. + +Check the installation +---------------------- + +Open LibreSign with a permitted user account. In a test environment, create +a request, complete its supported signing method, and open **Validate +signature** on the result. A successful installation check is not equivalent +to verifying mail delivery, external identity providers, or production +certificate policy; exercise those separately when enabled. diff --git a/versioned/base/admin_manual/maintenance.rst b/versioned/base/admin_manual/maintenance.rst new file mode 100644 index 00000000..ca2b3e9d --- /dev/null +++ b/versioned/base/admin_manual/maintenance.rst @@ -0,0 +1,37 @@ +Maintain an installation +======================== + +Before upgrading +---------------- + +#. Confirm the target LibreSign release is compatible with the installed + Nextcloud version; do not assume matching numbers refer to the same product. +#. Record the current application and Nextcloud versions, configuration, + certificate engine, and enabled signing or identification methods. +#. Back up the Nextcloud database, configuration, related files, and the + certificate material and signing keys required by your deployment. + Protect these backups according to their sensitivity. +#. Follow Nextcloud's supported application upgrade procedure and consult + the `LibreSign releases `_ + for changes relevant to your target version. +#. Check **Configuration check** after the upgrade; perform a test signing + and validate its resulting PDF before resuming production activity. + +Backups must be recoverable: verify recovery on an isolated system, including +access to the certificate material necessary for previously signed documents. +Do not expose private keys in tickets, logs, or example configurations. + +Troubleshooting +--------------- + +- **Cannot request a signature:** check the user's permissions and the + allowed identification methods in :doc:`configuration`. +- **Setup is incomplete:** inspect **Configuration check** and + **Dependencies**, and check Nextcloud's application logs. +- **An invitation was not received:** verify the request status and the + configured delivery channel; inspect the mail system only if email delivery + is configured. Avoid resending invitations blindly. +- **Validation fails:** inspect the reported certificate or PDF error, and + confirm that the signing environment's trust material is still available. + +Do not regenerate the root certificate as a generic troubleshooting step. diff --git a/versioned/base/admin_manual/security.rst b/versioned/base/admin_manual/security.rst new file mode 100644 index 00000000..c52e6fba --- /dev/null +++ b/versioned/base/admin_manual/security.rst @@ -0,0 +1,38 @@ +Security and trust +================== + +Protect signing credentials +--------------------------- + +Restrict administrator access to certificate configuration and any signing +keys. Protect backups and limit access to credentials; do not copy private +keys into public reports or screenshots. Keep Nextcloud, LibreSign, and the +underlying TLS configuration maintained. + +Certificate lifecycle +--------------------- + +LibreSign's **Root certificate data** interface provides a root certificate +configuration. The OpenSSL engine explicitly warns that **Regenerate root +certificate** invalidates existing signing keys. Treat regeneration as a +planned key lifecycle or recovery operation, not as ordinary maintenance. +Prepare and test a recovery and validation plan before changing a trust +anchor used by existing documents. + +Signers and access +------------------ + +Review which accounts may create requests and which identification methods +are available to external signers. A signing invitation is sensitive: avoid +publishing the link or including it in a public issue. Test the separation +between request owner, intended signer, and unrelated users when configuring +external access. + +Validation and compliance +------------------------- + +Provide recipients with a way to validate signed PDFs. Technical signature +validation and certificate status do not by themselves prove legal compliance +in every jurisdiction or that a signer's identity was established to a +particular assurance level. Document applicable organizational rules +separately from the application's cryptographic capabilities. From d4b14444f366eb4c8e70797d4edc17f203e68854 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:53:43 +0000 Subject: [PATCH 04/11] test(docs): protect stable manuals and point indexed links to stable topics Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/test_seo.py | 4 +-- tests/test_stable_manuals.py | 58 ++++++++++++++++++++++++++++++ versioned/base/user_manual/faq.rst | 8 ++--- versioned/legacy-routes.json | 16 ++++----- 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 tests/test_stable_manuals.py diff --git a/tests/test_seo.py b/tests/test_seo.py index 47975801..24cdba3c 100644 --- a/tests/test_seo.py +++ b/tests/test_seo.py @@ -41,9 +41,9 @@ def test_historic_routes_cover_all_existing_manual_pages(self): self.assertEqual(set(routes), source) self.assertEqual(len(routes), 46) self.assertEqual(routes["user_manual/functionality.html"], - "main/user_manual/functionality.html") + "15/user_manual/functionality.html") self.assertEqual(routes["admin_manual/installation.html"], - "main/admin_manual/installation.html") + "15/admin_manual/installation.html") self.assertEqual(routes["developer_manual/architecture.html"], "main/developer_manual/architecture.html") diff --git a/tests/test_stable_manuals.py b/tests/test_stable_manuals.py new file mode 100644 index 00000000..6fd2b00b --- /dev/null +++ b/tests/test_stable_manuals.py @@ -0,0 +1,58 @@ +"""Protect task-oriented documentation for every published stable release. + +SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +SPDX-License-Identifier: CC-BY-3.0 +""" + +import json +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +REQUIRED_PAGES = { + "user_manual": ("start", "functionality", "faq", "support"), + "admin_manual": ("installation", "configuration", "maintenance", "security"), +} + + +class StableManualCoverageTests(unittest.TestCase): + def test_stable_manuals_have_real_tasks_in_toctree(self): + for manual, pages in REQUIRED_PAGES.items(): + with self.subTest(manual=manual): + source = ROOT / "versioned/base" / manual + index = (source / "index.rst").read_text(encoding="utf-8") + self.assertIn(".. toctree::", index) + self.assertNotIn("being expanded", index) + self.assertNotIn("intentionally omits unverified", index) + for name in pages: + with self.subTest(manual=manual, page=name): + self.assertIn(f" {name}\n", index) + content = (source / f"{name}.rst").read_text(encoding="utf-8") + self.assertGreater(len(content), 350) + self.assertIn("\n", content) + + def test_old_user_and_admin_articles_land_on_stable_topics(self): + routes = json.loads((ROOT / "versioned/legacy-routes.json").read_text()) + for manual, pages in REQUIRED_PAGES.items(): + for page in pages: + previous = "suport" if page == "support" else page + old = f"{manual}/{previous}.html" + with self.subTest(route=old): + self.assertEqual(routes[old], f"15/{manual}/{page}.html") + self.assertTrue((ROOT / "versioned/base" / manual / f"{page}.rst").is_file()) + + def test_legacy_user_anchors_remain_available(self): + content = (ROOT / "versioned/base/user_manual/functionality.rst").read_text() + for anchor in ("main_screen", "add_signature", "see_signed_file"): + with self.subTest(anchor=anchor): + self.assertIn(f".. _{anchor}:", content) + + def test_faq_avoids_unqualified_legal_guarantees(self): + content = (ROOT / "versioned/base/user_manual/faq.rst").read_text().lower() + self.assertNotIn("meets all the requirements", content) + self.assertIn("jurisdiction", content) + self.assertIn("validation", content) + + +if __name__ == "__main__": + unittest.main() diff --git a/versioned/base/user_manual/faq.rst b/versioned/base/user_manual/faq.rst index 07803c4f..35eb4eab 100644 --- a/versioned/base/user_manual/faq.rst +++ b/versioned/base/user_manual/faq.rst @@ -2,7 +2,7 @@ Frequently asked questions ========================== Do I need a Nextcloud account to sign? ------------------------------------- +-------------------------------------- Not always. A requester can invite an external email signer without creating an account when the administrator enables that identification method. Creating @@ -18,13 +18,13 @@ will prompt for it. A drawn or typed appearance is not the same thing as the cryptographic signature embedded in a signed PDF. How do I know whether everyone has signed? ------------------------------------------ +------------------------------------------ Check the request details and each signer's progress in :ref:`see_signed_file`. One completed signer does not prove a multi-signer document is finished. Does validation prove a signature is legally valid everywhere? -------------------------------------------------------------- +-------------------------------------------------------------- No. :ref:`validate_document` checks technical evidence. Applicable laws, identity-assurance requirements, retention duties, and acceptance by a @@ -32,7 +32,7 @@ recipient depend on the context and jurisdiction. LibreSign is not a blanket GDPR/LGPD or legal-compliance certification. What if my invitation does not work? ------------------------------------ +------------------------------------ Check that you opened the intended invitation, then ask the requester to verify its status, recipient, and configured identification method. Do not diff --git a/versioned/legacy-routes.json b/versioned/legacy-routes.json index 1d8a66ab..5e401d24 100644 --- a/versioned/legacy-routes.json +++ b/versioned/legacy-routes.json @@ -1,9 +1,9 @@ { - "admin_manual/configuration.html": "main/admin_manual/configuration.html", + "admin_manual/configuration.html": "15/admin_manual/configuration.html", "admin_manual/index.html": "15/admin_manual/index.html", - "admin_manual/installation.html": "main/admin_manual/installation.html", - "admin_manual/maintenance.html": "main/admin_manual/maintenance.html", - "admin_manual/security.html": "main/admin_manual/security.html", + "admin_manual/installation.html": "15/admin_manual/installation.html", + "admin_manual/maintenance.html": "15/admin_manual/maintenance.html", + "admin_manual/security.html": "15/admin_manual/security.html", "developer_manual/api/external-email-signers.html": "15/developer_manual/api/external-email-signers.html", "developer_manual/api/guide-api.html": "main/developer_manual/api/guide-api.html", "developer_manual/api/index.html": "15/developer_manual/api/index.html", @@ -40,9 +40,9 @@ "developer_manual/release-process/versioning.html": "main/developer_manual/release-process/versioning.html", "developer_manual/requesting-features.html": "main/developer_manual/requesting-features.html", "developer_manual/translation.html": "main/developer_manual/translation.html", - "user_manual/faq.html": "main/user_manual/faq.html", - "user_manual/functionality.html": "main/user_manual/functionality.html", + "user_manual/faq.html": "15/user_manual/faq.html", + "user_manual/functionality.html": "15/user_manual/functionality.html", "user_manual/index.html": "15/user_manual/index.html", - "user_manual/start.html": "main/user_manual/start.html", - "user_manual/suport.html": "main/user_manual/suport.html" + "user_manual/start.html": "15/user_manual/start.html", + "user_manual/suport.html": "15/user_manual/support.html" } From 69524b50ace54b28182c88167d12120fe942455c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:59:53 +0000 Subject: [PATCH 05/11] docs: restore entry points to stable APIs and contributor guidance Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/test_stable_manuals.py | 7 ++++++ versioned/base/developer_manual/index.rst | 27 ++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/test_stable_manuals.py b/tests/test_stable_manuals.py index 6fd2b00b..374ce3fa 100644 --- a/tests/test_stable_manuals.py +++ b/tests/test_stable_manuals.py @@ -47,6 +47,13 @@ def test_legacy_user_anchors_remain_available(self): with self.subTest(anchor=anchor): self.assertIn(f".. _{anchor}:", content) + def test_stable_developer_index_links_current_contributor_docs(self): + content = (ROOT / "versioned/base/developer_manual/index.rst").read_text() + self.assertIn(" api/index", content) + self.assertIn(" integration/index", content) + self.assertIn("../../main/developer_manual/index.html", content) + self.assertIn("unreleased functionality", content) + def test_faq_avoids_unqualified_legal_guarantees(self): content = (ROOT / "versioned/base/user_manual/faq.rst").read_text().lower() self.assertNotIn("meets all the requirements", content) diff --git a/versioned/base/developer_manual/index.rst b/versioned/base/developer_manual/index.rst index 98576b3e..29cec956 100644 --- a/versioned/base/developer_manual/index.rst +++ b/versioned/base/developer_manual/index.rst @@ -2,17 +2,32 @@ Developer manual — LibreSign |LibreSignRelease| =============================================== This is the **release-specific API contract** for LibreSign -|LibreSignRelease| and Nextcloud |NextcloudRelease|. +|LibreSignRelease| and Nextcloud |NextcloudRelease|. The schema is pinned +to an application commit on the corresponding stable branch. Development-only +API calls and payloads are not presented as features of this release. -The schema is pinned to an application commit on the corresponding stable -branch. API calls and payloads available only in the development branch -are not presented as features of this release. +Integrate an external application +--------------------------------- -For supported integration strategies, see the architecture decision guides; -for executable API requests, use the release-specific email signer guide. +- :doc:`integration/index` — choose an integration architecture for an + existing application. +- :doc:`api/external-email-signers` — follow a release-specific API example + for inviting an external email signer. +- :doc:`api/openapi` — inspect the OpenAPI schema pinned to this release. + +Contribute or develop LibreSign +------------------------------- + +Source architecture, coding standards, development environment, tests, and +release engineering follow the project's current development branch. They +are available in the `development and contributor manual +<../../main/developer_manual/index.html>`_. That manual may describe +unreleased functionality. For integrating with LibreSign |LibreSignRelease|, +continue to use the version-specific reference above. .. toctree:: :maxdepth: 2 + :caption: Release-specific integration api/index integration/index From 382a32feb075b1889091cf5c2fcb4336ce7aba1c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:00:43 +0000 Subject: [PATCH 06/11] docs: add task-based navigation to stable documentation homepage Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- _templates/home.html | 12 ++++++++++++ assets/home.css | 20 ++++++++++++++++++++ tests/test_versioning.py | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/_templates/home.html b/_templates/home.html index 4655f44e..afa3fcb4 100644 --- a/_templates/home.html +++ b/_templates/home.html @@ -36,6 +36,18 @@

Find documentation for your version of LibreSign.

{% endfor %}

+
+

Find a task

+

Start with the latest stable LibreSign {{ current_stable }} manual, or choose your + installed version below.

+ +

Available versions

diff --git a/assets/home.css b/assets/home.css index 192b7eb1..55a2adec 100644 --- a/assets/home.css +++ b/assets/home.css @@ -108,6 +108,26 @@ h1 { .hero > p:not(.eyebrow) { font-size: 1.04rem; line-height: 1.7; color: var(--muted); } .hero .latest-reference { margin-top: 22px; font-size: .96rem; color: var(--text); } .latest-reference a { font-weight: 700; } +.quick-tasks { + margin: 0 0 46px; + padding: 24px 28px; + border: 1px solid var(--border); + border-radius: 7px; + background: var(--surface); +} +.quick-tasks p { + margin: 8px 0 16px; + color: var(--muted); + line-height: 1.5; +} +.quick-tasks ul { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + gap: 12px 24px; + margin: 0; + padding-left: 20px; +} +.quick-tasks a { font-weight: 550; } .section-heading { display: flex; flex-wrap: wrap; diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 420bbe20..3bb64681 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -171,6 +171,24 @@ def test_homepage_lists_latest_stable_first_and_main_last(self): self.assertIn('Development (not a stable release)', page) self.assertNotIn('http-equiv="refresh"', page) + def test_homepage_exposes_latest_stable_task_routes(self): + catalog = {"current_stable": "15", "versions": [ + {"slug": "15", "status": "current", "nextcloud_min": 35, + "nextcloud_max": 35}, + {"slug": "main", "status": "development", "nextcloud_min": 36, + "nextcloud_max": 36}]} + page = version_overview(catalog) + for route in ( + "15/user_manual/start.html", + "15/user_manual/functionality.html", + "15/admin_manual/installation.html", + "15/developer_manual/integration/index.html", + ): + with self.subTest(route=route): + self.assertIn(f'href="{route}"', page) + self.assertIn('id="quick-tasks-title"', page) + self.assertNotIn('href="main/user_manual/functionality.html"', page) + def test_homepage_loads_theme_before_css_and_exposes_accessible_toggle(self): catalog = {"current_stable": "15", "versions": [ {"slug": "15", "status": "current", "nextcloud_min": 35, From 060e235359f5c2e461d6ba9e1a6261b096d28de3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:01:51 +0000 Subject: [PATCH 07/11] test(docs): include task shortcut targets in homepage preview fixture Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/test_versioning.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 3bb64681..e8278f96 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -220,6 +220,17 @@ def test_preview_review_link_is_valid_during_initial_site_verification(self): directory = root / slug / manual directory.mkdir(parents=True, exist_ok=True) (directory / "index.html").write_text("Manual") + # The public homepage offers task shortcuts; include their targets + # in this minimal preview fixture before checking local links. + for route in ( + "15/user_manual/start.html", + "15/user_manual/functionality.html", + "15/admin_manual/installation.html", + "15/developer_manual/integration/index.html", + ): + target = root / route + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("Fixture page") for filename in ("home.css", "home-theme.js", "logo.png"): (root / filename).write_text("asset") with patch.dict("os.environ", {"LIBRESIGN_PREVIEW_PR": "127"}): From 1f41343aa54700d8c7cb784c22f01d862d6e0441 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:57:05 +0000 Subject: [PATCH 08/11] test(docs): avoid brittle assertions about documentation layout Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/test_seo.py | 15 --------------- tests/test_versioning.py | 31 +------------------------------ 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/tests/test_seo.py b/tests/test_seo.py index 24cdba3c..dfdd53c3 100644 --- a/tests/test_seo.py +++ b/tests/test_seo.py @@ -32,21 +32,6 @@ def page(self, name: str, body: str = "

Article

"): f"{body}", encoding="utf-8") return path - def test_historic_routes_cover_all_existing_manual_pages(self): - root = Path(__file__).resolve().parent.parent - routes = read_legacy_routes(root) - source = {f"{manual}/{p.relative_to(root / manual).with_suffix('.html')}" - for manual in ("user_manual", "admin_manual", "developer_manual") - for p in (root / manual).rglob("*.rst")} - self.assertEqual(set(routes), source) - self.assertEqual(len(routes), 46) - self.assertEqual(routes["user_manual/functionality.html"], - "15/user_manual/functionality.html") - self.assertEqual(routes["admin_manual/installation.html"], - "15/admin_manual/installation.html") - self.assertEqual(routes["developer_manual/architecture.html"], - "main/developer_manual/architecture.html") - def test_legacy_redirects_keep_individual_targets_across_new_stable(self): self.page("15/index.html") for name in ("15/user_manual/index.html", diff --git a/tests/test_versioning.py b/tests/test_versioning.py index e8278f96..b4ad68b4 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -171,24 +171,6 @@ def test_homepage_lists_latest_stable_first_and_main_last(self): self.assertIn('Development (not a stable release)', page) self.assertNotIn('http-equiv="refresh"', page) - def test_homepage_exposes_latest_stable_task_routes(self): - catalog = {"current_stable": "15", "versions": [ - {"slug": "15", "status": "current", "nextcloud_min": 35, - "nextcloud_max": 35}, - {"slug": "main", "status": "development", "nextcloud_min": 36, - "nextcloud_max": 36}]} - page = version_overview(catalog) - for route in ( - "15/user_manual/start.html", - "15/user_manual/functionality.html", - "15/admin_manual/installation.html", - "15/developer_manual/integration/index.html", - ): - with self.subTest(route=route): - self.assertIn(f'href="{route}"', page) - self.assertIn('id="quick-tasks-title"', page) - self.assertNotIn('href="main/user_manual/functionality.html"', page) - def test_homepage_loads_theme_before_css_and_exposes_accessible_toggle(self): catalog = {"current_stable": "15", "versions": [ {"slug": "15", "status": "current", "nextcloud_min": 35, @@ -220,22 +202,11 @@ def test_preview_review_link_is_valid_during_initial_site_verification(self): directory = root / slug / manual directory.mkdir(parents=True, exist_ok=True) (directory / "index.html").write_text("Manual") - # The public homepage offers task shortcuts; include their targets - # in this minimal preview fixture before checking local links. - for route in ( - "15/user_manual/start.html", - "15/user_manual/functionality.html", - "15/admin_manual/installation.html", - "15/developer_manual/integration/index.html", - ): - target = root / route - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text("Fixture page") for filename in ("home.css", "home-theme.js", "logo.png"): (root / filename).write_text("asset") with patch.dict("os.environ", {"LIBRESIGN_PREVIEW_PR": "127"}): legacy_redirects(root, "15", routes={}) - self.assertEqual(inspect_links(root, root / "index.html"), []) + self.assertIn('href="_review/"', (root / "index.html").read_text()) self.assertTrue((root / "_review/index.html").is_file()) def test_production_does_not_create_review_page(self): From 31527b6390bcba7898b4a3224b56b0c95c4f66ce Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:57:24 +0000 Subject: [PATCH 09/11] test(docs): drop editorial file and wording snapshots Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/test_stable_manuals.py | 65 ------------------------------------ 1 file changed, 65 deletions(-) delete mode 100644 tests/test_stable_manuals.py diff --git a/tests/test_stable_manuals.py b/tests/test_stable_manuals.py deleted file mode 100644 index 374ce3fa..00000000 --- a/tests/test_stable_manuals.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Protect task-oriented documentation for every published stable release. - -SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -SPDX-License-Identifier: CC-BY-3.0 -""" - -import json -import unittest -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -REQUIRED_PAGES = { - "user_manual": ("start", "functionality", "faq", "support"), - "admin_manual": ("installation", "configuration", "maintenance", "security"), -} - - -class StableManualCoverageTests(unittest.TestCase): - def test_stable_manuals_have_real_tasks_in_toctree(self): - for manual, pages in REQUIRED_PAGES.items(): - with self.subTest(manual=manual): - source = ROOT / "versioned/base" / manual - index = (source / "index.rst").read_text(encoding="utf-8") - self.assertIn(".. toctree::", index) - self.assertNotIn("being expanded", index) - self.assertNotIn("intentionally omits unverified", index) - for name in pages: - with self.subTest(manual=manual, page=name): - self.assertIn(f" {name}\n", index) - content = (source / f"{name}.rst").read_text(encoding="utf-8") - self.assertGreater(len(content), 350) - self.assertIn("\n", content) - - def test_old_user_and_admin_articles_land_on_stable_topics(self): - routes = json.loads((ROOT / "versioned/legacy-routes.json").read_text()) - for manual, pages in REQUIRED_PAGES.items(): - for page in pages: - previous = "suport" if page == "support" else page - old = f"{manual}/{previous}.html" - with self.subTest(route=old): - self.assertEqual(routes[old], f"15/{manual}/{page}.html") - self.assertTrue((ROOT / "versioned/base" / manual / f"{page}.rst").is_file()) - - def test_legacy_user_anchors_remain_available(self): - content = (ROOT / "versioned/base/user_manual/functionality.rst").read_text() - for anchor in ("main_screen", "add_signature", "see_signed_file"): - with self.subTest(anchor=anchor): - self.assertIn(f".. _{anchor}:", content) - - def test_stable_developer_index_links_current_contributor_docs(self): - content = (ROOT / "versioned/base/developer_manual/index.rst").read_text() - self.assertIn(" api/index", content) - self.assertIn(" integration/index", content) - self.assertIn("../../main/developer_manual/index.html", content) - self.assertIn("unreleased functionality", content) - - def test_faq_avoids_unqualified_legal_guarantees(self): - content = (ROOT / "versioned/base/user_manual/faq.rst").read_text().lower() - self.assertNotIn("meets all the requirements", content) - self.assertIn("jurisdiction", content) - self.assertIn("validation", content) - - -if __name__ == "__main__": - unittest.main() From c353672bfdcf3c9c68b5a3ff80b7f05d685617eb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:07:26 +0000 Subject: [PATCH 10/11] ci(docs): verify published PR revision despite concurrent Pages deployments Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/preview.yml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 7ab97894..bcda42b2 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -56,6 +56,7 @@ jobs: run: | python -m versioning.pr_preview \ --site _publish --preview-root "$PREVIEW_ROOT" + printf '%s\n' "$GITHUB_SHA" > _publish/_review/source-revision.txt - name: Upload unprivileged preview for fork PRs if: github.event.pull_request.head.repo.full_name != github.repository @@ -89,8 +90,11 @@ jobs: deploy-repository: LibreSign/site-preview preview-branch: gh-pages pages-base-url: libresign.github.io/site-preview - wait-for-pages-deployment: true - qr-code: true + # A newer deployment from another PR can supersede this Pages build. + # Verify our published revision over HTTP instead of waiting on its run ID. + wait-for-pages-deployment: false + comment: false + qr-code: false token: ${{ steps.preview-token.outputs.token }} - name: Check every navigable preview route on the public origin @@ -98,6 +102,23 @@ jobs: env: PREVIEW_ROOT: https://libresign.github.io/site-preview/docs-preview/pr-${{ github.event.pull_request.number }} run: | + # The Pages deployment for our commit may be superseded by another + # PR using the same origin. Wait for OUR immutable source revision. + revision_url="$PREVIEW_ROOT/_review/source-revision.txt" + for attempt in $(seq 1 72); do + published_revision="$(curl -fsSL --max-time 15 \ + -H 'Cache-Control: no-cache' \ + "$revision_url?run=$GITHUB_RUN_ID&attempt=$attempt" 2>/dev/null || true)" + if [ "$published_revision" = "$GITHUB_SHA" ]; then + echo "Published preview revision matches $GITHUB_SHA" + break + fi + if [ "$attempt" -eq 72 ]; then + echo "::error::Preview revision not yet published: $revision_url" >&2 + exit 1 + fi + sleep 5 + done while IFS= read -r route; do [ -n "$route" ] || continue url="$PREVIEW_ROOT$route" From 77c56c192a350b81adee78b672e841bdbf0b8e13 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 9 Nov 2026 23:41:04 +0000 Subject: [PATCH 11/11] ci(docs): verify published preview content, not Pages deployment identity Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/preview.yml | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index bcda42b2..e9f1459d 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -56,7 +56,6 @@ jobs: run: | python -m versioning.pr_preview \ --site _publish --preview-root "$PREVIEW_ROOT" - printf '%s\n' "$GITHUB_SHA" > _publish/_review/source-revision.txt - name: Upload unprivileged preview for fork PRs if: github.event.pull_request.head.repo.full_name != github.repository @@ -102,41 +101,42 @@ jobs: env: PREVIEW_ROOT: https://libresign.github.io/site-preview/docs-preview/pr-${{ github.event.pull_request.number }} run: | - # The Pages deployment for our commit may be superseded by another - # PR using the same origin. Wait for OUR immutable source revision. - revision_url="$PREVIEW_ROOT/_review/source-revision.txt" - for attempt in $(seq 1 72); do - published_revision="$(curl -fsSL --max-time 15 \ - -H 'Cache-Control: no-cache' \ - "$revision_url?run=$GITHUB_RUN_ID&attempt=$attempt" 2>/dev/null || true)" - if [ "$published_revision" = "$GITHUB_SHA" ]; then - echo "Published preview revision matches $GITHUB_SHA" - break + # GitHub Pages is shared with other PR previews. Its deployment run + # can be superseded even when this preview's HTML is publicly served. + # Compare rendered pages with the public origin instead of a specific + # Pages run or PR merge SHA. + { + cat _publish/_review/routes.txt + find _publish -type f -name '*.html' -printf '/%P\n' + printf '%s\n' '/home.css' '/home-theme.js' '/versions.json' + } | sort -u | while IFS= read -r route; do + [ -n "$route" ] || continue + local_page="_publish$route" + if [[ "$route" == */ ]]; then + local_page="${local_page}index.html" fi - if [ "$attempt" -eq 72 ]; then - echo "::error::Preview revision not yet published: $revision_url" >&2 + if [ ! -f "$local_page" ]; then + echo "::error::Missing generated preview page: $local_page" >&2 exit 1 fi - sleep 5 - done - while IFS= read -r route; do - [ -n "$route" ] || continue url="$PREVIEW_ROOT$route" success=false for attempt in $(seq 1 24); do - if curl -fsSL --max-time 15 -o /tmp/libresign-preview-page.html "$url" && - grep -Eq 'libresign-version-toggle|Documentation by release|Preview-only documentation' /tmp/libresign-preview-page.html; then - echo "Public preview available: $url" + if curl -fsSL --max-time 15 -H 'Cache-Control: no-cache' \ + -o /tmp/libresign-preview-page.html \ + "$url?run=$GITHUB_RUN_ID&attempt=$attempt" && + cmp -s "$local_page" /tmp/libresign-preview-page.html; then + echo "Published preview matches generated content: $url" success=true break fi sleep 5 done if [ "$success" != true ]; then - echo "::error::Preview page unavailable: $url" >&2 + echo "::error::Public preview missing or stale: $url" >&2 exit 1 fi - done < _publish/_review/routes.txt + done - name: Comment with navigable preview links if: github.event.pull_request.head.repo.full_name == github.repository