Skip to content

RES-2054: Group Tags - Network Coordinator Management#829

Merged
edwh merged 74 commits intodevelopfrom
RES-2054_group_tags
Apr 23, 2026
Merged

RES-2054: Group Tags - Network Coordinator Management#829
edwh merged 74 commits intodevelopfrom
RES-2054_group_tags

Conversation

@edwh
Copy link
Copy Markdown
Collaborator

@edwh edwh commented Feb 2, 2026

Summary

  • Enables Network Coordinators to create and manage tags for groups within their network
  • Adds network-scoped tags (tags belong to a specific network, with admin-managed global tags)
  • NC can create tags, tag/untag groups within their network(s)
  • New API endpoints: GET/POST/DELETE /api/v2/networks/{id}/tags
  • Tag filtering on network groups, events, and stats API endpoints
  • Tag selector on group edit page showing network-specific + global tags based on permissions
  • Tag badges on group list view, tag filter dropdown visible to NCs
  • French/French-Belgian translations for tag features

Also includes merge of Laravel 10 upgrade branch.

Related

Test plan

  • NC can view/create/delete tags for their network
  • NC can tag/untag groups in their network
  • Admin can manage global tags and all network tags
  • Tag filtering works on network groups/events/stats API endpoints
  • Unauthenticated API calls return no tags
  • Tag selector on group edit shows correct tags per user permissions
  • Tag badges display on group list view

🤖 Generated with Claude Code

edwh and others added 30 commits December 10, 2025 15:25
- Add network_id column to group_tags table with FK to networks
- Add GroupTags model scopes: global(), forNetwork(), availableForNetwork()
- Add Network model tags() relationship and availableTags() method
- Add GET/POST/DELETE /api/v2/networks/{id}/tags endpoints
- Add tag filter param to networks/{id}/groups, events, and stats
- Allow Network Coordinators to update tags on groups they coordinate
- Add 11 new tests for tag functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add canEditTags prop to GroupAddEdit/GroupAddEditPage for NC tag access
- Group tags by network name in multiselect (Global first, then alpha)
- Filter listTagsv2 API: unauthenticated=global only, NC=global+their networks
- Send api_token with tag list requests for proper user-based filtering
- Add network_name to Tag resource for UI grouping

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
NCs can now add global tags and tags from their networks, but not tags
from other networks. Updated testApprove to verify this behavior:
- NC CAN add global tags (network_id = null)
- NC CANNOT add tags from networks they don't coordinate

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update listTagsv2() to only return network tags to NCs (not global)
- Update updateGroupv2() to prevent NCs from adding global tags
- Update getNetworkTagsv2() to only show global tags to admins
- Update tests to reflect new global tags = admin-only rule

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix tag visibility intersection: NCs can only see/edit tags from networks
  where they coordinate AND the group belongs
- NC tag updates preserve tags from other networks they don't coordinate
- Add tag badges to group list view for NCs and admins
- Groups list tag filter now visible to NCs (was admin-only)
- Convert network page to Vue with stats and tag management
- Add groups_count to Tag resource for deletion warnings
- Add PHPUnit tests for tag intersection logic
- Update .gitignore for Vite build artifacts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update translation strings to use Laravel pluralization syntax
- Enhanced JS choice() function to handle {n} and [n,*] syntax
- Fixed networks.general.count, show.groups_count, add_groups_success
- Fixed tags.delete_warning pluralization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reverts accidental port change that broke CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Taskfile.yml: Update wait_for_service check to use port 8026
- CLAUDE.md: Update Mailhog URL
- docs/local-development.md: Update Mailhog URL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Refactored to recursively process nested translation structures
instead of failing when encountering arrays.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Logs EventsUsers status and role to help diagnose why attending
is sometimes true when it should be false in CI.

