Conversation
First end-to-end deploy run failed on Hostinger BR because the workflow hardcoded port 22 in both ssh-keyscan and rsync's `-e ssh ...`, while the hosting exposes SSH on port 65002. Two follow-ups landed: 1. New optional secret `TESTES_SSH_PORT` (default `22` so VPS-style setups keep working). Both ssh-keyscan and rsync now read it. 2. Rsync switched from `StrictHostKeyChecking=yes` to `accept-new` (TOFU). The keyscan step is now best-effort (`|| true`) — if a firewall/CDN blocks port-scanning, the first rsync connection transparently accepts the host key and pins it for the run. Safer than `no` (would be MITM-vulnerable); recovers from keyscan failures that previously aborted the whole deploy with no useful log. CLAUDE.md updated to document the new secret in the deploy-to-testes table with a note that managed hosting commonly uses non-standard ports. Co-authored-by: Claude <noreply@anthropic.com>
Last two deploy runs failed with `Permission denied (publickey,password)` despite the keypair on the testes server being verified as matching (fingerprints of `~/.ssh/rpgmem` and `~/.ssh/rpgmem.pub` are identical, public key is appended to `authorized_keys`, permissions are 700/600). That narrows the failure to the `TESTES_SSH_KEY` secret: the private key bytes GitHub is receiving don't match the public key on the server. Most likely culprits are CRLF line endings introduced by a Windows clipboard paste, a truncated copy, or accidentally pasting the .pub. This adds a temporary diagnostic block to the Configure SSH step that reports byte count, line count, file type (catches CRLF), header/footer lines (verifies BEGIN/END markers), and fingerprint of the key the runner actually received. None of those leak the key bytes themselves. Once we identify and fix the paste issue, a follow-up commit removes the DEBUG block. Co-authored-by: Claude <noreply@anthropic.com>
…391) The diagnostic block added in #390 served its purpose — it confirmed the secret bytes matched the server's keypair (same fingerprint, no CRLF, correct length). That isolated the real root cause: the private key on the testes server had been generated with a passphrase, and GitHub Actions has no way to enter passphrases interactively. The user regenerated a fresh ed25519 key with `-N ""` and the next deploy ran green end-to-end. Two changes here: - `.github/workflows/deploy-develop.yml`: removes the DEBUG block from the "Configure SSH" step. The workflow returns to its production shape (port-aware, accept-new TOFU, best-effort keyscan). - `CLAUDE.md`: adds a note to the `TESTES_SSH_KEY` row in the deploy secrets table calling out the no-passphrase requirement, with the exact `ssh-keygen` invocation that gets it right and the misleading error symptom (`Permission denied (publickey,password)` looks identical to a wrong key). Future sessions won't repeat the cycle. Co-authored-by: Claude <noreply@anthropic.com>
…392) User reported finding dev-only files on the testes server after the first successful deploy. Categories cleaned up: Repo metadata: - .githooks/, .distignore Build / dependency manifests: - composer.json, composer.lock, package.json, package-lock.json Static analysis / testing tools: - phpstan-stubs.php, patchwork.json Lint configs (the existing `.eslintrc*` pattern doesn't match ESLint v9 flat config naming `eslint.config.{js,mjs,cjs}` — added the flat pattern explicitly): - eslint.config.* Repo docs (live on GitHub, not in plugin runtime): - CONTRIBUTING.md, SECURITY.md Intentionally kept (per user preference): CHANGELOG.md — useful for historical lookup via SSH; not surfaced to end users (WP.org parses `readme.txt`'s own changelog section). The previous "composer.json e package.json são intencionalmente enviados" rationale was hand-wavy (managed hosting admins might inspect them) and the user disagreed in practice. Comment block rewritten to reflect the new policy. Next push to develop triggers a redeploy; rsync `--delete` will remove the listed files from the testes server in the same pass. Co-authored-by: Claude <noreply@anthropic.com>
The divisao_setor dependent-select options were hardcoded in ReregistrationFieldOptions::get_divisao_setor_map() (DRE São Miguel MP org structure) — Portuguese strings unreachable by Loco, and unusable by any other organization without a code edit. This adds a global, admin-editable map under Settings → Reregistration. Data layer - get_divisao_setor_map() now reads ffc_settings['divisao_setor_map'] via a new typed accessor SettingsReader::divisao_setor_map(), falling back to the hardcoded default. The hardcoded array moved to a new get_default_divisao_setor_map() — source of truth for both the seed and the runtime fallback. The fallback lives in the domain layer (not SettingsReader) to avoid a Settings → Reregistration dependency cycle. - The 3 existing consumers (validation, field seeder, frontend delegate) need no changes — they call get_divisao_setor_map() which is now configuration-aware. Display sync (the snapshot problem) - The dropdown the user sees is a per-audience snapshot frozen in wp_ffc_custom_fields.field_options['groups'] at seed time (the seeder is insert-only). Validation reads the map live. To keep DISPLAY consistent with the live map, ReregistrationStandardFieldsSeeder:: resync_divisao_setor_groups() rewrites every audience's snapshot (preserving parent_label / child_label) and the save handler invokes it after persist — only when the map actually changed. Admin UI - New TabReregistration settings tab + view rendering a nested repeater (divisions, each with a sector sub-list; add/remove rows). - ffc-divisao-setor-editor.js keeps a hidden JSON input in sync; the save handler decodes + sanitizes (sanitize_text_field per key/leaf, drops empty divisions, de-dups sectors). - Scoped CSS for the nested editor in ffc-admin-settings.css. Seed - Activator::seed_reregistration_field_options() seeds the hardcoded default into ffc_settings on activation when absent (idempotent), so the option is concrete and matches existing per-audience snapshots — no display resync needed at activation. Tests - PHP: SettingsReader accessor (set / absent / non-array), field-options configurable override + fallback, save-handler tab gating + JSON parse + sanitization + no-op resync, seeder resync (empty + populated), activator seed (writes default / skips when set). Existing tests that transitively hit the map now stub get_option. - JS: full editor coverage (sync, add/remove division+sector, de-dup) — keeps the JS line floor satisfied (86.2%). No FFC_VERSION bump (develop-targeted PR per CLAUDE.md). Co-authored-by: Claude <noreply@anthropic.com>
…hild replication (#394) Supersedes the global divisao_setor_map model from #393. Standard reregistration fields whose option lists are organization-specific (divisao_setor groups, sindicato / jornada choices) are now edited per-audience in the Custom Fields editor, and propagated down the audience hierarchy with an explicit "Replicate lists to children". Why per-audience: the option snapshots already live per-audience in wp_ffc_custom_fields.field_options; a global setting that synced into them was a redundant layer. Per-audience with cascade matches the 3-level hierarchy and lets children diverge for fine-tuning. Editing (unlock + UI) - ajax_save_custom_fields: standard fields were locked to label/group/ order/required/active. Now also accept field_options (select choices AND dependent_select groups) — but only when the payload carries non-empty options, so a bulk save can never null an existing list (wipe guard). Type/key/mask/profile_key stay immutable for standard. - dependent_select groups: new sanitize_dependent_groups() + a preserve_dependent_labels() that carries over parent_label / child_label the editor doesn't touch. - UI: the choices textarea is now editable for standard select fields; dependent_select rows embed the nested division→sector editor (reused ffc-divisao-setor-editor.js from #393, now mounted in the field row). ffc-custom-fields-admin.js collects `groups` from the synced hidden input and toggles the groups container on type change. Replication - "Replicate lists to children" button (shown only when the audience has children) → ajax_replicate_field_options → ReregistrationStandardFieldsSeeder::replicate_field_options_to_descendants(), which copies every standard field's field_options to all descendants (via AudienceRepository::get_descendant_ids) by field_key. Explicit, overwriting push; manual per-child edits survive until next replicate. Validation - ReregistrationDataProcessor now validates a dependent_select against the field's OWN per-audience groups (get_dependent_choices), not a global map — and generalizes from divisao_setor to any dependent_select field. Removed (global layer from #393) - TabReregistration settings tab + view, SettingsReader::divisao_setor_map(), the save-handler global map handlers, Activator seed, the ReregistrationFieldOptions global reader + ReregistrationFrontend delegate, and resync_divisao_setor_groups(). Kept get_default_divisao_setor_map() as the shipped seed default for new audiences, and the ffc-divisao-setor-editor.js component (repurposed). Tests - New: handler helpers (sanitize_dependent_groups, preserve_dependent_labels), replicate_field_options_to_descendants (empty + populated), per-audience dependent_select validation. - Removed obsolete tests for the deleted global code; repointed the remaining map assertions to get_default_divisao_setor_map(). - PHPUnit 4701 green; Vitest 965 green (JS lines 85.99% > floor). No FFC_VERSION bump (develop-targeted PR). Co-authored-by: Claude <noreply@anthropic.com>
…aceholders (#395) The ficha template referenced {{divisao}} / {{setor}}, but FichaGenerator only emits the combined divisao_setor value, so both cells printed the literal placeholder. Expose each dependent_select field's parent/child halves as {{<key>_parent}} / {{<key>_child}} and point the template at them; the combined {{<key>}} form stays for back-compat. Standard-field variable building moved into the unit-tested build_standard_field_variables(). https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…ficha PDF) (#396) The acknowledgment notice was hardcoded in both the reregistration form renderer and the ficha PDF template. It is now a display-only `acknowledgment` standard field whose HTML lives in field_options['html'], edited per-audience via wp_editor in the Custom Fields editor and propagated to descendants by the existing "Replicate lists to children" action. - New `acknowledgment` field type (display-only): skipped during value collection, validation and persistence. - Seeded per-audience with the shipped default notice (ReregistrationFieldOptions::get_default_termo_ciencia_html), which is also the render-time fallback for audiences predating the field. - Form renders the per-audience HTML block; ficha injects {{termo_ciencia}} via a dedicated replace so the notice's links survive (the per-variable allowlist omits <a>). - Admin: always-visible wp_editor in the acknowledgment row; builder JS collects the HTML and toggles the editor by type. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…log reasons (#402) * feat(preview): single-source placeholder samples + readable pre-flight log reasons Certificate previews (admin form-editor + public CSV-download) each kept their own short hardcoded sample map, so any other placeholder rendered as a raw {{token}}. Introduce CertificatePreviewSamples::get_map() as the single source of truth, surfaced to both previews (ffc_ajax.previewSamples and the ajax_cert_preview payload); the JS only overlays the live form title and the form's own field names. Activity Log: the preflight_blocked rows dumped the opaque "reason":"gps_prompt" code. Add a display-only summary mapping the reason codes to human labels (the stored enum stays a stable machine key the stats aggregator relies on) plus a friendlier action label. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY * test: stub DateFormatter-backed WP fns in AdminAssetsManagerTest The localization payload now eagerly builds CertificatePreviewSamples::get_map(), which routes through DateFormatter (wp_date/wp_timezone), get_option and get_bloginfo. Stub them so the enqueue tests don't hit undefined wp_date(). https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY --------- Co-authored-by: Claude <noreply@anthropic.com>
Dependabot had no target-branch, so it opened bumps against the default branch (main). Under the develop workflow, only release/hotfix PRs touch main; dependency bumps belong on develop like any other change. Set target-branch: develop for the composer, npm, and github-actions ecosystems. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
* chore(deps)(deps-dev): bump terser from 5.47.1 to 5.48.0 (#400) Bumps [terser](https://github.com/terser/terser) from 5.47.1 to 5.48.0. - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](terser/terser@v5.47.1...v5.48.0) --- updated-dependencies: - dependency-name: terser dependency-version: 5.48.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps)(deps-dev): bump jsdom from 25.0.1 to 29.1.1 Bumps [jsdom](https://github.com/jsdom/jsdom) from 25.0.1 to 29.1.1. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](jsdom/jsdom@v25.0.1...v29.1.1) --- updated-dependencies: - dependency-name: jsdom dependency-version: 29.1.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Meusburger <rpgmem@gmail.com>
Bumps vitest and @vitest/coverage-v8 2.x → 4.x together (they are a version-locked pair; splitting them breaks npm ci). The major bump surfaced two latent test-isolation issues and changed how coverage-v8 counts statements: - admin-submission-edit: repeated vi.spyOn($, 'post') without restore returned the same accumulating mock under v4, so a later test saw 4 calls instead of 1. Restore mocks in afterEach. - sprint1-followup-debug-toggle: the async diagnostics log bled into the next test's console spy under v4's tighter inter-test flushing. Drain pending microtasks + restore mocks in afterEach. coverage-v8 v4's AST-aware remapping re-measured the same suite ~2pts lower, dropping under the 82 floor. Rather than lower the floor, added real tests to lift it back: ffc-core helpers (log/error/warn, ajax, toggleFields, accessors, [data-confirm] guard), the already-submitted ajaxComplete tracker + LRU cap, and dynamic-fragments nonce/user-prefill patching. Gate metric now 82.4% (floor held at 82). CI Node bumped 20 → 22 in lint.yml: vitest 4 needs Node >=20.19/22.12 and matching the local toolchain keeps the coverage number reproducible. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…switch (#405) Swaps plain on/off checkboxes for the shared AdminUI::render_toggle() component in the spots that hadn't been converted yet: - CSV public-access metabox: regenerate_hash + reset_counter - Advanced settings: reset_counter (Reset ID counter to 1) - Audience field-builder flags (Required/Active/Sensitive) — both the wp.template for new rows and the server-rendered existing rows - Audience calendar per-user permission grid (can_book / can_cancel_others / can_override_conflicts) Input names, the JS-serialiser class hooks (.ffc-field-*, .ffc-perm-toggle) and data-perm are all preserved, so save and JS serialisation behave exactly as before. render_toggle gains an optional `title` arg so the Sensitive flag keeps its "encrypt at rest" tooltip. The self-scheduling calendar editor was already fully on render_toggle. Left as-is by design: list-table row selectors, multi-select checkbox groups, public/consent form checkboxes, and the WP user-edit capability fieldset. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…#406) Introduce MaintenanceToolInterface + MaintenanceToolRegistry under a new FreeFormCertificate\Maintenance namespace. ObsoleteShortcodeCleaner now implements the interface (id/title/description/is_actionable/ get_default_options/run) and the Settings → Data Migrations handler dispatches through MaintenanceToolRegistry::create_default() instead of newing the cleaner directly. Behaviour is identical; this is the foundation for the upcoming URL-shortener cleanup, public-operator-access disabling and submission-link audit tools, which each plug in by implementing the interface and registering in create_default(). The cleaner's run() converges on the interface signature run( array $options ) — the grace window moves from a positional int into $options['days']; callers and tests updated. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
* feat(maintenance): add Short URL Cleanup tool (PR 2/4) Second maintenance tool on the framework from PR 1. UrlShortenerCleaner deletes obsolete short URLs under three toggleable criteria — orphaned (target post gone), never-clicked + older than a grace window, and trashed — with a dry-run preview before the destructive pass. - includes/maintenance/class-ffc-url-shortener-cleaner.php (tool, lazy repo) - UrlShortenerRepository::find_cleanup_candidates() — OR-combined criteria, per-row is_orphaned/is_never_clicked/is_trashed flags via a posts LEFT JOIN - registered in MaintenanceToolRegistry::create_default() - Settings handler handle_url_shortener_cleanup() (preview persists criteria + grace window and runs dry-run; apply requires a fresh preview) - a new card on the Data Migrations tab (criteria checkboxes + days, preview/delete buttons, by-reason report) - UrlShortenerCleanerTest (criteria, dry-run vs delete, reasons, truncation) https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY * test(maintenance): cover URL cleanup handler + repo query (restore floor) The Short URL Cleanup PR added uncovered lines (the admin handler and the find_cleanup_candidates SQL method), dropping project line coverage below the 55% floor. Restore it without lowering the gate: - SettingsTest: exercise handle_url_shortener_cleanup() — no-request and bad-nonce guards plus the preview and apply happy paths, trapping the terminal wp_safe_redirect (the established pattern) so the full body runs. This transitively covers UrlShortenerCleaner's lazy repository() branch and find_cleanup_candidates via a mocked $wpdb. - UrlShortenerRepositoryTest: direct tests for find_cleanup_candidates — the no-criteria early return and the prepared-query path. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY --------- Co-authored-by: Claude <noreply@anthropic.com>
…4) (#408) Third maintenance tool on the framework. PublicOperatorAccessDisabler switches off Public Operator Access (the master _ffc_csv_public_enabled flag plus its four sub-feature flags) on published forms whose collection period ended more than the grace window ago. - "Old" reuses Geofence::has_form_expired_by_days() — same expiry source as the obsolete-shortcode cleaner. - Non-destructive to config: hash / limit / count / cpf_mode / whitelist are preserved, so access can be re-enabled later. Only the enable flags flip to '0'. - includes/maintenance/class-ffc-public-operator-access-disabler.php - registered in MaintenanceToolRegistry::create_default() - Settings handler handle_public_access_disabler() (preview persists the grace window + dry-runs; apply requires a fresh preview) - new card on the Data Migrations tab (days + preview/disable, report) - PublicOperatorAccessDisablerTest (expiry filter, dry-run vs execute, exactly the five enable flags set to '0', config untouched) + SettingsTest handler coverage (guards + preview + apply paths) https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
Final maintenance tool — and the only report-only one. SubmissionLinkAuditor scans for submissions wrongly linked to WP users and never writes (is_actionable() === false, no apply step). Four checks, all driven by the deterministic cpf_hash / rf_hash columns + a wp_users existence join (no decryption): - orphan_links — user_id points to a deleted WP user - multiple_identities — one user bound to >1 distinct CPF/RF - should_be_linked — no user_id, but the CPF matches a linked row - shared_identities — one CPF shared across multiple users - includes/maintenance/class-ffc-submission-link-auditor.php (lazy repo) - four read-only queries on SubmissionRepository - registered in MaintenanceToolRegistry::create_default() - Settings handler handle_submission_link_audit() (single scan mode) - a report-only card on the Data Migrations tab - SubmissionLinkAuditorTest + SubmissionRepository query tests + SettingsTest handler coverage https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…#410) Two Data Migrations tab polish items from review: 1. The maintenance cards are core .postbox elements, but the `.postbox .inside` / header padding lives in wp-admin's edit.css, which is not loaded on this custom settings page — content rendered flush against the border. Added explicit padding to `.ffc-migration-card` (header + .inside) to match the intro `.card`. 2. The three Short URL Cleanup criteria checkboxes are now AdminUI toggle switches, consistent with the rest of the admin. Field names unchanged, so the preview/apply form contract is identical. Rebuilt assets/css/ffc-admin-settings.min.css. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
The ten FFC capability checkboxes on the WordPress user-edit / profile
screen now render as AdminUI toggle switches, matching the rest of the
admin. Field names are unchanged, so save_capability_fields() and the
Grant/Revoke-all bulk JS (which selects by name and sets .prop('checked'))
work identically — the switch reflects :checked via CSS. Enqueues
ffc-common.css (the .ffc-toggle styles) on the profile screen, which
didn't load it before.
https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY
Co-authored-by: Claude <noreply@anthropic.com>
…s-to (#412) Items 5 & 6 of the review batch. - Notice editor: the public-column visibility grid (public_columns[...]) renders as toggle switches; mandatory columns stay a disabled toggle + hidden input pinning value=1. - Reason editor: the "applies to" status group (applies_to[]) renders as toggle switches. - ffc-common.css (the .ffc-toggle styles) is now a dependency of the recruitment-admin stylesheet so the switches are styled on these screens. - Added AdminUI::get_toggle() — returns the toggle markup as a string — for the notice renderer, which assembles its HTML into a string instead of echoing. Field names and the mandatory-column hidden-input trick are unchanged, so the save handlers work identically. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
#413) On the certificates dashboard, each form in a selected day's side-list now has a discreet dashicon link to the Submissions list pre-filtered to that form (page=ffc-submissions&filter_form_id[0]=<id>). The submissions list already reads filter_form_id[] from GET, so the clean URL is enough — no nonce/referer needed. - localized submissionsUrlBase + a viewSubmissions aria-label into ffcCertificatesDashboard - ffc-certificates-dashboard.js appends the link per entry (guarded on submissionsUrlBase so existing behaviour is unchanged when absent) - discreet muted styling (brightens on hover/focus) - Vitest: link present with correct href when base is set; absent otherwise - rebuilt the .min.js / .min.css bundles https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…ding (#414) * fix(self-scheduling): load ffc-common.css so editor toggles render as switches The self-scheduling calendar editor already renders its config controls via AdminUI::render_toggle, but the full .ffc-toggle switch component lives in ffc-common.css — which the editor screen never enqueued (it only loaded ffc-calendar-editor.css, whose lone .ffc-toggle rule is a layout tweak scoped to .ffc-email-toggles). Result: the Allow-cancellation / Requires-approval / Restrict-* / Admin-bypass toggles showed as raw checkboxes. Enqueue ffc-common.css as a dependency of ffc-calendar-editor.css on the ffc_self_scheduling edit screen so every switch is styled. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY * fix(admin): match migration-card header padding to the reference card Follow-up to the #410 padding fix. The header padding was applied to BOTH .postbox-header and .hndle (double padding) and the h3.hndle kept its default browser margin (edit.css, which would zero it, isn't loaded here), so the space above/below the card title didn't match the intro `.card`. Now mirror the reference rhythm: 20px above the title, 10px down to the header divider, 15px to the content (20px sides/bottom); header padding on .postbox-header only; .hndle margin/padding reset. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY --------- Co-authored-by: Claude <noreply@anthropic.com>
* docs: link REST API section in TOC + document recruitment/audience shortcodes (D1) In-plugin documentation refresh, part 1: - Add the section-19 "REST API Authentication" link to the Documentation TOC — the partial was loaded but had no nav entry, so it was invisible. - 01-shortcodes: document [ffc_recruitment_queue] (notice + adjutancy attrs, ?q/?adjutancy/?subscription/?page_* URL filters) and [ffc_recruitment_my_calls], and list the [ffc_audience] attributes (schedule_id / environment_id / view). https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY * chore: re-trigger CI (Vitest flake on a docs-only PR) https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY --------- Co-authored-by: Claude <noreply@anthropic.com>
In-plugin documentation refresh, part 2 — template variables:
- 02-variables: add the general certificate placeholders that were missing
({{display_name}}, {{reference_year}}, {{fill_date}}/{{date}}, {{status}})
+ a note that any collected profile field ({{rg}}, {{celular}},
{{endereco}}, {{cargo_funcao_acumulo}}, …) resolves in templates, pointing
to the full catalog in section 11 rather than duplicating ~25 rows.
- 11-ficha-pdf: add {{termo_ciencia}} (editable acknowledgment notice) and a
note documenting the dependent-select split placeholders ({{divisao_setor}}
+ {{divisao_setor_parent}} / {{divisao_setor_child}}, generalisable via the
_parent / _child suffixes).
https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY
Co-authored-by: Claude <noreply@anthropic.com>
In-plugin documentation refresh, part 3 — two brand-new sections: - 20. Recruitment: admin tabs (notices/adjutancies/candidates/reasons/ settings), notice lifecycle (draft → preliminary → active → closed) and which states are public, the two public shortcodes, the granular capabilities, and the PII-masking note. - 21. Maintenance Tools: the four Settings → Data Migrations tools (obsolete-shortcode cleanup, short-URL cleanup, disable Public Operator Access, report-only submission↔user link audit) and the preview-before-apply model. Both wired into the TOC and the require() include list. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
In-plugin documentation refresh, part 4 — correctness fixes after reviewing
sections 5–18 against the code:
- 09-audience-custom-fields: add the three real field types that were
missing (dependent_select, working_hours, acknowledgment).
- 17-hooks: add the undocumented hooks — ffcertificate_pdf_filename,
ffcertificate_before_data_deletion, ffcertificate_appointment_receipt_filename,
and the seven ffcertificate_self_scheduling_* email/lifecycle hooks.
- 05-qr-code: fix the size-range wording ("100px at 500px" → "100px–500px").
All other reviewed sections were accurate and left unchanged.
https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY
Co-authored-by: Claude <noreply@anthropic.com>
These are destructive / irreversible actions that should stand out in the Activity Log alongside the existing warning-level deletions: - data_cleanup (automatic deletion of old submissions) - recruitment_classification_deleted - recruitment_adjutancy_deleted - tickets_purged_expired Added level assertions to the two recruitment logger tests to lock the new level in. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…oad (#420) Three new delivery-audit events (all info level), per maintainer request: - pdf_generated — subscriber on ffcertificate_after_pdf_generation - certificate_emailed — subscriber on ffcertificate_before_email_send (form_id only in context; recipient email not stored) - csv_downloaded — at the public-operator CSV delivery point, mirroring the per-form audit ring buffer into the site-wide log Labels added to the activity-log viewer; subscriber tests cover the two new handlers + their hook registration. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY Co-authored-by: Claude <noreply@anthropic.com>
…3a) (#421) * feat(activity-log): granular control (minimum level + per-category toggles) Adds two filters to ActivityLog::log(), applied right after the master toggle and before any DB work: - Minimum level (activity_log_min_level): drop events below the configured severity. debug < info < warning < error; default debug (log all). - Per-category enable (activity_log_cat_<cat>): seven categories (submissions, scheduling, public_access, users, recruitment, migrations, system) via ActivityLog::category_for_action(); default all on. Both default to "log everything", so existing installs are unaffected. - SettingsReader: activity_log_min_level() (validated) + activity_log_category_enabled() (default true). - Settings → Advanced UI: min-level <select> + 7 category toggles. - Persisted via SettingsAjaxEndpoint allowlist (autosave) and the advanced-tab form save handler. - Tests: category map, both gating paths, and the two reader accessors. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY * style: align array arrows in activity-log category map (WPCS) phpcbf — fix WordPress.Arrays.MultipleStatementAlignment in the category_for_action() map and the save handler. No logic change. https://claude.ai/code/session_01BEUEoUQmxb5d7akkVmaSVY --------- Co-authored-by: Claude <noreply@anthropic.com>
…-use fix (Item 11) + token-cancel page (Item 9) (#480) * docs(roadmap): full plugin-wide inventory of inline JS (Item 10) Per the maintainer's request, surveyed the whole plugin (not just recruitment) for inline <script> debt. Group A (extract — real inline JS logic, ~516 lines / 6 files): notice-edit-page-renderer (7 blocks), form-editor-geofence-metabox (2), recruitment-admin-page (1), device-threshold-upgrade-notice (1), form-list-columns (1), audience-admin-import (1). Group B (soft — wp_add_inline_script, lower priority): admin-user-custom-fields, appointment-receipt-handler (print). Group C (not JS logic, leave): <script type="text/html"> wp.template and <script type="application/json"> data islands. First commit of the new Items 8/9/10 PR. https://claude.ai/code/session_011ErkuJ7jnF9zkqCkGs3qKN * Item 10 s19: extract device-threshold notice dismiss JS to dedicated file Move the inline <script> dismiss handler out of class-ffc-device-threshold-upgrade-notice.php into assets/js/ffc-device-threshold-notice.js, enqueued via wp_enqueue_script. The handler reads its AJAX action + nonce from the notice's data-* attributes and the global ajaxurl, so the extraction is verbatim with zero server-side interpolation. Adds 4 Vitest characterization tests (fetch POST, non-dismiss click ignored, XHR fallback, absent-notice no-op) and stubs wp_enqueue_script in the PHP gating test now that maybe_render() enqueues instead of echoing the script. ESLint + Vitest + php -l + PHPStan 8 + WPCS + PHPUnit all green. * Item 10 s20: extract forms-list copy-shortcode JS to dedicated file Move the copy-to-clipboard handler for the forms list-table shortcode buttons out of the inline <script> in class-ffc-form-list-columns.php into assets/js/ffc-form-list-copy-shortcode.js, enqueued on the existing admin_enqueue_scripts path for the screen. Removes the now-dead inline_styles() method (its CSS already moved out in the earlier audit step) and its admin_head-edit.php hook. The original inline script printed in admin_head, before DOMContentLoaded; the enqueued asset ships in the footer, which can run after the event has already fired. Added a document.readyState guard so init() binds whether the document is still parsing or already complete — without it the copy buttons would silently stop working. +4 Vitest tests (Clipboard API, .copied flash + timeout clear, execCommand fallback, no-button no-op). ESLint + Vitest + php -l + PHPStan 8 + WPCS + PHPUnit all green. * Item 10 s21: extract audience import/export tab-switcher to dedicated file Move the jQuery nav-tab switcher out of the inline <script> in class-ffc-audience-admin-import.php into assets/js/ffc-audience-admin-import.js, enqueued (dep: jquery) from render_content(). The handler is verbatim — it reads only from the DOM and the URL hash, no server-side interpolation. +4 Vitest tests (real jQuery: tab show/hide, preventDefault, hash restore, unknown-hash no-op). Stubbed wp_enqueue_script in the PHPUnit render test now that render_content() enqueues. ESLint + Vitest + php -l + PHPStan 8 + WPCS + PHPUnit all green. * Item 10 s22: extract geofence metabox UI-wiring JS to dedicated file Move both inline <script> blocks out of class-ffc-form-editor-geofence-metabox.php — the Date/Time "during" row dual-gate (render_time) and the geolocation area-source toggles (render_geolocation) — into a single assets/js/ffc-form-editor-geofence-metabox.js. The two jQuery initializers are each selector-guarded, so the one asset can be enqueued from both render methods; a private enqueue_metabox_script() helper does this idempotently (WP dedupes the second enqueue by handle). The PHPUnit geolocation render assertion that matched the inline 'toggleGeoSource' source now matches the geo_area_source radios that handler drives, plus a stubbed wp_enqueue_script. +5 Vitest tests (real jQuery: during-row init/show/resync, geo source toggle init/flip). ESLint + Vitest + php -l + PHPStan 8 + WPCS + PHPUnit all green. * Fix PHPUnit: stub wp_enqueue_script in FormEditorMetaboxRendererTest Sprint 22 moved the geofence metabox's inline <script> to an enqueued asset, but FormEditorMetaboxRendererTest renders that metabox indirectly (through FormEditorMetaboxRenderer) and didn't stub wp_enqueue_script — so the render path hit Brain\Monkey's MissingFunctionExpectations under the full suite (the per-file FormEditorGeofenceMetaboxTest I stubbed in sprint 22 passed, masking it). Add the same justReturn(null) stub. * Item 10 s23: extract recruitment candidates CSV-import JS to dedicated file Move the inline <script> from RecruitmentAdminPage's "Import candidates" section into assets/js/ffc-recruitment-candidates-import.js. The two handlers (ffcRecruitmentImportNoticeChanged / ffcRecruitmentImportFrom- Candidates) stay global because the markup invokes them via inline onchange/onsubmit; their config (REST notices root, nonce, i18n strings) now arrives via wp_localize_script (ffcRecruitmentCandidatesImport), enqueued + localized by RecruitmentAdminAssetsManager, so the PHP carries no inline interpolation. Removed the now-dead $nonce / label locals. Form controls are resolved through form.elements.namedItem() (the canonical API) instead of the legacy form.<name> shorthand — works identically in browsers and can't be shadowed by a control sharing a name with an HTMLFormElement property. +10 Vitest tests (status gating, prelim vs definitive endpoint, elapsed ticker, 2xx-reload / non-2xx-error / network-catch). ESLint + Vitest (floor holds) + php -l + PHPStan 8 + WPCS + PHPUnit all green. * docs(audit): register Item 11 — schedule-exception single-use bug diagnosis Diagnose the reported "differentiated exit time" defect: the exception banner survives a refresh and the same operator-issued schedule override can mint two certificates. Two independent root causes documented for joint decision (not yet fixed): A) ScheduleExceptionSession::clear() runs inside the shortcode render during the_content — after HTTP headers are flushed — so setcookie() can't delete the cookie; it survives its 30-min TTL and re-renders the banner on refresh. B) No server-side single-use enforcement of the HMAC token: FormProcessor only checks signature + expiry + form_id, and the payload's jti is never recorded/checked, so the token replays for the full window. Records fix directions (move clear() pre-headers for the UX symptom; add a jti consumption ledger for the authoritative single-use guarantee). * Item 11 s24: make schedule-exception genuinely single-use (A+B) An operator-issued differentiated exit time could mint more than one certificate with the adjusted schedule, and its banner kept showing after the submission (even on refresh). Two root causes, both fixed: Defect A — the cookie "clear" ran inside Shortcodes::render during the_content, after HTTP headers were flushed, so setcookie() was a no-op and the cookie survived its 30-min TTL → banner reappeared on refresh. Defect B — the signed token in the form body had no server-side replay guard; FormProcessor only checked signature + expiry, so the same token POSTed again within the window produced another adjusted certificate. Fix: - ScheduleExceptionSession gains a consumed-jti ledger: try_consume_jti() claims a token atomically via INSERT IGNORE (the option_name UNIQUE index is the lock, so the first claim wins and every replay loses), plus is_jti_consumed() and cleanup_expired_consumed() (daily-cron sweep of ffc_sched_exc_used_<jti> markers, autoload='no', value = token exp). - FormProcessor claims the jti at the success point (maybe_persist_schedule_exception) — not at verify time, so a downstream validation failure can't burn the one-use token. A racing double-click loses the claim and is recorded at baseline. A cheap pre-check (live_exception_payload) gates $has_exception (and thus the IP-rate-limit bypass) on verify + scope + jti-not-consumed. - Shortcodes::render suppresses the banner once the jti is consumed, even while the cookie lingers — the ledger, not the cookie, is the source of truth for one-use. +13 PHPUnit tests (consume win/lose/empty, is_consumed, cleanup; maybe_persist win/lose; live_exception_payload valid/consumed/mismatch/ garbage; banner suppression). Full suite 4913 green, PHPStan 8, WPCS clean. The ledger reuses the options table (no new schema/migration) with a 30-min TTL reaped by the existing daily cleanup cron. * Fix WPCS: align assignment group in handle_submission_ajax (#Item11) The s24 refactor flattened the early exception block, joining $token_form into the same assignment run as the longer $schedule_exception_payload — so its '=' fell out of alignment (Generic.Formatting.MultipleStatement- Alignment). The whole-PR WPCS job runs via cs2pr, which fails on warnings; phpcbf realigns the group. * Item 10 s25: extract recruitment Notice Edit inline JS (last file) Move all seven inline <script> blocks out of class-ffc-recruitment-notice-edit-page-renderer.php (~373 lines) into a single assets/js/ffc-recruitment-notice-edit.js: CSV import-from-edit, snapshot promote, adjutancy attach/detach, classification tab switch, per-row Call / bulk-call / status transitions (with out-of-order detection against the authoritative data-ffc-empties map), and the preliminary preview-status dropdowns. The two script-only render methods (render_classification_actions_script / render_preview_status_script) are deleted. The functions stay global because the markup invokes them via inline onclick/onsubmit. Every interpolated value — REST root, nonce, the per-status reason-required flags, and all i18n strings — moves to a single localized object (ffcRecruitmentNoticeEdit) on RecruitmentAdminAssetsManager; per-instance data rides existing data-attributes (notice id via a new data-notice-id on the import form, the empties map via data-ffc-empties). Form controls are read through form.elements.namedItem(). This completes the Item 10 inline-JS extraction sweep (6/6 Group-A files). +29 Vitest tests (tab switch, preliminary→batched / definitive→fetch import, snapshot guard+POST, attach/detach, bulk-call in-order vs OOO and the modal callback, per-row call OOO gate + cancel + status, preview-status required-reason skip + PATCH + empty-disable, and 2xx/non-2xx resolution paths). notice-edit.js at 90.78% statements; global JS floor (82) holds. ESLint + Vitest + php -l + PHPStan 8 + WPCS + PHPUnit all green. * Item 9 s26: public token-based appointment cancellation page Resolve the Item 6 get_cancellation_url debt: the appointment e-mail cancel link pointed at the dashboard with dead params and required login. New AppointmentCancellationHandler (mirrors AppointmentReceiptHandler): a public query var (ffc_cancel_appointment) + token, intercepted on template_redirect, rendering a self-contained page (assets/css/ ffc-appointment-cancellation.css). GET shows the appointment summary + an optional-reason form; a nonce-guarded POST delegates to AppointmentHandler::cancel_appointment(), which re-validates the token via hash_equals and enforces every calendar rule. No account required. Security: constant-time token check (token_matches), one generic message for invalid-link OR unknown-appointment so ids can't be probed, noindex, nonce on the POST. The branch decision is isolated in a pure classify_request() (invalid_link / invalid_token / already_cancelled / process / confirm) since the render methods end in exit(). get_cancellation_url() now builds the tokenised URL (falling back to the dashboard tab for legacy token-less rows) and drops the dead action=cancel&appointment_id params. Registered in the loader. A test caught a real double-encode bug: add_query_arg() already URL-encodes values, so the pre-rawurlencode was redundant (removed, now matching get_receipt_url). +16 PHPUnit tests; PHPStan 8 + WPCS + Stylelint green; self-scheduling / email / loader / receipt suites (191) pass. --------- Co-authored-by: Claude <noreply@anthropic.com>
… → empty (#481) Adds a privileged escape hatch (#Item 8) letting an operator with the ffc_manage_recruitment cap undo a realized classification decision and return the candidate to the waiting queue. - RecruitmentClassificationStateMachine::admin_override_to_empty(): a separate override path (restricted to hired/withdrew/not_shown) that bypasses TRANSITIONS, the terminal guard and the reopen-freeze by design, race-safe via set_status CAS, reason-gated. Vacancy reopen and original queue position fall out of the status flip (ranking untouched; vacancy derived from hired count). No candidate e-mail. - Distinct WARNING-level audit event classification_override_to_empty. - POST /classifications/{id}/override-to-empty REST route + handler. - 'Undo decision (admin)' row action on the notice editor with a destructive confirm + mandatory reason, wired in the extracted JS. - Tests: +9 state-machine, +2 REST, +3 Vitest. Co-authored-by: Claude <noreply@anthropic.com>
* User permissions: redesigned grouped capability editor + catalog Rebuilds the per-user FFC capability section on the WordPress user-edit screen as a grouped, card-based panel and fixes a latent save bug. - New CapabilityCatalog: single source of truth mapping every cap slug to a label, description, domain group and level. CI asserts it covers exactly CapabilityManager::get_all_capabilities(). - Render: grouped collapsible cards (admin groups start collapsed), live search across label+slug, Grant/Revoke-all presets, copyable monospace slug chip per row, origin badge (User vs Role), live per-group count, and a read-only role + audiences context summary (role stays in WP's native selector; audiences link to the Audiences screen). - Fix: the old form rendered ~10 of ~26 caps while save iterated all of them, so every cap without a checkbox was silently remove_cap()'d on save. Render and save now both derive from the catalog. - Assets: new ffc-user-permissions.css; ffc-user-capabilities.js rewritten (vanilla, no jQuery) for presets/search/collapse/copy/live-count. - Tests: CapabilityCatalogTest (registry parity + metadata), updated AdminUserCapabilitiesTest (new markup + enqueue), new Vitest suite, and retired the superseded tiny-scripts block. - Includes the design mockup under docs/mockups/. * User permissions: editable audience membership in the panel Turns the read-only audiences summary into an inline editor: a checklist of every active audience (pre-checked for current memberships), synced on the same profile-form save. - render_audience_membership(): lists active audiences as ffc_audience[] checkboxes with a color dot, pre-checked per membership; links to the Audiences screen. - sync_audience_membership(): whitelists the submission against the active set, diffs against current active memberships, and applies the minimal add_member/remove_member calls. Only active audiences participate, so a membership in an inactive audience is never touched; if no audiences are active (checklist not rendered) it is a no-op, so it can't wipe on an unrelated save. Changes are audit-logged. - CSS: audience checklist styles. Tests: +1 render, +3 save (add / remove on uncheck / no-op when none active). --------- Co-authored-by: Claude <noreply@anthropic.com>
A single SSH connection timeout to the testes host (server reboot or a network blip) failed the whole deploy after a ~2min SYN wait, leaving the testes domain stuck on the previous develop HEAD. Add an explicit ConnectTimeout (fail fast in 30s) plus keepalive options, and retry the idempotent rsync up to 3× with backoff before failing the job. Co-authored-by: Claude <noreply@anthropic.com>
Interactive mockup for the per-user permission panel direction where roles render as preset chips that illuminate the capabilities they grant, with live origin recompute (Role vs User). Design artifact referenced by issue #484; companion to docs/mockups/user-permissions-redesign.html. Co-authored-by: Claude <noreply@anthropic.com>
Frente A of the unified permissioning UX. Roles become first-class in the per-user panel: each FFC role renders as a preset chip that, on hover, illuminates the capabilities it grants; assigning/removing a role locks (or releases) those caps in the grid as role-granted. - Role writes are isolated via a dedicated AJAX endpoint (wp_ajax_ffc_toggle_user_role), one role at a time, NOT on the profile form submit — so they never race WordPress core's set_role or a third-party multi-role plugin. Restricted to FFC preset roles (those granting an FFC cap and not manage_options), refuses admin targets, cap- + nonce-gated, audited. - Preset roles are auto-discovered from wp_roles() (CapabilityCatalog intersection), so newly-registered FFC roles appear with no code change. - Role-granted caps render ON but disabled, so the form save never writes a redundant per-user override for something a role already grants. - JS: hover illumination, AJAX toggle, live origin/lock/count recompute from the localized role→caps map + per-row user-grant snapshot. - Tests: +6 PHPUnit (role chips render + AJAX assign/remove/guards), +3 Vitest (illuminate, assign-locks, remove-restores); WP_User test double gained remove_role. The editor for role *definitions* (global scope) stays tracked in #484. Co-authored-by: Claude <noreply@anthropic.com>
…e B) (#487) * Roles: global role-capability editor on Settings → User Access (Frente B) The global-scope half of the permissioning split (#484): edit which FFC capabilities each FFC role grants, from Settings → User Access. The per-user panel assigns roles + fine-tunes one user; this editor changes the role definition itself. - RoleCapabilityEditor: pick an FFC role, toggle its cataloged caps. Reuses the CapabilityCatalog grouped-card / search / slug-chip UI; shows each role's member count + a prominent global/retroactive impact banner. - Persistence is per-toggle via an isolated AJAX endpoint (wp_ajax_ffc_set_role_cap → WP_Role add_cap/remove_cap), separate from the User Access options form on the same tab. Restricted to CapabilityManager::ffc_managed_role_labels() and to cataloged caps — never touches core/super roles or non-FFC caps; cap- + nonce-gated; audit-logged. - CapabilityManager::ffc_managed_role_labels() exposes the canonical FFC role set (extracted from relabel_ffc_roles). - Rendered from the user-access tab view, outside the settings <form>. - Tests: +8 PHPUnit (editable-role discovery, role→caps map, render smoke, AJAX grant/remove + role/cap/cap-guard), +3 Vitest (role-picker swap, persist success, failure revert). Closes #484. * Fix WPCS: use array<int,string> param docblock for render_catalog_grid WPCS 3.3.0 (CI) rejects a list<...> pseudo-type on a parameter docblock ("Expected type hint list<string>; found array"). Match the codebase's array<...> param convention; the value passed is still a list<string>. --------- Co-authored-by: Claude <noreply@anthropic.com>
* docs(plan): GAP A capability taxonomy + 3-state permission model Planning document for review (refs #488, #489). Captures the ratified capability naming standard, the not-see/see/see-and-edit model, the full rename map (single 'appointments' domain), the eight new view caps, and the per-surface 3-state gates for certificates, custom fields and recruitment settings. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD * GAP A (1/4): capability taxonomy — renames + view caps + migration Establishes the plugin-wide capability naming standard (ffc_<action>_[own_] <domain>[_<qualifier>]) and the 3-state permission model, per #488. - Rename 10 caps to the standard (single 'appointments' domain). The ffc_view_self_scheduling -> ffc_view_own_appointments pair reverses the 4.5.0 rename migration, so it ships under a new option flag. - Add 8 read-only 'view' caps (the *só vê* tier): ffc_view_certificates, _appointments, _audiences, _reregistration, _custom_fields, _settings, _recruitment_settings, _recruitment_reasons. Total 26 -> 34 caps. - CapabilityCatalog updated (invariant all_slugs() == get_all_capabilities() preserved); CLAUDE.md documents the standard + gate rules. - One-shot migration (CapabilityManager::migrate_taxonomy_renames + Loader::ensure_taxonomy_renamed) rewrites grants on every user and role; admin caps version bumped v2 -> v3 so the new view caps reach the administrator role. ffc_operator gains the admin view caps. - phpcs custom_capabilities allowlist updated. Breaking: external integrations referencing the old slugs must update. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD * GAP A (2a/4): fix WPCS array alignment + manager roles carry view cap - phpcbf realignment of the catalog arrays after the slug renames (the WPCS 'Array double arrow not aligned' failure on the previous push). - Each FFC manage role now also carries its matching view cap so the admin menu/tab (gated by a single view-cap string) stays visible to managers; inline write gates still require the manage cap. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD * GAP A (2/4 + 4/4): wire dead caps as 3-state gates + tests + CHANGELOG Certificates: - Submissions menu + Certificates Dashboard gated by ffc_view_certificates; trash/restore/delete (single + bulk + AJAX) require ffc_manage_certificates; the per-record Edit link requires ffc_edit_certificates. Row/bulk write actions are hidden from read-only viewers. Custom fields: - Audience custom-field save/delete/replicate AJAX gated by ffc_manage_custom_fields; the editor section renders read-only (no add/save controls) for ffc_view_custom_fields-only users. Recruitment settings: - Settings tab governed by ffc_view_recruitment_settings (see, read-only) and ffc_manage_recruitment_settings (edit); the latter is also wired as the options.php capability for the recruitment option group, so a Recruitment Manager operates the module while only a Recruitment Admin configures it. The tab is hidden without the view cap and the form is a disabled fieldset (no submit) for read-only viewers. Roles: ffc_recruitment_admin gains the recruitment view caps; manage roles carry their matching view cap so menus/tabs stay visible. Tests: taxonomy_cap_renames map + migrate_taxonomy_renames coverage. CHANGELOG: breaking-change banner for the renames + the newly-enforced caps. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD * GAP A: update CertificatesDashboardTest for ffc_view_certificates cap The dashboard CAPABILITY const moved from edit_others_posts to ffc_view_certificates (3-state gating); the test's hardcoded expectations follow. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD --------- Co-authored-by: Claude <noreply@anthropic.com>
* docs(plan): GAP B manage_options delegation + 3-state model Planning document for review (refs #488). Classifies the ~48 manage_options occurrences into KEEP (admin-bypass, permission-management, infra, already delegated) vs SWAP (settings page, reregistration custom-fields submenu, submission REST admin, Short URLs). Settings delegates whole-page to ffc_manage_settings; Short URLs gets a new url_shortener domain (ffc_view_url_shortener / ffc_manage_url_shortener). Sequenced after GAP A so each surface lands in the full not-see/see/see-and-edit model. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD * GAP B: delegate blanket manage_options gates to granular caps Replaces sole-authorization manage_options gates on delegable admin surfaces with the 3-state caps from GAP A (admins unaffected — they hold every FFC admin cap). Per the plan in docs/plans/gap-b-manage-options-delegation.md. - Settings: menu gated by ffc_view_settings; the per-field autosave allowlist default + entries, the geofence-locations CRUD AJAX, and clear-all-cache now require ffc_manage_settings. - Submission REST read routes: manage_options -> ffc_view_certificates. - Reregistration Custom Fields submenu: manage_options -> ffc_manage_reregistration (aligns with the rest of the module). - Short URLs: new url_shortener domain — ffc_view_url_shortener (list/QR) + ffc_manage_url_shortener (CRUD AJAX, metabox regenerate, GET-link writes). Registered in CapabilityManager + CapabilityCatalog (invariant preserved), phpcs allowlist, and granted to ffc_operator (view). 34 -> 36 caps. Tests updated (settings allowlist cap; url-shortener handle_actions stubs + a new read-only no-write gate test). CHANGELOG updated. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD --------- Co-authored-by: Claude <noreply@anthropic.com>
…ents, audiences, recruitment (#493) * GAP C (1/n): reregistration read-only auditor (3-state) + plan Opens the Reregistration admin (menu + Campaigns) to ffc_view_reregistration as a read-only 'só vê' tier; writes still require ffc_manage_reregistration. The campaign editor (new/edit) is denied to viewers; Add New, per-row Edit/Delete, submission Approve/Reject/Return-to-draft and the bulk-actions bar are hidden; a read-only row's title links to Submissions instead of the editor. Server-side write gates (AJAX handler + GET-link handler) unchanged. Adds docs/plans/gap-c-readonly-admin.md capturing the full 4-surface design, including the security finding that RecruitmentAdminActions::dispatch must be per-action cap-gated before its page can be opened to viewers, and the Scheduling parent-menu cap decision. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD * GAP C (2/n): read-only tier for appointments, audiences, recruitment Opens each module's admin pages to the matching view cap as the 'só vê' tier; every write keeps its manage cap (server-side boundary). - Appointments: ffc-appointments submenu + render_appointments_page -> ffc_view_appointments; Confirm/Cancel row actions hidden (their mutation was already manage-gated in the view). - Audiences: the unified 'Scheduling' menu (parent + 7 submenus) -> ffc_view_audiences. render_page has no own gate; writes live in handle_actions (manage) + AJAX, both independent -> safe to open. - Recruitment: RecruitmentAdminActions::dispatch hardened to re-check ffc_manage_recruitment on every destructive action (previously relied only on the page gate); render_page + menu + data tabs -> ffc_view_recruitment; edit screens require manage. Realizes the auditor/operator roles (were REST-only). Tests: +1 dispatch no-op-without-manage security test; RecruitmentAdminActions setUp stubs current_user_can. Full unit suite green (4967). CHANGELOG + plan updated. UI polish (hiding Add/Create buttons for viewers on audiences + recruitment tabs) noted as a follow-up — writes are already server-side gated. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD * GAP C (3/n): hide top create buttons for read-only viewers Completes the 'hide top' depth for audiences and recruitment: - Audiences: the 'Add New' page-title-action on the audiences, calendars and environments list pages is hidden unless the user can manage audiences. - Recruitment: the Create-notice / Create-adjutancy / Create-reason forms render nothing for read-only viewers (the REST endpoints behind them are manage-gated regardless). Per-row write actions (edit/delete/call) still render but stay server-side gated — outside the 'top' scope. Full unit suite green (4967). https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD --------- Co-authored-by: Claude <noreply@anthropic.com>
Adds ffc_view_custom_fields, ffc_view_recruitment_settings and ffc_view_recruitment_reasons to the ffc_operator role so it can audit every module surface read-only. Deliberately excludes ffc_view_settings (SMTP/ security config), ffc_view_recruitment_pii (deliberate grant), ffc_view_forms_api and ffc_view_as_user. Existing roles self-heal via register_module_roles(). The core of GAP D was already resolved by the taxonomy work (the role's view caps + the self_scheduling->appointments semantics fix); this completes it. Test: CapabilityManagerTest asserts the operator's full view set and the forbidden caps. CHANGELOG updated. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
…tier (#495) Introduces seven dedicated destructive caps — ffc_delete_certificates, _appointments, _audiences, _reregistration, _custom_fields, _recruitment, _url_shortener — that strictly gate the irreversible removal paths of every admin module. Delete handlers no longer fall back to the broader ffc_manage_* cap, so a role can manage (create/edit/configure) without being able to delete. Gated surfaces: Submissions permanent delete (trash/restore stay under manage), appointment cleanup purges, audience/calendar/environment deletes, reregistration campaign delete, custom-field-definition delete, every recruitment record delete (admin actions + candidate-edit hard-delete + adjutancies bulk + REST DELETE routes; reason deletion stays under Manage reasons), and the full short-URL removal workflow (trash/restore/delete/empty-trash; create/edit/toggle stay under manage). A one-shot, option-flagged migration (ffc_delete_caps_granted_v1) seeds each delete cap onto every user/role already holding the matching manage cap, so behavior is preserved on upgrade; admins restrict by removing the delete cap. The manager roles carry their domain's delete cap; the read-only ffc_operator does not. Catalog + CapabilityManager updated together (CI invariant), WPCS ruleset learns the new caps, and tests cover the role grants, the grant map and the migration. Also fixes the user-permissions card width: .ffc-cap-panel was capped at max-width:860px (narrower than its column on wide screens) and now spans width:100% on both the user-edit screen and Settings -> User Access. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
…ge_options) (#496) Introduces a single role that carries every FFC capability — the complete admin surface (view/manage/delete/export/import, the full recruitment tier including settings/reasons/PII/call/import, scheduling bypass, activity log, impersonation, settings, URL shortener) plus the end-user self-service (own_) caps — but deliberately NOT manage_options. A site can delegate full plugin administration (configure, operate and delete across every module, and use the dashboard as a regular user) without granting WordPress super-admin. The role's cap set is defined as the live get_all_capabilities() list, so any capability added in a future release is granted to it automatically. It plugs into the existing role machinery via module_roles_definition(), so it is registered + self-healed on upgrade, listed in the Settings -> User Access role editor, label-translated, and removed on uninstall — no new caps (catalog invariant untouched) and no migration (brand-new role). Also tightens get_all_capabilities()'s return type to list<string> (it array_merges list consts) so the role definition typechecks at PHPStan level 8. Tests: expected-slugs list gains ffc_administrator; a dedicated test asserts it grants every get_all_capabilities() cap plus read and never manage_options. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
) Introduces three granular export capabilities — ffc_export_appointments, ffc_export_reregistration, ffc_export_audiences — that gate the bulk data-extraction paths previously riding on the broad ffc_manage_<domain> cap, strictly: a role can hold manage (configure/create/edit) without being able to download the dataset. Mirrors the long-standing standalone ffc_export_certificates model and the GAP E delete tier. Gates: - Appointments CSV export now requires ffc_export_appointments. - Reregistration submissions export re-checks ffc_export_reregistration in the delegated handler (the page-level manage gate still applies). - Audience members/audiences exports require ffc_export_audiences; the handler is restructured to gate per-action so import + sample-template downloads stay under ffc_manage_audiences and an export-only role works without manage. A one-shot, option-flagged migration (ffc_export_caps_granted_v1) seeds each new export cap onto every user/role already holding the matching manage cap, so existing behavior is preserved on upgrade. The three manager roles carry their domain's export cap; the read-only ffc_operator does not; ffc_administrator holds all of them automatically. Also adds the export caps to CapabilityCatalog (catalog<->manager invariant kept) and to uninstall.php, and fixes a GAP F omission: the ffc_administrator role is now removed on uninstall. Tests: ADMIN_CAPABILITIES export tier, export_cap_grant_map pairing (certificates absent), per-role grants with operator excluded, the migration seeding, plus a reregistration export deny-without-cap test. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
…498) Carves bulk CSV ingestion out of manage for the two domains where loading external data is the most sensitive action, mirroring the export (GAP G) and delete (GAP E) tiers: - New ffc_import_audiences gates the members/audiences CSV import (the AudienceAdminImport handler is restructured to gate per-action: import requires the import cap, export requires the export cap, the sample template follows manage-or-import; an import-only role works without manage). - Existing ffc_import_recruitment is tightened to STRICT enforcement — its handlers (the classifications REST routes via check_can_import_csv(), the standalone Candidates CSV import section, and the per-notice import section on the Notice Edit screen) no longer accept the umbrella ffc_manage_recruitment as a fallback. Import thus joins ffc_delete_recruitment as a carved-out tier; the umbrella still grants view/call/manage/reasons. The per-notice import UI section is now hidden from a manager lacking the import cap. A one-shot, option-flagged migration (ffc_import_caps_granted_v1) seeds each import cap onto every user/role already holding the matching manage cap, so existing behavior is preserved on upgrade. WP admins and the FFC recruitment roles already hold ffc_import_recruitment explicitly; ffc_audience_manager gains ffc_import_audiences; ffc_operator carries neither; ffc_administrator holds both. Also registers ffc_import_audiences in CapabilityCatalog (invariant kept) and uninstall.php. Tests: import tier in ADMIN_CAPABILITIES, import_cap_grant_map pairing, per-role grants with operator excluded, the migration seeding, and an updated trait test asserting manage alone no longer grants recruitment import. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
…#499) Permissioning-audit cleanup. The ffc_self_scheduling_manager role carried ffc_export_certificates — the certificate/submissions CSV export cap — which is a different domain (certificates), the only cross-domain grant on any module-manager role. The Self-Scheduling Manager administers appointments; the certificate export cap belongs to ffc_certificate_manager / ffc_administrator. Removed from the role definition only. register_module_roles() never strips extra caps from existing role instances (by design), so existing installs keep the cap and there is no behavior change on upgrade; only freshly-created roles are clean. To finish the cleanup on an existing site, uncheck the cap in Settings -> User Access. Adds a negative assertion locking the cap out of the role definition. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
Audit follow-up. The ffc_view_recruitment_reasons / ffc_manage_recruitment_reasons caps were half-wired: the Reasons tab was visible to anyone with the page-level ffc_view_recruitment, and reason editing rode on the umbrella ffc_manage_recruitment (the dedicated manage cap was only additively honored by the REST route). The bulk-delete handler checked only a nonce, so a read-only recruitment viewer could delete reasons via a crafted POST. Mirrors the recruitment Settings sub-domain exactly: - Reasons tab carved out by can_view_reasons() (view OR manage reasons cap), hidden from the nav otherwise. - Create form, reason edit screen, single-delete dispatch, REST check_can_manage_reasons(), and the bulk-delete handler now require ffc_manage_recruitment_reasons STRICTLY (umbrella no longer grants it). - Reasons list table renders read-only for viewers: no Edit/Delete row actions, no bulk-delete control, static color swatch instead of the inline picker. Behavior-preserving migration (ffc_reasons_caps_wired_v1) seeds the view cap onto ffc_view_recruitment holders and the manage cap onto ffc_manage_recruitment holders. FFC recruitment roles carry the caps explicitly (auditor/operator get view; manager/admin get both). Tests: reasons_cap_grant_map pairing, migration seeding, role grants, the trait strict gate (updated), and list-table read-only rendering. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
…rs (#501) The plugin Settings menu opens on ffc_view_settings (the só vê tier), but every save handler and AJAX endpoint requires ffc_manage_settings. A view-only user therefore saw fully editable fields that silently failed at the manage-gated save — read-only in name only. Wrap the active tab body in a disabled <fieldset> when the user lacks ffc_manage_settings (admins always pass). A disabled fieldset natively disables every descendant form control — inputs, selects, textareas, submit and action buttons — across whichever tab is active, so read-only is enforced for all tabs without touching each tab's template. Add a read-only banner and CSS that strips the default fieldset chrome so the locked render is visually identical to the editable one. Mirrors the recruitment Settings tab's existing fieldset-disabled pattern and completes the 3-state model for the global Settings surface. Server-side gating was already in place; this closes the UI half. Covered by two SettingsTest cases (locked for view-only, unlocked for manager). https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
The docs/ folder held six development-only artifacts (a frontend audit, two HTML permission mockups, and the GAP A/B/C planning docs) — none of it runtime code. It is already excluded from the distributed plugin zip via .distignore and is referenced by no PHP/JS, no CI gate, and no build step. The durable design decisions live in CLAUDE.md and CHANGELOG; the long-form working docs are preserved in git history. Also drops the now-dead 'See docs/plans/gap-c-readonly-admin.md' link from the GAP C CHANGELOG entry. (The older historical entry referencing docs/HOOKS-*.md is left as-is — those files were removed long ago and it is part of the shipped record.) The develop deploy rsync uses --delete, so this also clears docs/ from the testes server on the next deploy. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
Both capability-catalog surfaces — Settings -> User Access (role editor) and the per-user editor on the user-edit screen — rendered every admin cap in one giant 'Administration — Modules' card ordered by tier (all view, then all manage, delete, export, import), so finding everything about one module meant scanning five sub-sections. Split the admin bucket into one card per module (Certificates, Appointments, Audiences, Reregistration, Custom fields, Short URLs, Settings, System & tools, Recruitment), each ordered by tier internally, matching the per-module self-service cards. Add a Self-service / Administration section divider, start every group collapsed (the live search already auto-expands groups with hits), and tag the two caps whose execution surface isn't obvious from their section with a small badge: 'API' on ffc_view_forms_api, 'frontend' on ffc_scheduling_bypass. Pure presentation — the cap set, slugs and the catalog<->registry invariant are unchanged. Both editors read from CapabilityCatalog::groups(), so the render is driven entirely by the regrouped data; the divider/collapse/badge are the only render tweaks, shared via CapabilityCatalog helpers. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
The changelog had grown verbose again: each item from 6.0.0 to [Unreleased] was a dense 3-8 line paragraph carrying full implementation rationale, plus per-version meta subsections (Tests, Bundle cache, Bundles, Internal, i18n, Notes, Out of scope, Maintenance, Why-this-escaped, How-to-pin, Detection scope, Backwards compatibility) that are process noise for a user/integrator-facing changelog. Rewrites the 6.0.0 -> [Unreleased] range to one tight line per item, keeping the substantive Added/Changed/Fixed/Removed/Security/Performance bullets, the version headings + dates, the breaking-change markers and load-bearing cap/slug names; drops the rationale (it lives in the commits, code and CLAUDE.md) and the meta subsections. Merges the [Unreleased] section's two duplicate '### Changed' blocks. Pre-6.0.0 history is left untouched (scope was 'since 6.0.0'). No code change. Range shrinks ~1487 -> ~728 lines; file 3748 -> 2989. All 60 6.x version headings preserved. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
Bumps FFC_VERSION 6.9.0 -> 6.10.0 across the three sync sites (plugin header, FFC_VERSION constant, readme.txt Stable tag) and renames the [Unreleased] CHANGELOG section to [6.10.0] (2026-06-05), adding a fresh empty [Unreleased] above it. Consolidates the develop batch: the plugin-wide capability taxonomy + 3-state permission model (GAPs A-I), the ffc_administrator aggregator role, the role + per-user capability editors (now organized by module), real read-only Settings for the view tier, the recruitment Reasons/Settings tiers, login-free appointment cancellation, the admin undo-decision override, and the frontend-audit refactors. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD Co-authored-by: Claude <noreply@anthropic.com>
rpgmem
marked this pull request as ready for review
June 5, 2026 05:14
rpgmem
enabled auto-merge (squash)
June 5, 2026 05:14
…ease The 6.9.0 release squash (ca3c73e, #478) was never merged back into develop, so develop and main diverged: the same 6.9.0 work exists on both as different commits (squash on main vs the original commits + the later batch + the 6.0.0-onward CHANGELOG condensation + the 6.10.0 bump on develop). That made the develop -> main release PR (#506) conflict on CHANGELOG.md, the version files, two recruitment files and one test. develop already contains everything in ca3c73e, so this records main as an ancestor while keeping develop's tree verbatim — every conflict resolved to develop's side, and the resulting tree is byte-identical to develop HEAD (no main content dropped, no develop content changed). Unblocks the 6.10.0 release PR. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD
Sync develop with main (6.9.0 squash) to unblock the 6.10.0 release
rpgmem
pushed a commit
that referenced
this pull request
Jun 5, 2026
Records the 6.10.0 release squash (b1ba3db, #506) as an ancestor of develop so the next release PR starts from the bumped baseline and doesn't re-conflict on the release files (the missed-sync debt that blocked #506). develop and main already have identical trees post-release, so this is a clean no-op merge — tree is byte-identical to develop HEAD. https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 6.10.0 —
develop → mainConsolida o lote acumulado na
developnum único bump deFFC_VERSION(6.9.0 → 6.10.0) para produção.Destaques
Permissionamento (taxonomia + modelo 3-estados, GAPs A–I)
ffc_<action>_[own_]<domain>[_<qualifier>]; 10 caps renomeados + 8viewcaps (26 → 34), com migração one-shot. ⚠ breaking para integrações nos slugs antigos.manage: delete (GAP E), export (GAP G), import (GAP H), recruitment Reasons (GAP I) — cada um com migração que semeia nos holders demanage, preservando comportamento no upgrade.manage_optionssubstituídos por caps delegáveis (GAP B); read-only "só vê" admin em reregistration/appointments/audiences/recruitment (GAP C);ffc_operatorcomo auditor read-only completo (GAP D).ffc_administrator(todo o plugin semmanage_options) (GAP F).ffc_view_settings(G3).Editores de permissão
Recrutamento
withdrew, undo-decision do admin, paginação em janela na lista pública, e os tiers de Reasons/Settings.Outros
export_certificates; remoção da pastadocs/(dev-only).Housekeeping
FFC_VERSIONbumpado nas 3 fontes;[Unreleased]→[6.10.0] (2026-06-05).⚠ Breaking para integrações externas: renome de caps na taxonomia, tiers estritos de delete/export/import, REST
/formspaginado (limit→per_page). Instalações são migradas automaticamente; o aviso vale para automações que referenciam slugs/parâmetros antigos.https://claude.ai/code/session_015oyFHBoKYyRez9F3ARZjvD
Generated by Claude Code