Skip to content

feat: persist loot history across sessions (#104)#152

Merged
Xerrion merged 3 commits intomasterfrom
feat/104-persist-loot-history
Apr 26, 2026
Merged

feat: persist loot history across sessions (#104)#152
Xerrion merged 3 commits intomasterfrom
feat/104-persist-loot-history

Conversation

@Xerrion
Copy link
Copy Markdown
Owner

@Xerrion Xerrion commented Apr 26, 2026

Description

Loot history now persists to db.char.history.entries so it survives /reload, relog, and LOOT_HISTORY_CLEAR_HISTORY events. Entries stamp both an in-session timestamp and an absolute wallTime; FormatTimeAgo prefers wall-clock when session and wall disagree by more than 5 minutes (i.e. cross-session entries).

Type of Change

  • New feature (non-breaking change that adds functionality)

Related Issues

Closes #104.

Testing

  • Luacheck passes (luacheck .)
  • Tested in-game manually
  • WoW version(s) tested on: Retail, Classic

Screenshots

  • N/A

Checklist

  • My code follows the project's code style (4-space indent, 120 char lines)
  • I have tested my changes in-game
  • Luacheck reports no warnings
  • My commits follow conventional commit format

Summary by CodeRabbit

  • New Features

    • Loot history now persists per character across game sessions
    • Improved history time display with more accurate timestamps
    • Added "Clear History" option with confirmation dialog to permanently remove all stored entries
  • Chores

    • Updated linter configuration and added new locale strings

- Add db.char.history.entries with schemaVersion bump v3 -> v4
- Stamp wallTime alongside session timestamp in all history writers
- Restore persisted entries on HistoryFrame.Initialize; merge with
  Classic API results and seed Retail processedDrops to prevent
  cross-session duplicates
- Preserve earliest wallTime in UpdateEntryByKey carry-forward to
  fix cross-session timestamp clobber on API replay
- FormatTimeAgo prefers wall clock when session/wall disagree by >5m
- Add Clear History button with confirmation popup in options
- Add enUS locale keys (button reuses existing "Clear History")
@Xerrion Xerrion added C-Feature New feature or enhancement A-History Loot history frame and history listeners A-Config Options table, config window, AceDB A-Listeners Event listeners and version-specific loot parsing D-Complex Multiple files or systems involved P-All-Versions Affects all supported WoW versions labels Apr 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Warning

Rate limit exceeded

@Xerrion has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 56 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 56 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 8ade01aa-64e9-4c50-b505-a4b33e5adb14

📥 Commits

Reviewing files that changed from the base of the PR and between 9f63d47 and 56f021d.

📒 Files selected for processing (2)
  • DragonLoot/Listeners/HistoryListener_Classic.lua
  • spec/Config_spec.lua
📝 Walkthrough

Walkthrough

This PR adds persistent cross-session loot history storage via AceDB (char.history), capturing wall-clock times alongside session timestamps, deduplicating entries across API and persisted sources, and exposing a confirmation-guarded UI button to clear history.

Changes

Cohort / File(s) Summary
Linter & Database Configuration
.luacheckrc, DragonLoot/Core/Config.lua
Adds global time to luacheck config; initializes AceDB char.history scope with entries list and schemaVersion 4 with migration support.
History Persistence & Deduplication
DragonLoot/Display/HistoryFrame.lua, DragonLoot/Listeners/HistoryListener_Classic.lua, DragonLoot/Listeners/HistoryListener_Retail.lua, DragonLoot/Listeners/LootHistoryChat.lua
Captures dual timestamps (timestamp for session, wallTime for wall-clock via time()) on entries; persists entries to db.char.history.entries and reloads on init; merges API-sourced and persisted entries using dedup keys, respecting maxEntries cap; adds HistoryFrame_BuildDedupKey as exported function for deduplication across sources.
UI & Localization
DragonLoot_Options/Tabs/HistoryTab.lua, DragonLoot/Locales/enUS.lua
Adds "Clear History" button with confirmation popup (DRAGONLOOT_CLEAR_HISTORY) guarded by null checks; introduces locale entries for destructive action label and undo warning.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

core, display, listeners, options, localization

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title follows Conventional Commit format with 'feat' type and clear scope/description.
Linked Issues check ✅ Passed All objectives from #104 are addressed: persistence to AceDB [Config.lua, HistoryFrame.lua], maxEntries cap enforcement [HistoryListener_Classic.lua], deduplication by timestamp+item+player [HistoryListener_Retail.lua, HistoryListener_Classic.lua], and manual clear UI [HistoryTab.lua].
Out of Scope Changes check ✅ Passed All changes directly support persistence feature: configuration schema bump, history storage/retrieval, dual timestamp handling (session/wallTime), deduplication, and clear UI—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
DragonLoot/Listeners/HistoryListener_Classic.lua (1)

50-138: ⚠️ Potential issue | 🟠 Major

Cross-day-boundary duplicates in the Classic merge.

Every API-derived entry in RefreshFromAPI is stamped with nowWall = time(). Persisted entries restored from SavedVariables retain their original wallTime. BuildDedupKey in HistoryFrame.lua includes math_floor(wallTime / 86400) as part of the key:

return link .. "|" .. winner .. "|" .. bucket
-- (or dropKey .. "|" .. bucket)

If a /reload happens after midnight while C_LootHistory still has the same items, the API entries get today's bucket and the persisted entries keep yesterday's bucket → keys differ → both are kept, and the user sees the same drop twice. Same hazard exists for entries that do carry a dropKey (Retail-shaped entries that ever land in the Classic listener), since the bucket is part of the key for those too.

Retail dodges this because processedDrops[dropKey] is keyed without the bucket and is seeded from persisted entries before PopulateExistingHistory runs.

Suggested mitigation: when an API entry's item+winner (or dropKey) matches a persisted entry, reuse the persisted wallTime instead of overwriting with nowWall. Something like:

🔧 Proposed fix sketch
+    -- Index persisted entries by a wallTime-independent key for back-fill.
+    local persistedByKey = {}
+    if ns.historyData then
+        for _, p in ipairs(ns.historyData) do
+            local k = (p.dropKey)
+                or ((p.itemLink or "?") .. "|" .. (p.winner or "?"))
+            persistedByKey[k] = p
+        end
+    end
+
     local now = GetTime()
     local nowWall = time()
     local entries = {}
     for i = 1, numItems do
         ...
+        local k = (p.dropKey) or ((itemLink or "?") .. "|" .. (winner or "?"))
+        local prior = persistedByKey[k]
         entries[`#entries` + 1] = {
             ...
             timestamp = now,
-            wallTime = nowWall,
+            wallTime = (prior and prior.wallTime) or nowWall,
             ...
         }
     end

The exact key shape can match BuildDedupKey's bucket-less form to keep things consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DragonLoot/Listeners/HistoryListener_Classic.lua` around lines 50 - 138, The
API-derived entries are stamped with nowWall = time(), causing duplicate
cross-day keys versus persisted entries because BuildDedupKey includes a day
bucket; fix RefreshFromAPI so that when building each API entry you compute its
dedup key via ns.HistoryFrame_BuildDedupKey (buildKey) and if a matching
persisted entry exists in ns.historyData reuse that persisted entry's wallTime
(and optionally timestamp) instead of overwriting with nowWall before appending
to entries; locate the API loop that builds entries (uses nowWall, entries,
C_LootHistory.GetItem, C_LootHistory.GetPlayerInfo) and, for each constructed
entry, look up the persisted entry by key and copy persisted.wallTime into the
API entry when found so the dedup logic later treats them as the same drop.
🧹 Nitpick comments (2)
DragonLoot_Options/Tabs/HistoryTab.lua (1)

244-254: Optional: visually flag this as destructive.

The button shares the standard appearance of every other widget in the section. A destructive-action style (e.g., red text or RED_FONT_COLOR) on the button label, or at minimum an exclamation prefix on the tooltip, would help users avoid wiping history by reflex. Confirmation popup is a strong second line of defense, so this is a nice-to-have.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DragonLoot_Options/Tabs/HistoryTab.lua` around lines 244 - 254, Change the
Clear History button to visually indicate a destructive action by wrapping the
label in red font color codes and/or changing the tooltip to include an explicit
warning; e.g., when creating clearBtn via W.CreateButton, set text =
RED_FONT_COLOR_CODE .. L["Clear History"] .. FONT_COLOR_CODE_CLOSE (or use an
appropriate red font object) and update tooltip to prepend an exclamation or
"Danger:" (e.g., tooltip = "⚠️ " .. L["Permanently delete all stored loot
history entries"]). Keep the existing onClick that calls
StaticPopup_Show("DRAGONLOOT_CLEAR_HISTORY") so the confirmation popup remains
as the second line of defense.
DragonLoot/Display/HistoryFrame.lua (1)

107-121: PersistEntries aliases entry tables with the saved store.

store[i] = entry shares each entry table by reference between ns.historyData and db.char.history.entries. That's correct given current mutation patterns — every mutating path (AddEntry, UpdateEntryByKey, SetEntries, ClearHistory) wipes and re-copies — but it's a sharp edge. If anything later mutates an entry in place without calling PersistEntries, the saved-vars copy will silently drift in some cases and not in others (depending on whether UpdateEntryByKey swapped the slot). Worth a one-line comment, or a deep-copy in PersistEntries, depending on how paranoid you want to be about future contributors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DragonLoot/Display/HistoryFrame.lua` around lines 107 - 121, PersistEntries
currently writes references by doing store[i] = entry, which aliases
ns.historyData entries into db.char.history.entries; change this to store[i] =
CopyTable(entry) (or another deep-copy utility) so each saved entry is an
independent table, and keep references to ns.historyData, store,
db.char.history.entries and the PersistEntries function to locate the change;
alternatively, if you prefer not to deep-copy, add a one-line comment above the
loop in PersistEntries noting the intentional aliasing and the assumption that
all mutating paths re-copy entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DragonLoot/Display/HistoryFrame.lua`:
- Around line 92-103: BuildDedupKey is intentionally bucketing by wallTime/day
and should not be changed; instead fix the Classic listener replay so it
preserves the original persisted timestamp. In the Classic replay path in
HistoryListener_Classic.lua (the code that re-emits or reconstructs persisted
entries for the API replay), stop overwriting entry.wallTime with time() and
copy the persisted wallTime into the replayed entry (or carry the persisted
timestamp field forward) so the dedup key remains identical for the same
physical drop observed twice across midnight.

In `@DragonLoot/Listeners/HistoryListener_Classic.lua`:
- Line 132: The nil-check chain stops before .profile so it can still nil-index;
update the expression that sets maxEntries (currently referencing
ns.Addon.db.profile.history.maxEntries) to either rely on AceDB by using
ns.Addon.db.profile.history.maxEntries or extend the defensive checks to include
.profile and .profile.history, e.g. (ns.Addon and ns.Addon.db and
ns.Addon.db.profile and ns.Addon.db.profile.history and
ns.Addon.db.profile.history.maxEntries) or 100 so you never nil-index
.profile/.history.

---

Outside diff comments:
In `@DragonLoot/Listeners/HistoryListener_Classic.lua`:
- Around line 50-138: The API-derived entries are stamped with nowWall = time(),
causing duplicate cross-day keys versus persisted entries because BuildDedupKey
includes a day bucket; fix RefreshFromAPI so that when building each API entry
you compute its dedup key via ns.HistoryFrame_BuildDedupKey (buildKey) and if a
matching persisted entry exists in ns.historyData reuse that persisted entry's
wallTime (and optionally timestamp) instead of overwriting with nowWall before
appending to entries; locate the API loop that builds entries (uses nowWall,
entries, C_LootHistory.GetItem, C_LootHistory.GetPlayerInfo) and, for each
constructed entry, look up the persisted entry by key and copy
persisted.wallTime into the API entry when found so the dedup logic later treats
them as the same drop.

---

Nitpick comments:
In `@DragonLoot_Options/Tabs/HistoryTab.lua`:
- Around line 244-254: Change the Clear History button to visually indicate a
destructive action by wrapping the label in red font color codes and/or changing
the tooltip to include an explicit warning; e.g., when creating clearBtn via
W.CreateButton, set text = RED_FONT_COLOR_CODE .. L["Clear History"] ..
FONT_COLOR_CODE_CLOSE (or use an appropriate red font object) and update tooltip
to prepend an exclamation or "Danger:" (e.g., tooltip = "⚠️ " .. L["Permanently
delete all stored loot history entries"]). Keep the existing onClick that calls
StaticPopup_Show("DRAGONLOOT_CLEAR_HISTORY") so the confirmation popup remains
as the second line of defense.

In `@DragonLoot/Display/HistoryFrame.lua`:
- Around line 107-121: PersistEntries currently writes references by doing
store[i] = entry, which aliases ns.historyData entries into
db.char.history.entries; change this to store[i] = CopyTable(entry) (or another
deep-copy utility) so each saved entry is an independent table, and keep
references to ns.historyData, store, db.char.history.entries and the
PersistEntries function to locate the change; alternatively, if you prefer not
to deep-copy, add a one-line comment above the loop in PersistEntries noting the
intentional aliasing and the assumption that all mutating paths re-copy entries.
🪄 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 YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: e671ff33-1844-493b-b94a-94683ccf0439

📥 Commits

Reviewing files that changed from the base of the PR and between 273011a and 9f63d47.

📒 Files selected for processing (8)
  • .luacheckrc
  • DragonLoot/Core/Config.lua
  • DragonLoot/Display/HistoryFrame.lua
  • DragonLoot/Listeners/HistoryListener_Classic.lua
  • DragonLoot/Listeners/HistoryListener_Retail.lua
  • DragonLoot/Listeners/LootHistoryChat.lua
  • DragonLoot/Locales/enUS.lua
  • DragonLoot_Options/Tabs/HistoryTab.lua

Comment thread DragonLoot/Display/HistoryFrame.lua
Comment thread DragonLoot/Listeners/HistoryListener_Classic.lua Outdated
Xerrion added 2 commits April 26, 2026 10:20
Updates three assertions in spec/Config_spec.lua to track the
CURRENT_SCHEMA bump from 3 to 4 introduced by the persistent
loot history feature. Production behavior is unchanged.
Addresses CodeRabbit findings on PR #152:
- Cross-midnight duplicate: when a persisted drop is re-observed by
  C_LootHistory on a later UTC day, reuse the persisted wallTime so
  the day-bucketed dedup keys collapse.
- Tighten nil-chain on maxEntries lookup to guard .profile and
  .history consistently with the rest of the defensive chain.
@Xerrion
Copy link
Copy Markdown
Owner Author

Xerrion commented Apr 26, 2026

@coderabbitai Two review-body items addressed inline:

Clear History button styling - declining. DragonWidgets' CreateButton factory has no destructive variant exposed in its public API, and the StaticPopup confirmation already provides the safety gate (YES/NO with preferredIndex=3, matching the existing DRAGONLOOT_RESET_PROFILE precedent in ProfilesTab.lua). Adding a manual :SetTextColor would be inconsistent with the rest of the options UI which uses uniform button styling. Happy to reconsider if a future DragonWidgets release adds a destructive = true opt.

PersistEntries table aliasing - declining. The aliasing is intentional: AceDB requires the same table identity for its defaults handling (we wipe + copy refs rather than reassign for that reason), and the bidirectional aliasing means subsequent UpdateEntryByKey mutations on a live entry stay in sync with the saved store without needing a second write. Deep-copying on persist would double the memory footprint and force every mutation site to re-trigger a save. The existing entry shape (primitives + simple
ollResults table-of-primitives) is safe to share by reference because nothing else in the codebase mutates entries except through the four mutator helpers, all of which already call PersistEntries.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Tip

For best results, initiate chat on the files or code changes.

@Xerrion, understood on both — your reasoning is solid and I won't press further.

  • Button styling: The StaticPopup confirmation gate is the right safety mechanism here, and consistency with DRAGONLOOT_RESET_PROFILE precedent makes sense. Happy to revisit if DragonWidgets ever exposes a destructive variant.
  • Table aliasing: The AceDB table-identity constraint + the four-mutator-all-call-PersistEntries invariant make the shared-reference design sound. That's a useful architectural constraint to remember.

Recording both for future reviews:


✏️ Learnings added
Learnt from: Xerrion
URL: https://github.com/Xerrion/DragonLoot/pull/152

Timestamp: 2026-04-26T08:26:24.383Z
Learning: Applies to DragonLoot_Options/Tabs/HistoryTab.lua and other Options tabs: DragonWidgets' CreateButton factory has no destructive/danger variant in its public API. The StaticPopup confirmation dialog (YES/NO with preferredIndex=3) is the established safety gate for destructive actions; manual :SetTextColor on buttons is avoided to keep uniform button styling across the options UI. Precedent: DRAGONLOOT_RESET_PROFILE in ProfilesTab.lua.

Learnt from: Xerrion
URL: https://github.com/Xerrion/DragonLoot/pull/152

Timestamp: 2026-04-26T08:26:24.384Z
Learning: Applies to DragonLoot/Display/HistoryFrame.lua PersistEntries and ns.historyData: The aliasing between ns.historyData entries and db.char.history.entries is intentional. AceDB requires the same table identity for its defaults handling (wipe + copy refs, not reassign). Bidirectional aliasing means UpdateEntryByKey mutations on live entries stay in sync with the saved store without a second write. Only four mutator helpers mutate entries, and all of them call PersistEntries. Deep-copying would double memory footprint unnecessarily.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: Xerrion/DragonLoot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T09:00:00.250Z
Learning: Applies to DragonLoot/{Display/RollManager.lua,Listeners/HistoryListener_Retail.lua} : Use `ROLL_STATE_MAP` in HistoryListener_Retail to map Transmog to Greed (lossy) for history display, but preserve Transmog as a distinct roll type for notifications via `ns.NOTIFICATION_STATE_MAP` in RollManager

Learnt from: CR
Repo: Xerrion/DragonLoot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T09:00:00.250Z
Learning: Applies to DragonLoot/Listeners/HistoryListener_*.lua : Retail C_LootHistory is encounter-based; Classic (TBC/Cata/MoP) is roll-item indexed - use version-specific listeners to handle structural differences

Learnt from: CR
Repo: Xerrion/DragonLoot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-08T09:00:00.250Z
Learning: Applies to DragonLoot/Listeners/HistoryListener_Retail.lua : Retail C_LootHistory duplicate events: LOOT_HISTORY_UPDATE_ENCOUNTER re-fires for all drops - use processedDrops dedup table to prevent duplicate processing

@Xerrion Xerrion merged commit c337e94 into master Apr 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Config Options table, config window, AceDB A-History Loot history frame and history listeners A-Listeners Event listeners and version-specific loot parsing C-Feature New feature or enhancement ci core D-Complex Multiple files or systems involved display listeners localization options P-All-Versions Affects all supported WoW versions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Persist loot history across sessions and instance transitions

1 participant