Skip to content

feat(announcements): targeted news and notices with preview, likes, comments and category follows - #788

Open
rubenvdlinde wants to merge 56 commits into
developmentfrom
feat/engagement-announcements
Open

rubenvdlinde wants to merge 56 commits into
developmentfrom
feat/engagement-announcements

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What this does

Change engagement-announcements (rows e-announcements, e-maintenance, d-news-preview). Stacked on #787 (feat/launcher-tile-internal-address): this branch contains every PR below it (#743, #784 to #787). Land the stack in order, or land the top PR with a merge commit and the ones below close with it.

  • Authors (administrators, plus the groups set on the new "Announcements" admin tab) write a news item or a notice with a title, text, category, target groups, a publish time and an end time. Drafts are visible to authors only.
  • The server checks targeting on every read: a published item is visible to people in its target groups (everyone when none are set) inside its window. Anyone else gets 404 by id, so an id leaks nothing.
  • News shows in a new "Announcements" widget, newest first, with a category filter, like and comment counts and a follow button per category. Likes and comments are stored by Nextcloud's comments service (object type launchpad_announcement); LaunchPad keeps no comment rows.
  • A notice shows as a banner above the grid on every dashboard of the people it targets, until its end time. Its level shows as an icon and a word. A dismissible notice can be closed per person; a notice cannot be published without an end time ("A notice needs an end time").
  • The editor saves a draft, then shows a preview with the same banner or card readers see, "People reached: N" and "Preview as" for one user. Publishing is a separate button after the preview.
  • Followers of a category get one Nextcloud notification ("New announcement in Privacy: ...") when an item in it becomes visible to them: at once when published for now, otherwise from AnnouncementNotifyJob (every 5 minutes), never twice (notified_at).
  • New tables launchpad_announcements and launchpad_ann_follows (migration Version002013Date20261005190000, info.xml 1.1.7-unstable.20261005200000). A deleted user's follows go with them. Demo installs get the three demo announcements from the design.
  • Strings in all 37 locales. The spec is synced to openspec/specs/announcements/spec.md; the design records what the build changed (short table name, likes as comments with verb like, the reach line).

The rows move to building, not built: tasks 8 (axe check) and 12 (Playwright, written in tests/e2e/announcements.spec.ts) need the live instance. The change is archived after the live pass.

Tests (red first)

  • tests/Unit/Service/AnnouncementServiceTest.php (12): targeting, drafts, the window, author rights, the notice end time, comment refusals, follower notified once, "Preview as". Red run without the service: 12 errors (lane5/red-announcement-service.log).
  • tests/Unit/Controller/AnnouncementControllerTest.php (6) through the real service: 403 for a reader who posts, 404 by id for a non-member, 400 for a notice without an end time, the action per endpoint, settings admin-only.
  • tests/Unit/BackgroundJob/AnnouncementNotifyJobTest.php, tests/Unit/Notification/AnnouncementNotifierTest.php, the listener's follow cleanup in UserDeletedListenerTest, the demo seeding through DemoDataService::install() (red before the wiring: 1 failure), the action baseline.
  • tests/Unit/Database/AnnouncementsDatabaseTest.php: real database and Nextcloud's real comments manager (publish, like twice is one like, comment, unlike, follow once, follows removed by the real UserDeletedEvent). Skipped without a live Nextcloud; runs in CI.
  • Vitest: AnnouncementsWidget.spec.js, NoticeRegion.spec.js, AnnouncementEditorModal.spec.js (13), with axios mocked so the URLs and bodies are checked against the routes. Red run without the components: 3 files failed.

