Compare commits

...

22 Commits

Author SHA1 Message Date
lukaszraczylo 86902839fd Update go.mod and go.sum (#26) 2026-02-23 03:52:44 +00:00
lukaszraczylo 1407d276fb Update go.mod and go.sum (#25) 2026-02-18 03:51:39 +00:00
lukaszraczylo 18421908ea Update go.mod and go.sum (#24) 2026-02-17 03:50:33 +00:00
lukaszraczylo f9f193eac5 Update go.mod and go.sum (#23) 2026-02-15 03:53:08 +00:00
lukaszraczylo 6ee7cfaee4 Update go.mod and go.sum (#22) 2026-02-13 03:52:40 +00:00
lukaszraczylo 7f27aa6378 Update go.mod and go.sum (#21) 2026-02-11 03:58:27 +00:00
lukaszraczylo 0c0c464fbc Update go.mod and go.sum (#20) 2026-02-10 03:59:47 +00:00
lukaszraczylo 0aac0b76c0 Update go.mod and go.sum (#18) 2026-02-09 03:55:43 +00:00
lukaszraczylo 45adbbe84f Update go.mod and go.sum (#17) 2026-02-05 03:49:30 +00:00
lukaszraczylo 63825bdfd4 Update go.mod and go.sum (#16) 2026-02-04 03:49:12 +00:00
lukaszraczylo 5ca802cae7 Update go.mod and go.sum (#15) 2026-02-02 03:53:23 +00:00
lukaszraczylo 4dc9dc3d07 Update go.mod and go.sum (#14) 2026-01-26 03:40:24 +00:00
lukaszraczylo 7fc413ba92 Update go.mod and go.sum (#13) 2026-01-25 03:38:04 +00:00
lukaszraczylo 821891a890 Update go.mod and go.sum (#12) 2026-01-23 03:35:05 +00:00
lukaszraczylo 063b5acfbb Update go.mod and go.sum (#11) 2026-01-19 03:36:30 +00:00
lukaszraczylo e2bd053906 Update go.mod and go.sum (#10) 2026-01-16 03:33:55 +00:00
lukaszraczylo 7ba4d438dd improvements jan2025 (#9)
* feat(scoring): add tests bonus and fix average calculations

- [x] Add CommitsWithTests metric to track commits with test file changes
- [x] Add TestsBonus to score breakdown (15 points per commit with tests)
- [x] Fix AvgTimeToMerge calculation to use count of PRs with valid data
- [x] Fix AvgReviewTime calculation to use count of reviews with valid data
- [x] Fix AvgPRSize calculation to only include merged PRs
- [x] Add trackActivityDay helper to deduplicate activity tracking code
- [x] Track activity days for PR creation, reviews, and issue comments
- [x] Separate issue close tracking from issue open tracking
- [x] Update early bird window from 5am-9am to 6am-9am
- [x] Add time-based multipliers to velocity timeline scoring
- [x] Update GraphQL query to fetch OPEN, MERGED, CLOSED PRs
- [x] Fix PR filtering logic to handle all PR states correctly
- [x] Improve watch handlers in Vue components to prevent double-loading
- [x] Fix formatDuration to handle zero and negative values
- [x] Update scoring documentation to include Tests component

* refactor: use standard library and consolidate constants

- [x] Replace custom contains function with slices.Contains
- [x] Remove duplicate contains function implementations
- [x] Extract magic numbers to named constants in formatters
- [x] Create constants composable for app-wide values
- [x] Add ESLint configuration with browser globals
- [x] Add lint npm scripts to package.json
- [x] Reorder Vue template attributes for consistency
- [x] Remove unused variable in AchievementProgress
- [x] Add pnpm lock file
2026-01-13 11:39:35 +00:00
lukaszraczylo a23915c620 Update go.mod and go.sum (#8) 2026-01-13 03:33:35 +00:00
lukaszraczylo 8d79d058ec Update go.mod and go.sum (#7) 2026-01-12 03:36:11 +00:00
lukaszraczylo 186c856d59 Update go.mod and go.sum (#6) 2026-01-10 03:32:24 +00:00
lukaszraczylo b2c6e991d8 Update go.mod and go.sum (#5) 2026-01-09 03:34:15 +00:00
lukaszraczylo e4ec2470d3 Update go.mod and go.sum (#4) 2025-12-31 03:33:45 +00:00
24 changed files with 2547 additions and 247 deletions
+18 -19
View File
@@ -4,15 +4,15 @@ go 1.24.2
require (
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/go-git/go-git/v5 v5.16.4
github.com/go-git/go-git/v5 v5.16.5
github.com/goccy/go-json v0.10.5
github.com/google/go-github/v68 v68.0.0
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/oauth2 v0.34.0
golang.org/x/oauth2 v0.35.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -22,15 +22,14 @@ require (
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
@@ -40,15 +39,15 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-github/v75 v75.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
@@ -56,14 +55,14 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+39 -42
View File
@@ -13,30 +13,28 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
@@ -57,29 +55,29 @@ github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9n
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s=
github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68=
github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=
github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -95,8 +93,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -118,10 +116,10 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed h1:KT7hI8vYXgU0s2qaMkrfq9tCA1w/iEPgfredVP+4Tzw=
github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf h1:o1uxfymjZ7jZ4MsgCErcwWGtVKSiNAXtS59Lhs6uI/g=
github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
@@ -141,15 +139,15 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -158,16 +156,15 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+170 -56
View File
@@ -1,6 +1,7 @@
package aggregator
import (
"slices"
"sort"
"strings"
"time"
@@ -72,11 +73,38 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
// Per-repo activity days
repoActivityDays := make(map[string]map[string]map[string]bool) // repo -> login -> set of date strings
// Helper to track activity day for a contributor
trackActivityDay := func(login, repo string, date time.Time) {
dateStr := date.Format("2006-01-02")
// Global activity tracking
if activityDays[login] == nil {
activityDays[login] = make(map[string]bool)
}
activityDays[login][dateStr] = true
// Per-repo activity tracking
if repo != "" {
if repoActivityDays[repo] == nil {
repoActivityDays[repo] = make(map[string]map[string]bool)
}
if repoActivityDays[repo][login] == nil {
repoActivityDays[repo][login] = make(map[string]bool)
}
repoActivityDays[repo][login][dateStr] = true
}
}
// Track unique files per contributor for accurate FilesChanged count
contributorFiles := make(map[string]map[string]bool) // login -> set of file paths
// Per-repo unique files per contributor
repoContributorFiles := make(map[string]map[string]map[string]bool) // repo -> login -> set of file paths
// Track counts of items with valid time data (for accurate average calculations)
// These track only PRs/reviews that have valid time data, not total counts
reviewsWithResponseTime := make(map[string]int) // login -> count of reviews with valid ResponseTime
repoReviewsWithResponseTime := make(map[string]map[string]int) // repo -> login -> count
prsWithTimeToMerge := make(map[string]int) // login -> count of PRs with valid TimeToMerge
repoPRsWithTimeToMerge := make(map[string]map[string]int) // repo -> login -> count
// Helper to get or create per-repo contributor
getRepoContributor := func(repo, login, name, avatarURL string) *models.ContributorMetrics {
if repoContributorMap[repo] == nil {
@@ -140,6 +168,9 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
cm := contributorMap[login]
cm.CommitCount++
if commit.HasTests {
cm.CommitsWithTests++
}
cm.LinesAdded += commit.Additions
cm.LinesDeleted += commit.Deletions
cm.MeaningfulLinesAdded += commit.MeaningfulAdditions
@@ -157,6 +188,9 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
// Update per-repo contributor stats
rcm := getRepoContributor(commit.Repository, login, cm.Name, cm.AvatarURL)
rcm.CommitCount++
if commit.HasTests {
rcm.CommitsWithTests++
}
rcm.LinesAdded += commit.Additions
rcm.LinesDeleted += commit.Deletions
rcm.MeaningfulLinesAdded += commit.MeaningfulAdditions
@@ -178,8 +212,9 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
hour := commit.Date.Hour()
weekday := commit.Date.Weekday()
// Early bird: commits before 9am (for achievements)
if hour >= 5 && hour < 9 {
// Early bird: commits between 6am-9am (for achievements)
// Aligned with the early morning multiplier range
if hour >= 6 && hour < 9 {
cm.EarlyBirdCount++
rcm.EarlyBirdCount++
}
@@ -233,24 +268,11 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
rcm.EarlyMorningCount++
}
// Track activity days (global)
if activityDays[login] == nil {
activityDays[login] = make(map[string]bool)
}
dateStr := commit.Date.Format("2006-01-02")
activityDays[login][dateStr] = true
// Track activity days (per-repo)
if repoActivityDays[commit.Repository] == nil {
repoActivityDays[commit.Repository] = make(map[string]map[string]bool)
}
if repoActivityDays[commit.Repository][login] == nil {
repoActivityDays[commit.Repository][login] = make(map[string]bool)
}
repoActivityDays[commit.Repository][login][dateStr] = true
// Track activity day for this commit
trackActivityDay(login, commit.Repository, commit.Date)
// Track repository participation
if !contains(cm.RepositoriesContributed, commit.Repository) {
if !slices.Contains(cm.RepositoriesContributed, commit.Repository) {
cm.RepositoriesContributed = append(cm.RepositoriesContributed, commit.Repository)
}
@@ -307,6 +329,9 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
rcm := getRepoContributor(pr.Repository, login, cm.Name, cm.AvatarURL)
rcm.PRsOpened++
// Track activity day for PR creation
trackActivityDay(login, pr.Repository, pr.CreatedAt)
prSize := pr.Additions + pr.Deletions
if pr.IsMerged() {
@@ -316,6 +341,12 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
// Accumulate for average calculation
cm.AvgTimeToMerge += pr.TimeToMerge.Hours()
rcm.AvgTimeToMerge += pr.TimeToMerge.Hours()
// Track count of PRs with valid time data for accurate average
prsWithTimeToMerge[login]++
if repoPRsWithTimeToMerge[pr.Repository] == nil {
repoPRsWithTimeToMerge[pr.Repository] = make(map[string]int)
}
repoPRsWithTimeToMerge[pr.Repository][login]++
}
// Track largest PR
@@ -337,7 +368,7 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
}
// Track repository participation
if !contains(cm.RepositoriesContributed, pr.Repository) {
if !slices.Contains(cm.RepositoriesContributed, pr.Repository) {
cm.RepositoriesContributed = append(cm.RepositoriesContributed, pr.Repository)
}
@@ -372,6 +403,9 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
rcm.ReviewsGiven++
rcm.ReviewComments += review.CommentsCount
// Track activity day for review submission
trackActivityDay(login, review.Repository, review.SubmittedAt)
if review.IsApproval() {
cm.ApprovalsGiven++
rcm.ApprovalsGiven++
@@ -395,6 +429,12 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
if review.ResponseTime != nil {
cm.AvgReviewTime += review.ResponseTime.Hours()
rcm.AvgReviewTime += review.ResponseTime.Hours()
// Track count of reviews with valid time data for accurate average
reviewsWithResponseTime[login]++
if repoReviewsWithResponseTime[review.Repository] == nil {
repoReviewsWithResponseTime[review.Repository] = make(map[string]int)
}
repoReviewsWithResponseTime[review.Repository][login]++
}
// Track unique reviewees
@@ -452,21 +492,47 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
cm := contributorMap[login]
cm.IssuesOpened++
if issue.IsClosed() && issue.ClosedBy != nil && issue.ClosedBy.Login == login {
cm.IssuesClosed++
}
// Track activity day for issue creation
trackActivityDay(login, issue.Repository, issue.CreatedAt)
// Track repository participation
if !contains(cm.RepositoriesContributed, issue.Repository) {
if !slices.Contains(cm.RepositoriesContributed, issue.Repository) {
cm.RepositoriesContributed = append(cm.RepositoriesContributed, issue.Repository)
}
// Update per-repo contributor metrics
rcm := getRepoContributor(issue.Repository, login, cm.Name, cm.AvatarURL)
rcm.IssuesOpened++
if issue.IsClosed() && issue.ClosedBy != nil && issue.ClosedBy.Login == login {
rcm.IssuesClosed++
}
// Count issues closed by each contributor (separate from who opened them)
// This gives credit to whoever closed the issue, even if they didn't open it
for _, issue := range data.Issues {
if !issue.IsClosed() || issue.ClosedBy == nil || issue.ClosedBy.Login == "" {
continue
}
closerLogin := issue.ClosedBy.Login
// Initialize contributor if needed (someone who closes issues but didn't open any)
if _, ok := contributorMap[closerLogin]; !ok {
contributorMap[closerLogin] = &models.ContributorMetrics{
Login: closerLogin,
Period: period,
}
}
cm := contributorMap[closerLogin]
cm.IssuesClosed++
// Track repository participation for the closer
if !slices.Contains(cm.RepositoriesContributed, issue.Repository) {
cm.RepositoriesContributed = append(cm.RepositoriesContributed, issue.Repository)
}
// Update per-repo contributor metrics for the closer
rcm := getRepoContributor(issue.Repository, closerLogin, cm.Name, cm.AvatarURL)
rcm.IssuesClosed++
}
// Process issue comments
@@ -487,8 +553,11 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
cm := contributorMap[login]
cm.IssueComments++
// Track activity day for issue comment
trackActivityDay(login, comment.Repository, comment.CreatedAt)
// Track repository participation
if !contains(cm.RepositoriesContributed, comment.Repository) {
if !slices.Contains(cm.RepositoriesContributed, comment.Repository) {
cm.RepositoriesContributed = append(cm.RepositoriesContributed, comment.Repository)
}
@@ -550,20 +619,23 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
// Calculate averages and finalize contributor metrics
for login, cm := range contributorMap {
// Calculate average time to merge
// Calculate average time to merge (only from PRs that have TimeToMerge data)
if count := prsWithTimeToMerge[login]; count > 0 {
cm.AvgTimeToMerge = cm.AvgTimeToMerge / float64(count)
}
// Calculate average review time (only from reviews that have ResponseTime data)
if count := reviewsWithResponseTime[login]; count > 0 {
cm.AvgReviewTime = cm.AvgReviewTime / float64(count)
}
// Calculate average PR size (only for merged PRs to exclude abandoned PRs)
if cm.PRsMerged > 0 {
cm.AvgTimeToMerge = cm.AvgTimeToMerge / float64(cm.PRsMerged)
}
// Calculate average review time
if cm.ReviewsGiven > 0 {
cm.AvgReviewTime = cm.AvgReviewTime / float64(cm.ReviewsGiven)
}
// Calculate average PR size
if cm.PRsOpened > 0 {
totalPRLines := 0
for _, pr := range data.PullRequests {
if !pr.IsMerged() {
continue // Only count merged PRs
}
// Normalize PR author login before comparison
prLogin := pr.Author.Login
if normalized, ok := prAuthorToNormalizedLogin[prLogin]; ok {
@@ -573,7 +645,7 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
totalPRLines += pr.TotalChanges()
}
}
cm.AvgPRSize = float64(totalPRLines) / float64(cm.PRsOpened)
cm.AvgPRSize = float64(totalPRLines) / float64(cm.PRsMerged)
}
// Set unique reviewees count
@@ -617,17 +689,26 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
// Calculate averages for per-repo contributors
for login, rcm := range repoContribs {
if rcm.PRsMerged > 0 {
rcm.AvgTimeToMerge = rcm.AvgTimeToMerge / float64(rcm.PRsMerged)
// Use count of PRs with valid time data for accurate average
if repoPRCounts, ok := repoPRsWithTimeToMerge[repo]; ok {
if count := repoPRCounts[login]; count > 0 {
rcm.AvgTimeToMerge = rcm.AvgTimeToMerge / float64(count)
}
}
if rcm.ReviewsGiven > 0 {
rcm.AvgReviewTime = rcm.AvgReviewTime / float64(rcm.ReviewsGiven)
// Use count of reviews with valid time data for accurate average
if repoReviewCounts, ok := repoReviewsWithResponseTime[repo]; ok {
if count := repoReviewCounts[login]; count > 0 {
rcm.AvgReviewTime = rcm.AvgReviewTime / float64(count)
}
}
// Calculate average PR size for this repo
if rcm.PRsOpened > 0 {
// Calculate average PR size for this repo (only for merged PRs to exclude abandoned PRs)
if rcm.PRsMerged > 0 {
totalPRLines := 0
for _, pr := range data.PullRequests {
if !pr.IsMerged() {
continue // Only count merged PRs
}
// Normalize PR author login before comparison
prLogin := pr.Author.Login
if mapped, ok := loginToLogin[prLogin]; ok {
@@ -637,7 +718,7 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
totalPRLines += pr.TotalChanges()
}
}
rcm.AvgPRSize = float64(totalPRLines) / float64(rcm.PRsOpened)
rcm.AvgPRSize = float64(totalPRLines) / float64(rcm.PRsMerged)
}
// Calculate perfect PRs for this repo
@@ -761,15 +842,6 @@ func parseRepoName(fullName string) (owner, name string) {
return fullName, ""
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// normalizeForComparison normalizes a string for fuzzy comparison
// by lowercasing and removing spaces, hyphens, underscores, dots, and digits
func normalizeForComparison(s string) string {
@@ -1282,7 +1354,47 @@ func buildVelocityTimeline(data *models.RawData, period models.Period, scoringCo
pointsReview = 30
}
// Aggregate commits by week
// Get time-based multipliers with defaults
multRegular := scoringConfig.Points.MultiplierRegularHours
if multRegular == 0 {
multRegular = 1.0
}
multEvening := scoringConfig.Points.MultiplierEvening
if multEvening == 0 {
multEvening = 2.0
}
multLateNight := scoringConfig.Points.MultiplierLateNight
if multLateNight == 0 {
multLateNight = 2.5
}
multOvernight := scoringConfig.Points.MultiplierOvernight
if multOvernight == 0 {
multOvernight = 5.0
}
multEarlyMorning := scoringConfig.Points.MultiplierEarlyMorning
if multEarlyMorning == 0 {
multEarlyMorning = 2.0
}
// Helper to get time-based multiplier for a commit
getTimeMultiplier := func(hour int) float64 {
switch {
case hour >= 9 && hour < 17:
return multRegular // Regular hours: 9am-5pm
case hour >= 17 && hour < 21:
return multEvening // Evening: 5pm-9pm
case hour >= 21 && hour <= 23:
return multLateNight // Late night: 9pm-midnight
case hour >= 0 && hour < 6:
return multOvernight // Overnight: midnight-6am
case hour >= 6 && hour < 9:
return multEarlyMorning // Early morning: 6am-9am
default:
return multRegular
}
}
// Aggregate commits by week (with time-based multipliers)
for _, commit := range data.Commits {
if commit.Date.Before(start) || commit.Date.After(end) {
continue
@@ -1290,7 +1402,9 @@ func buildVelocityTimeline(data *models.RawData, period models.Period, scoringCo
idx := findWeekIndex(commit.Date)
if idx >= 0 && idx < len(weeks) {
weekCommits[idx]++
weekScore[idx] += float64(pointsCommit)
// Apply time-based multiplier to commit score
multiplier := getTimeMultiplier(commit.Date.Hour())
weekScore[idx] += float64(pointsCommit) * multiplier
}
}
-12
View File
@@ -349,18 +349,6 @@ func TestAggregator_MultipleRepositories(t *testing.T) {
assert.Len(t, metrics.Repositories, 2)
}
func TestContains(t *testing.T) {
t.Parallel()
slice := []string{"a", "b", "c"}
assert.True(t, contains(slice, "a"))
assert.True(t, contains(slice, "b"))
assert.True(t, contains(slice, "c"))
assert.False(t, contains(slice, "d"))
assert.False(t, contains([]string{}, "a"))
}
func TestParseRepoName(t *testing.T) {
t.Parallel()
+7 -5
View File
@@ -18,10 +18,11 @@ type ContributorMetrics struct {
Period Period `json:"period"`
// Commit metrics
CommitCount int `json:"commit_count"`
LinesAdded int `json:"lines_added"`
LinesDeleted int `json:"lines_deleted"`
FilesChanged int `json:"files_changed"`
CommitCount int `json:"commit_count"`
CommitsWithTests int `json:"commits_with_tests"` // Commits that include test files
LinesAdded int `json:"lines_added"`
LinesDeleted int `json:"lines_deleted"`
FilesChanged int `json:"files_changed"`
// Meaningful line counts (excludes comments and whitespace)
MeaningfulLinesAdded int `json:"meaningful_lines_added"`
@@ -98,7 +99,8 @@ type ScoreBreakdown struct {
Issues int `json:"issues"` // Issue-related points (opened, closed, comments, references)
ResponseBonus int `json:"response_bonus"`
LineChanges int `json:"line_changes"`
OutOfHours int `json:"out_of_hours"` // Bonus for out-of-hours commits
TestsBonus int `json:"tests_bonus"` // Bonus for commits that include test files
OutOfHours int `json:"out_of_hours"` // Bonus for out-of-hours commits
}
// RepositoryMetrics holds aggregated metrics for a single repository
+70 -54
View File
@@ -1,6 +1,7 @@
package scoring
import (
"slices"
"sort"
"github.com/lukaszraczylo/git-velocity/internal/config"
@@ -23,52 +24,73 @@ func (c *Calculator) Calculate(metrics *models.GlobalMetrics) *models.GlobalMetr
return metrics
}
// Collect all contributor metrics across repositories
// Build contributor map for scoring
// IMPORTANT: Prefer metrics.Contributors if populated (from aggregator) since it contains
// properly calculated values that can't be reconstructed from per-repo data:
// - Weighted average times (AvgReviewTime, AvgTimeToMerge)
// - Cross-repo streaks (ActiveDays, LongestStreak, WorkWeekStreak)
// - Max values (LargestPRSize)
// - Deduplicated counts (UniqueReviewees, FilesChanged)
// - Summed counts (SmallPRCount, PerfectPRs)
// Fall back to aggregating from repos only for tests that don't use the full pipeline.
contributorMap := make(map[string]*models.ContributorMetrics)
for _, repo := range metrics.Repositories {
for i := range repo.Contributors {
login := repo.Contributors[i].Login
if _, ok := contributorMap[login]; !ok {
// Copy the contributor metrics
cm := repo.Contributors[i]
contributorMap[login] = &cm
} else {
// Aggregate metrics from multiple repos
existing := contributorMap[login]
cm := repo.Contributors[i]
existing.CommitCount += cm.CommitCount
existing.LinesAdded += cm.LinesAdded
existing.LinesDeleted += cm.LinesDeleted
existing.MeaningfulLinesAdded += cm.MeaningfulLinesAdded
existing.MeaningfulLinesDeleted += cm.MeaningfulLinesDeleted
existing.CommentLinesAdded += cm.CommentLinesAdded
existing.CommentLinesDeleted += cm.CommentLinesDeleted
existing.PRsOpened += cm.PRsOpened
existing.PRsMerged += cm.PRsMerged
existing.ReviewsGiven += cm.ReviewsGiven
existing.ReviewComments += cm.ReviewComments
// Issue metrics
existing.IssuesOpened += cm.IssuesOpened
existing.IssuesClosed += cm.IssuesClosed
existing.IssueComments += cm.IssueComments
existing.IssueReferencesInCommits += cm.IssueReferencesInCommits
// Activity pattern metrics (for achievements)
existing.EarlyBirdCount += cm.EarlyBirdCount
existing.NightOwlCount += cm.NightOwlCount
existing.MidnightCount += cm.MidnightCount
existing.WeekendWarrior += cm.WeekendWarrior
existing.OutOfHoursCount += cm.OutOfHoursCount
// Time-based commit counts (for multiplier scoring)
existing.RegularHoursCount += cm.RegularHoursCount
existing.EveningCount += cm.EveningCount
existing.LateNightCount += cm.LateNightCount
existing.OvernightCount += cm.OvernightCount
existing.EarlyMorningCount += cm.EarlyMorningCount
// Combine unique repositories
for _, r := range cm.RepositoriesContributed {
if !contains(existing.RepositoriesContributed, r) {
existing.RepositoriesContributed = append(existing.RepositoriesContributed, r)
if len(metrics.Contributors) > 0 {
// Use already-aggregated global contributors (production path)
for i := range metrics.Contributors {
login := metrics.Contributors[i].Login
cm := metrics.Contributors[i]
contributorMap[login] = &cm
}
} else {
// Fallback: aggregate from per-repo contributors (test compatibility path)
// Note: This path cannot properly aggregate computed fields like AvgReviewTime,
// LongestStreak, etc. - it only sums count-based metrics.
for _, repo := range metrics.Repositories {
for i := range repo.Contributors {
login := repo.Contributors[i].Login
if _, ok := contributorMap[login]; !ok {
// Copy the contributor metrics
cm := repo.Contributors[i]
contributorMap[login] = &cm
} else {
// Aggregate metrics from multiple repos
existing := contributorMap[login]
cm := repo.Contributors[i]
existing.CommitCount += cm.CommitCount
existing.CommitsWithTests += cm.CommitsWithTests
existing.LinesAdded += cm.LinesAdded
existing.LinesDeleted += cm.LinesDeleted
existing.MeaningfulLinesAdded += cm.MeaningfulLinesAdded
existing.MeaningfulLinesDeleted += cm.MeaningfulLinesDeleted
existing.CommentLinesAdded += cm.CommentLinesAdded
existing.CommentLinesDeleted += cm.CommentLinesDeleted
existing.PRsOpened += cm.PRsOpened
existing.PRsMerged += cm.PRsMerged
existing.ReviewsGiven += cm.ReviewsGiven
existing.ReviewComments += cm.ReviewComments
// Issue metrics
existing.IssuesOpened += cm.IssuesOpened
existing.IssuesClosed += cm.IssuesClosed
existing.IssueComments += cm.IssueComments
existing.IssueReferencesInCommits += cm.IssueReferencesInCommits
// Activity pattern metrics (for achievements)
existing.EarlyBirdCount += cm.EarlyBirdCount
existing.NightOwlCount += cm.NightOwlCount
existing.MidnightCount += cm.MidnightCount
existing.WeekendWarrior += cm.WeekendWarrior
existing.OutOfHoursCount += cm.OutOfHoursCount
// Time-based commit counts (for multiplier scoring)
existing.RegularHoursCount += cm.RegularHoursCount
existing.EveningCount += cm.EveningCount
existing.LateNightCount += cm.LateNightCount
existing.OvernightCount += cm.OvernightCount
existing.EarlyMorningCount += cm.EarlyMorningCount
// Combine unique repositories
for _, r := range cm.RepositoriesContributed {
if !slices.Contains(existing.RepositoriesContributed, r) {
existing.RepositoriesContributed = append(existing.RepositoriesContributed, r)
}
}
}
}
@@ -260,13 +282,16 @@ func (c *Calculator) calculateScore(cm *models.ContributorMetrics) models.Score
}
}
// Tests bonus - bonus points for commits that include test files
breakdown.TestsBonus = cm.CommitsWithTests * points.CommitWithTests
// Out of hours bonus (legacy - kept for backwards compatibility but default is 0)
breakdown.OutOfHours = cm.OutOfHoursCount * points.OutOfHours
// Calculate total
total := breakdown.Commits + breakdown.LineChanges + breakdown.PRs +
breakdown.Reviews + breakdown.ResponseBonus + breakdown.Comments +
breakdown.Issues + breakdown.OutOfHours
breakdown.Issues + breakdown.TestsBonus + breakdown.OutOfHours
return models.Score{
Total: total,
@@ -406,12 +431,3 @@ func (c *Calculator) findTopAchievers(contributors []models.ContributorMetrics,
topAchievers["pull_requests"] = topPRAuthor
}
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
@@ -843,18 +843,6 @@ func TestCalculator_WorkWeekStreakAchievement(t *testing.T) {
assert.Contains(t, contributor.Achievements, "workweek-5")
}
func TestContains(t *testing.T) {
t.Parallel()
slice := []string{"a", "b", "c"}
assert.True(t, contains(slice, "a"))
assert.True(t, contains(slice, "b"))
assert.True(t, contains(slice, "c"))
assert.False(t, contains(slice, "d"))
assert.False(t, contains([]string{}, "a"))
}
func TestCalculator_MeaningfulLinesScoring(t *testing.T) {
t.Parallel()
+15 -9
View File
@@ -221,7 +221,7 @@ type gqlPRQuery struct {
TotalCount int
PageInfo PageInfo
Nodes []gqlPRNode
} `graphql:"pullRequests(first: 100, after: $cursor, states: [MERGED], orderBy: {field: UPDATED_AT, direction: DESC})"`
} `graphql:"pullRequests(first: 100, after: $cursor, states: [OPEN, MERGED, CLOSED], orderBy: {field: UPDATED_AT, direction: DESC})"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
@@ -329,23 +329,29 @@ func (g *GraphQLClient) FetchPRsWithReviews(ctx context.Context, owner, repo str
}
},
ProcessNode: func(node gqlPRNode, repoName string) ([]prWithReviews, bool, bool) {
// Skip if not merged - not counted as "old"
if node.MergedAt == nil {
return nil, false, false
// Determine the relevant date for filtering:
// - For merged PRs: use MergedAt
// - For closed PRs: use ClosedAt
// - For open PRs: use CreatedAt (they're still active)
var relevantDate time.Time
if node.MergedAt != nil {
relevantDate = *node.MergedAt
} else if node.ClosedAt != nil {
relevantDate = *node.ClosedAt
} else {
relevantDate = node.CreatedAt
}
mergedAt := *node.MergedAt
// Hard cutoff check - stop entirely if past this date
if hardCutoff != nil && mergedAt.Before(*hardCutoff) {
if hardCutoff != nil && relevantDate.Before(*hardCutoff) {
return nil, true, true // Hard stop
}
// Check date range - skip if outside range
if until != nil && mergedAt.After(*until) {
if until != nil && relevantDate.After(*until) {
return nil, false, false // Too new, not "old"
}
if since != nil && mergedAt.Before(*since) {
if since != nil && relevantDate.Before(*since) {
return nil, true, false // Too old - signal for early termination tracking
}
+42
View File
@@ -0,0 +1,42 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
export default [
js.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
languageOptions: {
globals: {
// Browser globals
window: 'readonly',
document: 'readonly',
fetch: 'readonly',
console: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
requestAnimationFrame: 'readonly',
cancelAnimationFrame: 'readonly'
}
},
rules: {
// Vue specific rules
'vue/multi-word-component-names': 'off', // Allow single-word component names
'vue/max-attributes-per-line': 'off', // Allow multiple attributes per line
'vue/singleline-html-element-content-newline': 'off',
'vue/html-self-closing': ['error', {
html: { void: 'always', normal: 'never', component: 'always' }
}],
// General JS rules
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': 'warn',
'prefer-const': 'error',
'no-var': 'error'
}
},
{
ignores: ['dist/**', 'node_modules/**']
}
]
+6 -1
View File
@@ -6,7 +6,9 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.17",
@@ -16,9 +18,12 @@
"vue-router": "^4.2.5"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@playwright/test": "^1.57.0",
"@vitejs/plugin-vue": "^6.0.2",
"autoprefixer": "^10.4.16",
"eslint": "^9.39.2",
"eslint-plugin-vue": "^10.6.2",
"postcss": "^8.4.32",
"tailwindcss": "^4.1.17",
"vite": "^7.2.7"
+2100
View File
File diff suppressed because it is too large Load Diff
@@ -234,7 +234,6 @@ const progressItems = computed(() => {
// Get count of remaining achievements (all unearned across all types)
const remainingCount = computed(() => {
const earnedSet = new Set(props.contributor.achievements || [])
let totalUnearned = 0
for (const type of achievementTypes) {
+1 -1
View File
@@ -13,6 +13,6 @@ defineProps({
hover ? 'hover:shadow-lg transition-shadow' : ''
]"
>
<slot />
<slot></slot>
</div>
</template>
-1
View File
@@ -1,7 +1,6 @@
<script setup>
import { RouterLink } from 'vue-router'
import Avatar from './Avatar.vue'
import { formatNumber } from '../composables/formatters'
defineProps({
contributor: {
+5 -5
View File
@@ -51,8 +51,8 @@ const repositories = computed(() => globalData.value?.Repositories || [])
<!-- Mobile Menu Button -->
<button
@click="mobileMenuOpen = !mobileMenuOpen"
class="md:hidden p-2 rounded-lg hover:bg-gray-700 transition"
@click="mobileMenuOpen = !mobileMenuOpen"
>
<i class="fas fa-bars text-gray-200"></i>
</button>
@@ -63,37 +63,37 @@ const repositories = computed(() => globalData.value?.Repositories || [])
<div class="flex flex-col space-y-1">
<RouterLink
to="/"
@click="mobileMenuOpen = false"
:class="[
'block px-4 py-3 rounded-lg text-base font-medium transition-colors',
route.path === '/'
? 'bg-primary-900/20 text-primary-400'
: 'text-gray-200 hover:bg-gray-800'
]"
@click="mobileMenuOpen = false"
>
<i class="fas fa-home mr-3 w-5 text-center"></i>Dashboard
</RouterLink>
<RouterLink
to="/leaderboard"
@click="mobileMenuOpen = false"
:class="[
'block px-4 py-3 rounded-lg text-base font-medium transition-colors',
route.path === '/leaderboard'
? 'bg-primary-900/20 text-primary-400'
: 'text-gray-200 hover:bg-gray-800'
]"
@click="mobileMenuOpen = false"
>
<i class="fas fa-trophy mr-3 w-5 text-center"></i>Leaderboard
</RouterLink>
<RouterLink
to="/how-scoring-works"
@click="mobileMenuOpen = false"
:class="[
'block px-4 py-3 rounded-lg text-base font-medium transition-colors',
route.path === '/how-scoring-works'
? 'bg-primary-900/20 text-primary-400'
: 'text-gray-200 hover:bg-gray-800'
]"
@click="mobileMenuOpen = false"
>
<i class="fas fa-calculator mr-3 w-5 text-center"></i>How Scoring Works
</RouterLink>
@@ -101,13 +101,13 @@ const repositories = computed(() => globalData.value?.Repositories || [])
v-for="repo in repositories"
:key="`${repo.Owner}/${repo.Name}`"
:to="`/repos/${repo.Owner}/${repo.Name}`"
@click="mobileMenuOpen = false"
:class="[
'block px-4 py-3 rounded-lg text-base font-medium transition-colors',
route.path.includes(`/repos/${repo.Owner}/${repo.Name}`)
? 'bg-primary-900/20 text-primary-400'
: 'text-gray-200 hover:bg-gray-800'
]"
@click="mobileMenuOpen = false"
>
<i class="fas fa-code-branch mr-3 w-5 text-center"></i>{{ repo.Name }}
</RouterLink>
+2 -1
View File
@@ -3,6 +3,7 @@ import { RouterLink } from 'vue-router'
import Card from './Card.vue'
import Avatar from './Avatar.vue'
import { formatNumber, slugify } from '../composables/formatters'
import { DEFAULT_TEAM_COLOR } from '../composables/constants'
defineProps({
team: {
@@ -24,7 +25,7 @@ defineProps({
</h3>
<span
class="w-3 h-3 rounded-full"
:style="{ backgroundColor: team.color || '#8b5cf6' }"
:style="{ backgroundColor: team.color || DEFAULT_TEAM_COLOR }"
></span>
</div>
+12
View File
@@ -0,0 +1,12 @@
/**
* Application constants
*/
// Default colors
export const DEFAULT_TEAM_COLOR = '#8b5cf6' // Purple - matches accent color palette
// Data paths
export const DATA_BASE_PATH = './data'
export const GLOBAL_DATA_PATH = `${DATA_BASE_PATH}/global.json`
export const CONTRIBUTORS_PATH = `${DATA_BASE_PATH}/contributors`
export const REPOS_PATH = `${DATA_BASE_PATH}/repos`
+16 -8
View File
@@ -1,13 +1,21 @@
// Number formatting thresholds
const ONE_MILLION = 1_000_000
const ONE_THOUSAND = 1_000
// Time conversion constants
const MINUTES_PER_HOUR = 60
const HOURS_PER_DAY = 24
/**
* Format a number with K/M suffixes for large values
*/
export function formatNumber(n) {
if (n === null || n === undefined) return '0'
if (n >= 1000000) {
return (n / 1000000).toFixed(1) + 'M'
if (n >= ONE_MILLION) {
return (n / ONE_MILLION).toFixed(1) + 'M'
}
if (n >= 1000) {
return (n / 1000).toFixed(1) + 'K'
if (n >= ONE_THOUSAND) {
return (n / ONE_THOUSAND).toFixed(1) + 'K'
}
return String(n)
}
@@ -16,14 +24,14 @@ export function formatNumber(n) {
* Format hours as a human-readable duration
*/
export function formatDuration(hours) {
if (hours === null || hours === undefined) return '-'
if (hours === null || hours === undefined || hours <= 0) return '-'
if (hours < 1) {
return Math.round(hours * 60) + 'm'
return Math.round(hours * MINUTES_PER_HOUR) + 'm'
}
if (hours < 24) {
if (hours < HOURS_PER_DAY) {
return hours.toFixed(1) + 'h'
}
return (hours / 24).toFixed(1) + 'd'
return (hours / HOURS_PER_DAY).toFixed(1) + 'd'
}
/**
+16 -2
View File
@@ -79,8 +79,22 @@ async function loadContributor() {
}
onMounted(loadContributor)
watch(() => route.params, loadContributor)
watch(globalData, loadContributor)
// Watch for route changes (navigation to different contributor)
watch(() => route.params.login, (newLogin, oldLogin) => {
if (newLogin && newLogin !== oldLogin) {
loadContributor()
}
})
// Watch for globalData changes, but only reload if we don't have contributor data yet
// This prevents double-loading when both route and globalData change on initial navigation
watch(globalData, (newData, oldData) => {
// Only reload if globalData became available and we have an error or no data
if (newData && !oldData && (error.value || !contributor.value)) {
loadContributor()
}
})
</script>
<template>
+1 -1
View File
@@ -57,8 +57,8 @@ const showScoreInChart = ref(false)
<SectionHeader title="Velocity Timeline" icon="fas fa-chart-line" icon-color="text-primary-500" />
<label class="flex items-center space-x-2 text-sm text-gray-400 cursor-pointer">
<input
type="checkbox"
v-model="showScoreInChart"
type="checkbox"
class="rounded border-gray-600 text-primary-500 focus:ring-primary-500"
/>
<span>Show Score</span>
+2 -1
View File
@@ -64,7 +64,7 @@ import SectionHeader from '../components/SectionHeader.vue'
Score Formula
</h3>
<div class="bg-gray-900 text-gray-100 p-3 sm:p-4 rounded-lg overflow-x-auto mb-4 -mx-2 sm:mx-0">
<pre class="text-xs sm:text-sm font-mono whitespace-pre-wrap sm:whitespace-pre"><code>Total Score = Commits + Lines + PRs + Reviews + Comments + Issues + Response
<pre class="text-xs sm:text-sm font-mono whitespace-pre-wrap sm:whitespace-pre"><code>Total Score = Commits + Lines + PRs + Reviews + Comments + Issues + Tests + Response
Where:
Commits = sum of (commits x 10 x time_multiplier)
@@ -73,6 +73,7 @@ Where:
Reviews = reviews_given x 30 pts
Comments = review_comments x 5 pts
Issues = (opened x 10) + (closed x 20) + (comments x 5) + (refs x 5) pts
Tests = commits_with_tests x 15 pts
Response = fast review bonus (0-50 pts)
Time Multipliers:
+1 -11
View File
@@ -34,16 +34,6 @@ const tableColumns = [
{ key: 'team', label: 'Team', align: 'left', headerClass: 'hidden xl:table-cell' },
{ key: 'score', label: 'Score', align: 'right' }
]
const categoryIcon = (category) => {
const icons = {
'Commits': 'fas fa-code-commit text-green-500',
'PRs': 'fas fa-code-pull-request text-blue-500',
'Reviews': 'fas fa-eye text-purple-500',
'Comments': 'fas fa-comment text-orange-500'
}
return icons[category] || ''
}
</script>
<template>
@@ -71,8 +61,8 @@ const categoryIcon = (category) => {
/>
<button
v-if="searchQuery"
@click="searchQuery = ''"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200"
@click="searchQuery = ''"
>
<i class="fas fa-times"></i>
</button>
+8 -2
View File
@@ -61,7 +61,13 @@ async function loadRepository() {
}
onMounted(loadRepository)
watch(() => route.params, loadRepository)
// Watch for route changes (navigation to different repository)
watch(() => [route.params.owner, route.params.name], ([newOwner, newName], [oldOwner, oldName]) => {
if ((newOwner && newName) && (newOwner !== oldOwner || newName !== oldName)) {
loadRepository()
}
})
</script>
<template>
@@ -132,8 +138,8 @@ watch(() => route.params, loadRepository)
/>
<button
v-if="searchQuery"
@click="searchQuery = ''"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200"
@click="searchQuery = ''"
>
<i class="fas fa-times"></i>
</button>
+16 -3
View File
@@ -8,6 +8,7 @@ import StatCard from '../components/StatCard.vue'
import MemberCard from '../components/MemberCard.vue'
import SectionHeader from '../components/SectionHeader.vue'
import { slugify } from '../composables/formatters'
import { DEFAULT_TEAM_COLOR } from '../composables/constants'
const route = useRoute()
const globalData = inject('globalData')
@@ -38,8 +39,20 @@ function loadTeam() {
}
onMounted(loadTeam)
watch(() => route.params, loadTeam)
watch(globalData, loadTeam)
// Watch for route changes (navigation to different team)
watch(() => route.params.slug, (newSlug, oldSlug) => {
if (newSlug && newSlug !== oldSlug) {
loadTeam()
}
})
// Watch for globalData changes, but only reload if we don't have team data yet
watch(globalData, (newData, oldData) => {
if (newData && !oldData && (error.value || !team.value)) {
loadTeam()
}
})
</script>
<template>
@@ -56,7 +69,7 @@ watch(globalData, loadTeam)
<template #prefix>
<div
class="w-4 h-4 rounded-full mr-4"
:style="{ backgroundColor: team.color || '#8b5cf6' }"
:style="{ backgroundColor: team.color || DEFAULT_TEAM_COLOR }"
></div>
</template>
</PageHeader>