| 1 | module main |
| 2 | |
| 3 | import time |
| 4 | import veb |
| 5 | |
| 6 | const admin_stats_days = 30 |
| 7 | const stats_month_short = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', |
| 8 | 'Nov', 'Dec'] |
| 9 | |
| 10 | struct DayBucket { |
| 11 | label string |
| 12 | count int |
| 13 | } |
| 14 | |
| 15 | struct AdminStats { |
| 16 | mut: |
| 17 | days int |
| 18 | users []DayBucket |
| 19 | repos []DayBucket |
| 20 | commits []DayBucket |
| 21 | issues []DayBucket |
| 22 | total_users int |
| 23 | total_repos int |
| 24 | total_commits int |
| 25 | total_issues int |
| 26 | max_users int |
| 27 | max_repos int |
| 28 | max_commits int |
| 29 | max_issues int |
| 30 | } |
| 31 | |
| 32 | fn stats_day_label(ts i64) string { |
| 33 | t := time.unix(ts) |
| 34 | return '${stats_month_short[t.month - 1]} ${t.day}' |
| 35 | } |
| 36 | |
| 37 | fn stats_bucket_index(ts i64, range_start i64, one_day i64, days int) int { |
| 38 | if ts < range_start { |
| 39 | return -1 |
| 40 | } |
| 41 | idx := int((ts - range_start) / one_day) |
| 42 | if idx < 0 || idx >= days { |
| 43 | return -1 |
| 44 | } |
| 45 | return idx |
| 46 | } |
| 47 | |
| 48 | pub fn (mut app App) get_admin_stats(days int) AdminStats { |
| 49 | one_day := i64(86400) |
| 50 | today_start := time.now().unix() / one_day * one_day |
| 51 | range_start := today_start - i64(days - 1) * one_day |
| 52 | |
| 53 | mut user_counts := []int{len: days, init: 0} |
| 54 | mut repo_counts := []int{len: days, init: 0} |
| 55 | mut commit_counts := []int{len: days, init: 0} |
| 56 | mut issue_counts := []int{len: days, init: 0} |
| 57 | |
| 58 | registered_users := sql app.db { |
| 59 | select from User where is_registered == true |
| 60 | } or { []User{} } |
| 61 | for u in registered_users { |
| 62 | idx := stats_bucket_index(u.created_at.unix(), range_start, one_day, days) |
| 63 | if idx >= 0 { |
| 64 | user_counts[idx]++ |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | repo_rows := db_exec_values(app.db, |
| 69 | 'select created_at from ${sql_table('Repo')} where created_at >= ${range_start}') or { |
| 70 | [][]string{} |
| 71 | } |
| 72 | for row in repo_rows { |
| 73 | if row.len == 0 { |
| 74 | continue |
| 75 | } |
| 76 | idx := stats_bucket_index(row[0].i64(), range_start, one_day, days) |
| 77 | if idx >= 0 { |
| 78 | repo_counts[idx]++ |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | commit_rows := db_exec_values(app.db, |
| 83 | 'select created_at from ${sql_table('Commit')} where created_at >= ${range_start}') or { |
| 84 | [][]string{} |
| 85 | } |
| 86 | for row in commit_rows { |
| 87 | if row.len == 0 { |
| 88 | continue |
| 89 | } |
| 90 | idx := stats_bucket_index(row[0].i64(), range_start, one_day, days) |
| 91 | if idx >= 0 { |
| 92 | commit_counts[idx]++ |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | issue_rows := db_exec_values(app.db, |
| 97 | 'select created_at from ${sql_table('Issue')} where is_pr is false and created_at >= ${range_start}') or { |
| 98 | [][]string{} |
| 99 | } |
| 100 | for row in issue_rows { |
| 101 | if row.len == 0 { |
| 102 | continue |
| 103 | } |
| 104 | idx := stats_bucket_index(row[0].i64(), range_start, one_day, days) |
| 105 | if idx >= 0 { |
| 106 | issue_counts[idx]++ |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | mut user_series := []DayBucket{cap: days} |
| 111 | mut repo_series := []DayBucket{cap: days} |
| 112 | mut commit_series := []DayBucket{cap: days} |
| 113 | mut issue_series := []DayBucket{cap: days} |
| 114 | mut max_u := 0 |
| 115 | mut max_r := 0 |
| 116 | mut max_c := 0 |
| 117 | mut max_i := 0 |
| 118 | for i in 0 .. days { |
| 119 | ts := range_start + i64(i) * one_day |
| 120 | lbl := stats_day_label(ts) |
| 121 | user_series << DayBucket{lbl, user_counts[i]} |
| 122 | repo_series << DayBucket{lbl, repo_counts[i]} |
| 123 | commit_series << DayBucket{lbl, commit_counts[i]} |
| 124 | issue_series << DayBucket{lbl, issue_counts[i]} |
| 125 | if user_counts[i] > max_u { |
| 126 | max_u = user_counts[i] |
| 127 | } |
| 128 | if repo_counts[i] > max_r { |
| 129 | max_r = repo_counts[i] |
| 130 | } |
| 131 | if commit_counts[i] > max_c { |
| 132 | max_c = commit_counts[i] |
| 133 | } |
| 134 | if issue_counts[i] > max_i { |
| 135 | max_i = issue_counts[i] |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | total_users := sql app.db { |
| 140 | select count from User where is_registered == true |
| 141 | } or { 0 } |
| 142 | total_repos := sql app.db { |
| 143 | select count from Repo |
| 144 | } or { 0 } |
| 145 | total_commits := sql app.db { |
| 146 | select count from Commit |
| 147 | } or { 0 } |
| 148 | total_issues := sql app.db { |
| 149 | select count from Issue where is_pr == false |
| 150 | } or { 0 } |
| 151 | |
| 152 | return AdminStats{ |
| 153 | days: days |
| 154 | users: user_series |
| 155 | repos: repo_series |
| 156 | commits: commit_series |
| 157 | issues: issue_series |
| 158 | total_users: total_users |
| 159 | total_repos: total_repos |
| 160 | total_commits: total_commits |
| 161 | total_issues: total_issues |
| 162 | max_users: max_u |
| 163 | max_repos: max_r |
| 164 | max_commits: max_c |
| 165 | max_issues: max_i |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | fn render_stat_chart(buckets []DayBucket, max int, color string) veb.RawHtml { |
| 170 | chart_w := 720 |
| 171 | chart_h := 200 |
| 172 | bar_area_h := 160 |
| 173 | bar_top := 10 |
| 174 | bar_count := buckets.len |
| 175 | if bar_count == 0 { |
| 176 | return veb.RawHtml('') |
| 177 | } |
| 178 | slot := (chart_w - 20) / bar_count |
| 179 | bar_w := if slot > 4 { slot - 2 } else { slot } |
| 180 | mut s := '<svg class="stat-chart" viewBox="0 0 ${chart_w} ${chart_h}" preserveAspectRatio="none">' |
| 181 | s += '<g class="stat-chart-grid">' |
| 182 | for i in 1 .. 5 { |
| 183 | y := bar_top + bar_area_h - bar_area_h * i / 4 |
| 184 | s += '<line x1="10" y1="${y}" x2="${chart_w - 10}" y2="${y}"></line>' |
| 185 | } |
| 186 | s += '</g>' |
| 187 | for i, b in buckets { |
| 188 | h := if max == 0 { 0 } else { b.count * bar_area_h / max } |
| 189 | x := 10 + i * slot |
| 190 | y := bar_top + bar_area_h - h |
| 191 | s += '<rect class="stat-chart-bar" x="${x}" y="${y}" width="${bar_w}" height="${h}" fill="${color}">' |
| 192 | s += '<title>${b.label}: ${b.count}</title></rect>' |
| 193 | } |
| 194 | label_y := chart_h - 6 |
| 195 | for i, b in buckets { |
| 196 | if i % 5 == 0 || i == buckets.len - 1 { |
| 197 | x := 10 + i * slot + bar_w / 2 |
| 198 | s += '<text class="stat-chart-label" x="${x}" y="${label_y}" text-anchor="middle">${b.label}</text>' |
| 199 | } |
| 200 | } |
| 201 | s += '</svg>' |
| 202 | return veb.RawHtml(s) |
| 203 | } |
| 204 | |