chore: record status transitions and surface per-stage durations#230
Open
dan2k3k4 wants to merge 1 commit into
Open
chore: record status transitions and surface per-stage durations#230dan2k3k4 wants to merge 1 commit into
dan2k3k4 wants to merge 1 commit into
Conversation
Implements plan 011 (promoted from the audit's parked DX-04 by maintainer decision): "how long does each stage take — pre-warm to claimed, fresh to claimed, and time sitting unclaimed" was previously unanswerable because the activity log deliberately does not record status and no transition history existed. - New polydock_app_instance_status_transitions table: one immutable row per status change (from/to/created_at), cascade-deleted with the instance, indexed on (instance, created_at). - New RecordPolydockAppInstanceStatusTransition listener on the existing PolydockAppInstanceStatusChanged event (which already carries previousStatus). Synchronous by design — a single INSERT whose ordering makes duration math trustworthy. - PolydockAppInstance gains statusTransitions() plus three helpers: secondsBetweenStatuses(), secondsUnclaimedBeforeClaim() (pool wait), and secondsFromCreationToClaimed(). All return null for instances that predate recording. - The instance view page gains a collapsed "Stage Timings" section: headline durations (New → Claimed, pool wait) plus every transition with its timestamp and delta; hidden when no transitions exist. - 5 tests: recording, pool-wait math under time travel, creation→claimed, null for unobserved statuses, cascade delete.
dan2k3k4
force-pushed
the
chore/stage-duration-metrics
branch
from
July 17, 2026 17:21
c7f11ab to
8667216
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
PR 5 of 6 executing the advisory plan set — plan 011, promoted from the audit's parked items by maintainer decision during the plan grilling: "I would like to see how long each stage takes when going from pre-warm (healthy unclaimed) to healthy claimed, or brand new fresh to healthy claim... time in unclaimed state could also be interesting."
Changes
polydock_app_instance_status_transitions: one immutable row per status change (from_statusnullable for creation rows,to_status,created_at), FK cascade-delete with the instance, composite index. ~15–20 rows per instance lifetime.PolydockAppInstanceStatusChangedevent (it already carriespreviousStatus— no model/state-machine changes). Synchronous single INSERT so row ordering keeps duration math honest. Verified alongside the two existing listeners viaevent:list.PolydockAppInstance:secondsBetweenStatuses(),secondsUnclaimedBeforeClaim()(pool wait: UNCLAIMED → claim start),secondsFromCreationToClaimed()(fresh → healthy-claimed, anchored on the instance's owncreated_at). Null-safe for instances that predate recording.Notes
Testing
php artisan test— 456 passed (5 new)./vendor/bin/phpstan analyse— no errors; Pint cleanGreptile Summary
This PR adds an immutable
polydock_app_instance_status_transitionstable to record every status change, wires an event listener to populate it on both instance-creation and update events, exposes two duration helpers onPolydockAppInstance, and adds a collapsed "Stage Timings" section to the admin infolist that shows per-step deltas.from_statusnullable (creation row), a FK cascade-delete, and a composite index on(polydock_app_instance_id, created_at).RecordPolydockAppInstanceStatusTransitionhandles bothPolydockAppInstanceStatusChangedandPolydockAppInstanceCreatedWithNewStatusvia a union type hint, writing one INSERT per event synchronously.secondsBetweenStatuses,secondsUnclaimedBeforeClaim,secondsFromCreationToClaimed) scan the in-memory collection and return null for pre-feature instances.Confidence Score: 5/5
Safe to merge — the new table, listener, helpers, and admin UI are all self-contained and additive, with cascade-delete protecting referential integrity and null-safe paths for pre-feature instances.
The migration is reversible and index-appropriate. The listener correctly distinguishes creation rows from update rows, and the in-memory collection scans in the duration helpers are bounded to ~15-20 rows per instance. The admin section hides itself for instances that predate recording. No data is mutated and no existing behaviour is altered.
app/Filament/Admin/Resources/PolydockAppInstanceResource.php makes two round-trips to the transitions table per page load (exists + full select); app/Listeners/RecordPolydockAppInstanceStatusTransition.php is absent from EventServiceProvider::$listen and relies on auto-discovery.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant M as PolydockAppInstance (Model) participant E1 as PolydockAppInstanceCreatedWithNewStatus participant E2 as PolydockAppInstanceStatusChanged participant L as RecordPolydockAppInstanceStatusTransition participant DB as polydock_app_instance_status_transitions participant UI as Admin Infolist M->>M: "save() [status=NEW]" M->>E1: event(PolydockAppInstanceCreatedWithNewStatus) E1->>L: handle(event) [auto-discovered] L->>DB: "INSERT (from_status=NULL, to_status=NEW)" M->>M: setStatus(X).save() M->>E2: event(PolydockAppInstanceStatusChanged, previousStatus) E2->>L: handle(event) [auto-discovered] L->>DB: "INSERT (from_status=prev, to_status=X)" UI->>M: "statusTransitions()->exists()" DB-->>UI: true UI->>M: statusTransitions (collection) DB-->>UI: ordered rows UI->>M: secondsFromCreationToClaimed() M-->>UI: "int|null (from cached collection)" UI->>M: secondsUnclaimedBeforeClaim() M-->>UI: "int|null (from cached collection)"%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant M as PolydockAppInstance (Model) participant E1 as PolydockAppInstanceCreatedWithNewStatus participant E2 as PolydockAppInstanceStatusChanged participant L as RecordPolydockAppInstanceStatusTransition participant DB as polydock_app_instance_status_transitions participant UI as Admin Infolist M->>M: "save() [status=NEW]" M->>E1: event(PolydockAppInstanceCreatedWithNewStatus) E1->>L: handle(event) [auto-discovered] L->>DB: "INSERT (from_status=NULL, to_status=NEW)" M->>M: setStatus(X).save() M->>E2: event(PolydockAppInstanceStatusChanged, previousStatus) E2->>L: handle(event) [auto-discovered] L->>DB: "INSERT (from_status=prev, to_status=X)" UI->>M: "statusTransitions()->exists()" DB-->>UI: true UI->>M: statusTransitions (collection) DB-->>UI: ordered rows UI->>M: secondsFromCreationToClaimed() M-->>UI: "int|null (from cached collection)" UI->>M: secondsUnclaimedBeforeClaim() M-->>UI: "int|null (from cached collection)"Reviews (2): Last reviewed commit: "chore: record status transitions and sur..." | Re-trigger Greptile