Verified

  • Local mirror of the required CI jobs on 375ab2e: npm lint, format, check:manifest, check:schema-l10n, l10n-parity and Vitest (976 of 976) 0 on 30bd319 (the later source change is the editor's select binding: eslint, prettier and its spec green); composer check:strict parts on 30bd319: phpcs, phpmd, psalm, phpstan 0, PHPUnit 1987 tests OK (17 skipped database tests that run in CI); AnnouncementControllerTest re-run on 375ab2e.
  • Hydra gates at CI's version (hydra-gates@main, full scope, --require-full-coverage) on 375ab2e: 85 of 86 applicable gates pass; gate 23 fails with the one match development has (PermissionService, RoleFeaturePermissionService), inherited and waiting on a decision. The first run found two untagged computed setters (gate 16) and the comments list without a contract test (gate 25), both fixed.
  • No local production build; CI's Frontend Build is the check. No local coverage driver: the pgsql cell's coverage guard is read in CI.

Live check

As admin, Admin settings, LaunchPad, "Announcements": add group "Redactie". As Karin (in Redactie), on a dashboard with the Announcements widget: "New announcement", kind "Notice above the dashboard", level Warning, title "Onderhoud zaaksysteem zaterdag 08:00 tot 12:00", target group "Burgerzaken", no end time, "Preview": banner and "People reached: N"; "Publish": "A notice needs an end time". Set an end time, Preview, Publish. As Pieter (in Burgerzaken): the banner shows above every dashboard, with no close button. As Sanne: no banner. Publish a news item in "Facilitair" for everyone; Pieter follows "Privacy", Karin publishes in "Privacy": Pieter gets one notification. Pieter likes and comments: the card shows 1 and 1.

…ly, by most or last used, or at random

Most and last used read counts kept in the viewer's own browser, never sent to the
server; a viewer can forget them. The order is applied when the page is viewed and
never written back, and random holds for the page load.

REQ-TSO-001..004, REQ-CONT-007 (launcher-tile-sorting)
…ends a query straight to a site

search_shortcuts holds up to 30 entries {prefix, name, urlTemplate}; a prefix is !
and 1 to 10 letters or digits, unique; the address passes the https {query} check
the quick-search fallback already uses. The list reaches every page through the
workspace initial state.

REQ-QSP-001, REQ-QSP-002 (search-ai-prefix-shortcuts)
…d ? lists the shortcuts

In the search widget a query whose first word is a shortcut prefix shows one
result, Search TOPdesk for printer, and Enter opens the site in a new tab; an
unknown prefix filters tiles as before; ! or ? lists the shortcuts. Administrators
edit the list on a new admin tab.

REQ-SPX-001..003 (search-ai-prefix-shortcuts)
…ming a group of tiles

The browser reads the file (Firefox, Chrome and Edge exports), shows folders and
loose bookmarks to pick from, and refuses files over 5 MB or 2,000 bookmarks. The
server keeps only http and https addresses and lists the rest as skipped, checks the
widget quota once for the whole import (409 names the room left), and creates a
container of link tiles per folder plus loose tiles at the bottom in one transaction.
The entry sits in the active dashboard's menu for people who may add widgets.

REQ-BMI-001..004 (launcher-bookmark-import)
…carry an address for inside the office

office_networks holds IPv4 and IPv6 ranges, validated with Nextcloud's IP factory;
the server decides from the request address and tells the page onOfficeNetwork. A
tile's content.internalUrl must be an http or https address or a path on this
Nextcloud, or the update is refused.

REQ-TIA-001, REQ-TIA-002 (launcher-tile-internal-address)
…nd administrators set the office networks

The tile editor gets "Address on the office network" and says which address the
author gets right now; the tile opens the internal address only when the server says
the request comes from an office network. A new admin tab lists the ranges and names
the administrator's own address.

REQ-TIA-001..003 (launcher-tile-internal-address)
…e tile wrapper is not a fake control (gate-32)
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ de3491f

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
format ✅
check-schema-l10n ✅
composer ✅ ✅ 104/104
npm ✅ ✅ 636/636
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ❌
Newman ✅
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development.
Hydra gates ❌

Quality workflow — 2026-10-05 18:19 UTC

Download the full PDF report from the workflow artifacts.

…tile-sorting

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
…shortcuts

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	lib/Controller/PageController.php
#	lib/Db/AdminSettingKey.php
#	lib/Service/InitialStateBuilder.php
#	src/components/admin/AdminSettings.vue
#	src/utils/loadInitialState.js
…kmark-import

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	src/views/Views.vue
…internal-address

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	lib/Controller/PageController.php
#	lib/Db/AdminSettingKey.php
#	lib/Service/InitialStateBuilder.php
#	src/components/admin/AdminSettings.vue
#	src/utils/loadInitialState.js
…nt-announcements

# Conflicts:
#	l10n/be.js
#	l10n/be.json
#	l10n/bg.js
#	l10n/bg.json
#	l10n/bs.js
#	l10n/bs.json
#	l10n/ca.js
#	l10n/ca.json
#	l10n/cs.js
#	l10n/cs.json
#	l10n/da.js
#	l10n/da.json
#	l10n/de.js
#	l10n/de.json
#	l10n/el.js
#	l10n/el.json
#	l10n/en.js
#	l10n/en.json
#	l10n/es.js
#	l10n/es.json
#	l10n/et.js
#	l10n/et.json
#	l10n/fi.js
#	l10n/fi.json
#	l10n/fr.js
#	l10n/fr.json
#	l10n/ga.js
#	l10n/ga.json
#	l10n/hr.js
#	l10n/hr.json
#	l10n/hu.js
#	l10n/hu.json
#	l10n/is.js
#	l10n/is.json
#	l10n/it.js
#	l10n/it.json
#	l10n/lb.js
#	l10n/lb.json
#	l10n/lt.js
#	l10n/lt.json
#	l10n/lv.js
#	l10n/lv.json
#	l10n/mk.js
#	l10n/mk.json
#	l10n/mt.js
#	l10n/mt.json
#	l10n/nb.js
#	l10n/nb.json
#	l10n/nl.js
#	l10n/nl.json
#	l10n/pl.js
#	l10n/pl.json
#	l10n/pt.js
#	l10n/pt.json
#	l10n/rm.js
#	l10n/rm.json
#	l10n/ro.js
#	l10n/ro.json
#	l10n/ru.js
#	l10n/ru.json
#	l10n/sk.js
#	l10n/sk.json
#	l10n/sl.js
#	l10n/sl.json
#	l10n/sq.js
#	l10n/sq.json
#	l10n/sr.js
#	l10n/sr.json
#	l10n/sv.js
#	l10n/sv.json
#	l10n/tr.js
#	l10n/tr.json
#	l10n/uk.js
#	l10n/uk.json
#	lib/Db/AdminSettingKey.php
#	src/components/admin/AdminSettings.vue
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ 404d4fe

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
format ❌
check-schema-l10n ✅
composer ✅ ✅ 107/107
npm ❌ ✅ 636/636
app:check-code ⏭️
info.xml ✅
REUSE ❌
lockfile sync ✅
PHPUnit ❌
Newman ❌
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development.
Hydra gates ❌

Quality workflow — 2026-10-23 05:24 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant