diff --git a/db/seeds.rb b/db/seeds.rb index 0368308..44e0d20 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -83,6 +83,41 @@ # Backfill any plans without a plan type CoPlan::Plan.where(plan_type_id: nil).update_all(plan_type_id: general.id) +puts "Seeding tags on plans..." +CoPlan::Plan.includes(:tags, :plan_type).find_each do |p| + if p.tags.empty? && p.plan_type&.default_tags&.any? + p.tag_names = p.plan_type.default_tags + end +end + +# Add some demo plans with tags for local development +if CoPlan::Plan.count < 3 + rfc_type = CoPlan::PlanType.find_by(name: "RFC") + design_type = CoPlan::PlanType.find_by(name: "Design Doc") + + api_plan = CoPlan::Plans::Create.call( + title: "API Rate Limiting Strategy", + content: "# API Rate Limiting Strategy\n\n## Problem\n\nOur API endpoints have no rate limiting, leading to occasional abuse.\n\n## Proposal\n\nImplement token-bucket rate limiting at the gateway level.\n\n## Alternatives Considered\n\n- Per-IP limiting\n- API key quotas\n", + user: hampton, + plan_type_id: rfc_type&.id + ) + api_plan.update!(status: "considering") + api_plan.tag_names = ["api", "infrastructure", "security"] + + auth_plan = CoPlan::Plans::Create.call( + title: "Authentication System Redesign", + content: "# Authentication System Redesign\n\n## Goals\n\n- Migrate from session-based to token-based auth\n- Support SSO providers\n- Improve security posture\n\n## Architecture\n\nOIDC-based flow with JWT access tokens.\n", + user: hampton, + plan_type_id: design_type&.id + ) + auth_plan.update!(status: "developing") + auth_plan.tag_names = ["security", "infrastructure", "design"] +end + +# Tag the original Q3 roadmap if it has no tags +q3 = CoPlan::Plan.find_by(title: "Q3 Product Roadmap") +q3.tag_names = ["roadmap", "product"] if q3 && q3.tags.empty? + puts "Seeding automated plan reviewers..." CoPlan::AutomatedPlanReviewer.create_defaults diff --git a/engine/app/assets/stylesheets/coplan/application.css b/engine/app/assets/stylesheets/coplan/application.css index f6a7c2f..7430e20 100644 --- a/engine/app/assets/stylesheets/coplan/application.css +++ b/engine/app/assets/stylesheets/coplan/application.css @@ -374,6 +374,10 @@ img, svg { .badge--resolved { background: #d1fae5; color: var(--color-status-live); } .badge--agent { background: var(--color-agent-bg); color: var(--color-agent); font-size: 0.65rem; padding: 1px 6px; vertical-align: middle; } .badge--type { background: #f0fdf4; color: #166534; } +.badge--tag { background: #f3f4f6; color: #374151; text-decoration: none; font-size: 0.7rem; text-transform: none; letter-spacing: normal; } +.badge--tag:hover { background: #e5e7eb; color: #111827; } +.badge--tag-active { background: #374151; color: #fff; } +.badge--tag-active:hover { background: #1f2937; color: #fff; } /* Forms */ .form-group { @@ -525,10 +529,39 @@ img, svg { color: var(--color-primary); } +.plans-list__tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + margin-top: var(--space-xs); +} + .plans-list__meta { margin-top: var(--space-xs); } +.active-filter { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: #f9fafb; + border: 1px solid var(--color-border); + border-radius: var(--radius); + font-size: var(--text-sm); + margin-bottom: var(--space-md); +} + +.active-filter__clear { + color: var(--color-text-muted); + text-decoration: none; + font-size: var(--text-sm); +} + +.active-filter__clear:hover { + color: var(--color-text); +} + /* Plan form */ .plan-form__actions { display: flex; diff --git a/engine/app/controllers/coplan/plans_controller.rb b/engine/app/controllers/coplan/plans_controller.rb index 77a6349..d600399 100644 --- a/engine/app/controllers/coplan/plans_controller.rb +++ b/engine/app/controllers/coplan/plans_controller.rb @@ -3,10 +3,11 @@ class PlansController < ApplicationController before_action :set_plan, only: [:show, :edit, :update, :update_status] def index - @plans = Plan.includes(:plan_type).order(updated_at: :desc) + @plans = Plan.includes(:plan_type, :tags).order(updated_at: :desc) @plans = @plans.where(status: params[:status]) if params[:status].present? @plans = @plans.where(created_by_user: current_user) if params[:scope] == "mine" @plans = @plans.where(plan_type_id: params[:plan_type]) if params[:plan_type].present? + @plans = @plans.with_tag(params[:tag]) if params[:tag].present? @plan_types = PlanType.order(:name) diff --git a/engine/app/models/coplan/plan.rb b/engine/app/models/coplan/plan.rb index eac9e91..e45b5cf 100644 --- a/engine/app/models/coplan/plan.rb +++ b/engine/app/models/coplan/plan.rb @@ -21,6 +21,8 @@ class Plan < ApplicationRecord validates :title, presence: true validates :status, presence: true, inclusion: { in: STATUSES } + scope :with_tag, ->(name) { joins(:tags).where(coplan_tags: { name: name }) } + def self.ransackable_attributes(auth_object = nil) %w[id title status plan_type_id created_by_user_id current_plan_version_id current_revision created_at updated_at] end diff --git a/engine/app/views/coplan/plans/_header.html.erb b/engine/app/views/coplan/plans/_header.html.erb index 4edb56d..ec8a065 100644 --- a/engine/app/views/coplan/plans/_header.html.erb +++ b/engine/app/views/coplan/plans/_header.html.erb @@ -6,6 +6,9 @@ <% if plan.plan_type %> · <%= plan.plan_type.name %> <% end %> + <% plan.tags.each do |tag| %> + <%= link_to tag.name, plans_path(tag: tag.name), class: "badge badge--tag" %> + <% end %>