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