Monorepo for Tangled
at 34d8a513e4512bc8b1443478f390319282cb6cbe 1552 lines 40 kB view raw
1package pages 2 3import ( 4 "crypto/sha256" 5 "embed" 6 "encoding/hex" 7 "fmt" 8 "html/template" 9 "io" 10 "io/fs" 11 "log/slog" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strings" 16 "sync" 17 "time" 18 19 "tangled.org/core/api/tangled" 20 "tangled.org/core/appview/commitverify" 21 "tangled.org/core/appview/config" 22 "tangled.org/core/appview/db" 23 "tangled.org/core/appview/models" 24 "tangled.org/core/appview/oauth" 25 "tangled.org/core/appview/pages/markup" 26 "tangled.org/core/appview/pages/repoinfo" 27 "tangled.org/core/appview/pagination" 28 "tangled.org/core/idresolver" 29 "tangled.org/core/patchutil" 30 "tangled.org/core/types" 31 32 "github.com/bluesky-social/indigo/atproto/identity" 33 "github.com/bluesky-social/indigo/atproto/syntax" 34 "github.com/go-git/go-git/v5/plumbing" 35) 36 37//go:embed templates/* static legal 38var Files embed.FS 39 40type Pages struct { 41 mu sync.RWMutex 42 cache *TmplCache[string, *template.Template] 43 44 avatar config.AvatarConfig 45 resolver *idresolver.Resolver 46 db *db.DB 47 dev bool 48 embedFS fs.FS 49 templateDir string // Path to templates on disk for dev mode 50 rctx *markup.RenderContext 51 logger *slog.Logger 52} 53 54func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, logger *slog.Logger) *Pages { 55 // initialized with safe defaults, can be overriden per use 56 rctx := &markup.RenderContext{ 57 IsDev: config.Core.Dev, 58 Hostname: config.Core.AppviewHost, 59 CamoUrl: config.Camo.Host, 60 CamoSecret: config.Camo.SharedSecret, 61 Sanitizer: markup.NewSanitizer(), 62 Files: Files, 63 } 64 65 p := &Pages{ 66 mu: sync.RWMutex{}, 67 cache: NewTmplCache[string, *template.Template](), 68 dev: config.Core.Dev, 69 avatar: config.Avatar, 70 rctx: rctx, 71 resolver: res, 72 db: database, 73 templateDir: "appview/pages", 74 logger: logger, 75 } 76 77 if p.dev { 78 p.embedFS = os.DirFS(p.templateDir) 79 } else { 80 p.embedFS = Files 81 } 82 83 return p 84} 85 86// reverse of pathToName 87func (p *Pages) nameToPath(s string) string { 88 return "templates/" + s + ".html" 89} 90 91func (p *Pages) fragmentPaths() ([]string, error) { 92 var fragmentPaths []string 93 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 94 if err != nil { 95 return err 96 } 97 if d.IsDir() { 98 return nil 99 } 100 if !strings.HasSuffix(path, ".html") { 101 return nil 102 } 103 if !strings.Contains(path, "fragments/") { 104 return nil 105 } 106 fragmentPaths = append(fragmentPaths, path) 107 return nil 108 }) 109 if err != nil { 110 return nil, err 111 } 112 113 return fragmentPaths, nil 114} 115 116// parse without memoization 117func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 118 paths, err := p.fragmentPaths() 119 if err != nil { 120 return nil, err 121 } 122 for _, s := range stack { 123 paths = append(paths, p.nameToPath(s)) 124 } 125 126 funcs := p.funcMap() 127 top := stack[len(stack)-1] 128 parsed, err := template.New(top). 129 Funcs(funcs). 130 ParseFS(p.embedFS, paths...) 131 if err != nil { 132 return nil, err 133 } 134 135 return parsed, nil 136} 137 138func (p *Pages) parse(stack ...string) (*template.Template, error) { 139 key := strings.Join(stack, "|") 140 141 // never cache in dev mode 142 if cached, exists := p.cache.Get(key); !p.dev && exists { 143 return cached, nil 144 } 145 146 result, err := p.rawParse(stack...) 147 if err != nil { 148 return nil, err 149 } 150 151 p.cache.Set(key, result) 152 return result, nil 153} 154 155func (p *Pages) parseBase(top string) (*template.Template, error) { 156 stack := []string{ 157 "layouts/base", 158 top, 159 } 160 return p.parse(stack...) 161} 162 163func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 164 stack := []string{ 165 "layouts/base", 166 "layouts/repobase", 167 top, 168 } 169 return p.parse(stack...) 170} 171 172func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 173 stack := []string{ 174 "layouts/base", 175 "layouts/profilebase", 176 top, 177 } 178 return p.parse(stack...) 179} 180 181func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 182 stack := []string{ 183 "layouts/base", 184 "layouts/loginbase", 185 top, 186 } 187 return p.parse(stack...) 188} 189 190func (p *Pages) executePlain(name string, w io.Writer, params any) error { 191 tpl, err := p.parse(name) 192 if err != nil { 193 return err 194 } 195 196 return tpl.Execute(w, params) 197} 198 199func (p *Pages) executeLogin(name string, w io.Writer, params any) error { 200 tpl, err := p.parseLoginBase(name) 201 if err != nil { 202 return err 203 } 204 205 return tpl.ExecuteTemplate(w, "layouts/base", params) 206} 207 208func (p *Pages) execute(name string, w io.Writer, params any) error { 209 tpl, err := p.parseBase(name) 210 if err != nil { 211 return err 212 } 213 214 return tpl.ExecuteTemplate(w, "layouts/base", params) 215} 216 217func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 218 tpl, err := p.parseRepoBase(name) 219 if err != nil { 220 return err 221 } 222 223 return tpl.ExecuteTemplate(w, "layouts/base", params) 224} 225 226func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 227 tpl, err := p.parseProfileBase(name) 228 if err != nil { 229 return err 230 } 231 232 return tpl.ExecuteTemplate(w, "layouts/base", params) 233} 234 235type DollyParams struct { 236 Classes string 237 FillColor string 238} 239 240func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 241 return p.executePlain("fragments/dolly/logo", w, params) 242} 243 244func (p *Pages) Favicon(w io.Writer) error { 245 return p.Dolly(w, DollyParams{ 246 Classes: "text-black dark:text-white", 247 }) 248} 249 250type LoginParams struct { 251 ReturnUrl string 252 ErrorCode string 253 AddAccount bool 254 LoggedInUser *oauth.MultiAccountUser 255} 256 257func (p *Pages) Login(w io.Writer, params LoginParams) error { 258 return p.executeLogin("user/login", w, params) 259} 260 261type SignupParams struct { 262 CloudflareSiteKey string 263} 264 265func (p *Pages) Signup(w io.Writer, params SignupParams) error { 266 return p.executeLogin("user/signup", w, params) 267} 268 269func (p *Pages) CompleteSignup(w io.Writer) error { 270 return p.executeLogin("user/completeSignup", w, nil) 271} 272 273type TermsOfServiceParams struct { 274 LoggedInUser *oauth.MultiAccountUser 275 Content template.HTML 276} 277 278func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 279 filename := "terms.md" 280 filePath := filepath.Join("legal", filename) 281 282 file, err := p.embedFS.Open(filePath) 283 if err != nil { 284 return fmt.Errorf("failed to read %s: %w", filename, err) 285 } 286 defer file.Close() 287 288 markdownBytes, err := io.ReadAll(file) 289 if err != nil { 290 return fmt.Errorf("failed to read %s: %w", filename, err) 291 } 292 293 p.rctx.RendererType = markup.RendererTypeDefault 294 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 295 sanitized := p.rctx.SanitizeDefault(htmlString) 296 params.Content = template.HTML(sanitized) 297 298 return p.execute("legal/terms", w, params) 299} 300 301type PrivacyPolicyParams struct { 302 LoggedInUser *oauth.MultiAccountUser 303 Content template.HTML 304} 305 306func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 307 filename := "privacy.md" 308 filePath := filepath.Join("legal", filename) 309 310 file, err := p.embedFS.Open(filePath) 311 if err != nil { 312 return fmt.Errorf("failed to read %s: %w", filename, err) 313 } 314 defer file.Close() 315 316 markdownBytes, err := io.ReadAll(file) 317 if err != nil { 318 return fmt.Errorf("failed to read %s: %w", filename, err) 319 } 320 321 p.rctx.RendererType = markup.RendererTypeDefault 322 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 323 sanitized := p.rctx.SanitizeDefault(htmlString) 324 params.Content = template.HTML(sanitized) 325 326 return p.execute("legal/privacy", w, params) 327} 328 329type BrandParams struct { 330 LoggedInUser *oauth.MultiAccountUser 331} 332 333func (p *Pages) Brand(w io.Writer, params BrandParams) error { 334 return p.execute("brand/brand", w, params) 335} 336 337type TimelineParams struct { 338 LoggedInUser *oauth.MultiAccountUser 339 Timeline []models.TimelineEvent 340 Repos []models.Repo 341 GfiLabel *models.LabelDefinition 342} 343 344func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 345 return p.execute("timeline/timeline", w, params) 346} 347 348type GoodFirstIssuesParams struct { 349 LoggedInUser *oauth.MultiAccountUser 350 Issues []models.Issue 351 RepoGroups []*models.RepoGroup 352 LabelDefs map[string]*models.LabelDefinition 353 GfiLabel *models.LabelDefinition 354 Page pagination.Page 355} 356 357func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 358 return p.execute("goodfirstissues/index", w, params) 359} 360 361type UserProfileSettingsParams struct { 362 LoggedInUser *oauth.MultiAccountUser 363 Tab string 364 PunchcardPreference models.PunchcardPreference 365} 366 367func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 368 params.Tab = "profile" 369 return p.execute("user/settings/profile", w, params) 370} 371 372type NotificationsParams struct { 373 LoggedInUser *oauth.MultiAccountUser 374 Notifications []*models.NotificationWithEntity 375 UnreadCount int 376 Page pagination.Page 377 Total int64 378} 379 380func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 381 return p.execute("notifications/list", w, params) 382} 383 384type NotificationItemParams struct { 385 Notification *models.Notification 386} 387 388func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 389 return p.executePlain("notifications/fragments/item", w, params) 390} 391 392type NotificationCountParams struct { 393 Count int64 394} 395 396func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 397 return p.executePlain("notifications/fragments/count", w, params) 398} 399 400type UserKeysSettingsParams struct { 401 LoggedInUser *oauth.MultiAccountUser 402 PubKeys []models.PublicKey 403 Tab string 404} 405 406func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 407 params.Tab = "keys" 408 return p.execute("user/settings/keys", w, params) 409} 410 411type UserEmailsSettingsParams struct { 412 LoggedInUser *oauth.MultiAccountUser 413 Emails []models.Email 414 Tab string 415} 416 417func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 418 params.Tab = "emails" 419 return p.execute("user/settings/emails", w, params) 420} 421 422type UserNotificationSettingsParams struct { 423 LoggedInUser *oauth.MultiAccountUser 424 Preferences *models.NotificationPreferences 425 Tab string 426} 427 428func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 429 params.Tab = "notifications" 430 return p.execute("user/settings/notifications", w, params) 431} 432 433type UpgradeBannerParams struct { 434 Registrations []models.Registration 435 Spindles []models.Spindle 436} 437 438func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 439 return p.executePlain("banner", w, params) 440} 441 442type KnotsParams struct { 443 LoggedInUser *oauth.MultiAccountUser 444 Registrations []models.Registration 445 Tab string 446} 447 448func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 449 params.Tab = "knots" 450 return p.execute("knots/index", w, params) 451} 452 453type KnotParams struct { 454 LoggedInUser *oauth.MultiAccountUser 455 Registration *models.Registration 456 Members []string 457 Repos map[string][]models.Repo 458 IsOwner bool 459 Tab string 460} 461 462func (p *Pages) Knot(w io.Writer, params KnotParams) error { 463 return p.execute("knots/dashboard", w, params) 464} 465 466type KnotListingParams struct { 467 *models.Registration 468} 469 470func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 471 return p.executePlain("knots/fragments/knotListing", w, params) 472} 473 474type SpindlesParams struct { 475 LoggedInUser *oauth.MultiAccountUser 476 Spindles []models.Spindle 477 Tab string 478} 479 480func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 481 params.Tab = "spindles" 482 return p.execute("spindles/index", w, params) 483} 484 485type SpindleListingParams struct { 486 models.Spindle 487 Tab string 488} 489 490func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 491 return p.executePlain("spindles/fragments/spindleListing", w, params) 492} 493 494type SpindleDashboardParams struct { 495 LoggedInUser *oauth.MultiAccountUser 496 Spindle models.Spindle 497 Members []string 498 Repos map[string][]models.Repo 499 Tab string 500} 501 502func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 503 return p.execute("spindles/dashboard", w, params) 504} 505 506type NewRepoParams struct { 507 LoggedInUser *oauth.MultiAccountUser 508 Knots []string 509} 510 511func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 512 return p.execute("repo/new", w, params) 513} 514 515type ForkRepoParams struct { 516 LoggedInUser *oauth.MultiAccountUser 517 Knots []string 518 RepoInfo repoinfo.RepoInfo 519} 520 521func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 522 return p.execute("repo/fork", w, params) 523} 524 525type ProfileCard struct { 526 UserDid string 527 HasProfile bool 528 FollowStatus models.FollowStatus 529 Punchcard *models.Punchcard 530 Profile *models.Profile 531 Stats ProfileStats 532 Active string 533} 534 535type ProfileStats struct { 536 RepoCount int64 537 StarredCount int64 538 StringCount int64 539 FollowersCount int64 540 FollowingCount int64 541} 542 543func (p *ProfileCard) GetTabs() [][]any { 544 tabs := [][]any{ 545 {"overview", "overview", "square-chart-gantt", nil}, 546 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 547 {"starred", "starred", "star", p.Stats.StarredCount}, 548 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 549 } 550 551 return tabs 552} 553 554type ProfileOverviewParams struct { 555 LoggedInUser *oauth.MultiAccountUser 556 Repos []models.Repo 557 CollaboratingRepos []models.Repo 558 ProfileTimeline *models.ProfileTimeline 559 Card *ProfileCard 560 Active string 561 ShowPunchcard bool 562} 563 564func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 565 params.Active = "overview" 566 return p.executeProfile("user/overview", w, params) 567} 568 569type ProfileReposParams struct { 570 LoggedInUser *oauth.MultiAccountUser 571 Repos []models.Repo 572 Card *ProfileCard 573 Active string 574} 575 576func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 577 params.Active = "repos" 578 return p.executeProfile("user/repos", w, params) 579} 580 581type ProfileStarredParams struct { 582 LoggedInUser *oauth.MultiAccountUser 583 Repos []models.Repo 584 Card *ProfileCard 585 Active string 586} 587 588func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 589 params.Active = "starred" 590 return p.executeProfile("user/starred", w, params) 591} 592 593type ProfileStringsParams struct { 594 LoggedInUser *oauth.MultiAccountUser 595 Strings []models.String 596 Card *ProfileCard 597 Active string 598} 599 600func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 601 params.Active = "strings" 602 return p.executeProfile("user/strings", w, params) 603} 604 605type FollowCard struct { 606 UserDid string 607 LoggedInUser *oauth.MultiAccountUser 608 FollowStatus models.FollowStatus 609 FollowersCount int64 610 FollowingCount int64 611 Profile *models.Profile 612} 613 614type ProfileFollowersParams struct { 615 LoggedInUser *oauth.MultiAccountUser 616 Followers []FollowCard 617 Card *ProfileCard 618 Active string 619} 620 621func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 622 params.Active = "overview" 623 return p.executeProfile("user/followers", w, params) 624} 625 626type ProfileFollowingParams struct { 627 LoggedInUser *oauth.MultiAccountUser 628 Following []FollowCard 629 Card *ProfileCard 630 Active string 631} 632 633func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 634 params.Active = "overview" 635 return p.executeProfile("user/following", w, params) 636} 637 638type FollowFragmentParams struct { 639 UserDid string 640 FollowStatus models.FollowStatus 641 FollowersCount int64 642} 643 644func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 645 return p.executePlain("user/fragments/follow-oob", w, params) 646} 647 648type EditBioParams struct { 649 LoggedInUser *oauth.MultiAccountUser 650 Profile *models.Profile 651} 652 653func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 654 return p.executePlain("user/fragments/editBio", w, params) 655} 656 657type EditPinsParams struct { 658 LoggedInUser *oauth.MultiAccountUser 659 Profile *models.Profile 660 AllRepos []PinnedRepo 661} 662 663type PinnedRepo struct { 664 IsPinned bool 665 models.Repo 666} 667 668func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 669 return p.executePlain("user/fragments/editPins", w, params) 670} 671 672type StarBtnFragmentParams struct { 673 IsStarred bool 674 SubjectAt syntax.ATURI 675 StarCount int 676 HxSwapOob bool 677} 678 679func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 680 params.HxSwapOob = true 681 return p.executePlain("fragments/starBtn", w, params) 682} 683 684type RepoIndexParams struct { 685 LoggedInUser *oauth.MultiAccountUser 686 RepoInfo repoinfo.RepoInfo 687 Active string 688 TagMap map[string][]string 689 CommitsTrunc []types.Commit 690 TagsTrunc []*types.TagReference 691 BranchesTrunc []types.Branch 692 // ForkInfo *types.ForkInfo 693 HTMLReadme template.HTML 694 Raw bool 695 EmailToDid map[string]string 696 VerifiedCommits commitverify.VerifiedCommits 697 Languages []types.RepoLanguageDetails 698 Pipelines map[string]models.Pipeline 699 NeedsKnotUpgrade bool 700 types.RepoIndexResponse 701} 702 703func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 704 params.Active = "overview" 705 if params.IsEmpty { 706 return p.executeRepo("repo/empty", w, params) 707 } 708 709 if params.NeedsKnotUpgrade { 710 return p.executeRepo("repo/needsUpgrade", w, params) 711 } 712 713 p.rctx.RepoInfo = params.RepoInfo 714 p.rctx.RepoInfo.Ref = params.Ref 715 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 716 717 if params.ReadmeFileName != "" { 718 ext := filepath.Ext(params.ReadmeFileName) 719 switch ext { 720 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 721 params.Raw = false 722 htmlString := p.rctx.RenderMarkdown(params.Readme) 723 sanitized := p.rctx.SanitizeDefault(htmlString) 724 params.HTMLReadme = template.HTML(sanitized) 725 default: 726 params.Raw = true 727 } 728 } 729 730 return p.executeRepo("repo/index", w, params) 731} 732 733type RepoLogParams struct { 734 LoggedInUser *oauth.MultiAccountUser 735 RepoInfo repoinfo.RepoInfo 736 TagMap map[string][]string 737 Active string 738 EmailToDid map[string]string 739 VerifiedCommits commitverify.VerifiedCommits 740 Pipelines map[string]models.Pipeline 741 742 types.RepoLogResponse 743} 744 745func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 746 params.Active = "overview" 747 return p.executeRepo("repo/log", w, params) 748} 749 750type RepoCommitParams struct { 751 LoggedInUser *oauth.MultiAccountUser 752 RepoInfo repoinfo.RepoInfo 753 Active string 754 EmailToDid map[string]string 755 Pipeline *models.Pipeline 756 DiffOpts types.DiffOpts 757 758 // singular because it's always going to be just one 759 VerifiedCommit commitverify.VerifiedCommits 760 761 types.RepoCommitResponse 762} 763 764func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 765 params.Active = "overview" 766 return p.executeRepo("repo/commit", w, params) 767} 768 769type RepoTreeParams struct { 770 LoggedInUser *oauth.MultiAccountUser 771 RepoInfo repoinfo.RepoInfo 772 Active string 773 BreadCrumbs [][]string 774 TreePath string 775 Raw bool 776 HTMLReadme template.HTML 777 EmailToDid map[string]string 778 LastCommitInfo *types.LastCommitInfo 779 types.RepoTreeResponse 780} 781 782type RepoTreeStats struct { 783 NumFolders uint64 784 NumFiles uint64 785} 786 787func (r RepoTreeParams) TreeStats() RepoTreeStats { 788 numFolders, numFiles := 0, 0 789 for _, f := range r.Files { 790 if !f.IsFile() { 791 numFolders += 1 792 } else if f.IsFile() { 793 numFiles += 1 794 } 795 } 796 797 return RepoTreeStats{ 798 NumFolders: uint64(numFolders), 799 NumFiles: uint64(numFiles), 800 } 801} 802 803func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 804 params.Active = "overview" 805 806 p.rctx.RepoInfo = params.RepoInfo 807 p.rctx.RepoInfo.Ref = params.Ref 808 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 809 810 if params.ReadmeFileName != "" { 811 ext := filepath.Ext(params.ReadmeFileName) 812 switch ext { 813 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 814 params.Raw = false 815 htmlString := p.rctx.RenderMarkdown(params.Readme) 816 sanitized := p.rctx.SanitizeDefault(htmlString) 817 params.HTMLReadme = template.HTML(sanitized) 818 default: 819 params.Raw = true 820 } 821 } 822 823 return p.executeRepo("repo/tree", w, params) 824} 825 826type RepoBranchesParams struct { 827 LoggedInUser *oauth.MultiAccountUser 828 RepoInfo repoinfo.RepoInfo 829 Active string 830 types.RepoBranchesResponse 831} 832 833func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 834 params.Active = "overview" 835 return p.executeRepo("repo/branches", w, params) 836} 837 838type RepoTagsParams struct { 839 LoggedInUser *oauth.MultiAccountUser 840 RepoInfo repoinfo.RepoInfo 841 Active string 842 types.RepoTagsResponse 843 ArtifactMap map[plumbing.Hash][]models.Artifact 844 DanglingArtifacts []models.Artifact 845} 846 847func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 848 params.Active = "overview" 849 return p.executeRepo("repo/tags", w, params) 850} 851 852type RepoTagParams struct { 853 LoggedInUser *oauth.MultiAccountUser 854 RepoInfo repoinfo.RepoInfo 855 Active string 856 types.RepoTagResponse 857 ArtifactMap map[plumbing.Hash][]models.Artifact 858 DanglingArtifacts []models.Artifact 859} 860 861func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 862 params.Active = "overview" 863 return p.executeRepo("repo/tag", w, params) 864} 865 866type RepoArtifactParams struct { 867 LoggedInUser *oauth.MultiAccountUser 868 RepoInfo repoinfo.RepoInfo 869 Artifact models.Artifact 870} 871 872func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 873 return p.executePlain("repo/fragments/artifact", w, params) 874} 875 876type RepoBlobParams struct { 877 LoggedInUser *oauth.MultiAccountUser 878 RepoInfo repoinfo.RepoInfo 879 Active string 880 BreadCrumbs [][]string 881 BlobView models.BlobView 882 EmailToDid map[string]string 883 LastCommitInfo *types.LastCommitInfo 884 *tangled.RepoBlob_Output 885} 886 887func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 888 switch params.BlobView.ContentType { 889 case models.BlobContentTypeMarkup: 890 p.rctx.RepoInfo = params.RepoInfo 891 } 892 893 params.Active = "overview" 894 return p.executeRepo("repo/blob", w, params) 895} 896 897type Collaborator struct { 898 Did string 899 Role string 900} 901 902type RepoSettingsParams struct { 903 LoggedInUser *oauth.MultiAccountUser 904 RepoInfo repoinfo.RepoInfo 905 Collaborators []Collaborator 906 Active string 907 Branches []types.Branch 908 Spindles []string 909 CurrentSpindle string 910 Secrets []*tangled.RepoListSecrets_Secret 911 912 // TODO: use repoinfo.roles 913 IsCollaboratorInviteAllowed bool 914} 915 916func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 917 params.Active = "settings" 918 return p.executeRepo("repo/settings", w, params) 919} 920 921type RepoGeneralSettingsParams struct { 922 LoggedInUser *oauth.MultiAccountUser 923 RepoInfo repoinfo.RepoInfo 924 Labels []models.LabelDefinition 925 DefaultLabels []models.LabelDefinition 926 SubscribedLabels map[string]struct{} 927 ShouldSubscribeAll bool 928 Active string 929 Tab string 930 Branches []types.Branch 931} 932 933func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 934 params.Active = "settings" 935 params.Tab = "general" 936 return p.executeRepo("repo/settings/general", w, params) 937} 938 939type RepoAccessSettingsParams struct { 940 LoggedInUser *oauth.MultiAccountUser 941 RepoInfo repoinfo.RepoInfo 942 Active string 943 Tab string 944 Collaborators []Collaborator 945} 946 947func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 948 params.Active = "settings" 949 params.Tab = "access" 950 return p.executeRepo("repo/settings/access", w, params) 951} 952 953type RepoPipelineSettingsParams struct { 954 LoggedInUser *oauth.MultiAccountUser 955 RepoInfo repoinfo.RepoInfo 956 Active string 957 Tab string 958 Spindles []string 959 CurrentSpindle string 960 Secrets []map[string]any 961} 962 963func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 964 params.Active = "settings" 965 params.Tab = "pipelines" 966 return p.executeRepo("repo/settings/pipelines", w, params) 967} 968 969type RepoWebhooksSettingsParams struct { 970 LoggedInUser *oauth.MultiAccountUser 971 RepoInfo repoinfo.RepoInfo 972 Active string 973 Tab string 974 Webhooks []models.Webhook 975 WebhookDeliveries map[int64][]models.WebhookDelivery 976} 977 978func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 979 params.Active = "settings" 980 params.Tab = "hooks" 981 return p.executeRepo("repo/settings/hooks", w, params) 982} 983 984type WebhookDeliveriesListParams struct { 985 LoggedInUser *oauth.MultiAccountUser 986 RepoInfo repoinfo.RepoInfo 987 Webhook *models.Webhook 988 Deliveries []models.WebhookDelivery 989} 990 991func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 992 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 993 if err != nil { 994 return err 995 } 996 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 997} 998 999type RepoIssuesParams struct { 1000 LoggedInUser *oauth.MultiAccountUser 1001 RepoInfo repoinfo.RepoInfo 1002 Active string 1003 Issues []models.Issue 1004 IssueCount int 1005 LabelDefs map[string]*models.LabelDefinition 1006 Page pagination.Page 1007 FilterState string 1008 FilterQuery string 1009} 1010 1011func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1012 params.Active = "issues" 1013 return p.executeRepo("repo/issues/issues", w, params) 1014} 1015 1016type RepoSingleIssueParams struct { 1017 LoggedInUser *oauth.MultiAccountUser 1018 RepoInfo repoinfo.RepoInfo 1019 Active string 1020 Issue *models.Issue 1021 CommentList []models.CommentListItem 1022 Backlinks []models.RichReferenceLink 1023 LabelDefs map[string]*models.LabelDefinition 1024 1025 Reactions map[models.ReactionKind]models.ReactionDisplayData 1026 UserReacted map[models.ReactionKind]bool 1027} 1028 1029func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1030 params.Active = "issues" 1031 return p.executeRepo("repo/issues/issue", w, params) 1032} 1033 1034type EditIssueParams struct { 1035 LoggedInUser *oauth.MultiAccountUser 1036 RepoInfo repoinfo.RepoInfo 1037 Issue *models.Issue 1038 Action string 1039} 1040 1041func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1042 params.Action = "edit" 1043 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1044} 1045 1046type ThreadReactionFragmentParams struct { 1047 ThreadAt syntax.ATURI 1048 Kind models.ReactionKind 1049 Count int 1050 Users []string 1051 IsReacted bool 1052} 1053 1054func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1055 return p.executePlain("repo/fragments/reaction", w, params) 1056} 1057 1058type RepoNewIssueParams struct { 1059 LoggedInUser *oauth.MultiAccountUser 1060 RepoInfo repoinfo.RepoInfo 1061 Issue *models.Issue // existing issue if any -- passed when editing 1062 Active string 1063 Action string 1064} 1065 1066func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1067 params.Active = "issues" 1068 params.Action = "create" 1069 return p.executeRepo("repo/issues/new", w, params) 1070} 1071 1072type EditIssueCommentParams struct { 1073 LoggedInUser *oauth.MultiAccountUser 1074 RepoInfo repoinfo.RepoInfo 1075 Issue *models.Issue 1076 Comment *models.IssueComment 1077} 1078 1079func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1080 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1081} 1082 1083type ReplyIssueCommentPlaceholderParams struct { 1084 LoggedInUser *oauth.MultiAccountUser 1085 RepoInfo repoinfo.RepoInfo 1086 Issue *models.Issue 1087 Comment *models.IssueComment 1088} 1089 1090func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1091 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1092} 1093 1094type ReplyIssueCommentParams struct { 1095 LoggedInUser *oauth.MultiAccountUser 1096 RepoInfo repoinfo.RepoInfo 1097 Issue *models.Issue 1098 Comment *models.IssueComment 1099} 1100 1101func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1102 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1103} 1104 1105type IssueCommentBodyParams struct { 1106 LoggedInUser *oauth.MultiAccountUser 1107 RepoInfo repoinfo.RepoInfo 1108 Issue *models.Issue 1109 Comment *models.IssueComment 1110} 1111 1112func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1113 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1114} 1115 1116type RepoNewPullParams struct { 1117 LoggedInUser *oauth.MultiAccountUser 1118 RepoInfo repoinfo.RepoInfo 1119 Branches []types.Branch 1120 Strategy string 1121 SourceBranch string 1122 TargetBranch string 1123 Title string 1124 Body string 1125 Active string 1126} 1127 1128func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1129 params.Active = "pulls" 1130 return p.executeRepo("repo/pulls/new", w, params) 1131} 1132 1133type RepoPullsParams struct { 1134 LoggedInUser *oauth.MultiAccountUser 1135 RepoInfo repoinfo.RepoInfo 1136 Pulls []*models.Pull 1137 Active string 1138 FilterState string 1139 FilterQuery string 1140 Stacks map[string]models.Stack 1141 Pipelines map[string]models.Pipeline 1142 LabelDefs map[string]*models.LabelDefinition 1143 Page pagination.Page 1144 PullCount int 1145} 1146 1147func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1148 params.Active = "pulls" 1149 return p.executeRepo("repo/pulls/pulls", w, params) 1150} 1151 1152type ResubmitResult uint64 1153 1154const ( 1155 ShouldResubmit ResubmitResult = iota 1156 ShouldNotResubmit 1157 Unknown 1158) 1159 1160func (r ResubmitResult) Yes() bool { 1161 return r == ShouldResubmit 1162} 1163func (r ResubmitResult) No() bool { 1164 return r == ShouldNotResubmit 1165} 1166func (r ResubmitResult) Unknown() bool { 1167 return r == Unknown 1168} 1169 1170type RepoSinglePullParams struct { 1171 LoggedInUser *oauth.MultiAccountUser 1172 RepoInfo repoinfo.RepoInfo 1173 Active string 1174 Pull *models.Pull 1175 Stack models.Stack 1176 AbandonedPulls []*models.Pull 1177 Backlinks []models.RichReferenceLink 1178 BranchDeleteStatus *models.BranchDeleteStatus 1179 MergeCheck types.MergeCheckResponse 1180 ResubmitCheck ResubmitResult 1181 Pipelines map[string]models.Pipeline 1182 Diff types.DiffRenderer 1183 DiffOpts types.DiffOpts 1184 ActiveRound int 1185 IsInterdiff bool 1186 1187 Reactions map[models.ReactionKind]models.ReactionDisplayData 1188 UserReacted map[models.ReactionKind]bool 1189 1190 LabelDefs map[string]*models.LabelDefinition 1191} 1192 1193func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1194 params.Active = "pulls" 1195 return p.executeRepo("repo/pulls/pull", w, params) 1196} 1197 1198type RepoPullPatchParams struct { 1199 LoggedInUser *oauth.MultiAccountUser 1200 RepoInfo repoinfo.RepoInfo 1201 Pull *models.Pull 1202 Stack models.Stack 1203 Diff *types.NiceDiff 1204 Round int 1205 Submission *models.PullSubmission 1206 DiffOpts types.DiffOpts 1207} 1208 1209// this name is a mouthful 1210func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1211 return p.execute("repo/pulls/patch", w, params) 1212} 1213 1214type RepoPullInterdiffParams struct { 1215 LoggedInUser *oauth.MultiAccountUser 1216 RepoInfo repoinfo.RepoInfo 1217 Pull *models.Pull 1218 Round int 1219 Interdiff *patchutil.InterdiffResult 1220 DiffOpts types.DiffOpts 1221} 1222 1223// this name is a mouthful 1224func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1225 return p.execute("repo/pulls/interdiff", w, params) 1226} 1227 1228type PullPatchUploadParams struct { 1229 RepoInfo repoinfo.RepoInfo 1230} 1231 1232func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1233 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1234} 1235 1236type PullCompareBranchesParams struct { 1237 RepoInfo repoinfo.RepoInfo 1238 Branches []types.Branch 1239 SourceBranch string 1240} 1241 1242func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1243 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1244} 1245 1246type PullCompareForkParams struct { 1247 RepoInfo repoinfo.RepoInfo 1248 Forks []models.Repo 1249 Selected string 1250} 1251 1252func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1253 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1254} 1255 1256type PullCompareForkBranchesParams struct { 1257 RepoInfo repoinfo.RepoInfo 1258 SourceBranches []types.Branch 1259 TargetBranches []types.Branch 1260} 1261 1262func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1263 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1264} 1265 1266type PullResubmitParams struct { 1267 LoggedInUser *oauth.MultiAccountUser 1268 RepoInfo repoinfo.RepoInfo 1269 Pull *models.Pull 1270 SubmissionId int 1271} 1272 1273func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1274 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1275} 1276 1277type PullActionsParams struct { 1278 LoggedInUser *oauth.MultiAccountUser 1279 RepoInfo repoinfo.RepoInfo 1280 Pull *models.Pull 1281 RoundNumber int 1282 MergeCheck types.MergeCheckResponse 1283 ResubmitCheck ResubmitResult 1284 BranchDeleteStatus *models.BranchDeleteStatus 1285 Stack models.Stack 1286} 1287 1288func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1289 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1290} 1291 1292type PullNewCommentParams struct { 1293 LoggedInUser *oauth.MultiAccountUser 1294 RepoInfo repoinfo.RepoInfo 1295 Pull *models.Pull 1296 RoundNumber int 1297} 1298 1299func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1300 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1301} 1302 1303type RepoCompareParams struct { 1304 LoggedInUser *oauth.MultiAccountUser 1305 RepoInfo repoinfo.RepoInfo 1306 Forks []models.Repo 1307 Branches []types.Branch 1308 Tags []*types.TagReference 1309 Base string 1310 Head string 1311 Diff *types.NiceDiff 1312 DiffOpts types.DiffOpts 1313 1314 Active string 1315} 1316 1317func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1318 params.Active = "overview" 1319 return p.executeRepo("repo/compare/compare", w, params) 1320} 1321 1322type RepoCompareNewParams struct { 1323 LoggedInUser *oauth.MultiAccountUser 1324 RepoInfo repoinfo.RepoInfo 1325 Forks []models.Repo 1326 Branches []types.Branch 1327 Tags []*types.TagReference 1328 Base string 1329 Head string 1330 1331 Active string 1332} 1333 1334func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1335 params.Active = "overview" 1336 return p.executeRepo("repo/compare/new", w, params) 1337} 1338 1339type RepoCompareAllowPullParams struct { 1340 LoggedInUser *oauth.MultiAccountUser 1341 RepoInfo repoinfo.RepoInfo 1342 Base string 1343 Head string 1344} 1345 1346func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1347 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1348} 1349 1350type RepoCompareDiffFragmentParams struct { 1351 Diff types.NiceDiff 1352 DiffOpts types.DiffOpts 1353} 1354 1355func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1356 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1357} 1358 1359type LabelPanelParams struct { 1360 LoggedInUser *oauth.MultiAccountUser 1361 RepoInfo repoinfo.RepoInfo 1362 Defs map[string]*models.LabelDefinition 1363 Subject string 1364 State models.LabelState 1365} 1366 1367func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1368 return p.executePlain("repo/fragments/labelPanel", w, params) 1369} 1370 1371type EditLabelPanelParams struct { 1372 LoggedInUser *oauth.MultiAccountUser 1373 RepoInfo repoinfo.RepoInfo 1374 Defs map[string]*models.LabelDefinition 1375 Subject string 1376 State models.LabelState 1377} 1378 1379func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1380 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1381} 1382 1383type PipelinesParams struct { 1384 LoggedInUser *oauth.MultiAccountUser 1385 RepoInfo repoinfo.RepoInfo 1386 Pipelines []models.Pipeline 1387 Active string 1388 FilterKind string 1389 Total int64 1390} 1391 1392func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1393 params.Active = "pipelines" 1394 return p.executeRepo("repo/pipelines/pipelines", w, params) 1395} 1396 1397type LogBlockParams struct { 1398 Id int 1399 Name string 1400 Command string 1401 Collapsed bool 1402 StartTime time.Time 1403} 1404 1405func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1406 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1407} 1408 1409type LogBlockEndParams struct { 1410 Id int 1411 StartTime time.Time 1412 EndTime time.Time 1413} 1414 1415func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1416 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1417} 1418 1419type LogLineParams struct { 1420 Id int 1421 Content string 1422} 1423 1424func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1425 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1426} 1427 1428type WorkflowParams struct { 1429 LoggedInUser *oauth.MultiAccountUser 1430 RepoInfo repoinfo.RepoInfo 1431 Pipeline models.Pipeline 1432 Workflow string 1433 LogUrl string 1434 Active string 1435} 1436 1437func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1438 params.Active = "pipelines" 1439 return p.executeRepo("repo/pipelines/workflow", w, params) 1440} 1441 1442type PutStringParams struct { 1443 LoggedInUser *oauth.MultiAccountUser 1444 Action string 1445 1446 // this is supplied in the case of editing an existing string 1447 String models.String 1448} 1449 1450func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1451 return p.execute("strings/put", w, params) 1452} 1453 1454type StringsDashboardParams struct { 1455 LoggedInUser *oauth.MultiAccountUser 1456 Card ProfileCard 1457 Strings []models.String 1458} 1459 1460func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1461 return p.execute("strings/dashboard", w, params) 1462} 1463 1464type StringTimelineParams struct { 1465 LoggedInUser *oauth.MultiAccountUser 1466 Strings []models.String 1467} 1468 1469func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1470 return p.execute("strings/timeline", w, params) 1471} 1472 1473type SingleStringParams struct { 1474 LoggedInUser *oauth.MultiAccountUser 1475 ShowRendered bool 1476 RenderToggle bool 1477 RenderedContents template.HTML 1478 String *models.String 1479 Stats models.StringStats 1480 IsStarred bool 1481 StarCount int 1482 Owner identity.Identity 1483} 1484 1485func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1486 return p.execute("strings/string", w, params) 1487} 1488 1489func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1490 return p.execute("timeline/home", w, params) 1491} 1492 1493func (p *Pages) Static() http.Handler { 1494 if p.dev { 1495 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1496 } 1497 1498 sub, err := fs.Sub(p.embedFS, "static") 1499 if err != nil { 1500 p.logger.Error("no static dir found? that's crazy", "err", err) 1501 panic(err) 1502 } 1503 // Custom handler to apply Cache-Control headers for font files 1504 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1505} 1506 1507func Cache(h http.Handler) http.Handler { 1508 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1509 path := strings.Split(r.URL.Path, "?")[0] 1510 1511 if strings.HasSuffix(path, ".css") { 1512 // on day for css files 1513 w.Header().Set("Cache-Control", "public, max-age=86400") 1514 } else { 1515 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1516 } 1517 h.ServeHTTP(w, r) 1518 }) 1519} 1520 1521func (p *Pages) CssContentHash() string { 1522 cssFile, err := p.embedFS.Open("static/tw.css") 1523 if err != nil { 1524 slog.Debug("Error opening CSS file", "err", err) 1525 return "" 1526 } 1527 defer cssFile.Close() 1528 1529 hasher := sha256.New() 1530 if _, err := io.Copy(hasher, cssFile); err != nil { 1531 slog.Debug("Error hashing CSS file", "err", err) 1532 return "" 1533 } 1534 1535 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1536} 1537 1538func (p *Pages) Error500(w io.Writer) error { 1539 return p.execute("errors/500", w, nil) 1540} 1541 1542func (p *Pages) Error404(w io.Writer) error { 1543 return p.execute("errors/404", w, nil) 1544} 1545 1546func (p *Pages) ErrorKnot404(w io.Writer) error { 1547 return p.execute("errors/knot404", w, nil) 1548} 1549 1550func (p *Pages) Error503(w io.Writer) error { 1551 return p.execute("errors/503", w, nil) 1552}