diff --git a/.forgejo/workflows/documentation.yml b/.forgejo/workflows/documentation.yml
deleted file mode 100644
index 5ccba1ce2..000000000
--- a/.forgejo/workflows/documentation.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Publish docs
-
-# Docs deploy ONLY from the dedicated `documentation` branch — decoupled from main/development
-# so doc edits never trigger releases and code releases never trigger doc builds. No cron.
-on:
- push:
- branches: [documentation]
- pull_request:
- branches: [documentation]
- workflow_dispatch:
-
-jobs:
- build:
- uses: Conduction/.github/.forgejo/workflows/documentation-build.yml@main
- with:
- source-folder: docs
- secrets: inherit
-
- deploy:
- needs: build
- if: github.event_name != 'pull_request'
- uses: Conduction/.github/.forgejo/workflows/documentation-deploy.yml@main
- with:
- cf-project-name: launchpad-docs
- secrets: inherit
diff --git a/.forgejo/workflows/l10n-parity.yml b/.forgejo/workflows/l10n-parity.yml
deleted file mode 100644
index 3fbbf583a..000000000
--- a/.forgejo/workflows/l10n-parity.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# Translation-PARITY hard gate.
-#
-# Asserts every required locale (nl/de/fr/es/it) is at full parity with the
-# English source — no missing keys, no empty values — for both the frontend
-# (l10n/*.js) and backend (l10n/*.json) translation sets. Without this, a new
-# English string ships and the other languages silently fall back to English.
-# Pure Node, no NC, no npm install.
-name: l10n-parity
-
-on:
- pull_request:
- push:
- branches: [main, master, development]
-
-jobs:
- l10n-parity:
- name: l10n translation parity (nl/de/fr/es/it)
- runs-on: codeberg-small
- container:
- image: node:20-alpine
- steps:
- - name: Checkout
- uses: https://code.forgejo.org/actions/checkout@v4
- - name: Assert every required locale is at full parity
- run: node tests/l10n/check-l10n-parity.js
diff --git a/.forgejo/workflows/pre-merge-check-strict.yaml b/.forgejo/workflows/pre-merge-check-strict.yaml
deleted file mode 100644
index 863f2b03e..000000000
--- a/.forgejo/workflows/pre-merge-check-strict.yaml
+++ /dev/null
@@ -1,70 +0,0 @@
-# Pre-merge quality gate — enforced lint + phpcs + all Hydra gates on every PR.
-# Required status check on protected branches.
-#
-# Runner/container mirror the proven release-semrel workflow: codeberg-medium +
-# official php:8.3-cli + a base-tooling step. The old code.forgejo.org/oci/ci-php:8.3
-# image 404s ("manifest unknown"), which fast-failed every run at container-pull.
-#
-# The gate runs `composer lint` + `composer phpcs` directly: check:strict's
-# psalm/phpstan/phpmd/test:all are wrapped in `|| echo skipping` so they never
-# affect pass/fail (ADR-022 parks static analysis), and running them on the medium
-# runner OOMs it. lint+phpcs is the identical enforced gate, fast and deterministic.
-
-name: pre-merge-check-strict
-
-on:
- pull_request:
- branches:
- - development
- - main
- - beta
-
-jobs:
- quality-gates:
- runs-on: codeberg-medium
- container:
- image: php:8.3-cli
- timeout-minutes: 15
- steps:
- - name: Install base tooling
- run: |
- apt-get update
- apt-get install -y --no-install-recommends \
- git curl ca-certificates gnupg jq unzip zip \
- libzip-dev libpng-dev python3
- # Node is required by actions/checkout@v4 (a JS action) which runs
- # inside this php:8.3-cli container; the stock image ships no node.
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
- apt-get install -y --no-install-recommends nodejs
- docker-php-ext-install -j"$(nproc)" zip gd
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
-
- - name: Checkout PR
- uses: https://github.com/actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Install composer deps
- run: composer install --no-interaction --no-progress --prefer-dist --ignore-platform-reqs
-
- - name: Run lint + phpcs (the enforced gate)
- run: |
- composer lint
- composer phpcs
-
- - name: Clone Hydra (for gate runner)
- uses: https://github.com/actions/checkout@v4
- with:
- repository: Conduction/hydra
- ref: development
- path: .hydra
-
- - name: Run all Hydra gates (diff-scoped per ADR-020)
- run: |
- git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} || true
- bash .hydra/scripts/run-hydra-gates.sh --scope-to-diff --base origin/${{ github.base_ref }} .
-
- - name: Gate-19 e2e coverage report (informational)
- if: always()
- run: |
- python3 .hydra/scripts/lib/check_e2e_coverage.py . --mode report || true
diff --git a/.forgejo/workflows/release-beta.yml b/.forgejo/workflows/release-beta.yml
deleted file mode 100644
index ed8215b18..000000000
--- a/.forgejo/workflows/release-beta.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-name: Beta Release
-
-on:
- push:
- branches: [beta]
- workflow_dispatch:
-
-jobs:
- release:
- uses: Conduction/.github/.forgejo/workflows/release-semrel-beta.yml@main
- with:
- app-name: launchpad
- secrets: inherit
diff --git a/.forgejo/workflows/release-stable.yml b/.forgejo/workflows/release-stable.yml
deleted file mode 100644
index 2f54bf1eb..000000000
--- a/.forgejo/workflows/release-stable.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: Stable Release
-
-on:
- push:
- branches: [main]
- workflow_dispatch:
-
-jobs:
- release:
- uses: Conduction/.github/.forgejo/workflows/release-stable.yml@main
- with:
- app-name: launchpad
- secrets:
- CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
- NEXTCLOUD_SIGNING_KEY: ${{ secrets.NEXTCLOUD_SIGNING_KEY }}
- NEXTCLOUD_SIGNING_CERT: ${{ secrets.NEXTCLOUD_SIGNING_CERT }}
- NEXTCLOUD_APPSTORE_TOKEN: ${{ secrets.NEXTCLOUD_APPSTORE_TOKEN }}
diff --git a/.forgejo/issue_template/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
similarity index 100%
rename from .forgejo/issue_template/bug-report.yml
rename to .github/ISSUE_TEMPLATE/bug-report.yml
diff --git a/.forgejo/issue_template/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
similarity index 100%
rename from .forgejo/issue_template/feature-request.yml
rename to .github/ISSUE_TEMPLATE/feature-request.yml
diff --git a/.forgejo/issue_template/technical-task.yml b/.github/ISSUE_TEMPLATE/technical-task.yml
similarity index 100%
rename from .forgejo/issue_template/technical-task.yml
rename to .github/ISSUE_TEMPLATE/technical-task.yml
diff --git a/.forgejo/issue_template/user-story.yml b/.github/ISSUE_TEMPLATE/user-story.yml
similarity index 100%
rename from .forgejo/issue_template/user-story.yml
rename to .github/ISSUE_TEMPLATE/user-story.yml
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d33d37d06..1ffe9d367 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,6 +4,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
+ target-branch: "development"
open-pull-requests-limit: 10
cooldown:
default-days: 1
@@ -29,6 +30,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
+ target-branch: "development"
open-pull-requests-limit: 10
cooldown:
default-days: 2
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index 1b06230df..396725e33 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -68,7 +68,47 @@ on:
# Proven in openconnector#1158: its first-ever completed `development` push run
# (31048998594) executed Coverage Baseline Check, SBOM and Features Extract.
concurrency:
- group: quality-${{ github.head_ref || github.ref_name }}${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'development')) && '-push' || '' }}
+ # SUFFIXED BY EVENT NAME, not just by `-push`.
+ #
+ # The previous expression gave a push on `development` its own lane
+ # (`-push`) but left EVERYTHING ELSE sharing `quality-development` — and
+ # that is not a quiet lane: `Sync to Beta` keeps a PR open whose head_ref
+ # IS `development`, so its run computes the same group and is re-triggered
+ # on every merge.
+ #
+ # A `workflow_dispatch` therefore shared a group with that PR and was
+ # cancelled by it. Measured on shillinq 2026-08-21: dispatch 32487948678
+ # cancelled by pull_request run 32490160836 (head_branch `development`).
+ # A run someone deliberately asked for could essentially never complete.
+ #
+ # That reaches past ad-hoc verification: the fleet gate-drift sweep
+ # (.github#523) dispatches per app with `--ref development`, because
+ # `schedule:` cannot choose a branch. Under the old group those runs are
+ # cancelled and report neither pass nor fail — and a routine that produces
+ # no verdict is indistinguishable from one that never ran.
+ #
+ # This is hermiq's form, already live there. Pull requests keep the bare
+ # group (so a PR still supersedes its own earlier run); push, dispatch and
+ # schedule each get their own lane.
+ #
+ # THE BRANCH RESTRICTION IS GONE, because it contradicted the sentence above.
+ #
+ # The suffix used to apply only when `ref_name` was `main` or `development`,
+ # so on every OTHER branch push and pull_request computed the SAME group —
+ # and `cancel-in-progress` made them kill each other. That became reachable
+ # when the push allow-list widened on 2026-08-14 to include `feat/**`,
+ # `fix/**`, `perf/**`, `refactor/**` and `chore/**`: those branches now get
+ # both a push run and a pull_request run for one commit.
+ #
+ # `quality / Quality Report` is a `needs:`-gated aggregator and reports
+ # FAILURE when its dependencies are CANCELLED, so the collision shows up as a
+ # red gate on a PR that was never actually evaluated — and re-running collides
+ # the same way. Measured on openregister#2821: a push run left queued and a
+ # pull_request run cancelled, 18 seconds apart, on one commit.
+ #
+ # A branch name is not a unique lane when two event types can each produce a
+ # run for it, so the event is now always part of the key.
+ group: quality-${{ github.head_ref || github.ref_name }}${{ github.event_name != 'pull_request' && format('-{0}', github.event_name) || '' }}
cancel-in-progress: true
# Permission CEILING for the called quality pipeline. GitHub statically
@@ -216,7 +256,12 @@ jobs:
# Measured on this tree before enabling: PASSES, 237 of 279 tracked
# frontend files in scope (l10n/ and docs/ excluded via .prettierignore /
# .gitignore, which prettier 3 also reads).
- frontend-checks: '["check:manifest", "format"]'
+ # `check:schema-l10n` is a RATCHET, not a gate. Every string inside a form
+ # comes from the schema and is a key in THIS app's catalogue; an absent key
+ # renders the English source inside an otherwise translated form, silently.
+ # The fleet had 30,459 such strings, so this records the current count and
+ # fails only when it GROWS — burning it down stays an ordinary PR.
+ frontend-checks: '["check:manifest", "format", "check:schema-l10n"]'
# ── Coverage ratchet ─────────────────────────────────────────────────
# `enable-coverage-guard` defaults to FALSE, which is why both
diff --git a/.github/workflows/openspec-sync.yml b/.github/workflows/openspec-sync.yml
new file mode 100644
index 000000000..cd5de2c3a
--- /dev/null
+++ b/.github/workflows/openspec-sync.yml
@@ -0,0 +1,51 @@
+name: OpenSpec Sync
+
+# RESTORED, on evidence that the condition which justified dropping it is gone.
+#
+# This caller was removed from `development` in #42 on 2026-08-04 for a good
+# reason: every run in its history was a startup failure reporting `jobs=0`.
+# That is the signature of a reusable workflow that never resolved — no log, no
+# step, no verdict, just a permanently red branch. As #42 put it, a
+# permanently-red workflow is worse than an absent one, because the next
+# genuine failure lands on an already-red branch and cannot be told apart from
+# the standing noise. That argument was correct.
+#
+# What has changed since:
+#
+# 1. The unresolvable ref was FIXED — on `main`, by 9a695ae on 2026-08-03
+# ("fix all 8 shared-workflow callers on main (wrong org)"), one day
+# BEFORE #42 dropped this file from development. The fix and the removal
+# crossed: `main` got the repair, `development` got the deletion, and
+# because main is 345 commits behind development the two never met.
+#
+# 2. It has now been observed working. Dispatched against main on
+# 2026-08-21, this exact caller ran to success and created 12 OpenSpec
+# issues — the first non-zero result this workflow has ever produced.
+#
+# On the PROJECT_TOKEN: #42 and openregister#2111 both attributed the failure
+# to an expired project-board PAT. That is not what blocks it here — launchpad
+# has no PROJECT_TOKEN secret at all, and the 2026-08-21 run succeeded anyway.
+# The token is for the project BOARD; the issues themselves are written with
+# the workflow's own github.token. The line below is kept so the board sync
+# starts working by itself if a token is ever added, and is harmless while the
+# secret is absent.
+#
+# Why it belongs on `development` and not only on `main`: the push trigger
+# below watches `development`, and GitHub reads the workflow file from the
+# branch being pushed. With the file only on main, that trigger could never
+# fire — which is why the sync had to be kicked by hand and why only main's 16
+# changes were covered while development's 28 went unsynced.
+
+on:
+ push:
+ branches: [development]
+ paths: ['openspec/**']
+ workflow_dispatch:
+
+jobs:
+ sync:
+ uses: ConductionNL/.github/.github/workflows/openspec-sync.yml@main
+ with:
+ app-name: launchpad
+ secrets:
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 33e4d687c..5f731da4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,3 +75,9 @@ bom-npm.cdx.json
# Local MCP server configuration — carries API keys, must never be committed.
# A live n8n API key reached the tip tree of 37 local branches before this was added.
.mcp.json
+
+# Agent/test scratch and tool caches — generated, never source.
+# Added by the 2026-08-25 fleet hygiene sweep (ADR-100 Decision 2).
+.stale/
+/.e2e-state/
+.phpunit.cache
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 2b32e4ecc..20b207678 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -52,25 +52,25 @@ Ideaal voor organisaties die consistente, samengestelde dashboards willen voor h
Vrij en open source onder de EUPL-1.2-licentie.
]]>
- 1.0.13-unstable.20260820060123
+ 1.0.13-unstable.20260825094451
EUPL-1.2
Conduction
LaunchPad
- https://codeberg.org/Conduction/launchpad
- https://codeberg.org/Conduction/launchpad
- https://codeberg.org/Conduction/launchpad
+ https://github.com/ConductionNL/launchpad
+ https://github.com/ConductionNL/launchpad
+ https://github.com/ConductionNL/launchpad
customization
organization
dashboard
- https://codeberg.org/Conduction/launchpad
- https://codeberg.org/Conduction/launchpad/issues
- https://codeberg.org/Conduction/launchpad
+ https://github.com/ConductionNL/launchpad
+ https://github.com/ConductionNL/launchpad/issues
+ https://github.com/ConductionNL/launchpad
- https://codeberg.org/Conduction/launchpad/raw/branch/main/img/screenshot-dashboard.png
- https://codeberg.org/Conduction/launchpad/raw/branch/main/img/screenshot-widgets.png
- https://codeberg.org/Conduction/launchpad/raw/branch/main/img/screenshot-admin.png
+ https://raw.githubusercontent.com/ConductionNL/launchpad/main/img/screenshot-dashboard.png
+ https://raw.githubusercontent.com/ConductionNL/launchpad/main/img/screenshot-widgets.png
+ https://raw.githubusercontent.com/ConductionNL/launchpad/main/img/screenshot-admin.png