feat: triage workspace + PAT auth + upgrade infrastructure + seed dataset + OpenAPI hardening#731
feat: triage workspace + PAT auth + upgrade infrastructure + seed dataset + OpenAPI hardening#731aaronlippold wants to merge 436 commits into
Conversation
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 33453144 | Triggered | Generic Password | 4f5f67c | doc/openapi.yaml | View secret |
| 33453143 | Triggered | Generic Password | 4f5f67c | doc/openapi/paths/users_unlink_identity.yaml | View secret |
| 33788689 | Triggered | Generic Password | 4271c98 | spec/requests/api/auth_spec.rb | View secret |
| 33788689 | Triggered | Generic Password | 4271c98 | spec/contracts/api_auth_contract_spec.rb | View secret |
| 33788689 | Triggered | Generic Password | 4271c98 | spec/contracts/api_auth_contract_spec.rb | View secret |
| 33788688 | Triggered | Generic Password | 4271c98 | doc/openapi/paths/api_auth_login.yaml | View secret |
| 33788688 | Triggered | Generic Password | 4271c98 | doc/openapi.yaml | View secret |
| 33788689 | Triggered | Generic Password | 4271c98 | spec/requests/api/auth_spec.rb | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
There was a problem hiding this comment.
Pull request overview
This PR modernizes the comment/triage workflow and development seeding by introducing a split-pane triage UI (replacing the modal flow), modular/idempotent seed helpers + dev rake tasks, and a DRY’d comment composer state model (ReplyComposerMixin) with supporting factory/test improvements.
Changes:
- Add split-pane triage experience (queue navigation + rule context panel + extracted triage form) and related API support (
include_rule_content, HTML redirect to triage). - Modularize and validate seed pipeline (SeedHelpers, numbered seed files,
dev:*rake tasks, seed verification specs). - Consolidate composer/triage vocabulary utilities and standardize factories/spec fixtures around new traits.
Reviewed changes
Copilot reviewed 83 out of 83 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/services/import/json_archive_importer_spec.rb | Switch review setup to factories/traits for import lifecycle fixtures. |
| spec/services/import/integration/backup_round_trip_spec.rb | Use factory-created reviews in backup round-trip integration spec. |
| spec/services/export/serializers/backup_serializer_spec.rb | Use factory traits for review lifecycle/duplicate-of serialization coverage. |
| spec/services/export/base_spec.rb | Use factory traits for disposition-related export setup. |
| spec/seeds/seed_pipeline_spec.rb | Add end-to-end seed pipeline spec (tagged, truncation strategy). |
| spec/requests/users_spec.rb | Use review factory traits for “My Comments” request fixtures. |
| spec/requests/rules_spec.rb | Use review factory traits for component JSON sidebar/history tests. |
| spec/requests/reactions_spec.rb | Use factory-created reviews/replies for reaction request specs. |
| spec/requests/rack_attack_spec.rb | Use factory-created review for rack-attack reaction target fixture. |
| spec/requests/projects_import_backup_spec.rb | Use factory-created review for project import backup tests. |
| spec/requests/projects_disposition_matrix_export_spec.rb | Use factory traits for triage-status filtered disposition export tests. |
| spec/requests/projects_create_from_backup_spec.rb | Use factory-created review for include_reviews param coverage. |
| spec/requests/project_comments_aggregate_spec.rb | Use factory traits to build aggregate comment fixtures. |
| spec/requests/components_spec.rb | Add HTML redirect expectation for /components/:id/comments; use review factory. |
| spec/requests/components_disposition_matrix_export_spec.rb | Use review factory traits for component disposition export fixtures. |
| spec/requests/component_reviews_spec.rb | Use :component_comment factory trait for component-scoped review tests. |
| spec/rails_helper.rb | Exclude :seed_pipeline specs by default to avoid parallel DB corruption. |
| spec/models/user_spec.rb | Use review factory traits for imported attribution behavior tests. |
| spec/models/reviews_spec.rb | Standardize review creation on factories/traits across invariants + scopes. |
| spec/models/review_polymorphic_commentable_spec.rb | Use :comment/:component_comment traits for polymorphic coverage. |
| spec/models/review_cross_scope_validations_spec.rb | Use review factory traits for cross-scope validation fixtures. |
| spec/models/reaction_spec.rb | Use review factory traits for reaction invariants and summary tests. |
| spec/models/query_performance_spec.rb | Use factory-created reviews to drive component review query tests. |
| spec/models/project_pending_comment_counts_spec.rb | Use factory traits for pending/total count fixtures. |
| spec/models/paginated_comments_pii_spec.rb | Use review factory traits for paginated comment response/reaction counts. |
| spec/models/components_spec.rb | Add assertions for include_rule_content payload shape + updated_at presence. |
| spec/models/component_pending_comment_counts_spec.rb | Use review factory traits for per-component pending count fixtures. |
| spec/lib/seed_helpers_spec.rb | Add unit specs for SeedHelpers API (seed_xccdf/component/review/reply/status/verify). |
| spec/lib/disposition_matrix_export_spec.rb | Switch to review factory traits for export and CSV defang coverage. |
| spec/javascript/mixins/ReplyComposerMixin.spec.js | Add vitest coverage for unified composer state mixin behaviors. |
| spec/javascript/constants/triageVocabulary.spec.js | Add tests for buildStatusFilterOptions(). |
| spec/javascript/components/triage/TriageQueueNav.spec.js | Add tests for 2D triage queue navigation component. |
| spec/javascript/components/triage/CommentTriageForm.spec.js | Add tests for extracted triage form component validation/events. |
| spec/javascript/components/components/ComponentComments.spec.js | Update ComponentComments tests for split-mode behavior and sorting/filter visibility. |
| spec/javascript/components/components/CommentTriageModal.spec.js | Update modal tests to validate delegation to CommentTriageForm and new doTriage API. |
| spec/factory_specs/stig_deadlock_spec.rb | Add regression spec preventing STIG factory deadlock/import side effects. |
| spec/factory_specs/factory_traits_spec.rb | Add spec verifying new FactoryBot traits across multiple models. |
| spec/factories/stigs.rb | Add :skip_rules trait to disable rule import on STIG factory. |
| spec/factories/stig_rules.rb | Ensure stig_rule factory uses a skip-import STIG by default. |
| spec/factories/rules.rb | Add rule status/locked traits used by tests/seeds. |
| spec/factories/reviews.rb | Add review comment/triage/lifecycle traits + auto-membership wiring. |
| spec/factories/projects.rb | Add project membership traits (:with_admin, :with_members). |
| spec/factories/memberships.rb | Add explicit :viewer trait. |
| spec/factories/components.rb | Add component traits for comment period, PoC, released/locked rules. |
| spec/config/seed_idempotency_spec.rb | Update idempotency checks to match modular seed loader + SeedHelpers. |
| spec/blueprints/rule_blueprint_spec.rb | Use review factory traits for comment summary blueprint tests. |
| spec/blueprints/review_membership_blueprints_spec.rb | Use review factory traits for blueprint output coverage. |
| lib/tasks/dev.rake | Add dev:prime/status/verify/reset seed tasks. |
| lib/seed_helpers.rb | Introduce SeedHelpers module (XCCDF import, component seeding, comment seeding, verify/status). |
| docs/superpowers/plans/2026-05-19-comment-interaction-dry.md | Document DRY plan for composer state/event standardization. |
| docs/development/testing.md | Document available FactoryBot traits and usage patterns. |
| docs/development/seed-system.md | Document seed architecture, commands, and SeedHelpers API. |
| db/seeds/data/00_users.rb | Modular seed: demo admin + role-tier + filler users. |
| db/seeds/data/01_projects.rb | Modular seed: demo projects. |
| db/seeds/data/02_srgs.rb | Modular seed: SRG imports via SeedHelpers. |
| db/seeds/data/03_stigs.rb | Modular seed: STIG imports via SeedHelpers. |
| db/seeds/data/04_components.rb | Modular seed: demo components, overlays, dummy stress components, PoC backfill. |
| db/seeds/data/05_memberships.rb | Modular seed: demo RBAC wiring and counter cache reset. |
| db/seeds/data/06_rule_statuses.rb | Modular seed: vary rule statuses for demo coverage. |
| db/seeds/data/10_comments.rb | Modular seed: demo comment threads + triage states. |
| db/seeds/data/11_cross_project.rb | Modular seed: cross-project comment fixtures + triage examples. |
| app/models/review.rb | Improve sync_commentable_from_rule to backfill rule_id from commentable when needed. |
| app/models/component.rb | Add include_rule_content option to paginated_comments, include updated_at, and serialize rule content. |
| app/controllers/components_controller.rb | Redirect HTML /components/:id/comments to triage; add include_rule_content param handling for JSON. |
| app/javascript/mixins/ReplyComposerMixin.vue | Add unified comment composer state + open/close/post hooks. |
| app/javascript/constants/triageVocabulary.js | Add terminal/single-button status sets + buildStatusFilterOptions(). |
| app/javascript/composables/ruleFieldConfig.js | Add canonical FIELD_LABELS mapping for rule context display. |
| app/javascript/components/users/UserComments.vue | Adopt ReplyComposerMixin; switch truncation to CSS; use shared status options builder. |
| app/javascript/components/shared/CommentThread.vue | Switch date formatting to DateFormatMixin helper and remove ad-hoc formatter. |
| app/javascript/components/rules/RulesCodeEditorView.vue | Adopt ReplyComposerMixin and unify composer open/post handling. |
| app/javascript/components/components/ProjectComponent.vue | Adopt ReplyComposerMixin and unify composer open/post handling. |
| app/javascript/components/components/ComponentTriagePage.vue | Add command bar controls for split-mode context/admin panel; wire split-mode events. |
| app/javascript/components/components/ComponentComments.vue | Add split-mode triage integration; default sort by ID; hide filters in split mode; use status options builder; adopt ReplyComposerMixin. |
| app/javascript/components/components/CommentTriageModal.vue | Embed CommentTriageForm; refactor to doTriage API; use DateFormatMixin. |
| app/javascript/components/components/CommentDedupBanner.vue | Switch date formatting to DateFormatMixin helper and remove ad-hoc formatter. |
| app/javascript/components/triage/TriageQueueNav.vue | Add queue navigation UI (prev/next comment + prev/next rule + jump-to dropdown). |
| app/javascript/components/triage/RuleContextPanel.vue | Add rule context side panel with collapsible sections and related comments list. |
| app/javascript/components/triage/CommentTriageForm.vue | Extract triage form component used by modal + split view. |
| app/javascript/components/triage/TriageSplitView.vue | Add split-pane triage view with optimistic-lock support and admin sidebar actions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| elsif is_stig | ||
| record = Stig.from_mapping(parsed) | ||
| existing = Stig.find_by(stig_id: record.stig_id) | ||
| if existing | ||
| puts " Already exists: #{existing.name} (Stig)" | ||
| return existing | ||
| end | ||
| record.xml = Nokogiri::XML(xml) | ||
| else |
| trait :reply do | ||
| action { 'comment' } | ||
| comment { 'Reply to parent comment' } | ||
| triage_status { nil } | ||
|
|
||
| after(:build) do |review, _evaluator| | ||
| unless review.responding_to_review_id | ||
| parent = create(:review, :comment, user: review.user, rule: review.rule) | ||
| review.responding_to_review_id = parent.id | ||
| review.section = parent.section | ||
| end | ||
| end |
| trait :component_comment do | ||
| action { 'comment' } | ||
| rule { nil } | ||
| section { nil } | ||
| comment { 'Component-level comment' } | ||
|
|
||
| after(:build) do |review| | ||
| unless review.commentable_type == 'Component' | ||
| component = create(:component, :skip_rules) | ||
| review.commentable = component | ||
| review.commentable_type = 'Component' | ||
|
|
||
| if review.user | ||
| project = component.project | ||
| create(:membership, user: review.user, membership: project, role: 'viewer') unless Membership.exists?(user: review.user, membership: project) | ||
| end | ||
| end | ||
| end |
| trait :duplicate do | ||
| triage_status { 'duplicate' } | ||
|
|
||
| after(:build) do |review| | ||
| review.triage_set_by ||= create(:user) | ||
| review.triage_set_at ||= Time.current | ||
| unless review.duplicate_of_review_id | ||
| target = create(:review, :comment, rule: review.rule, user: review.user) | ||
| review.duplicate_of_review_id = target.id | ||
| end | ||
| end |
| comments: { type: Array, required: true }, | ||
| currentId: { type: [Number, String], default: null }, | ||
| }, | ||
| computed: { | ||
| ruleGroups() { | ||
| const groups = []; | ||
| const seen = new Map(); | ||
| for (const c of this.comments) { | ||
| const key = c.rule_id || `component-${c.id}`; | ||
| if (!seen.has(key)) { | ||
| const group = { | ||
| ruleId: key, | ||
| ruleName: c.rule_displayed_name || "(component)", | ||
| comments: [], | ||
| }; | ||
| seen.set(key, group); | ||
| groups.push(group); | ||
| } | ||
| seen.get(key).comments.push(c); | ||
| } | ||
| return groups; | ||
| }, | ||
| currentPosition() { | ||
| for (let gi = 0; gi < this.ruleGroups.length; gi++) { | ||
| const group = this.ruleGroups[gi]; | ||
| for (let ci = 0; ci < group.comments.length; ci++) { | ||
| if (group.comments[ci].id === this.currentId) { | ||
| return { ruleIndex: gi, commentIndex: ci }; | ||
| } | ||
| } | ||
| } | ||
| return { ruleIndex: -1, commentIndex: -1 }; | ||
| }, |
| props: { | ||
| rows: { type: Array, required: true }, | ||
| initialCommentId: { type: [Number, String], required: true }, | ||
| componentId: { type: [Number, String], required: true }, | ||
| effectivePermissions: { type: String, default: null }, | ||
| adminPanelOpen: { type: Boolean, default: false }, | ||
| contextMode: { type: String, default: "commented" }, | ||
| }, | ||
| data() { | ||
| return { | ||
| activeCommentId: this.initialCommentId, | ||
| isDirty: false, | ||
| saving: false, | ||
| conflictAlert: null, | ||
| adminAction: null, | ||
| adminAuditComment: "", | ||
| adminConfirmationId: "", | ||
| adminTargetRuleId: null, | ||
| }; | ||
| }, | ||
| computed: { | ||
| sortedRows() { | ||
| return [...this.rows].sort((a, b) => a.id - b.id); | ||
| }, | ||
| activeComment() { | ||
| return this.sortedRows.find((r) => r.id === this.activeCommentId) || null; | ||
| }, | ||
| canTriage() { |
| it 'creates a review comment using FactoryBot' do | ||
| review = described_class.find_or_seed_review( | ||
| rule: rule, user: user, section: 'check_content', | ||
| comment: 'Test comment for seed helper' | ||
| ) | ||
| expect(review).to be_persisted | ||
| expect(review.action).to eq('comment') | ||
| expect(review.triage_status).to eq('pending') | ||
| end |
| async onTriageSave(decision) { | ||
| await this.doSave(decision, false); | ||
| }, | ||
| async onTriageSaveAndNext(decision) { | ||
| await this.doSave(decision, true); | ||
| }, | ||
| async doSave(decision, advance) { | ||
| if (!this.activeComment) return; | ||
| this.saving = true; | ||
| this.conflictAlert = null; | ||
| try { | ||
| const payload = { | ||
| triage_status: decision.triage_status, | ||
| expected_updated_at: this.activeComment.updated_at, | ||
| }; | ||
| if (decision.response_comment) { | ||
| payload.response_comment = decision.response_comment; | ||
| } | ||
| if (decision.triage_status === "duplicate") { | ||
| payload.duplicate_of_review_id = decision.duplicate_of_review_id; | ||
| } | ||
|
|
||
| const triageRes = await axios.patch(`/reviews/${this.activeComment.id}/triage`, payload); | ||
| this.$emit("triaged", triageRes.data.review); | ||
|
|
||
| if (triageRes.data.response_review) { | ||
| this.$emit("response-posted", { | ||
| parentId: this.activeComment.id, | ||
| responseReview: triageRes.data.response_review, | ||
| }); | ||
| } | ||
|
|
||
| if (!SINGLE_BUTTON_STATUSES.has(decision.triage_status)) { | ||
| const adjRes = await axios.patch(`/reviews/${this.activeComment.id}/adjudicate`, {}); | ||
| this.$emit("adjudicated", adjRes.data.review); | ||
| } | ||
|
|
|
Some notes after review:
Some side notes about locked fields:
|
|
That 3NF doc design is a big enough change to warrant its own PR. Going to cherry pick it and make a new PR off main. |
|
@wdower — heads up, this PR is still in progress. All your review feedback items from the first round are addressed (admin inline, toggle moved, rename, locked comments, staleness badge, etc.) but I still have a few cards to finish:
Will push these in the next session. Don't need a full re-review yet — just wanted you to know the status. — Aaron |
Added/Changed/Fixed sections covering three-column layout, progress bar, DRY color palette, ARIA accessibility, InfoTooltip/InfoNotice adoption, and bug fixes Authored by: Aaron Lippold<lippold@gmail.com>
- Api::NavigationController: nav_links + access_requests + locked_users - Extracts logic from ApplicationController#setup_navigation - Requires authentication (401 for unauthenticated) - OpenAPI 3.2 spec + NavigationResponse schema - Contract test validates response against schema - Live tested with PAT: 5 nav links, real access request data All 7 layers: controller, route, request spec, OpenAPI schema, contract test, bundle+lint, live curl test 9 tests (7 request + 2 contract), 0 failures Authored by: Aaron Lippold<lippold@gmail.com>
- Component#reviews: replaced .as_json with ReviewBlueprint (fixes user_id privacy leak, adds rule_displayed_name via options) - ReviewsController#responses: replaced hand-built hash with ReviewBlueprint (DRY, consistent field set) - ReviewBlueprint: added commentable_type, responses_count, rule_displayed_name (via options[:rule_names]) - OpenAPI ReviewSummary schema: added 3 new fields - responses path spec: now references ReviewSummary schema All 7 layers: Blueprint, controller, request spec, OpenAPI schema, contract test (via existing), bundle+lint, live curl test 26 tests, 0 failures Authored by: Aaron Lippold<lippold@gmail.com>
Research findings:
- GitLab, GitHub, Stripe, JSON:API patterns evaluated
- Pagy (pagination) + has_scope (filtering) recommended
- Hand-rolled pagination rejected — use proper gems
- Response envelope: {rows, pagination: {page, per_page, total}}
- Sort: ?sort=field&order=asc|desc (GitLab pattern)
- Search: ILIKE + pg_search (already installed)
- Latest: ?latest=true with numeric V{x}R{y} parsing
Authored by: Aaron Lippold<lippold@gmail.com>
- Install pagy 43.5.5 (pagination) and has_scope 0.9.0 (declarative filtering) - ApiFilterable concern: paginate(), pagy_response(), apply_sort() — glue only - Pagy initializer: 25/page default, max 100, raise on out-of-range - Api::ProjectsController as first consumer with search scope - Project.search scope with sanitize_sql_like for ILIKE safety - Pagy::RangeError rescue in Api::BaseController - 19 request specs covering pagination, sorting, filtering, SQL injection Design doc: docs/decisions/design-api-filterable.md ADR: docs/decisions/adr-api-filtering-concern.md Authored by: Aaron Lippold<lippold@gmail.com>
- VersionSortable concern: DISTINCT ON + CAST(SUBSTRING) for V{major}R{minor}
- SecurityRequirementsGuide: include concern, replace broken self.latest
- Stig: include concern, gains latest_versions method
- V10R1 correctly ranks above V4R4 (was broken with string MAX)
- V2R10 correctly ranks above V2R9 (minor version numeric sort)
- 9 model specs covering edge cases + non-standard versions
Authored by: Aaron Lippold<lippold@gmail.com>
- All 50 render_as_hash calls in production code switched - String keys match parsed_body and JSON wire format - inject_reactions_mine! updated to string keys (was broken) - CommentQueryService symbol-key injection fixed - Blueprint nested calls deferred to next card - Wire format unchanged for HTTP responses Design doc: docs/decisions/design-render-as-json-migration.md Authored by: Aaron Lippold<lippold@gmail.com>
- commenter_email gated by include_email option - ReviewSummary: rule_id nullable, commenter_email added - Settings: ttl.to_i for integer coercion - Auth coverage: register 6 new API endpoints - query_performance_spec: fix stale field name - Rebundled doc/openapi.yaml Authored by: Aaron Lippold<lippold@gmail.com>
- lefthook pre-commit: openapi-bundle-check on doc/openapi/** - lefthook pre-push: same check as safety net - Fails with actionable message if doc/openapi.yaml is stale - Remove .overcommit.yml — lefthook replaced it, config was dead Authored by: Aaron Lippold<lippold@gmail.com>
- Map --scalar-* CSS vars to --vulcan-* via HAML style tag - MutationObserver syncs body class with [data-bs-theme] toggle - hideDarkModeToggle: true (Vulcan navbar is single control) - Removed hardcoded darkMode: true — reads from theme attribute - 8 Vitest unit tests for theme sync behavior - VitePress docs page + sidebar entry for API docs - Research doc: docs/decisions/research-scalar-theming.md Authored by: Aaron Lippold<lippold@gmail.com>
- RuleBlueprint: render_as_json for nested srg_rule_attributes - ComponentBlueprint: render_as_json for nested project - ProjectBlueprint: render_as_json for nested user in access_requests - users/index.html.haml: render_as_json + string key access - ReviewBlueprint: update 2 comment references Zero render_as_hash calls remain in app/. Authored by: Aaron Lippold<lippold@gmail.com>
Rubocop's Ruby parser produces Lint/Syntax false positives on every .haml file (HAML template syntax is not valid Ruby). Added **/*.haml to AllCops Exclude. HAML linting should use haml_lint if needed. Authored by: Aaron Lippold<lippold@gmail.com>
Extract version SQL fragments to module-level constants (no string
interpolation in Arel.sql). Use arel_table for column/table names
instead of #{table_name} interpolation. Brakeman now reports zero
security warnings.
Authored by: Aaron Lippold<lippold@gmail.com>
- Gemfile: single quotes, alphabetical order (has_scope before pagy) - lefthook: remove --quiet from openapi:bundle (unsupported by Redocly) Authored by: Aaron Lippold<lippold@gmail.com>
- 12 spec files: render_as_hash → render_as_json
- All symbol-key accesses ([:field]) → string keys (['field'])
- have_key(:sym) → have_key('str'), %i[] → %w[]
- Expected value hashes: {sym: val} → {'str' => val}
- Fixed created_at Time→ISO8601 string assertion
- Removed .keys.map(&:to_s) workaround in components_show_spec
- Zero render_as_hash remaining in codebase (app/ + spec/)
Authored by: Aaron Lippold<lippold@gmail.com>
- New Blueprint with nested user (UserBlueprint) + project fields - Replaced hand-built hash in ApplicationController#check_access_request_notifications - Replaced hand-built hash in ProjectBlueprint :show access_requests field - Consistent string keys at all nesting levels - Live tested: navbar notification dropdown renders correctly Authored by: Aaron Lippold<lippold@gmail.com>
- New Blueprint matching Audit#format shape (id, action, auditable_type/id, name, audited_name, comment, created_at, audited_changes array) - Handles AdditionalAnswer field name resolution - Replaced .map(&:format) in UsersController#index - Replaced .map(&:format) in RegistrationsController#edit_activity - Live tested: Users page + Profile Activity page render correctly Authored by: Aaron Lippold<lippold@gmail.com>
Replaces .slice(:id, :name, :email).to_json with UserBlueprint.render(current_user) in application.html.haml. Blueprint is now the single shape definition for current_user data on both the HAML prop path and the API path (/api/auth/me). Authored by: Aaron Lippold<lippold@gmail.com>
Settings.consent.to_h returns string keys from YAML, but
.merge(required:) added a symbol key — mixed types in the
same hash. Changed to .merge('required' => ...) for consistency.
Wire format unchanged (both serialize identically to JSON).
Authored by: Aaron Lippold<lippold@gmail.com>
Vue 2.7 Composition API composable for tree-scoped permissions.
Wraps inject('effectivePermissions') with computed helpers:
canView, canEdit, canReview, canAdmin, isMember.
roleGteTo pure utility replaces RoleComparisonMixin logic with
index-based hierarchy comparison. Foundation for provide/inject
migration — no behavior change in this commit.
28 new tests (8 utility + 20 composable).
Authored by: Aaron Lippold<lippold@gmail.com>
All 6 root Vue components now read effective_permissions from their
initial Blueprint data via setup() and provide('effectivePermissions')
for descendant injection. All 6 HAML effective_permissions props
removed. Server-side conditional on components/show.html.haml line 1
intentionally preserved (Rails template routing decision).
Bug fix: rules_controller#index was not passing current_user to
ComponentBlueprint.render, causing effective_permissions to be null.
10 new tests across 4 spec files. All 6 pages Playwright verified.
Authored by: Aaron Lippold<lippold@gmail.com>
Mixin provided isEmpty(obj) helper but no component imported it. Deleted mixin file and its test spec (10 tests). Authored by: Aaron Lippold<lippold@gmail.com>
- useDateFormat composable: friendlyDateTime matching DateFormatMixin - ControlsCommandBar: replaced DateFormatMixin with composable, now zero mixins (usePermissions + useDateFormat via setup) - ControlsSidepanels: removed dead DateFormatMixin import (friendlyDateTime never called in template) - 8 new composable tests, 67 ControlsCommandBar tests pass Authored by: Aaron Lippold<lippold@gmail.com>
Reads CSRF token from meta[name=csrf-token]. Returns null if missing. Replaces FormMixin's authenticityToken computed. 3 tests. Consumer migration in Phase 2 cards. Authored by: Aaron Lippold<lippold@gmail.com>
- useSortRules: compareRules sort function - useHistoryGrouping: roundToNearestInterval + groupHistories - useHumanizedTypes: humanizedType + HUMANIZED_TYPES map - useDisplayedComponent: addDisplayNameToComponents 25 new tests. Consumer migration in Phase 2 cards. Authored by: Aaron Lippold<lippold@gmail.com>
- jwt: CVE-2026-45363 (empty-key HMAC bypass, High) - puma: CVE-2026-47736 (PROXY Protocol memory exhaustion) - puma: CVE-2026-47737 (repeated PROXY headers) - bundler-audit: zero vulnerabilities after update Authored by: Aaron Lippold<lippold@gmail.com>
upgrade:verify: stub db_exists? so test is environment-independent. Tests both clean path and legacy DB detection path. admin:bootstrap: use .at_least(:once) for logger expectations. Root cause: admin:bootstrap hooks into db:prepare via enhance — other task specs trigger db:prepare, invoking bootstrap multiple times within a single example. Stable across random seeds. Authored by: Aaron Lippold<lippold@gmail.com>
bin/db-rename-legacy runs as standalone Ruby without Bundler. In Docker production (BUNDLE_DEPLOYMENT=1), pg gem is only in Bundler's load path. Standard Ruby pattern fix. Authored by: Aaron Lippold<lippold@gmail.com>
7 spec files used symbol keys on Blueprint row output after the iq2 migration. CI caught 3; full sweep found 4 more. Authored by: Aaron Lippold<lippold@gmail.com>
- Added /api/docs/openapi.yml + .json route aliases - Added spec_json controller action (YAML→JSON conversion) - Changed Scalar config from url to sources array format - Agent still can't load spec — needs further investigation Authored by: Aaron Lippold<lippold@gmail.com>
|




Summary
Major feature branch: 247 commits, 668 files changed, 53K insertions. Builds on the triage split-pane workspace (PR #717 follow-up) and adds infrastructure standardization, API authentication, upgrade system, seed data, and documentation.
Core Features (earlier commits on this branch)
New in this push
{title, message: Array, variant}across all mutation endpointsvulcan_development/test/productionnaming, test DB name hardcoded to prevent collision, port registry (5435)drop_invalid_reviewsnow deletes children before parents (RESTRICT safety)Bug fixes
Test plan
bin/parallel_rspec spec/— full backend suiteyarn test:unit— Vue component testsyarn lint:ci— 0 warningsbundle exec rubocop— 0 offensesyarn openapi:bundle && yarn openapi:lint— 0 errorsbundle exec rspec spec/contracts/— 107 contract testsyarn docs:build— VitePress builds cleanbundle exec brakeman— 0 warningsAuthored by: Aaron Lippoldlippold@gmail.com