Monorepo for Tangled
at 0d3bd036cb16614a66f3fc5792b4c2bf42824009 1557 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 KnotUnreachable bool 701 types.RepoIndexResponse 702} 703 704func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 705 params.Active = "overview" 706 if params.IsEmpty { 707 return p.executeRepo("repo/empty", w, params) 708 } 709 710 if params.NeedsKnotUpgrade { 711 return p.executeRepo("repo/needsUpgrade", w, params) 712 } 713 714 if params.KnotUnreachable { 715 return p.executeRepo("repo/knotUnreachable", w, params) 716 } 717 718 p.rctx.RepoInfo = params.RepoInfo 719 p.rctx.RepoInfo.Ref = params.Ref 720 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 721 722 if params.ReadmeFileName != "" { 723 ext := filepath.Ext(params.ReadmeFileName) 724 switch ext { 725 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 726 params.Raw = false 727 htmlString := p.rctx.RenderMarkdown(params.Readme) 728 sanitized := p.rctx.SanitizeDefault(htmlString) 729 params.HTMLReadme = template.HTML(sanitized) 730 default: 731 params.Raw = true 732 } 733 } 734 735 return p.executeRepo("repo/index", w, params) 736} 737 738type RepoLogParams struct { 739 LoggedInUser *oauth.MultiAccountUser 740 RepoInfo repoinfo.RepoInfo 741 TagMap map[string][]string 742 Active string 743 EmailToDid map[string]string 744 VerifiedCommits commitverify.VerifiedCommits 745 Pipelines map[string]models.Pipeline 746 747 types.RepoLogResponse 748} 749 750func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 751 params.Active = "overview" 752 return p.executeRepo("repo/log", w, params) 753} 754 755type RepoCommitParams struct { 756 LoggedInUser *oauth.MultiAccountUser 757 RepoInfo repoinfo.RepoInfo 758 Active string 759 EmailToDid map[string]string 760 Pipeline *models.Pipeline 761 DiffOpts types.DiffOpts 762 763 // singular because it's always going to be just one 764 VerifiedCommit commitverify.VerifiedCommits 765 766 types.RepoCommitResponse 767} 768 769func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 770 params.Active = "overview" 771 return p.executeRepo("repo/commit", w, params) 772} 773 774type RepoTreeParams struct { 775 LoggedInUser *oauth.MultiAccountUser 776 RepoInfo repoinfo.RepoInfo 777 Active string 778 BreadCrumbs [][]string 779 TreePath string 780 Raw bool 781 HTMLReadme template.HTML 782 EmailToDid map[string]string 783 LastCommitInfo *types.LastCommitInfo 784 types.RepoTreeResponse 785} 786 787type RepoTreeStats struct { 788 NumFolders uint64 789 NumFiles uint64 790} 791 792func (r RepoTreeParams) TreeStats() RepoTreeStats { 793 numFolders, numFiles := 0, 0 794 for _, f := range r.Files { 795 if !f.IsFile() { 796 numFolders += 1 797 } else if f.IsFile() { 798 numFiles += 1 799 } 800 } 801 802 return RepoTreeStats{ 803 NumFolders: uint64(numFolders), 804 NumFiles: uint64(numFiles), 805 } 806} 807 808func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 809 params.Active = "overview" 810 811 p.rctx.RepoInfo = params.RepoInfo 812 p.rctx.RepoInfo.Ref = params.Ref 813 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 814 815 if params.ReadmeFileName != "" { 816 ext := filepath.Ext(params.ReadmeFileName) 817 switch ext { 818 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 819 params.Raw = false 820 htmlString := p.rctx.RenderMarkdown(params.Readme) 821 sanitized := p.rctx.SanitizeDefault(htmlString) 822 params.HTMLReadme = template.HTML(sanitized) 823 default: 824 params.Raw = true 825 } 826 } 827 828 return p.executeRepo("repo/tree", w, params) 829} 830 831type RepoBranchesParams struct { 832 LoggedInUser *oauth.MultiAccountUser 833 RepoInfo repoinfo.RepoInfo 834 Active string 835 types.RepoBranchesResponse 836} 837 838func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 839 params.Active = "overview" 840 return p.executeRepo("repo/branches", w, params) 841} 842 843type RepoTagsParams struct { 844 LoggedInUser *oauth.MultiAccountUser 845 RepoInfo repoinfo.RepoInfo 846 Active string 847 types.RepoTagsResponse 848 ArtifactMap map[plumbing.Hash][]models.Artifact 849 DanglingArtifacts []models.Artifact 850} 851 852func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 853 params.Active = "overview" 854 return p.executeRepo("repo/tags", w, params) 855} 856 857type RepoTagParams struct { 858 LoggedInUser *oauth.MultiAccountUser 859 RepoInfo repoinfo.RepoInfo 860 Active string 861 types.RepoTagResponse 862 ArtifactMap map[plumbing.Hash][]models.Artifact 863 DanglingArtifacts []models.Artifact 864} 865 866func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 867 params.Active = "overview" 868 return p.executeRepo("repo/tag", w, params) 869} 870 871type RepoArtifactParams struct { 872 LoggedInUser *oauth.MultiAccountUser 873 RepoInfo repoinfo.RepoInfo 874 Artifact models.Artifact 875} 876 877func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 878 return p.executePlain("repo/fragments/artifact", w, params) 879} 880 881type RepoBlobParams struct { 882 LoggedInUser *oauth.MultiAccountUser 883 RepoInfo repoinfo.RepoInfo 884 Active string 885 BreadCrumbs [][]string 886 BlobView models.BlobView 887 EmailToDid map[string]string 888 LastCommitInfo *types.LastCommitInfo 889 *tangled.RepoBlob_Output 890} 891 892func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 893 switch params.BlobView.ContentType { 894 case models.BlobContentTypeMarkup: 895 p.rctx.RepoInfo = params.RepoInfo 896 } 897 898 params.Active = "overview" 899 return p.executeRepo("repo/blob", w, params) 900} 901 902type Collaborator struct { 903 Did string 904 Role string 905} 906 907type RepoSettingsParams struct { 908 LoggedInUser *oauth.MultiAccountUser 909 RepoInfo repoinfo.RepoInfo 910 Collaborators []Collaborator 911 Active string 912 Branches []types.Branch 913 Spindles []string 914 CurrentSpindle string 915 Secrets []*tangled.RepoListSecrets_Secret 916 917 // TODO: use repoinfo.roles 918 IsCollaboratorInviteAllowed bool 919} 920 921func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 922 params.Active = "settings" 923 return p.executeRepo("repo/settings", w, params) 924} 925 926type RepoGeneralSettingsParams struct { 927 LoggedInUser *oauth.MultiAccountUser 928 RepoInfo repoinfo.RepoInfo 929 Labels []models.LabelDefinition 930 DefaultLabels []models.LabelDefinition 931 SubscribedLabels map[string]struct{} 932 ShouldSubscribeAll bool 933 Active string 934 Tab string 935 Branches []types.Branch 936} 937 938func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 939 params.Active = "settings" 940 params.Tab = "general" 941 return p.executeRepo("repo/settings/general", w, params) 942} 943 944type RepoAccessSettingsParams struct { 945 LoggedInUser *oauth.MultiAccountUser 946 RepoInfo repoinfo.RepoInfo 947 Active string 948 Tab string 949 Collaborators []Collaborator 950} 951 952func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 953 params.Active = "settings" 954 params.Tab = "access" 955 return p.executeRepo("repo/settings/access", w, params) 956} 957 958type RepoPipelineSettingsParams struct { 959 LoggedInUser *oauth.MultiAccountUser 960 RepoInfo repoinfo.RepoInfo 961 Active string 962 Tab string 963 Spindles []string 964 CurrentSpindle string 965 Secrets []map[string]any 966} 967 968func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 969 params.Active = "settings" 970 params.Tab = "pipelines" 971 return p.executeRepo("repo/settings/pipelines", w, params) 972} 973 974type RepoWebhooksSettingsParams struct { 975 LoggedInUser *oauth.MultiAccountUser 976 RepoInfo repoinfo.RepoInfo 977 Active string 978 Tab string 979 Webhooks []models.Webhook 980 WebhookDeliveries map[int64][]models.WebhookDelivery 981} 982 983func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 984 params.Active = "settings" 985 params.Tab = "hooks" 986 return p.executeRepo("repo/settings/hooks", w, params) 987} 988 989type WebhookDeliveriesListParams struct { 990 LoggedInUser *oauth.MultiAccountUser 991 RepoInfo repoinfo.RepoInfo 992 Webhook *models.Webhook 993 Deliveries []models.WebhookDelivery 994} 995 996func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 997 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 998 if err != nil { 999 return err 1000 } 1001 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1002} 1003 1004type RepoIssuesParams struct { 1005 LoggedInUser *oauth.MultiAccountUser 1006 RepoInfo repoinfo.RepoInfo 1007 Active string 1008 Issues []models.Issue 1009 IssueCount int 1010 LabelDefs map[string]*models.LabelDefinition 1011 Page pagination.Page 1012 FilterState string 1013 FilterQuery string 1014} 1015 1016func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1017 params.Active = "issues" 1018 return p.executeRepo("repo/issues/issues", w, params) 1019} 1020 1021type RepoSingleIssueParams struct { 1022 LoggedInUser *oauth.MultiAccountUser 1023 RepoInfo repoinfo.RepoInfo 1024 Active string 1025 Issue *models.Issue 1026 CommentList []models.CommentListItem 1027 Backlinks []models.RichReferenceLink 1028 LabelDefs map[string]*models.LabelDefinition 1029 1030 Reactions map[models.ReactionKind]models.ReactionDisplayData 1031 UserReacted map[models.ReactionKind]bool 1032} 1033 1034func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1035 params.Active = "issues" 1036 return p.executeRepo("repo/issues/issue", w, params) 1037} 1038 1039type EditIssueParams struct { 1040 LoggedInUser *oauth.MultiAccountUser 1041 RepoInfo repoinfo.RepoInfo 1042 Issue *models.Issue 1043 Action string 1044} 1045 1046func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1047 params.Action = "edit" 1048 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1049} 1050 1051type ThreadReactionFragmentParams struct { 1052 ThreadAt syntax.ATURI 1053 Kind models.ReactionKind 1054 Count int 1055 Users []string 1056 IsReacted bool 1057} 1058 1059func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1060 return p.executePlain("repo/fragments/reaction", w, params) 1061} 1062 1063type RepoNewIssueParams struct { 1064 LoggedInUser *oauth.MultiAccountUser 1065 RepoInfo repoinfo.RepoInfo 1066 Issue *models.Issue // existing issue if any -- passed when editing 1067 Active string 1068 Action string 1069} 1070 1071func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1072 params.Active = "issues" 1073 params.Action = "create" 1074 return p.executeRepo("repo/issues/new", w, params) 1075} 1076 1077type EditIssueCommentParams struct { 1078 LoggedInUser *oauth.MultiAccountUser 1079 RepoInfo repoinfo.RepoInfo 1080 Issue *models.Issue 1081 Comment *models.IssueComment 1082} 1083 1084func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1085 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1086} 1087 1088type ReplyIssueCommentPlaceholderParams struct { 1089 LoggedInUser *oauth.MultiAccountUser 1090 RepoInfo repoinfo.RepoInfo 1091 Issue *models.Issue 1092 Comment *models.IssueComment 1093} 1094 1095func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1096 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1097} 1098 1099type ReplyIssueCommentParams struct { 1100 LoggedInUser *oauth.MultiAccountUser 1101 RepoInfo repoinfo.RepoInfo 1102 Issue *models.Issue 1103 Comment *models.IssueComment 1104} 1105 1106func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1107 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1108} 1109 1110type IssueCommentBodyParams struct { 1111 LoggedInUser *oauth.MultiAccountUser 1112 RepoInfo repoinfo.RepoInfo 1113 Issue *models.Issue 1114 Comment *models.IssueComment 1115} 1116 1117func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1118 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1119} 1120 1121type RepoNewPullParams struct { 1122 LoggedInUser *oauth.MultiAccountUser 1123 RepoInfo repoinfo.RepoInfo 1124 Branches []types.Branch 1125 Strategy string 1126 SourceBranch string 1127 TargetBranch string 1128 Title string 1129 Body string 1130 Active string 1131} 1132 1133func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1134 params.Active = "pulls" 1135 return p.executeRepo("repo/pulls/new", w, params) 1136} 1137 1138type RepoPullsParams struct { 1139 LoggedInUser *oauth.MultiAccountUser 1140 RepoInfo repoinfo.RepoInfo 1141 Pulls []*models.Pull 1142 Active string 1143 FilterState string 1144 FilterQuery string 1145 Stacks map[string]models.Stack 1146 Pipelines map[string]models.Pipeline 1147 LabelDefs map[string]*models.LabelDefinition 1148 Page pagination.Page 1149 PullCount int 1150} 1151 1152func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1153 params.Active = "pulls" 1154 return p.executeRepo("repo/pulls/pulls", w, params) 1155} 1156 1157type ResubmitResult uint64 1158 1159const ( 1160 ShouldResubmit ResubmitResult = iota 1161 ShouldNotResubmit 1162 Unknown 1163) 1164 1165func (r ResubmitResult) Yes() bool { 1166 return r == ShouldResubmit 1167} 1168func (r ResubmitResult) No() bool { 1169 return r == ShouldNotResubmit 1170} 1171func (r ResubmitResult) Unknown() bool { 1172 return r == Unknown 1173} 1174 1175type RepoSinglePullParams struct { 1176 LoggedInUser *oauth.MultiAccountUser 1177 RepoInfo repoinfo.RepoInfo 1178 Active string 1179 Pull *models.Pull 1180 Stack models.Stack 1181 AbandonedPulls []*models.Pull 1182 Backlinks []models.RichReferenceLink 1183 BranchDeleteStatus *models.BranchDeleteStatus 1184 MergeCheck types.MergeCheckResponse 1185 ResubmitCheck ResubmitResult 1186 Pipelines map[string]models.Pipeline 1187 Diff types.DiffRenderer 1188 DiffOpts types.DiffOpts 1189 ActiveRound int 1190 IsInterdiff bool 1191 1192 Reactions map[models.ReactionKind]models.ReactionDisplayData 1193 UserReacted map[models.ReactionKind]bool 1194 1195 LabelDefs map[string]*models.LabelDefinition 1196} 1197 1198func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1199 params.Active = "pulls" 1200 return p.executeRepo("repo/pulls/pull", w, params) 1201} 1202 1203type RepoPullPatchParams struct { 1204 LoggedInUser *oauth.MultiAccountUser 1205 RepoInfo repoinfo.RepoInfo 1206 Pull *models.Pull 1207 Stack models.Stack 1208 Diff *types.NiceDiff 1209 Round int 1210 Submission *models.PullSubmission 1211 DiffOpts types.DiffOpts 1212} 1213 1214// this name is a mouthful 1215func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1216 return p.execute("repo/pulls/patch", w, params) 1217} 1218 1219type RepoPullInterdiffParams struct { 1220 LoggedInUser *oauth.MultiAccountUser 1221 RepoInfo repoinfo.RepoInfo 1222 Pull *models.Pull 1223 Round int 1224 Interdiff *patchutil.InterdiffResult 1225 DiffOpts types.DiffOpts 1226} 1227 1228// this name is a mouthful 1229func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1230 return p.execute("repo/pulls/interdiff", w, params) 1231} 1232 1233type PullPatchUploadParams struct { 1234 RepoInfo repoinfo.RepoInfo 1235} 1236 1237func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1238 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1239} 1240 1241type PullCompareBranchesParams struct { 1242 RepoInfo repoinfo.RepoInfo 1243 Branches []types.Branch 1244 SourceBranch string 1245} 1246 1247func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1248 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1249} 1250 1251type PullCompareForkParams struct { 1252 RepoInfo repoinfo.RepoInfo 1253 Forks []models.Repo 1254 Selected string 1255} 1256 1257func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1258 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1259} 1260 1261type PullCompareForkBranchesParams struct { 1262 RepoInfo repoinfo.RepoInfo 1263 SourceBranches []types.Branch 1264 TargetBranches []types.Branch 1265} 1266 1267func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1268 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1269} 1270 1271type PullResubmitParams struct { 1272 LoggedInUser *oauth.MultiAccountUser 1273 RepoInfo repoinfo.RepoInfo 1274 Pull *models.Pull 1275 SubmissionId int 1276} 1277 1278func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1279 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1280} 1281 1282type PullActionsParams struct { 1283 LoggedInUser *oauth.MultiAccountUser 1284 RepoInfo repoinfo.RepoInfo 1285 Pull *models.Pull 1286 RoundNumber int 1287 MergeCheck types.MergeCheckResponse 1288 ResubmitCheck ResubmitResult 1289 BranchDeleteStatus *models.BranchDeleteStatus 1290 Stack models.Stack 1291} 1292 1293func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1294 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1295} 1296 1297type PullNewCommentParams struct { 1298 LoggedInUser *oauth.MultiAccountUser 1299 RepoInfo repoinfo.RepoInfo 1300 Pull *models.Pull 1301 RoundNumber int 1302} 1303 1304func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1305 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1306} 1307 1308type RepoCompareParams struct { 1309 LoggedInUser *oauth.MultiAccountUser 1310 RepoInfo repoinfo.RepoInfo 1311 Forks []models.Repo 1312 Branches []types.Branch 1313 Tags []*types.TagReference 1314 Base string 1315 Head string 1316 Diff *types.NiceDiff 1317 DiffOpts types.DiffOpts 1318 1319 Active string 1320} 1321 1322func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1323 params.Active = "overview" 1324 return p.executeRepo("repo/compare/compare", w, params) 1325} 1326 1327type RepoCompareNewParams struct { 1328 LoggedInUser *oauth.MultiAccountUser 1329 RepoInfo repoinfo.RepoInfo 1330 Forks []models.Repo 1331 Branches []types.Branch 1332 Tags []*types.TagReference 1333 Base string 1334 Head string 1335 1336 Active string 1337} 1338 1339func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1340 params.Active = "overview" 1341 return p.executeRepo("repo/compare/new", w, params) 1342} 1343 1344type RepoCompareAllowPullParams struct { 1345 LoggedInUser *oauth.MultiAccountUser 1346 RepoInfo repoinfo.RepoInfo 1347 Base string 1348 Head string 1349} 1350 1351func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1352 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1353} 1354 1355type RepoCompareDiffFragmentParams struct { 1356 Diff types.NiceDiff 1357 DiffOpts types.DiffOpts 1358} 1359 1360func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1361 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1362} 1363 1364type LabelPanelParams struct { 1365 LoggedInUser *oauth.MultiAccountUser 1366 RepoInfo repoinfo.RepoInfo 1367 Defs map[string]*models.LabelDefinition 1368 Subject string 1369 State models.LabelState 1370} 1371 1372func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1373 return p.executePlain("repo/fragments/labelPanel", w, params) 1374} 1375 1376type EditLabelPanelParams struct { 1377 LoggedInUser *oauth.MultiAccountUser 1378 RepoInfo repoinfo.RepoInfo 1379 Defs map[string]*models.LabelDefinition 1380 Subject string 1381 State models.LabelState 1382} 1383 1384func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1385 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1386} 1387 1388type PipelinesParams struct { 1389 LoggedInUser *oauth.MultiAccountUser 1390 RepoInfo repoinfo.RepoInfo 1391 Pipelines []models.Pipeline 1392 Active string 1393 FilterKind string 1394 Total int64 1395} 1396 1397func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1398 params.Active = "pipelines" 1399 return p.executeRepo("repo/pipelines/pipelines", w, params) 1400} 1401 1402type LogBlockParams struct { 1403 Id int 1404 Name string 1405 Command string 1406 Collapsed bool 1407 StartTime time.Time 1408} 1409 1410func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1411 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1412} 1413 1414type LogBlockEndParams struct { 1415 Id int 1416 StartTime time.Time 1417 EndTime time.Time 1418} 1419 1420func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1421 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1422} 1423 1424type LogLineParams struct { 1425 Id int 1426 Content string 1427} 1428 1429func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1430 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1431} 1432 1433type WorkflowParams struct { 1434 LoggedInUser *oauth.MultiAccountUser 1435 RepoInfo repoinfo.RepoInfo 1436 Pipeline models.Pipeline 1437 Workflow string 1438 LogUrl string 1439 Active string 1440} 1441 1442func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1443 params.Active = "pipelines" 1444 return p.executeRepo("repo/pipelines/workflow", w, params) 1445} 1446 1447type PutStringParams struct { 1448 LoggedInUser *oauth.MultiAccountUser 1449 Action string 1450 1451 // this is supplied in the case of editing an existing string 1452 String models.String 1453} 1454 1455func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1456 return p.execute("strings/put", w, params) 1457} 1458 1459type StringsDashboardParams struct { 1460 LoggedInUser *oauth.MultiAccountUser 1461 Card ProfileCard 1462 Strings []models.String 1463} 1464 1465func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1466 return p.execute("strings/dashboard", w, params) 1467} 1468 1469type StringTimelineParams struct { 1470 LoggedInUser *oauth.MultiAccountUser 1471 Strings []models.String 1472} 1473 1474func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1475 return p.execute("strings/timeline", w, params) 1476} 1477 1478type SingleStringParams struct { 1479 LoggedInUser *oauth.MultiAccountUser 1480 ShowRendered bool 1481 RenderToggle bool 1482 RenderedContents template.HTML 1483 String *models.String 1484 Stats models.StringStats 1485 IsStarred bool 1486 StarCount int 1487 Owner identity.Identity 1488} 1489 1490func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1491 return p.execute("strings/string", w, params) 1492} 1493 1494func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1495 return p.execute("timeline/home", w, params) 1496} 1497 1498func (p *Pages) Static() http.Handler { 1499 if p.dev { 1500 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1501 } 1502 1503 sub, err := fs.Sub(p.embedFS, "static") 1504 if err != nil { 1505 p.logger.Error("no static dir found? that's crazy", "err", err) 1506 panic(err) 1507 } 1508 // Custom handler to apply Cache-Control headers for font files 1509 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1510} 1511 1512func Cache(h http.Handler) http.Handler { 1513 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1514 path := strings.Split(r.URL.Path, "?")[0] 1515 1516 if strings.HasSuffix(path, ".css") { 1517 // on day for css files 1518 w.Header().Set("Cache-Control", "public, max-age=86400") 1519 } else { 1520 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1521 } 1522 h.ServeHTTP(w, r) 1523 }) 1524} 1525 1526func (p *Pages) CssContentHash() string { 1527 cssFile, err := p.embedFS.Open("static/tw.css") 1528 if err != nil { 1529 slog.Debug("Error opening CSS file", "err", err) 1530 return "" 1531 } 1532 defer cssFile.Close() 1533 1534 hasher := sha256.New() 1535 if _, err := io.Copy(hasher, cssFile); err != nil { 1536 slog.Debug("Error hashing CSS file", "err", err) 1537 return "" 1538 } 1539 1540 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1541} 1542 1543func (p *Pages) Error500(w io.Writer) error { 1544 return p.execute("errors/500", w, nil) 1545} 1546 1547func (p *Pages) Error404(w io.Writer) error { 1548 return p.execute("errors/404", w, nil) 1549} 1550 1551func (p *Pages) ErrorKnot404(w io.Writer) error { 1552 return p.execute("errors/knot404", w, nil) 1553} 1554 1555func (p *Pages) Error503(w io.Writer) error { 1556 return p.execute("errors/503", w, nil) 1557}