Class: CardsController
- Inherits:
-
ApplicationController
- Object
- ActionController::Base
- ApplicationController
- CardsController
- Defined in:
- app/controllers/cards_controller.rb
Instance Method Summary collapse
-
#approve ⇒ Object
Review verdicts (§3, §14.2).
-
#archive ⇒ Object
Archive (card #42): off the board, never gone — /board/archive lists, searches, and restores.
-
#compact ⇒ Object
Generate a technical "compact" journal on demand (card #34).
- #create ⇒ Object
-
#destroy ⇒ Object
Rarely needed, deliberately buried in the card modal.
- #move ⇒ Object
- #new ⇒ Object
-
#share_summary ⇒ Object
Push the customer summary outward: to the source Asana task as a comment, or to the card's PR.
- #show ⇒ Object
-
#summarize ⇒ Object
Generate a customer-friendly summary on demand (card #35).
- #unarchive ⇒ Object
- #update ⇒ Object
Instance Method Details
#approve ⇒ Object
Review verdicts (§3, §14.2). Approve is reversible — the merge happens as Done's entry rule when the human drags the card there.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'app/controllers/cards_controller.rb', line 80 def approve if @card.in_review? @card.update!(status: "approved") @card.log!("status_change", actor: "user", text: "Work approved — drag to Done to ship") end respond_to do |format| # Flash the verdict in place, then let the dismiss controller minimize the # modal. Patch the board face too — Turbo suppresses this tab's own refresh # broadcast, so without this the card keeps its stale status once we close. format.turbo_stream do render turbo_stream: [ turbo_stream.replace(helpers.dom_id(@card), partial: "cards/card", locals: { card: @card }), turbo_stream.replace("review-callout", partial: "cards/dismiss_flash", locals: { message: "Approved — closing…" }) ] end format.html { redirect_to card_path(@card) } end end |
#archive ⇒ Object
Archive (card #42): off the board, never gone — /board/archive lists, searches, and restores. Running cards can't be archived out from under their agent.
177 178 179 180 181 182 183 184 185 |
# File 'app/controllers/cards_controller.rb', line 177 def archive if @card.running? redirect_to card_path(@card), alert: "Card is running — cancel or finish the run first." else @card.update!(status: "archived") @card.log!("status_change", actor: "user", text: "Archived") redirect_to root_path end end |
#compact ⇒ Object
Generate a technical "compact" journal on demand (card #34). The AI-readable mirror of #summarize: flip the card into its "working" state, morph the Compact panel so the button reflects it, and let CompactJob do the one-shot synthesis in the background. Skipped when one is already running.
118 119 120 121 122 123 124 125 |
# File 'app/controllers/cards_controller.rb', line 118 def compact unless @card.compact_working? @card.update!(compact_status: "working") CompactJob.perform_later(@card) end render turbo_stream: turbo_stream.replace("card_compact", partial: "cards/compact_panel", locals: { card: @card }) end |
#create ⇒ Object
9 10 11 12 13 14 15 16 17 |
# File 'app/controllers/cards_controller.rb', line 9 def create board = Board.first! column = board.columns.inbox.order(:position).first || board.columns.first parent = board.cards.find_by(id: params.dig(:card, :parent_id)) card = board.cards.create!(column:, parent:, **card_params) card.log!("status_change", actor: "user", text: parent ? "Card created as a child of ##{parent.number} #{parent.title}" : "Card created") parent&.log!("status_change", actor: "user", text: "Child card added: #{card.title}") redirect_to root_path end |
#destroy ⇒ Object
Rarely needed, deliberately buried in the card modal. A working card must be cancelled first — no killing live agents by deleting their card.
21 22 23 24 25 26 27 28 29 30 |
# File 'app/controllers/cards_controller.rb', line 21 def destroy if @card.working? redirect_to card_path(@card) return end workspace_path = Agent::Workspace::Local.new(@card).path @card.destroy! FileUtils.rm_rf(workspace_path) redirect_to root_path end |
#move ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'app/controllers/cards_controller.rb', line 127 def move from_column = @card.column to_column = @card.board.columns.find(params[:column_id]) result = CardTransition.new(@card, to_column: to_column, position: params[:position]&.to_i).call if result.success? # Fresh markup for the affected columns: Turbo suppresses this tab's own # refresh broadcasts, so without this the dragged card keeps its stale # face (no queued ghosting, no ticker bump) until a job-thread broadcast. render turbo_stream: [from_column, to_column].uniq.map { |col| turbo_stream.replace(helpers.dom_id(col), partial: "columns/column", locals: { column: col.reload }) } else render json: { error: result.error }, status: :unprocessable_entity end end |
#new ⇒ Object
4 5 6 7 |
# File 'app/controllers/cards_controller.rb', line 4 def new @board = Board.first! @parent = @board.cards.find_by(id: params[:parent_id]) end |
#share_summary ⇒ Object
Push the customer summary outward: to the source Asana task as a comment, or to the card's PR. The summary was written for exactly this audience.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'app/controllers/cards_controller.rb', line 145 def share_summary summary = @card.summary.to_s.strip if summary.blank? @card.log!("error", text: "Nothing to share — the summary is empty.") return redirect_to card_path(@card, zoom: "summary") end flash_text, flash_error = nil, false case params[:to] when "asana" Asana.comment!(@card.asana_url, "Update from Cardinal:\n\n#{summary}") @card.log!("status_change", actor: "user", text: "Summary posted to the Asana task as a comment") flash_text = "✓ Posted to Asana" when "pr" out, status = Open3.capture2e("gh", "pr", "comment", @card.pr_url, "--body", summary) if status.success? @card.log!("status_change", actor: "user", text: "Summary posted as a PR comment") flash_text = "✓ Posted to the PR" else @card.log!("error", text: "Couldn't comment on the PR: #{out.truncate(160)}") flash_text, flash_error = "✗ PR comment failed — see the timeline", true end end respond_with_share_flash(flash_text, flash_error) rescue Asana::Error => e @card.log!("error", text: "Couldn't post to Asana: #{e.}") respond_with_share_flash("✗ Asana refused — see the timeline", true) end |
#show ⇒ Object
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'app/controllers/cards_controller.rb', line 32 def show @zoom = params[:zoom].presence_in(%w[conversation activity debug summary compact]) || "conversation" @events = case @zoom when "conversation" then @card.events.conversation when "activity" then @card.events.activity when "summary", "compact" then Event.none # these tabs show a card panel, not events else @card.events end # A frame navigation (opening the modal from the board) only needs the modal # frame. A direct visit — bookmark, reload, or history restore of /cards/:id — # must render the whole board with the modal already open behind it. unless turbo_frame_request? @board = @card.board render "boards/show" end end |
#summarize ⇒ Object
Generate a customer-friendly summary on demand (card #35). Non-blocking, mirroring the board's deep dive: flip the card into its "working" state, morph the Summary panel so the button reflects it, and let SummaryJob do the one-shot synthesis in the background. Skipped when one is already running.
105 106 107 108 109 110 111 112 |
# File 'app/controllers/cards_controller.rb', line 105 def summarize unless @card.summary_working? @card.update!(summary_status: "working") SummaryJob.perform_later(@card) end render turbo_stream: turbo_stream.replace("card_summary", partial: "cards/summary_panel", locals: { card: @card }) end |
#unarchive ⇒ Object
187 188 189 190 191 192 |
# File 'app/controllers/cards_controller.rb', line 187 def unarchive restore = Card::ARCHIVE_RESTORE.fetch(@card.column.archetype, "draft") @card.update!(status: restore, position: (@card.column.cards.active.maximum(:position) || -1) + 1) @card.log!("status_change", actor: "user", text: "Restored from the archive to #{@card.column.name} (#{restore.humanize.downcase})") redirect_to archive_board_path end |
#update ⇒ Object
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'app/controllers/cards_controller.rb', line 50 def update attrs = card_params attrs.delete(:title) if params[:autosave] && attrs[:title].blank? # mid-edit blank, not a delete # branch_name and pr_url lock once set (by the user or the agent) — never # let a later edit clobber a value that work may already depend on. attrs.delete(:branch_name) if @card.branch_name.present? attrs.delete(:pr_url) if @card.pr_url.present? @card.update!(attrs) log_changelog! log_config_change! respond_to do |format| # Explicitly patch the board face in this tab too — Turbo suppresses a # tab's own refresh broadcasts, so the morph won't cover the originator. # Autosave must NOT replace the modal (it would steal focus mid-typing). format.turbo_stream do streams = [turbo_stream.replace(helpers.dom_id(@card), partial: "cards/card", locals: { card: @card })] unless params[:autosave] @zoom = "conversation" @events = @card.events.conversation streams << turbo_stream.replace("modal", template: "cards/show", formats: [:html]) end render turbo_stream: streams end format.html { redirect_to card_path(@card) } end end |