Also adds test artifacts to .gitignore.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add fr and fr-BE translations for new network strings
- Remove unused translation keys (general.count, show.about_modal_header)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use "Etiquette" (without accent) to match existing usage
- Use "Repair Café" instead of "groupe" for consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removed fwrite(STDERR) which was causing TeamCity to mark the test
as failed even when assertions passed. Debug info now only appears
in assertion failure messages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- /api/v2/networks/{id}/tags returns empty for unauthenticated users
- /api/v2/groups/tags returns empty for unauthenticated users
- Added tests for both endpoints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test occasionally fails in CI with all_confirmed_restarters_count
being 1 instead of 0. Added debug info to show group membership state
when assertion fails.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After making unauthenticated API calls return empty tags, the test
needs to authenticate first to see the tags.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test expected unauthenticated users to see network tags, but we
changed the API to return empty for unauthenticated users.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Must be authenticated to see tags after unauthenticated API change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Center title and intro text on landing page
- Reduce line-height on h1 for tighter title spacing
- Add white-space: nowrap to stat headers to prevent line breaks
- Add missing description field to skills seeder (required NOT NULL column)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use more specific selector (label.btn.btn-checkbox) for higher specificity
- Override background color to gray (#E4E4E4)
- Remove uppercase text-transform
- Add background-image: none to prevent gradient overrides

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add docker:vite, docker:vite:start, docker:vite:stop tasks
- Change HMR host from hardcoded www.example.com to env variable with localhost default
- Update CLAUDE.md with critical Docker task command documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Create EmailValidation.vue component that checks if email already
exists on blur and displays validation message. Replaces inline
HTML with proper Vue component following codebase patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The templates use kebab-case (<categories-table>, <roles-table>) but
components were registered without hyphens. Vue's template-to-component
resolution requires matching names.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Past events were missing timezone in the expanded event data, causing
date/time display issues on group pages. Now explicitly passing
timezone alongside event_date_local, start_local, and end_local.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The route was incorrectly placed in the ensureAPIToken middleware group
but requires authentication since it calls auth()->user(). Moved to the
auth+verifyUserConsent+ensureAPIToken group where it belongs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Hide hosts section in EventDetails when no hosts assigned
- Update StatsShare to use __() instead of $lang.get/choice methods

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brings in post-release fixes:
- Event invite modal converted to Vue component with Tab key support
- Fix notifications mark as read (event delegation)
- Fix skills list dropdown arrow and add Ctrl-click hint
- Fix landing page 'Waste prevented' layout wrapping

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
edwh and others added 13 commits March 19, 2026 12:01
… collapsible sections, CI deploy option

- Reorganised to lead with What Changes / What Stays the Same
- Added Metabase direct DB access as known risk
- Moved Fly config setup to pre-cutover (days before, not during maintenance window)
- Added CircleCI automated deploy option
- Clarified DNS is at iwantmyname.com (not Cloudflare, no CNAME flattening)
- Made all sections collapsible with <details> tags
- Fixed section numbering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… title are on same line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Fly.io, nginx serves /uploads/ from Tigris (S3), not local disk.
FixometerFile writes to local disk for Intervention Image processing,
but those files were never synced to Tigris, so images were broken.

After local processing (orientation fix, thumbnail/mid generation),
files are now synced to S3 via Storage facade. The syncToCloud method
is a no-op when FILESYSTEM_DISK is not 's3', so existing production
and test environments are unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, hauts-de-france)

All three point to current production server and need DNS + TLS certs on Fly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… DNS cutover

Wildcard cert already created on Fly. Needs _acme-challenge CNAME at
iwantmyname.com for DNS-01 validation (can be done before cutover).
Covers repairtogether, repairshare, hauts-de-france subdomains.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Deploy L10+Fly first, group tags later as preview
- Scale up to shared-cpu-2x/4GB for launch
- Fix MAIL_FROM_ADDRESS to noreply@mg.restarters.net
- Hourly backups to Google Drive, Metabase pulls from there
- Simplified rollback: just switch DNS back
- Add API compatibility check for third parties
- Add ERES preview deployment plan
- Concrete timeline: migration ~Apr 9, group tags ~Apr 30
- Note production branch cleanup needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Mount /var/log as persistent volume so logs survive redeploys
- Add request timing to nginx access log (rt, uct, uht, urt fields)
- Enable PHP-FPM slow log (5s threshold) and request_terminate_timeout (60s)
- Install sysstat and run sar collection every 60s via supervisord
- Create log directories on volume in startup.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Health check was hitting the homepage which runs Fixometer::loginRegisterStats()
causing 60s+ timeouts and saturating PHP-FPM workers. /robots.txt is a static
file served by nginx without touching PHP.

Added logrotate for nginx and PHP-FPM slow logs (14 day retention).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Symlinks /var/www/storage/framework/cache/data to /var/log/cache/data
on the persistent volume. Previously the cache was on ephemeral container
storage and blown away on every deploy, causing 15s+ homepage loads
while Fixometer stats recalculated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Homepage request causes PHP-FPM to spike to ~400MB, exhausting all
available memory (985MB total, ~400MB free at idle). Without swap,
the OOM killer terminates PHP-FPM workers causing 502 errors.

Uses fallocate (instant) with dd fallback. On root fs, recreated each boot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The all_stats cache was storing 17,291 full Party model objects (96MB
serialized). Every homepage load deserialized this, taking 5+ seconds.

Only the count was ever used. Now caches just the integer count.
Cache file drops from 96MB to 112KB, read time from 5s to <1ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Includes: persistent logging, swap, health check fix, cache optimisation,
FixometerFile S3 sync. Resolved conflicts keeping group_tags' COUNT query
approach for party stats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread scripts/fly-migrate.sh
if [[ -n "$DB_PASS" ]]; then
log_step "Setting MYSQL_PASSWORD and MYSQL_ROOT_PASSWORD on ${FLY_DB_APP}..."
if [[ "$DRY_RUN" = true ]]; then
log_dry "echo 'MYSQL_PASSWORD=***\nMYSQL_ROOT_PASSWORD=***' | fly secrets import -a ${FLY_DB_APP}"

