The activity log is never written, so the dashboard's activity panel is permanently empty #104
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
/System/ActivityLog/Entriesanswers an empty listing — honest, and better than a 404 — andActivityLogsis the one tablediff-dbexempts outright, because every row in it is upstream's alone.Upstream writes a row per login, per failed login, per playback start and stop, per session, per library scan outcome, and per user creation or deletion. An operator reads that panel to answer "who logged in from where, and when", which is the one question a server with no log cannot answer after the fact.
The write side is small — the table is bound already and the events all exist in the session and auth code paths. Two things to settle from the reference: the exact
Typestrings and severity per event, since clients group on them, and theItemId/UserIdcolumns' population per kind.Once rows are written, the
diff-dbexemption should narrow from the whole table to whatever genuinely cannot agree (the ids and the timestamps), andCleanActivityLogbecomes real work rather than a sweep over nothing.Where to start
/System/ActivityLog/Entriesanswers an empty listing fromcrates/jellyfin-http/src/routes/admin/system.rs; theActivityLogstable is bound incrates/jellyfin-db. The events all already exist as code paths —crates/jellyfin-app/src/session/sessions.rs, the auth module, the scan, and the userroutes.
Upstream writes from
ActivityLogEntryPointand friends; find every call siterather than the ones you remember, because a client groups on
Typeand amissing kind is an empty row in a filtered view.
Two things to take from the reference rather than the C#: the exact
Typestrings per event and the severity, and which of
ItemId/UserIdeach kindpopulates.
How to know it is done
listings carry the same kinds in the same order.
crates/jellyfin-harness/db-ignore-list.jsonexempts the whole
ActivityLogstable today with the reason "nothing herewrites one and nothing will". That reason expires with this issue: the entry
should shrink to the ids and timestamps that genuinely cannot agree, or go. An
ignore list that only ever grows is the smell the risk table names.
House rules
CLAUDE.mdis the short version and these are the parts that bite: tests rununder
cargo nextest; upstream's source is the checkout inupstream/at thepin (
v12.0-rc5,6d501ba4) and not the clone beside the repository;generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and
Dnreferences stay out of code and comments.ARCHITECTURE.mddecides which crate a thing goes in and which dependency edgesare illegal,
DECISIONS.mdsays why a rule exists.The reference pair is
cargo xtask reference up(andreference provision,which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,with
diff-dbfor the two databases.crates/jellyfin-harness/README.mdsayswhat a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.
Done, on
main(3ad8cb5, plusb0e3ae7for what the pair found). The write side, the read side, and the exemption narrowed.What is written now
A row per login and per refused login, per session opening and closing, per account made, given a password or taken away, per download, and per scheduled task that failed.
The
Typestrings, the severity and which ofItemId/UserIdeach kind fills came off the reference's own rows — theActivityLogstable in the fixture snapshots already held 21 of them, so the sentences are quoted rather than reconstructed.jellyfin_core::activitybuilds them in one place because the sentence is the contract a client renders.Two silences are upstream's own and are kept deliberately:
Item. Upstream reads the title and the medium off what the client sent, not off the row it looked up;MediaInfonull means the logger warns and returns. The reference bears this out — 27 downloads and 181 logins recorded, and zero playback rows, because the harness posts an id alone.SecurityExceptionbefore the event is raised, so a disabled account or one outside its schedule writes nothing.UserLockedOutis not written and the builder for it was deleted: nothing here locks an account out, and a builder with no caller is the same dead name the tasks module refuses.What the pair found
Replaying
handshake.jsonaftertasks.jsonshowed upstream writingSessionEnded,SessionStarted,AuthenticationSucceededon the second login where this server wrote only the last. The cause was not the audit line — issuing a token already logged the account's existing one on that device out, and upstream'sLogoutdeletes the device row and ends every session on it. Only the token half was here. A session that was never away is one/Sessionsgoes on listing and one the audit line never closes. Fixed inb0e3ae7; the two servers now write the same five rows in the same order.The exemption
Narrowed from the whole table to the value of three columns, each argued on the entry:
DateCreated— the wall clock of whichever server answered, the same argument asBaseItems.DateLastSaved.Id— anAUTOINCREMENT; one extra row on either side renumbers every row after it, so a single difference would be reported as the whole table twice over and hide the row that really differs.ShortOverview— carries the caller's address, which is the docker bridge on one side and the loopback on the other. The one column that varies by how the pair was stood up rather than by what either server did.Name,Type,UserId,ItemId,OverviewandLogSeverityare diffed outright, which is what makes a missing kind, a misspelled type or the wrong severity show. Verified:diff-dbafter atasks+handshakereplay reports nothing forActivityLogs, where before the run it was one of three exempt tables.The read side
/System/ActivityLog/Entriesreads real rows with every filter upstream's action offers — the four substring matches, the date range,hasUserId,itemId,username(through the join, so a deleted account's rows fall out of it exactly as upstream's do),severity, andsortBy/sortOrder. Ordering ends on the row's own id in the direction the last term asked for; upstream does not, and it cannot disagree either, because it only ever decides between rows the named columns tie on — which a paged listing otherwise leaves to the query plan.Eight route tests in
crates/jellyfin-http/tests/activity_log.rs, andMILESTONES.md's "what M7 does not do" entry for the activity log is corrected.The review round (
0ff1992)Eight findings; all eight acted on. Three were worth the round on their own:
A playback report that names only an id does write a row — my earlier claim here was wrong. Upstream fills the
Itemon the report from the library row before it raises the event (UpdateNowPlayingItemon the way in, the same normalising atSessionManager.cs:1082on the way out), so the sentence names the item whether or not the client sent one. The "measured: the reference holds no playback entry" note was drawn from a reference that had simply never been sent a playback report since its snapshot. Posting one to it writes bothVideoPlaybackandVideoPlaybackStopped. Fixed, and re-measured with the pair: a cleanplaystate.jsonreplay is 0 differences and the two servers file the same three rows —Without the fix ours had none of them, which the now-diffed table would have reported.
A login refused for a username nobody has writes nothing.
UserManager.AuthenticateUserthrowsAuthenticationExceptionatUserManager.cs:568when no user is found, so the event is never raised; only a real account with a wrong password reachesPublishAsync(AuthenticationRequestEventArgs)— it returnsnullrather than throwing on that path. A bogus name was writing a row here and none upstream.A socket outliving the session it was opened on could end the session that replaced it. Mine, from
b0e3ae7: a session's id is its client and device hashed, so the one a re-login opens comes back under the same id and a staleSocketHandlematched it —disconnectthen found the new entry, saw no sockets, and removed it, filing a spuriousSessionEndedand dropping the caller out of/Sessions. An entry carries a generation now and a handle names the incarnation it belongs to. The test fails without the fix.Also fixed: every session on the device goes when a login replaces the token rather than only the account's own (
Logout(Device)ends them all); a sort field past the last direction given takes the first, which is whatRequestHelpers.GetOrderBydoes; and the device in a playback line is the register's name, so a device an operator renamed reads as itself.One divergence kept and written down instead: a negative retention posted to
/System/Configurationis refused rather than stored. Upstream stores it and letsCleanActivityLogTaskthrow on every run — which, now that the table is diffed, would be aTaskFailedrow per run to agree with.Verified
cargo nextest run --workspace: 2376 passed, 0 skippedActivityLogsdiffs clean aftertasks+handshake+playstate, row for rowf3b9015gives the identical counts (2 / 5 / 73 / 142 / 8 / 864)