this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

filter #1

open opened by chown.de targeting main from filter
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:x3ni2r3jgdqms5euhzt2qqdr/sh.tangled.repo.pull/3mkvjg7tvto22
+285 -12
Diff #2
+99 -3
store/store.go
··· 10 10 "encoding/json" 11 11 "fmt" 12 12 "io" 13 + "sort" 13 14 "time" 14 15 15 16 "entgo.io/ent/dialect" ··· 395 396 return out, rows.Err() 396 397 } 397 398 398 - // RecentRepos returns repos created since `since`, optionally filtered by 399 - // primary language. Pass limit <= 0 for no limit. Uses raw SQL for the 400 - // scalar star-count subquery and handle JOIN — both awkward in ent. 399 + // PrimaryLanguages returns the sorted set of "primary" languages observed 400 + // across all enriched repos — i.e., the highest-bytes language per repo, 401 + // deduplicated. Used to populate the language filter dropdown. 402 + func (s *Store) PrimaryLanguages(ctx context.Context) ([]string, error) { 403 + rows, err := s.db.QueryContext(ctx, `SELECT languages FROM repos WHERE languages IS NOT NULL AND languages != '' AND languages != '{}'`) 404 + if err != nil { 405 + return nil, err 406 + } 407 + defer rows.Close() 408 + seen := make(map[string]struct{}) 409 + for rows.Next() { 410 + var raw string 411 + if err := rows.Scan(&raw); err != nil { 412 + return nil, err 413 + } 414 + var langs map[string]int64 415 + if err := json.Unmarshal([]byte(raw), &langs); err != nil { 416 + continue 417 + } 418 + var best string 419 + var bestN int64 420 + for k, v := range langs { 421 + if v > bestN { 422 + bestN, best = v, k 423 + } 424 + } 425 + if best != "" { 426 + seen[best] = struct{}{} 427 + } 428 + } 429 + if err := rows.Err(); err != nil { 430 + return nil, err 431 + } 432 + out := make([]string, 0, len(seen)) 433 + for k := range seen { 434 + out = append(out, k) 435 + } 436 + sort.Strings(out) 437 + return out, nil 438 + } 439 + 440 + // ReposFilter narrows the result of RecentRepos. Zero values mean "no 441 + // filter" so callers can mix and match. 442 + type ReposFilter struct { 443 + Language string // primary language match (post-filter in Go over JSON map) 444 + Since time.Time // repos created at or after this time (zero = no time floor) 445 + ForksOnly bool // only rows with non-empty source 446 + NoForks bool // exclude rows with non-empty source 447 + } 448 + 449 + // RecentRepos returns repos matching the filter, sorted newest first. Pass 450 + // limit <= 0 for no limit. Uses raw SQL because the scalar star-count 451 + // subquery and handle LEFT JOIN are awkward to express via ent's query API. 452 + func (s *Store) RecentRepos(ctx context.Context, f ReposFilter, limit int) ([]Repo, error) { 453 + candidateLimit := -1 454 + if limit > 0 { 455 + candidateLimit = limit 456 + if f.Language != "" { 457 + candidateLimit = limit * 5 458 + } 459 + } 460 + forksOnly := 0 461 + if f.ForksOnly { 462 + forksOnly = 1 463 + } 464 + noForks := 0 465 + if f.NoForks { 466 + noForks = 1 467 + } 468 + rows, err := s.db.QueryContext(ctx, ` 469 + SELECT r.at_uri, r.did, r.rkey, r.name, r.knot, r.description, r.topics, 470 + r.website, r.source, r.spindle, r.repo_did, r.created_at, r.seen_at, 471 + 472 + 473 + 474 + LEFT JOIN handles h ON h.did = r.did 475 + WHERE r.created_at >= ? 476 + AND (? = '' OR r.languages IS NOT NULL) 477 + AND (? = 0 OR (r.source IS NOT NULL AND r.source != '')) 478 + AND (? = 0 OR r.source IS NULL OR r.source = '') 479 + ORDER BY r.created_at DESC 480 + LIMIT ? 481 + `, f.Since.UnixMilli(), f.Language, forksOnly, noForks, candidateLimit) 482 + if err != nil { 483 + return nil, err 484 + } 485 + 486 + 487 + 488 + 489 + 490 + if err != nil { 491 + return nil, err 492 + } 493 + if f.Language != "" && r.Primary() != f.Language { 494 + continue 495 + } 496 + out = append(out, r)
+71 -7
web/server.go
··· 27 27 28 28 29 29 30 + Language string 31 + SinceRaw string // empty = no time filter 32 + Threshold time.Time // zero = no time filter 33 + Forks string // "" or "only" 34 + } 35 + 36 + func parseQuery(r *http.Request) query { 37 + q := query{ 38 + Language: r.URL.Query().Get("language"), 39 + SinceRaw: r.URL.Query().Get("since"), 40 + Forks: r.URL.Query().Get("forks"), 41 + } 42 + if q.SinceRaw != "" { 43 + if d, err := time.ParseDuration(q.SinceRaw); err == nil { 30 44 31 45 32 46 47 + return q 48 + } 33 49 50 + func (q query) filter() store.ReposFilter { 51 + return store.ReposFilter{ 52 + Language: q.Language, 53 + Since: q.Threshold, 54 + ForksOnly: q.Forks == "only", 55 + NoForks: q.Forks == "no", 56 + } 57 + } 34 58 59 + func reposJSON(s *store.Store) http.HandlerFunc { 60 + return func(w http.ResponseWriter, r *http.Request) { 61 + q := parseQuery(r) 62 + repos, err := s.RecentRepos(r.Context(), q.filter(), 0) 63 + if err != nil { 64 + http.Error(w, err.Error(), http.StatusInternalServerError) 65 + return 35 66 36 67 37 68 38 69 39 70 40 71 72 + func reposHTML(s *store.Store) http.HandlerFunc { 73 + return func(w http.ResponseWriter, r *http.Request) { 74 + q := parseQuery(r) 75 + repos, err := s.RecentRepos(r.Context(), q.filter(), 0) 76 + if err != nil { 77 + http.Error(w, err.Error(), http.StatusInternalServerError) 78 + return 79 + } 80 + langs, err := s.PrimaryLanguages(r.Context()) 81 + if err != nil { 82 + http.Error(w, err.Error(), http.StatusInternalServerError) 83 + return 84 + } 85 + render(w, "repos", struct { 86 + Active string 87 + Stats Stats 88 + Query query 89 + Repos []store.Repo 90 + Languages []string 91 + }{"repos", collectStats(), q, repos, langs}) 92 + } 93 + } 94 + 95 + 96 + 97 + 98 + 41 99 42 100 101 + } 102 + render(w, "knots", struct { 103 + Active string 104 + Stats Stats 105 + Knots []store.KnotEntry 106 + }{"knots", collectStats(), knots}) 107 + } 108 + } 43 109 44 110 45 111 ··· 63 129 64 130 65 131 66 - http.Error(w, err.Error(), http.StatusInternalServerError) 67 - return 68 132 } 69 - render(w, "repos", struct { 70 - Active string 71 - Query query 72 - Repos []store.Repo 73 - }{"repos", q, repos}) 133 + render(w, "handles", struct { 134 + Active string 135 + Stats Stats 136 + Handles []store.HandleEntry 137 + }{"handles", collectStats(), handles}) 74 138 } 75 139 } 76 140
+33 -1
web/templates/repos.html
··· 1 1 {{define "repos"}}{{template "head" .}} 2 2 <form method="get"> 3 - <label>language<input name="language" value="{{.Query.Language}}" placeholder="any"></label> 3 + <label>language 4 + <select name="language"> 5 + <option value=""{{if eq .Query.Language ""}} selected{{end}}>any</option> 6 + {{range .Languages}} 7 + <option value="{{.}}"{{if eq $.Query.Language .}} selected{{end}}>{{.}}</option> 8 + {{end}} 9 + </select> 10 + </label> 4 11 <label>since<input name="since" value="{{.Query.SinceRaw}}" placeholder="e.g. 24h"></label> 12 + <label>forks 13 + <select name="forks"> 14 + <option value=""{{if eq .Query.Forks ""}} selected{{end}}>any</option> 15 + <option value="only"{{if eq .Query.Forks "only"}} selected{{end}}>only forks</option> 16 + <option value="no"{{if eq .Query.Forks "no"}} selected{{end}}>no forks</option> 17 + </select> 18 + </label> 5 19 <button>filter</button> 6 20 </form> 21 + <p class="summary">{{len .Repos}} repos · <a href="/repos.json?language={{.Query.Language}}&since={{.Query.SinceRaw}}&forks={{.Query.Forks}}">json</a></p> 22 + {{if .Repos}} 23 + <table> 24 + <thead><tr><th>created</th><th>repo</th><th>knot</th><th>language</th><th>★</th><th>description</th></tr></thead> 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + {{- else -}} 33 + {{.DID}}/{{.Name}} 34 + {{- end -}} 35 + {{if .Source}} <span class="fork" title="forked from {{.Source}}">↗</span>{{end}} 36 + </td> 37 + <td class="knot">{{.Knot}}</td> 38 + <td class="lang">{{.Primary}}</td>
+31
web/templates/layout.html
··· 22 22 td.handle { font-weight: 600; } 23 23 td.handle a, td.knot a, .link a { color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; } 24 24 td.handle a:hover, td.knot a:hover, .link a:hover { border-bottom-style: solid; } 25 + .fork { color: #888; cursor: help; margin-left: .25em; } 25 26 td.knot, td.did { color: #888; } 26 27 td.lang { color: #6a6; } 27 28 td.num { text-align: right; font-variant-numeric: tabular-nums; color: #888; } 29 + td.num.has { color: inherit; } 30 + td.desc { color: #aaa; max-width: 480px; word-break: break-word; } 31 + .empty { padding: 3em; text-align: center; color: #888; border: 1px dashed currentColor; opacity: .4; } 32 + footer.stats { margin-top: 2em; padding-top: .5em; border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); color: #888; font-size: 12px; display: flex; gap: 1em; flex-wrap: wrap; } 33 + footer.stats span { white-space: nowrap; } 34 + </style> 35 + </head> 36 + <body> 37 + 38 + 39 + 40 + 41 + 42 + 43 + {{end}} 44 + 45 + {{define "foot"}} 46 + {{with .Stats}} 47 + <footer class="stats"> 48 + <span>uptime {{.Uptime}}</span> 49 + <span>goroutines {{.Goroutines}}</span> 50 + <span>heap {{printf "%.1f" .HeapAllocMB}} MB</span> 51 + <span>rss {{printf "%.1f" .RSSMB}} MB</span> 52 + <span>cpu {{printf "%.1f" .CPUSec}}s</span> 53 + <span>gc {{.GCRuns}}</span> 54 + </footer> 55 + {{end}} 56 + </body> 57 + </html> 58 + {{end}}
+49
web/stats.go
··· 1 + package web 2 + 3 + import ( 4 + "runtime" 5 + "syscall" 6 + "time" 7 + ) 8 + 9 + // Stats is a small snapshot of process-level resource usage rendered into 10 + // every page footer. Pure stdlib — runtime for Go heap, getrusage for OS-level 11 + // RSS and CPU time. 12 + type Stats struct { 13 + Goroutines int 14 + HeapAllocMB float64 // Go heap currently in use 15 + SysMB float64 // total bytes obtained from the OS by the runtime 16 + GCRuns uint32 17 + RSSMB float64 // resident set size (peak since start) 18 + CPUSec float64 // user + system CPU seconds since start 19 + Uptime time.Duration // process uptime, rounded to seconds 20 + } 21 + 22 + var startTime = time.Now() 23 + 24 + func collectStats() Stats { 25 + var ms runtime.MemStats 26 + runtime.ReadMemStats(&ms) 27 + 28 + var ru syscall.Rusage 29 + _ = syscall.Getrusage(syscall.RUSAGE_SELF, &ru) 30 + 31 + // ru.Maxrss is bytes on Darwin/BSD, kilobytes on Linux. 32 + rssBytes := int64(ru.Maxrss) 33 + if runtime.GOOS == "linux" { 34 + rssBytes *= 1024 35 + } 36 + 37 + cpuSec := float64(ru.Utime.Sec) + float64(ru.Utime.Usec)/1e6 + 38 + float64(ru.Stime.Sec) + float64(ru.Stime.Usec)/1e6 39 + 40 + return Stats{ 41 + Goroutines: runtime.NumGoroutine(), 42 + HeapAllocMB: float64(ms.HeapAlloc) / 1024 / 1024, 43 + SysMB: float64(ms.Sys) / 1024 / 1024, 44 + GCRuns: ms.NumGC, 45 + RSSMB: float64(rssBytes) / 1024 / 1024, 46 + CPUSec: cpuSec, 47 + Uptime: time.Since(startTime).Round(time.Second), 48 + } 49 + }
+2 -1
.tangled/workflows/ci.yml
··· 7 7 dependencies: 8 8 nixpkgs: 9 9 - go 10 + - gnumake 10 11 11 12 steps: 12 - - name: regenerate ent client 13 + - name: generate schema files 13 14 command: make generate 14 15 15 16 - name: vet

History

3 rounds 0 comments
sign up or login to add to the discussion
4 commits
expand
feat: filter by language
feat(web): additional filters
feat(web): live stats
fix(ci): install make
merge conflicts detected
expand
  • store/store.go:10
  • web/server.go:66
  • web/templates/repos.html:1
expand 0 comments
3 commits
expand
feat: filter by language
feat: additional filters
feat: live stats
expand 0 comments
chown.de submitted #0
2 commits
expand
feat: filter by language
feat: additional filters
expand 0 comments