Monorepo for Tangled
at default-knot 1556 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 AvailableKnots []string 447 DefaultKnot string 448} 449 450func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 451 params.Tab = "knots" 452 return p.execute("knots/index", w, params) 453} 454 455type KnotParams struct { 456 LoggedInUser *oauth.MultiAccountUser 457 Registration *models.Registration 458 Members []string 459 Repos map[string][]models.Repo 460 IsOwner bool 461 Tab string 462} 463 464func (p *Pages) Knot(w io.Writer, params KnotParams) error { 465 return p.execute("knots/dashboard", w, params) 466} 467 468type KnotListingParams struct { 469 *models.Registration 470} 471 472func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 473 return p.executePlain("knots/fragments/knotListing", w, params) 474} 475 476type SpindlesParams struct { 477 LoggedInUser *oauth.MultiAccountUser 478 Spindles []models.Spindle 479 Tab string 480} 481 482func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 483 params.Tab = "spindles" 484 return p.execute("spindles/index", w, params) 485} 486 487type SpindleListingParams struct { 488 models.Spindle 489 Tab string 490} 491 492func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 493 return p.executePlain("spindles/fragments/spindleListing", w, params) 494} 495 496type SpindleDashboardParams struct { 497 LoggedInUser *oauth.MultiAccountUser 498 Spindle models.Spindle 499 Members []string 500 Repos map[string][]models.Repo 501 Tab string 502} 503 504func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 505 return p.execute("spindles/dashboard", w, params) 506} 507 508type NewRepoParams struct { 509 LoggedInUser *oauth.MultiAccountUser 510 Knots []string 511 DefaultKnot string 512} 513 514func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 515 return p.execute("repo/new", w, params) 516} 517 518type ForkRepoParams struct { 519 LoggedInUser *oauth.MultiAccountUser 520 Knots []string 521 RepoInfo repoinfo.RepoInfo 522 DefaultKnot string 523} 524 525func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 526 return p.execute("repo/fork", w, params) 527} 528 529type ProfileCard struct { 530 UserDid string 531 HasProfile bool 532 FollowStatus models.FollowStatus 533 Punchcard *models.Punchcard 534 Profile *models.Profile 535 Stats ProfileStats 536 Active string 537} 538 539type ProfileStats struct { 540 RepoCount int64 541 StarredCount int64 542 StringCount int64 543 FollowersCount int64 544 FollowingCount int64 545} 546 547func (p *ProfileCard) GetTabs() [][]any { 548 tabs := [][]any{ 549 {"overview", "overview", "square-chart-gantt", nil}, 550 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 551 {"starred", "starred", "star", p.Stats.StarredCount}, 552 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 553 } 554 555 return tabs 556} 557 558type ProfileOverviewParams struct { 559 LoggedInUser *oauth.MultiAccountUser 560 Repos []models.Repo 561 CollaboratingRepos []models.Repo 562 ProfileTimeline *models.ProfileTimeline 563 Card *ProfileCard 564 Active string 565 ShowPunchcard bool 566} 567 568func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 569 params.Active = "overview" 570 return p.executeProfile("user/overview", w, params) 571} 572 573type ProfileReposParams struct { 574 LoggedInUser *oauth.MultiAccountUser 575 Repos []models.Repo 576 Card *ProfileCard 577 Active string 578} 579 580func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 581 params.Active = "repos" 582 return p.executeProfile("user/repos", w, params) 583} 584 585type ProfileStarredParams struct { 586 LoggedInUser *oauth.MultiAccountUser 587 Repos []models.Repo 588 Card *ProfileCard 589 Active string 590} 591 592func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 593 params.Active = "starred" 594 return p.executeProfile("user/starred", w, params) 595} 596 597type ProfileStringsParams struct { 598 LoggedInUser *oauth.MultiAccountUser 599 Strings []models.String 600 Card *ProfileCard 601 Active string 602} 603 604func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 605 params.Active = "strings" 606 return p.executeProfile("user/strings", w, params) 607} 608 609type FollowCard struct { 610 UserDid string 611 LoggedInUser *oauth.MultiAccountUser 612 FollowStatus models.FollowStatus 613 FollowersCount int64 614 FollowingCount int64 615 Profile *models.Profile 616} 617 618type ProfileFollowersParams struct { 619 LoggedInUser *oauth.MultiAccountUser 620 Followers []FollowCard 621 Card *ProfileCard 622 Active string 623} 624 625func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 626 params.Active = "overview" 627 return p.executeProfile("user/followers", w, params) 628} 629 630type ProfileFollowingParams struct { 631 LoggedInUser *oauth.MultiAccountUser 632 Following []FollowCard 633 Card *ProfileCard 634 Active string 635} 636 637func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 638 params.Active = "overview" 639 return p.executeProfile("user/following", w, params) 640} 641 642type FollowFragmentParams struct { 643 UserDid string 644 FollowStatus models.FollowStatus 645 FollowersCount int64 646} 647 648func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 649 return p.executePlain("user/fragments/follow-oob", w, params) 650} 651 652type EditBioParams struct { 653 LoggedInUser *oauth.MultiAccountUser 654 Profile *models.Profile 655} 656 657func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 658 return p.executePlain("user/fragments/editBio", w, params) 659} 660 661type EditPinsParams struct { 662 LoggedInUser *oauth.MultiAccountUser 663 Profile *models.Profile 664 AllRepos []PinnedRepo 665} 666 667type PinnedRepo struct { 668 IsPinned bool 669 models.Repo 670} 671 672func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 673 return p.executePlain("user/fragments/editPins", w, params) 674} 675 676type StarBtnFragmentParams struct { 677 IsStarred bool 678 SubjectAt syntax.ATURI 679 StarCount int 680 HxSwapOob bool 681} 682 683func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 684 params.HxSwapOob = true 685 return p.executePlain("fragments/starBtn", w, params) 686} 687 688type RepoIndexParams struct { 689 LoggedInUser *oauth.MultiAccountUser 690 RepoInfo repoinfo.RepoInfo 691 Active string 692 TagMap map[string][]string 693 CommitsTrunc []types.Commit 694 TagsTrunc []*types.TagReference 695 BranchesTrunc []types.Branch 696 // ForkInfo *types.ForkInfo 697 HTMLReadme template.HTML 698 Raw bool 699 EmailToDid map[string]string 700 VerifiedCommits commitverify.VerifiedCommits 701 Languages []types.RepoLanguageDetails 702 Pipelines map[string]models.Pipeline 703 NeedsKnotUpgrade bool 704 types.RepoIndexResponse 705} 706 707func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 708 params.Active = "overview" 709 if params.IsEmpty { 710 return p.executeRepo("repo/empty", w, params) 711 } 712 713 if params.NeedsKnotUpgrade { 714 return p.executeRepo("repo/needsUpgrade", w, params) 715 } 716 717 p.rctx.RepoInfo = params.RepoInfo 718 p.rctx.RepoInfo.Ref = params.Ref 719 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 720 721 if params.ReadmeFileName != "" { 722 ext := filepath.Ext(params.ReadmeFileName) 723 switch ext { 724 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 725 params.Raw = false 726 htmlString := p.rctx.RenderMarkdown(params.Readme) 727 sanitized := p.rctx.SanitizeDefault(htmlString) 728 params.HTMLReadme = template.HTML(sanitized) 729 default: 730 params.Raw = true 731 } 732 } 733 734 return p.executeRepo("repo/index", w, params) 735} 736 737type RepoLogParams struct { 738 LoggedInUser *oauth.MultiAccountUser 739 RepoInfo repoinfo.RepoInfo 740 TagMap map[string][]string 741 Active string 742 EmailToDid map[string]string 743 VerifiedCommits commitverify.VerifiedCommits 744 Pipelines map[string]models.Pipeline 745 746 types.RepoLogResponse 747} 748 749func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 750 params.Active = "overview" 751 return p.executeRepo("repo/log", w, params) 752} 753 754type RepoCommitParams struct { 755 LoggedInUser *oauth.MultiAccountUser 756 RepoInfo repoinfo.RepoInfo 757 Active string 758 EmailToDid map[string]string 759 Pipeline *models.Pipeline 760 DiffOpts types.DiffOpts 761 762 // singular because it's always going to be just one 763 VerifiedCommit commitverify.VerifiedCommits 764 765 types.RepoCommitResponse 766} 767 768func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 769 params.Active = "overview" 770 return p.executeRepo("repo/commit", w, params) 771} 772 773type RepoTreeParams struct { 774 LoggedInUser *oauth.MultiAccountUser 775 RepoInfo repoinfo.RepoInfo 776 Active string 777 BreadCrumbs [][]string 778 TreePath string 779 Raw bool 780 HTMLReadme template.HTML 781 EmailToDid map[string]string 782 LastCommitInfo *types.LastCommitInfo 783 types.RepoTreeResponse 784} 785 786type RepoTreeStats struct { 787 NumFolders uint64 788 NumFiles uint64 789} 790 791func (r RepoTreeParams) TreeStats() RepoTreeStats { 792 numFolders, numFiles := 0, 0 793 for _, f := range r.Files { 794 if !f.IsFile() { 795 numFolders += 1 796 } else if f.IsFile() { 797 numFiles += 1 798 } 799 } 800 801 return RepoTreeStats{ 802 NumFolders: uint64(numFolders), 803 NumFiles: uint64(numFiles), 804 } 805} 806 807func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 808 params.Active = "overview" 809 810 p.rctx.RepoInfo = params.RepoInfo 811 p.rctx.RepoInfo.Ref = params.Ref 812 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 813 814 if params.ReadmeFileName != "" { 815 ext := filepath.Ext(params.ReadmeFileName) 816 switch ext { 817 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 818 params.Raw = false 819 htmlString := p.rctx.RenderMarkdown(params.Readme) 820 sanitized := p.rctx.SanitizeDefault(htmlString) 821 params.HTMLReadme = template.HTML(sanitized) 822 default: 823 params.Raw = true 824 } 825 } 826 827 return p.executeRepo("repo/tree", w, params) 828} 829 830type RepoBranchesParams struct { 831 LoggedInUser *oauth.MultiAccountUser 832 RepoInfo repoinfo.RepoInfo 833 Active string 834 types.RepoBranchesResponse 835} 836 837func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 838 params.Active = "overview" 839 return p.executeRepo("repo/branches", w, params) 840} 841 842type RepoTagsParams struct { 843 LoggedInUser *oauth.MultiAccountUser 844 RepoInfo repoinfo.RepoInfo 845 Active string 846 types.RepoTagsResponse 847 ArtifactMap map[plumbing.Hash][]models.Artifact 848 DanglingArtifacts []models.Artifact 849} 850 851func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 852 params.Active = "overview" 853 return p.executeRepo("repo/tags", w, params) 854} 855 856type RepoTagParams struct { 857 LoggedInUser *oauth.MultiAccountUser 858 RepoInfo repoinfo.RepoInfo 859 Active string 860 types.RepoTagResponse 861 ArtifactMap map[plumbing.Hash][]models.Artifact 862 DanglingArtifacts []models.Artifact 863} 864 865func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 866 params.Active = "overview" 867 return p.executeRepo("repo/tag", w, params) 868} 869 870type RepoArtifactParams struct { 871 LoggedInUser *oauth.MultiAccountUser 872 RepoInfo repoinfo.RepoInfo 873 Artifact models.Artifact 874} 875 876func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 877 return p.executePlain("repo/fragments/artifact", w, params) 878} 879 880type RepoBlobParams struct { 881 LoggedInUser *oauth.MultiAccountUser 882 RepoInfo repoinfo.RepoInfo 883 Active string 884 BreadCrumbs [][]string 885 BlobView models.BlobView 886 EmailToDid map[string]string 887 LastCommitInfo *types.LastCommitInfo 888 *tangled.RepoBlob_Output 889} 890 891func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 892 switch params.BlobView.ContentType { 893 case models.BlobContentTypeMarkup: 894 p.rctx.RepoInfo = params.RepoInfo 895 } 896 897 params.Active = "overview" 898 return p.executeRepo("repo/blob", w, params) 899} 900 901type Collaborator struct { 902 Did string 903 Role string 904} 905 906type RepoSettingsParams struct { 907 LoggedInUser *oauth.MultiAccountUser 908 RepoInfo repoinfo.RepoInfo 909 Collaborators []Collaborator 910 Active string 911 Branches []types.Branch 912 Spindles []string 913 CurrentSpindle string 914 Secrets []*tangled.RepoListSecrets_Secret 915 916 // TODO: use repoinfo.roles 917 IsCollaboratorInviteAllowed bool 918} 919 920func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 921 params.Active = "settings" 922 return p.executeRepo("repo/settings", w, params) 923} 924 925type RepoGeneralSettingsParams struct { 926 LoggedInUser *oauth.MultiAccountUser 927 RepoInfo repoinfo.RepoInfo 928 Labels []models.LabelDefinition 929 DefaultLabels []models.LabelDefinition 930 SubscribedLabels map[string]struct{} 931 ShouldSubscribeAll bool 932 Active string 933 Tab string 934 Branches []types.Branch 935} 936 937func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 938 params.Active = "settings" 939 params.Tab = "general" 940 return p.executeRepo("repo/settings/general", w, params) 941} 942 943type RepoAccessSettingsParams struct { 944 LoggedInUser *oauth.MultiAccountUser 945 RepoInfo repoinfo.RepoInfo 946 Active string 947 Tab string 948 Collaborators []Collaborator 949} 950 951func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 952 params.Active = "settings" 953 params.Tab = "access" 954 return p.executeRepo("repo/settings/access", w, params) 955} 956 957type RepoPipelineSettingsParams struct { 958 LoggedInUser *oauth.MultiAccountUser 959 RepoInfo repoinfo.RepoInfo 960 Active string 961 Tab string 962 Spindles []string 963 CurrentSpindle string 964 Secrets []map[string]any 965} 966 967func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 968 params.Active = "settings" 969 params.Tab = "pipelines" 970 return p.executeRepo("repo/settings/pipelines", w, params) 971} 972 973type RepoWebhooksSettingsParams struct { 974 LoggedInUser *oauth.MultiAccountUser 975 RepoInfo repoinfo.RepoInfo 976 Active string 977 Tab string 978 Webhooks []models.Webhook 979 WebhookDeliveries map[int64][]models.WebhookDelivery 980} 981 982func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 983 params.Active = "settings" 984 params.Tab = "hooks" 985 return p.executeRepo("repo/settings/hooks", w, params) 986} 987 988type WebhookDeliveriesListParams struct { 989 LoggedInUser *oauth.MultiAccountUser 990 RepoInfo repoinfo.RepoInfo 991 Webhook *models.Webhook 992 Deliveries []models.WebhookDelivery 993} 994 995func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 996 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 997 if err != nil { 998 return err 999 } 1000 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1001} 1002 1003type RepoIssuesParams struct { 1004 LoggedInUser *oauth.MultiAccountUser 1005 RepoInfo repoinfo.RepoInfo 1006 Active string 1007 Issues []models.Issue 1008 IssueCount int 1009 LabelDefs map[string]*models.LabelDefinition 1010 Page pagination.Page 1011 FilterState string 1012 FilterQuery string 1013} 1014 1015func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1016 params.Active = "issues" 1017 return p.executeRepo("repo/issues/issues", w, params) 1018} 1019 1020type RepoSingleIssueParams struct { 1021 LoggedInUser *oauth.MultiAccountUser 1022 RepoInfo repoinfo.RepoInfo 1023 Active string 1024 Issue *models.Issue 1025 CommentList []models.CommentListItem 1026 Backlinks []models.RichReferenceLink 1027 LabelDefs map[string]*models.LabelDefinition 1028 1029 Reactions map[models.ReactionKind]models.ReactionDisplayData 1030 UserReacted map[models.ReactionKind]bool 1031} 1032 1033func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1034 params.Active = "issues" 1035 return p.executeRepo("repo/issues/issue", w, params) 1036} 1037 1038type EditIssueParams struct { 1039 LoggedInUser *oauth.MultiAccountUser 1040 RepoInfo repoinfo.RepoInfo 1041 Issue *models.Issue 1042 Action string 1043} 1044 1045func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1046 params.Action = "edit" 1047 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1048} 1049 1050type ThreadReactionFragmentParams struct { 1051 ThreadAt syntax.ATURI 1052 Kind models.ReactionKind 1053 Count int 1054 Users []string 1055 IsReacted bool 1056} 1057 1058func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1059 return p.executePlain("repo/fragments/reaction", w, params) 1060} 1061 1062type RepoNewIssueParams struct { 1063 LoggedInUser *oauth.MultiAccountUser 1064 RepoInfo repoinfo.RepoInfo 1065 Issue *models.Issue // existing issue if any -- passed when editing 1066 Active string 1067 Action string 1068} 1069 1070func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1071 params.Active = "issues" 1072 params.Action = "create" 1073 return p.executeRepo("repo/issues/new", w, params) 1074} 1075 1076type EditIssueCommentParams struct { 1077 LoggedInUser *oauth.MultiAccountUser 1078 RepoInfo repoinfo.RepoInfo 1079 Issue *models.Issue 1080 Comment *models.IssueComment 1081} 1082 1083func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1084 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1085} 1086 1087type ReplyIssueCommentPlaceholderParams struct { 1088 LoggedInUser *oauth.MultiAccountUser 1089 RepoInfo repoinfo.RepoInfo 1090 Issue *models.Issue 1091 Comment *models.IssueComment 1092} 1093 1094func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1095 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1096} 1097 1098type ReplyIssueCommentParams struct { 1099 LoggedInUser *oauth.MultiAccountUser 1100 RepoInfo repoinfo.RepoInfo 1101 Issue *models.Issue 1102 Comment *models.IssueComment 1103} 1104 1105func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1106 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1107} 1108 1109type IssueCommentBodyParams struct { 1110 LoggedInUser *oauth.MultiAccountUser 1111 RepoInfo repoinfo.RepoInfo 1112 Issue *models.Issue 1113 Comment *models.IssueComment 1114} 1115 1116func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1117 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1118} 1119 1120type RepoNewPullParams struct { 1121 LoggedInUser *oauth.MultiAccountUser 1122 RepoInfo repoinfo.RepoInfo 1123 Branches []types.Branch 1124 Strategy string 1125 SourceBranch string 1126 TargetBranch string 1127 Title string 1128 Body string 1129 Active string 1130} 1131 1132func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1133 params.Active = "pulls" 1134 return p.executeRepo("repo/pulls/new", w, params) 1135} 1136 1137type RepoPullsParams struct { 1138 LoggedInUser *oauth.MultiAccountUser 1139 RepoInfo repoinfo.RepoInfo 1140 Pulls []*models.Pull 1141 Active string 1142 FilterState string 1143 FilterQuery string 1144 Stacks map[string]models.Stack 1145 Pipelines map[string]models.Pipeline 1146 LabelDefs map[string]*models.LabelDefinition 1147 Page pagination.Page 1148 PullCount int 1149} 1150 1151func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1152 params.Active = "pulls" 1153 return p.executeRepo("repo/pulls/pulls", w, params) 1154} 1155 1156type ResubmitResult uint64 1157 1158const ( 1159 ShouldResubmit ResubmitResult = iota 1160 ShouldNotResubmit 1161 Unknown 1162) 1163 1164func (r ResubmitResult) Yes() bool { 1165 return r == ShouldResubmit 1166} 1167func (r ResubmitResult) No() bool { 1168 return r == ShouldNotResubmit 1169} 1170func (r ResubmitResult) Unknown() bool { 1171 return r == Unknown 1172} 1173 1174type RepoSinglePullParams struct { 1175 LoggedInUser *oauth.MultiAccountUser 1176 RepoInfo repoinfo.RepoInfo 1177 Active string 1178 Pull *models.Pull 1179 Stack models.Stack 1180 AbandonedPulls []*models.Pull 1181 Backlinks []models.RichReferenceLink 1182 BranchDeleteStatus *models.BranchDeleteStatus 1183 MergeCheck types.MergeCheckResponse 1184 ResubmitCheck ResubmitResult 1185 Pipelines map[string]models.Pipeline 1186 Diff types.DiffRenderer 1187 DiffOpts types.DiffOpts 1188 ActiveRound int 1189 IsInterdiff bool 1190 1191 Reactions map[models.ReactionKind]models.ReactionDisplayData 1192 UserReacted map[models.ReactionKind]bool 1193 1194 LabelDefs map[string]*models.LabelDefinition 1195} 1196 1197func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1198 params.Active = "pulls" 1199 return p.executeRepo("repo/pulls/pull", w, params) 1200} 1201 1202type RepoPullPatchParams struct { 1203 LoggedInUser *oauth.MultiAccountUser 1204 RepoInfo repoinfo.RepoInfo 1205 Pull *models.Pull 1206 Stack models.Stack 1207 Diff *types.NiceDiff 1208 Round int 1209 Submission *models.PullSubmission 1210 DiffOpts types.DiffOpts 1211} 1212 1213// this name is a mouthful 1214func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1215 return p.execute("repo/pulls/patch", w, params) 1216} 1217 1218type RepoPullInterdiffParams struct { 1219 LoggedInUser *oauth.MultiAccountUser 1220 RepoInfo repoinfo.RepoInfo 1221 Pull *models.Pull 1222 Round int 1223 Interdiff *patchutil.InterdiffResult 1224 DiffOpts types.DiffOpts 1225} 1226 1227// this name is a mouthful 1228func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1229 return p.execute("repo/pulls/interdiff", w, params) 1230} 1231 1232type PullPatchUploadParams struct { 1233 RepoInfo repoinfo.RepoInfo 1234} 1235 1236func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1237 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1238} 1239 1240type PullCompareBranchesParams struct { 1241 RepoInfo repoinfo.RepoInfo 1242 Branches []types.Branch 1243 SourceBranch string 1244} 1245 1246func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1247 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1248} 1249 1250type PullCompareForkParams struct { 1251 RepoInfo repoinfo.RepoInfo 1252 Forks []models.Repo 1253 Selected string 1254} 1255 1256func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1257 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1258} 1259 1260type PullCompareForkBranchesParams struct { 1261 RepoInfo repoinfo.RepoInfo 1262 SourceBranches []types.Branch 1263 TargetBranches []types.Branch 1264} 1265 1266func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1267 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1268} 1269 1270type PullResubmitParams struct { 1271 LoggedInUser *oauth.MultiAccountUser 1272 RepoInfo repoinfo.RepoInfo 1273 Pull *models.Pull 1274 SubmissionId int 1275} 1276 1277func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1278 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1279} 1280 1281type PullActionsParams struct { 1282 LoggedInUser *oauth.MultiAccountUser 1283 RepoInfo repoinfo.RepoInfo 1284 Pull *models.Pull 1285 RoundNumber int 1286 MergeCheck types.MergeCheckResponse 1287 ResubmitCheck ResubmitResult 1288 BranchDeleteStatus *models.BranchDeleteStatus 1289 Stack models.Stack 1290} 1291 1292func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1293 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1294} 1295 1296type PullNewCommentParams struct { 1297 LoggedInUser *oauth.MultiAccountUser 1298 RepoInfo repoinfo.RepoInfo 1299 Pull *models.Pull 1300 RoundNumber int 1301} 1302 1303func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1304 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1305} 1306 1307type RepoCompareParams struct { 1308 LoggedInUser *oauth.MultiAccountUser 1309 RepoInfo repoinfo.RepoInfo 1310 Forks []models.Repo 1311 Branches []types.Branch 1312 Tags []*types.TagReference 1313 Base string 1314 Head string 1315 Diff *types.NiceDiff 1316 DiffOpts types.DiffOpts 1317 1318 Active string 1319} 1320 1321func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1322 params.Active = "overview" 1323 return p.executeRepo("repo/compare/compare", w, params) 1324} 1325 1326type RepoCompareNewParams struct { 1327 LoggedInUser *oauth.MultiAccountUser 1328 RepoInfo repoinfo.RepoInfo 1329 Forks []models.Repo 1330 Branches []types.Branch 1331 Tags []*types.TagReference 1332 Base string 1333 Head string 1334 1335 Active string 1336} 1337 1338func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1339 params.Active = "overview" 1340 return p.executeRepo("repo/compare/new", w, params) 1341} 1342 1343type RepoCompareAllowPullParams struct { 1344 LoggedInUser *oauth.MultiAccountUser 1345 RepoInfo repoinfo.RepoInfo 1346 Base string 1347 Head string 1348} 1349 1350func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1351 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1352} 1353 1354type RepoCompareDiffFragmentParams struct { 1355 Diff types.NiceDiff 1356 DiffOpts types.DiffOpts 1357} 1358 1359func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1360 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1361} 1362 1363type LabelPanelParams struct { 1364 LoggedInUser *oauth.MultiAccountUser 1365 RepoInfo repoinfo.RepoInfo 1366 Defs map[string]*models.LabelDefinition 1367 Subject string 1368 State models.LabelState 1369} 1370 1371func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1372 return p.executePlain("repo/fragments/labelPanel", w, params) 1373} 1374 1375type EditLabelPanelParams struct { 1376 LoggedInUser *oauth.MultiAccountUser 1377 RepoInfo repoinfo.RepoInfo 1378 Defs map[string]*models.LabelDefinition 1379 Subject string 1380 State models.LabelState 1381} 1382 1383func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1384 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1385} 1386 1387type PipelinesParams struct { 1388 LoggedInUser *oauth.MultiAccountUser 1389 RepoInfo repoinfo.RepoInfo 1390 Pipelines []models.Pipeline 1391 Active string 1392 FilterKind string 1393 Total int64 1394} 1395 1396func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1397 params.Active = "pipelines" 1398 return p.executeRepo("repo/pipelines/pipelines", w, params) 1399} 1400 1401type LogBlockParams struct { 1402 Id int 1403 Name string 1404 Command string 1405 Collapsed bool 1406 StartTime time.Time 1407} 1408 1409func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1410 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1411} 1412 1413type LogBlockEndParams struct { 1414 Id int 1415 StartTime time.Time 1416 EndTime time.Time 1417} 1418 1419func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1420 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1421} 1422 1423type LogLineParams struct { 1424 Id int 1425 Content string 1426} 1427 1428func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1429 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1430} 1431 1432type WorkflowParams struct { 1433 LoggedInUser *oauth.MultiAccountUser 1434 RepoInfo repoinfo.RepoInfo 1435 Pipeline models.Pipeline 1436 Workflow string 1437 LogUrl string 1438 Active string 1439} 1440 1441func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1442 params.Active = "pipelines" 1443 return p.executeRepo("repo/pipelines/workflow", w, params) 1444} 1445 1446type PutStringParams struct { 1447 LoggedInUser *oauth.MultiAccountUser 1448 Action string 1449 1450 // this is supplied in the case of editing an existing string 1451 String models.String 1452} 1453 1454func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1455 return p.execute("strings/put", w, params) 1456} 1457 1458type StringsDashboardParams struct { 1459 LoggedInUser *oauth.MultiAccountUser 1460 Card ProfileCard 1461 Strings []models.String 1462} 1463 1464func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1465 return p.execute("strings/dashboard", w, params) 1466} 1467 1468type StringTimelineParams struct { 1469 LoggedInUser *oauth.MultiAccountUser 1470 Strings []models.String 1471} 1472 1473func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1474 return p.execute("strings/timeline", w, params) 1475} 1476 1477type SingleStringParams struct { 1478 LoggedInUser *oauth.MultiAccountUser 1479 ShowRendered bool 1480 RenderToggle bool 1481 RenderedContents template.HTML 1482 String *models.String 1483 Stats models.StringStats 1484 IsStarred bool 1485 StarCount int 1486 Owner identity.Identity 1487} 1488 1489func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1490 return p.execute("strings/string", w, params) 1491} 1492 1493func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1494 return p.execute("timeline/home", w, params) 1495} 1496 1497func (p *Pages) Static() http.Handler { 1498 if p.dev { 1499 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1500 } 1501 1502 sub, err := fs.Sub(p.embedFS, "static") 1503 if err != nil { 1504 p.logger.Error("no static dir found? that's crazy", "err", err) 1505 panic(err) 1506 } 1507 // Custom handler to apply Cache-Control headers for font files 1508 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1509} 1510 1511func Cache(h http.Handler) http.Handler { 1512 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1513 path := strings.Split(r.URL.Path, "?")[0] 1514 1515 if strings.HasSuffix(path, ".css") { 1516 // on day for css files 1517 w.Header().Set("Cache-Control", "public, max-age=86400") 1518 } else { 1519 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1520 } 1521 h.ServeHTTP(w, r) 1522 }) 1523} 1524 1525func (p *Pages) CssContentHash() string { 1526 cssFile, err := p.embedFS.Open("static/tw.css") 1527 if err != nil { 1528 slog.Debug("Error opening CSS file", "err", err) 1529 return "" 1530 } 1531 defer cssFile.Close() 1532 1533 hasher := sha256.New() 1534 if _, err := io.Copy(hasher, cssFile); err != nil { 1535 slog.Debug("Error hashing CSS file", "err", err) 1536 return "" 1537 } 1538 1539 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1540} 1541 1542func (p *Pages) Error500(w io.Writer) error { 1543 return p.execute("errors/500", w, nil) 1544} 1545 1546func (p *Pages) Error404(w io.Writer) error { 1547 return p.execute("errors/404", w, nil) 1548} 1549 1550func (p *Pages) ErrorKnot404(w io.Writer) error { 1551 return p.execute("errors/knot404", w, nil) 1552} 1553 1554func (p *Pages) Error503(w io.Writer) error { 1555 return p.execute("errors/503", w, nil) 1556}