Check failure

Code scanning / SonarCloud

MySQL database passwords should not be disclosed High

Make sure this MySQL password gets changed and removed from the code. See more on SonarQube Cloud
Comment thread scripts/fly-migrate.sh
if [[ "$DRY_RUN" = true ]]; then
log_dry "echo 'MYSQL_PASSWORD=***\nMYSQL_ROOT_PASSWORD=***' | fly secrets import -a ${FLY_DB_APP}"
else
printf "MYSQL_PASSWORD=%s\nMYSQL_ROOT_PASSWORD=%s\n" "$DB_PASS" "$DB_PASS" \

Check failure

Code scanning / SonarCloud

MySQL database passwords should not be disclosed High

Make sure this MySQL password gets changed and removed from the code. See more on SonarQube Cloud
edwh and others added 11 commits March 26, 2026 10:08
Bug 1: Edit tag modal now prevents auto-close on OK, shows duplicate
name error inline instead of silently closing.

Bug 2: "Group could not be edited" when adding/removing tags - fixed
archived_at validation to be nullable (empty string was failing date
validation) and FormData filter to not drop falsy values.

Bug 3: /api/v2/networks/{id}/tags returns empty without api_token -
this is expected API behaviour, not a bug. The UI passes the token.

Feature 1: Tags now displayed as badges on individual group page
in GroupHeading component, below the group name.

Feature 2: API already uses 'group_tag' parameter name (not 'tag').

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The public/hot file (created by local Vite dev server) was being copied
into the Docker image, causing Laravel to load assets from localhost:5173
instead of the built files. Added to .dockerignore and rm -f after build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- NetworkController: simplify getNetworkTagsv2 to return only network-specific
  tags (public endpoint), fixing empty response for unauthenticated requests
- NetworkController: accept both 'tag' and 'group_tag' params for stats filter
- GroupAddEdit.vue: remove admin bypass in tag filtering so admins only see
  tags from networks the group belongs to (not all networks)
- LoginController: add try-catch logging around post-auth to capture 500 errors
- Update test to match new public tags endpoint behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mediawiki-api v3.1 changed its namespace from Mediawiki\Api\MediawikiApi
to Addwiki\Mediawiki\Api\Client\MediaWiki. The old catch(\Exception) didn't
catch the resulting Error (class not found), causing a 500 on every login
when the login event listener tried to resolve the MediaWiki service.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The addwiki/mediawiki-api v3.1 changed namespace from Mediawiki\Api to
Addwiki\Mediawiki\Api. This causes Error (not Exception) when class not
found. Changed catch(\Exception) to catch(\Throwable) in all mediawiki
integration points so login doesn't 500. Made UserCreator constructor
param nullable since it may fail to resolve. Wiki integration itself
needs a separate v3 namespace migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New public endpoint GET /api/v2/networks/{id}/stats?group_tag={tagId}
returns network impact statistics, optionally filtered by group tag.
Includes OpenAPI annotations and two feature tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Configure phpMyAdmin as publicly accessible Fly app with basic auth
- Add docker/Dockerfile.pma for custom phpMyAdmin image
- Upgrade @playwright/test to 1.59.1
- Add .mcp.json to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… Admin, API)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers all test cases from Neil's testing doc:
- NC: group page tag display, all network tag CRUD, group tag management
- Admin: network tag CRUD, group tag management, group page display
- Admin: global tag management (/tags page) - view, create, edit, delete
- Host: verified no tag visibility on groups list and group page
- API: tags, groups/events/stats filtered by tag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add NC user, host user, network, and group creation to Taskfile
  Playwright setup so grouptags tests have required data in CI
- Make "tags displayed on group page" tests self-contained by
  creating and assigning a tag before checking the view page,
  rather than relying on state from earlier tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use .first() on table locators that can match multiple elements
in CI environment where page may contain multiple tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@edwh edwh changed the base branch from upgrade-laravel-10x-restart to develop April 23, 2026 19:47
Bring in the Laravel 10 upgrade (via PR #816 merge) plus develop drift.

Single conflict in app/Listeners/LogInToWiki.php constructor signature:
kept develop's ?UserCreator $mediawikiUserCreator (nullable type, no
default) over the older UserCreator $mediawikiUserCreator = null form.
Both work with the MediawikiServiceProvider binding that returns null
when Wiki is unavailable; keeping develop's form for consistency with
the tested wiki-fallback flow.

package-lock.json regenerated via npm install.
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
9 Security Hotspots
14.8% Duplication on New Code (required ≤ 3%)
E Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@edwh edwh merged commit e90d55c into develop Apr 23, 2026
1 of 3 checks passed
@edwh edwh mentioned this pull request Apr 23, 2026
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.

2 participants