generated from coulomb/repo-seed
feat(P7): IHF Phase 7 complete — advanced observability and operational integration
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
T01 schema: friction_scores, bottleneck_records, hub_health_snapshots, cross_hub_propagations + migration 1743552000. T02 Widget Pain Heatmap: computeFrictionScore (formula documented), RecomputeFriction action, colour-coded grid view (green/yellow/amber/red). T03 Workflow Bottleneck Analysis: detectBottlenecks across 4 pipeline stages (candidate 30d, requirement 60d, decision 30d, observation 14d), idempotent, severity from age ratio, resolve action. T04 Hub Health Correlation: computeHubHealth (deduction table documented), append-only HubHealthSnapshot, health history view, badge on hub Show page. T05 Cross-Hub Propagation: annotation_cluster + widget_type_friction heuristics, idempotent detection, acknowledge/resolve lifecycle. T06 Operational Review Board: 4-panel AutoRefresh global dashboard — health matrix, top-10 friction, bottleneck stage counts, open propagations. T07 gate: 5 describe blocks in Test/Integration.hs; SCOPE.md updated Phase 7 complete; docs/phase7-summary.md written. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
64
Application/Helper/FrictionScore.hs
Normal file
64
Application/Helper/FrictionScore.hs
Normal file
@@ -0,0 +1,64 @@
|
||||
module Application.Helper.FrictionScore where
|
||||
|
||||
import IHP.Prelude
|
||||
import IHP.ModelSupport
|
||||
import Generated.Types
|
||||
import Data.Time.Clock (addUTCTime, getCurrentTime)
|
||||
|
||||
-- | Friction score formula (documented):
|
||||
--
|
||||
-- score = min 100 $
|
||||
-- annotationCount * 5
|
||||
-- + errorEventCount * 10
|
||||
-- + (if regressionFlag then 20 else 0)
|
||||
-- + staleCandidateCount * 8
|
||||
--
|
||||
-- Inputs are computed from the widget's related records.
|
||||
computeFrictionScore
|
||||
:: (?modelContext :: ModelContext)
|
||||
=> Id Widget
|
||||
-> [Annotation] -- all annotations for this widget
|
||||
-> [InteractionEvent] -- all events for this widget
|
||||
-> Bool -- True if widget is in regression
|
||||
-> [RequirementCandidate] -- all candidates for this widget
|
||||
-> IO FrictionScore
|
||||
computeFrictionScore wid annotations events isRegressed candidates = do
|
||||
now <- getCurrentTime
|
||||
let thirtyDaysAgo = addUTCTime (negate $ 30 * 86400) now
|
||||
annCount = length annotations
|
||||
errCount = length (filter (\e -> e.eventType == "errored") events)
|
||||
staleCount = length (filter (\c -> c.status == "open" && c.createdAt < thirtyDaysAgo) candidates)
|
||||
rawScore = annCount * 5 + errCount * 10 + (if isRegressed then 20 else 0) + staleCount * 8
|
||||
finalScore = min 100 rawScore
|
||||
-- Upsert: update if row exists, insert otherwise
|
||||
existingRows <- sqlQuery
|
||||
"SELECT * FROM friction_scores WHERE widget_id = ? LIMIT 1"
|
||||
(Only wid)
|
||||
case (existingRows :: [FrictionScore]) of
|
||||
(existing : _) -> do
|
||||
existing
|
||||
|> set #score finalScore
|
||||
|> set #annotationCount annCount
|
||||
|> set #errorEventCount errCount
|
||||
|> set #regressionFlag isRegressed
|
||||
|> set #staleCandidateCount staleCount
|
||||
|> set #lastComputedAt now
|
||||
|> updateRecord
|
||||
[] -> do
|
||||
newRecord @FrictionScore
|
||||
|> set #widgetId wid
|
||||
|> set #score finalScore
|
||||
|> set #annotationCount annCount
|
||||
|> set #errorEventCount errCount
|
||||
|> set #regressionFlag isRegressed
|
||||
|> set #staleCandidateCount staleCount
|
||||
|> set #lastComputedAt now
|
||||
|> createRecord
|
||||
|
||||
-- | Score band for Tailwind colour coding.
|
||||
scoreBand :: Int -> Text
|
||||
scoreBand s
|
||||
| s < 20 = "bg-green-100 text-green-800"
|
||||
| s < 40 = "bg-yellow-100 text-yellow-800"
|
||||
| s < 60 = "bg-orange-100 text-orange-800"
|
||||
| otherwise = "bg-red-100 text-red-800"
|
||||
Reference in New Issue
Block a user