feat: track card column history for burndown charts - #180
Cletus-Mccoy wants to merge 7 commits into
Conversation
Adds a CardColumnHistory table that records every cross-column card
move: who moved it, from/to column names + IDs, and a timestamp
(inherited from AbstractDbEntity.CreatedAt). Column names are stored
at move-time so history survives future column renames or deletes.
Nullable FKs (SetNull on column/user delete) protect history from
cascade deletion.
Hooks into both move paths:
- ColumnView.OnMovedFromColumn (drag-and-drop)
- EditCardModal.MoveToPreviousList / MoveToNextList (modal arrows)
Adds a /boards/{id}/history page with:
- An SVG burndown chart (ideal vs actual open card count over time)
rendered in pure C#/Razor with no external charting dependency
- A recent moves timeline (last 50 moves, who/from→to/when)
Navigation: a fa-chart-line icon button is added to the board header
alongside the existing fa-chart-simple stats toggle.
Note: the migration .Designer.cs file is omitted; run
`dotnet ef migrations add` after merging to regenerate designer and
keep the snapshot current.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughA new ChangesCard History and Burndown Analytics
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Ticky.Base/Entities/CardColumnHistory.cs`:
- Around line 10-12: The CardColumnHistory class captures the destination
column's identity and name but lacks a snapshot of whether that destination
column was marked as a finished/completed column at the time of the card move.
Add a boolean property (for example, ToColumnFinished) to the CardColumnHistory
class alongside the existing ToColumnId and ToColumnName properties to persist
whether the destination was a finished column at move time, enabling accurate
burndown recomputation even when column settings change or columns are deleted
later.
In `@Ticky.Web/Components/Pages/BoardHistory.razor`:
- Around line 146-173: The query starting with `var history = await
db.CardColumnHistories` materializes the entire table into memory before
filtering and taking 50 rows. To fix this, split the logic into two separate
database queries: first, use a query with `.GroupBy()` and aggregation functions
at the database level to calculate completion dates without materializing all
rows, and second, move the `.OrderByDescending(h => h.CreatedAt).Take(50)`
operation into the database query (apply it before `.ToListAsync()`) so the
database handles the sorting and limiting rather than doing it in-process. This
keeps the result sets bounded to only the data needed.
- Around line 101-107: The OnAfterRenderAsync method in the BoardHistory
component only loads data on the first render, which means stale board data
persists when navigating between different board history routes (e.g., from
`/boards/1/history` to `/boards/2/history`) since the component instance is
reused. To fix this, add a field to track the previously loaded board Id, then
in the OnAfterRenderAsync method check if the current Id route parameter has
changed from the stored value. Call LoadAsync() not only when firstRender is
true, but also when the Id parameter differs from the previously stored value,
and update the stored Id after loading to prevent unnecessary reloads.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b4e89ce6-8ea4-4244-9049-8239fa609d0e
📒 Files selected for processing (8)
Ticky.Base/Entities/CardColumnHistory.csTicky.Internal/Data/DataContext.csTicky.Internal/Migrations/20260620000001_CardColumnHistory.csTicky.Internal/Migrations/DataContextModelSnapshot.csTicky.Web/Components/Dialogs/EditCardModal.razorTicky.Web/Components/Elements/ColumnView.razorTicky.Web/Components/Pages/BoardHistory.razorTicky.Web/Components/Pages/BoardView.razor
Without the designer file the [Migration] attribute was missing, causing EF Core to skip the migration at startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Summary bar shows Total / Open / Done card counts above the chart - Y-axis rotated label "Open cards" added to SVG - Legend uses inline SVG lines instead of CSS borders so dashed/solid styles render correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ToColumnFinished boolean snapshot to CardColumnHistory so burndown is accurate even if column settings change or FKs become NULL - Switch OnAfterRenderAsync to OnParametersSetAsync so navigating between boards refreshes data correctly - Push recent moves query to DB (OrderByDescending+Take(50) with Join) instead of materializing all history into memory Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Ticky.Web/Components/Dialogs/EditCardModal.razor (1)
756-758:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSet destination index during modal arrow moves.
Line 757 and Line 858 move the card across columns without recalculating
card.Index, so destination ordering can drift or collide.💡 Suggested fix
if (targetColumn is null) return; + var targetIndex = targetColumn.Cards.Any() ? targetColumn.Cards.Max(x => x.Index) + 1 : 0; var fromColumn = card.Column; card.Column = targetColumn; + card.Index = targetIndex; card.Activities.Add(new Activity { Text = $"<b>moved</b> the card to <b>{targetColumn.Name}</b>", UserId = _user.Id, CardId = card.Id, });if (targetColumn is null) return; + var targetIndex = targetColumn.Cards.Any() ? targetColumn.Cards.Max(x => x.Index) + 1 : 0; var fromColumnNext = card.Column; card.Column = targetColumn; + card.Index = targetIndex; card.Activities.Add(new Activity { Text = $"<b>moved</b> the card to <b>{targetColumn.Name}</b>", UserId = _user.Id, CardId = card.Id, });Also applies to: 857-859
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Ticky.Web/Components/Dialogs/EditCardModal.razor` around lines 756 - 758, When moving a card to a destination column by setting card.Column = targetColumn, you must also recalculate and set the card.Index property to maintain proper ordering within the destination column. After the line that assigns the new column value to card.Column, determine the appropriate destination index (typically by counting existing cards in the targetColumn or using the position where the card should be placed) and assign it to card.Index. Apply this fix to both locations where cards are moved across columns, around line 757 and line 858.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Ticky.Web/Components/Pages/BoardHistory.razor`:
- Around line 178-194: The OrderByDescending(h => h.CreatedAt).Take(50)
operation in the _recentMoves query happens before the Join and GroupJoin
operations, which means SQL does not guarantee the ordering is preserved through
those join operations, resulting in unpredictable final result ordering. To fix
this, add an OrderByDescending operation at the end of the query chain, after
the Select statement that creates the new RecentMove objects, ordering by the
MovedAt property (which corresponds to the CreatedAt field) in descending order
to ensure the final results are properly sorted.
---
Outside diff comments:
In `@Ticky.Web/Components/Dialogs/EditCardModal.razor`:
- Around line 756-758: When moving a card to a destination column by setting
card.Column = targetColumn, you must also recalculate and set the card.Index
property to maintain proper ordering within the destination column. After the
line that assigns the new column value to card.Column, determine the appropriate
destination index (typically by counting existing cards in the targetColumn or
using the position where the card should be placed) and assign it to card.Index.
Apply this fix to both locations where cards are moved across columns, around
line 757 and line 858.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 439ce048-0767-43e3-b3bc-ed0c2cf8aff2
📒 Files selected for processing (6)
Ticky.Base/Entities/CardColumnHistory.csTicky.Internal/Migrations/20260620000001_CardColumnHistory.csTicky.Internal/Migrations/DataContextModelSnapshot.csTicky.Web/Components/Dialogs/EditCardModal.razorTicky.Web/Components/Elements/ColumnView.razorTicky.Web/Components/Pages/BoardHistory.razor
…query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Ticky.Web/Components/Pages/BoardHistory.razor (2)
165-173: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueConsider pre-sorting for O(n) burndown computation on large boards.
The current approach performs a linear scan of
cardsandcompletionDates.Valuesfor each day in the range, yielding O(days × cards) complexity. For boards spanning years with hundreds of cards, this could become noticeable.A running-sum approach with pre-sorted dates would reduce this to O(days + cards log cards).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Ticky.Web/Components/Pages/BoardHistory.razor` around lines 165 - 173, The _burndownPoints calculation currently iterates through all cards and completion dates for each day in the range, creating O(days × cards) complexity. Replace this nested iteration approach by pre-sorting the created dates from cards and completion dates before the loop, then use running-sum accumulators to track cumulative created and completed counts as you iterate through each day. This way, you only increment counters based on pre-sorted positions rather than re-scanning all items daily, reducing complexity to O(days + cards log cards).
178-195: 🧹 Nitpick | 🔵 TrivialConsider enabling ConfigureWarnings in development to verify the entire query translates to SQL.
The query's
OrderByDescending(x => x.MovedAt)after theSelectprojection is a valid concern—EF Core must mapMovedAtback to the underlyingCreatedAtcolumn for SQL translation. While EF Core 9.0 has improved projection tracking and should handle this, the precedingGroupJoinwithFirstOrDefault()adds complexity that could trigger client-side evaluation earlier in the pipeline.If client evaluation occurs anywhere in the chain, the final ordering would operate on the already-fetched 50 rows in memory, which is functionally correct but loses potential database optimization. Enable
ConfigureWarningsin yourProgram.csduring development to catch unintended client evaluation and confirm the entire pipeline translates to SQL.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Ticky.Web/Components/Pages/BoardHistory.razor` around lines 178 - 195, Enable ConfigureWarnings in your EF Core configuration within Program.cs during development to detect and verify that the entire LINQ query chain in the GetRecentMoves section (containing the GroupJoin with FirstOrDefault followed by Select and final OrderByDescending on MovedAt) translates fully to SQL without triggering unintended client-side evaluation. This warning configuration will help catch any client evaluation that might occur in the complex query pipeline and ensure the database optimization is actually being applied as intended.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Ticky.Web/Components/Pages/BoardHistory.razor`:
- Around line 165-173: The _burndownPoints calculation currently iterates
through all cards and completion dates for each day in the range, creating
O(days × cards) complexity. Replace this nested iteration approach by
pre-sorting the created dates from cards and completion dates before the loop,
then use running-sum accumulators to track cumulative created and completed
counts as you iterate through each day. This way, you only increment counters
based on pre-sorted positions rather than re-scanning all items daily, reducing
complexity to O(days + cards log cards).
- Around line 178-195: Enable ConfigureWarnings in your EF Core configuration
within Program.cs during development to detect and verify that the entire LINQ
query chain in the GetRecentMoves section (containing the GroupJoin with
FirstOrDefault followed by Select and final OrderByDescending on MovedAt)
translates fully to SQL without triggering unintended client-side evaluation.
This warning configuration will help catch any client evaluation that might
occur in the complex query pipeline and ensure the database optimization is
actually being applied as intended.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: accfb1a8-9cd8-4a47-a5fd-c9f9853b129c
📒 Files selected for processing (1)
Ticky.Web/Components/Pages/BoardHistory.razor
|
Hey there @Cletus-Mccoy! Sorry for the late response and thank you for the contribution. Looks nice, but one thing I'm wondering about is maybe whether instead of creating a new table for this, maybe we could just use the activity table, or even potentially this could be a part of an improved activity tracking. Do you have any ideas in that regard? |
|
Hi
No worries about late reply, saw you weren't that active so did not expect
a super quick reply at all. Also I made this change specifically to unblock
my own agents as they liked to forget the flow of the cards sometimes so
for that time my fix was already running on my forked container.
I did already consider it might be better implemented it in such a way but
did not want to propose an all too drastic approach. I will study this a
little bit and get back to you!
…On Sun, 5 Jul 2026, 21:27 dkorecko, ***@***.***> wrote:
*dkorecko* left a comment (dkorecko/Ticky#180)
<#180 (comment)>
Hey there @Cletus-Mccoy <https://github.com/Cletus-Mccoy>! Sorry for the
late response and thank you for the contribution. Looks nice, but one thing
I'm wondering about is maybe whether instead of creating a new table for
this, maybe we could just use the activity table, or even potentially this
could be a part of an improved activity tracking. Do you have any ideas in
that regard?
—
Reply to this email directly, view it on GitHub
<#180?email_source=notifications&email_token=BGJHVO6G5BJL6SFXL4XFFEL5DKT3JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOBYG4ZTCNRTGEYKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-4887316310>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/BGJHVOZDP6MDAXSAQP2WQIT5DKT3JAVCNFSNUABFKJSXA33TNF2G64TZHM4TSNRZHE4TKMRXHNEXG43VMU5TINZQGY2TANJQGI32C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/BGJHVO2XPUOQKQRLVWQZ3GT5DKT3JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOBYG4ZTCNRTGEYKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJKTGN5XXIZLSL5UW64Y>
and Android
<https://github.com/notifications/mobile/android/BGJHVO42TTMDJQR6DWNN4MD5DKT3JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOBYG4ZTCNRTGEYKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI>.
Download it today!
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Right, had a proper look. Short version: I agree, this belongs in Activity but it needs a couple of things that aren't there yet.
On "done": I'm not using column names for that. Column.Finished is already the app's own definition (board progress bar, the confetti, the green deadline), so the chart keys off that and nothing else. The one catch is that it's a checkbox that can be un-ticked later, which would retroactively change what every past move meant. So I'd snapshot it per move, for the same reason as the column names: so old rows still read correctly after a rename, not to identify anything by. Which lands on giving Activity a typed payload instead of a second table: ActivityType enum CardMoved, TitleChanged, ... (old rows -> Generic) Text stays untouched so the card timeline renders exactly as it does now, and old rows just carry nulls. The burndown becomes Where(x => x.ActivityType == CardMoved) scoped to the board, with an index on (ActivityType, CreatedAt) added in the same migration. CardColumnHistories disappears. No backfill is possible, so charts would only start from ship date which is the same as with the separate table. If the shape works for you I'll redo the PR: drop the entity, extend Activity, add the missing logging in OnMovedFromColumn, repoint /boards/{id}/history. The one thing I'd like your call on is how far to take it. Minimum is CardMoved + Generic and the burndown, nothing else touched. The fuller version threads the enum through every existing new Activity call site and adds a board-level activity feed. Activities are only visible inside a single card's modal today, and once ActivityType exists that feed is mostly just my "Recent moves" list with the filter removed. Bigger diff, but it's the improved activity tracking you were describing rather than half of it. |
|
Slightly separate topic Context for why I care about the history stuff: I drive Ticky from agents and from Ansible in my homelab in the form of an MCP server for the agents, a small CLI and playbook for scripted work, plus a /stats endpoint for dashboards. It's been running a flow with Ticky and Git as the two sources of truth, 300+ cards in, and it's held up well. All of it talks straight to MySQL, because there's no HTTP API for cards. That works fine as far as the database is concerned, but it means anything the app would normally do around a write, I end up reimplementing including, to get CLI moves showing in the card timeline, a copy of your own activity text sitting in a bash script. If you reword that string, my rows quietly diverge. I also can't attribute a move to a real user, so it lands on whichever admin account the config knows about. And I only did that for the CLI while my MCP server still writes cards with no activity row, so moves my agents make are invisible while moves my scripts make aren't. That inconsistency is mine, but it exists because the database is the only way in. The blocker here I believe is auth: everything today is cookie-based Identity, so a machine client has nothing to present. Would you be open to some form of API token (per-user and/or scoped to a board)? If that sounds reasonable I'll open a separate PR with the token concept plus a small api/cards and api/boards/{id}/stats behind it, and keep this one focused on the move history. Happy to share the MCP server as a reference either way. No urgency! I'm just raising this while we're already on the subject of activity tracking. |
…umnHistory
Per review feedback, drop the separate CardColumnHistories table and store
move history on Activity with a typed payload:
- ActivityType enum (Generic, CardMoved); existing rows default to Generic
- FromColumnId/ToColumnId (FK, SET NULL on column delete) plus
FromColumnName/ToColumnName/ToColumnFinished snapshots, so old rows read
correctly after a column is renamed, deleted or has Finished toggled
- index on (ActivityType, CreatedAt) for board-level queries
- Activity.CardMoved() factory used by all move paths; timeline text is
unchanged
- drag-and-drop (ColumnView.OnMovedFromColumn) now logs an activity, so
dragged moves appear in the card timeline for the first time
- /boards/{id}/history reads from Activities; burndown maths moved to
BurndownHelper with unit tests
- fix burndown SVG on comma-decimal locales (coordinates were rendered as
"48,0", collapsing the chart)
Migration ActivityMovePayload is additive only and includes its Designer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Builds on the typed Activity payload:
- ActivityType extended to 33 explicit, append-only values; every activity
call site (36 plus the CardMoved factory) now sets its type
- ActivityType is a required member, so the compiler rejects new untyped
activities
- /boards/{id}/history: "Recent moves" becomes an "Activity" feed for the
whole board with a type filter, "Load more" paging and links to cards;
moves keep their from -> to rendering, other entries render like the
card timeline
- tests: persisted enum values are stable, every value has a display name
Activity texts are unchanged, so card timelines render exactly as before.
No schema change beyond the previous commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reworked this to use the Activity table as you suggested: I'm already running this in my own homelab, so no rush, review it whenever suits you. I also built the API/MCP idea on top of it in a separate branch (feat/api-mcp); happy to PR it later or keep it in my fork. |

What this adds
A
CardColumnHistoriestable that records every cross-column card move, enabling burndown charts and move timelines.Schema (
CardColumnHistoryentity)IdCardIdFromColumnIdFromColumnNameToColumnIdToColumnNameMovedByUserIdCreatedAtWhere history is recorded
ColumnView.OnMovedFromColumn— drag-and-drop between columnsEditCardModal.MoveToPreviousList/MoveToNextList— modal arrow buttonsNew page:
/boards/{id}/historyAccessible via a
fa-chart-lineicon in the board header (alongside the existing stats toggle).Finished = true.Migration notes
Migration
20260620000001_CardColumnHistoryis additive-only — no existing tables are modified. Safe to apply to an existing database.The
.Designer.csfile is omitted from this PR. Please rundotnet ef migrations add(or regenerate the designer) after merging so the model snapshot stays in sync for future migrations.Testing
CardColumnHistories/boards/{id}/history→ burndown chart and recent moves renderFromColumnId/ToColumnIdare nulled, names are intactSummary by CodeRabbit
/boards/{Id}/history, including a burndown chart and a “Recent moves” list (up to 50).