A vibe coded tangled fork which supports pijul.
at 2a2718a4a548236a0c704a596fd836f263e9740d 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 EmailId string 264} 265 266func (p *Pages) Signup(w io.Writer, params SignupParams) error { 267 return p.executeLogin("user/signup", w, params) 268} 269 270func (p *Pages) CompleteSignup(w io.Writer) error { 271 return p.executeLogin("user/completeSignup", w, nil) 272} 273 274type TermsOfServiceParams struct { 275 LoggedInUser *oauth.MultiAccountUser 276 Content template.HTML 277} 278 279func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 280 filename := "terms.md" 281 filePath := filepath.Join("legal", filename) 282 283 file, err := p.embedFS.Open(filePath) 284 if err != nil { 285 return fmt.Errorf("failed to read %s: %w", filename, err) 286 } 287 defer file.Close() 288 289 markdownBytes, err := io.ReadAll(file) 290 if err != nil { 291 return fmt.Errorf("failed to read %s: %w", filename, err) 292 } 293 294 p.rctx.RendererType = markup.RendererTypeDefault 295 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 296 sanitized := p.rctx.SanitizeDefault(htmlString) 297 params.Content = template.HTML(sanitized) 298 299 return p.execute("legal/terms", w, params) 300} 301 302type PrivacyPolicyParams struct { 303 LoggedInUser *oauth.MultiAccountUser 304 Content template.HTML 305} 306 307func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 308 filename := "privacy.md" 309 filePath := filepath.Join("legal", filename) 310 311 file, err := p.embedFS.Open(filePath) 312 if err != nil { 313 return fmt.Errorf("failed to read %s: %w", filename, err) 314 } 315 defer file.Close() 316 317 markdownBytes, err := io.ReadAll(file) 318 if err != nil { 319 return fmt.Errorf("failed to read %s: %w", filename, err) 320 } 321 322 p.rctx.RendererType = markup.RendererTypeDefault 323 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 324 sanitized := p.rctx.SanitizeDefault(htmlString) 325 params.Content = template.HTML(sanitized) 326 327 return p.execute("legal/privacy", w, params) 328} 329 330type BrandParams struct { 331 LoggedInUser *oauth.MultiAccountUser 332} 333 334func (p *Pages) Brand(w io.Writer, params BrandParams) error { 335 return p.execute("brand/brand", w, params) 336} 337 338type TimelineParams struct { 339 LoggedInUser *oauth.MultiAccountUser 340 Timeline []models.TimelineEvent 341 Repos []models.Repo 342 GfiLabel *models.LabelDefinition 343 BlueskyPosts []models.BskyPost 344} 345 346func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 347 return p.execute("timeline/timeline", w, params) 348} 349 350type GoodFirstIssuesParams struct { 351 LoggedInUser *oauth.MultiAccountUser 352 Issues []models.Issue 353 RepoGroups []*models.RepoGroup 354 LabelDefs map[string]*models.LabelDefinition 355 GfiLabel *models.LabelDefinition 356 Page pagination.Page 357} 358 359func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 360 return p.execute("goodfirstissues/index", w, params) 361} 362 363type UserProfileSettingsParams struct { 364 LoggedInUser *oauth.MultiAccountUser 365 Tab string 366 PunchcardPreference models.PunchcardPreference 367} 368 369func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 370 params.Tab = "profile" 371 return p.execute("user/settings/profile", w, params) 372} 373 374type NotificationsParams struct { 375 LoggedInUser *oauth.MultiAccountUser 376 Notifications []*models.NotificationWithEntity 377 UnreadCount int 378 Page pagination.Page 379 Total int64 380} 381 382func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 383 return p.execute("notifications/list", w, params) 384} 385 386type NotificationItemParams struct { 387 Notification *models.Notification 388} 389 390func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 391 return p.executePlain("notifications/fragments/item", w, params) 392} 393 394type NotificationCountParams struct { 395 Count int64 396} 397 398func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 399 return p.executePlain("notifications/fragments/count", w, params) 400} 401 402type UserKeysSettingsParams struct { 403 LoggedInUser *oauth.MultiAccountUser 404 PubKeys []models.PublicKey 405 Tab string 406} 407 408func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 409 params.Tab = "keys" 410 return p.execute("user/settings/keys", w, params) 411} 412 413type UserEmailsSettingsParams struct { 414 LoggedInUser *oauth.MultiAccountUser 415 Emails []models.Email 416 Tab string 417} 418 419func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 420 params.Tab = "emails" 421 return p.execute("user/settings/emails", w, params) 422} 423 424type UserNotificationSettingsParams struct { 425 LoggedInUser *oauth.MultiAccountUser 426 Preferences *models.NotificationPreferences 427 Tab string 428} 429 430func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 431 params.Tab = "notifications" 432 return p.execute("user/settings/notifications", w, params) 433} 434 435type UpgradeBannerParams struct { 436 Registrations []models.Registration 437 Spindles []models.Spindle 438} 439 440func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 441 return p.executePlain("banner", w, params) 442} 443 444type KnotsParams struct { 445 LoggedInUser *oauth.MultiAccountUser 446 Registrations []models.Registration 447 Tab 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} 512 513func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 514 return p.execute("repo/new", w, params) 515} 516 517type ForkRepoParams struct { 518 LoggedInUser *oauth.MultiAccountUser 519 Knots []string 520 RepoInfo repoinfo.RepoInfo 521} 522 523func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 524 return p.execute("repo/fork", w, params) 525} 526 527type ProfileCard struct { 528 UserDid string 529 HasProfile bool 530 FollowStatus models.FollowStatus 531 Punchcard *models.Punchcard 532 Profile *models.Profile 533 Stats ProfileStats 534 Active string 535} 536 537type ProfileStats struct { 538 RepoCount int64 539 StarredCount int64 540 StringCount int64 541 FollowersCount int64 542 FollowingCount int64 543} 544 545func (p *ProfileCard) GetTabs() [][]any { 546 tabs := [][]any{ 547 {"overview", "overview", "square-chart-gantt", nil}, 548 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 549 {"starred", "starred", "star", p.Stats.StarredCount}, 550 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 551 } 552 553 return tabs 554} 555 556type ProfileOverviewParams struct { 557 LoggedInUser *oauth.MultiAccountUser 558 Repos []models.Repo 559 CollaboratingRepos []models.Repo 560 ProfileTimeline *models.ProfileTimeline 561 Card *ProfileCard 562 Active string 563 ShowPunchcard bool 564} 565 566func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 567 params.Active = "overview" 568 return p.executeProfile("user/overview", w, params) 569} 570 571type ProfileReposParams struct { 572 LoggedInUser *oauth.MultiAccountUser 573 Repos []models.Repo 574 Card *ProfileCard 575 Active string 576} 577 578func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 579 params.Active = "repos" 580 return p.executeProfile("user/repos", w, params) 581} 582 583type ProfileStarredParams struct { 584 LoggedInUser *oauth.MultiAccountUser 585 Repos []models.Repo 586 Card *ProfileCard 587 Active string 588} 589 590func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 591 params.Active = "starred" 592 return p.executeProfile("user/starred", w, params) 593} 594 595type ProfileStringsParams struct { 596 LoggedInUser *oauth.MultiAccountUser 597 Strings []models.String 598 Card *ProfileCard 599 Active string 600} 601 602func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 603 params.Active = "strings" 604 return p.executeProfile("user/strings", w, params) 605} 606 607type FollowCard struct { 608 UserDid string 609 LoggedInUser *oauth.MultiAccountUser 610 FollowStatus models.FollowStatus 611 FollowersCount int64 612 FollowingCount int64 613 Profile *models.Profile 614} 615 616type ProfileFollowersParams struct { 617 LoggedInUser *oauth.MultiAccountUser 618 Followers []FollowCard 619 Card *ProfileCard 620 Active string 621} 622 623func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 624 params.Active = "overview" 625 return p.executeProfile("user/followers", w, params) 626} 627 628type ProfileFollowingParams struct { 629 LoggedInUser *oauth.MultiAccountUser 630 Following []FollowCard 631 Card *ProfileCard 632 Active string 633} 634 635func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 636 params.Active = "overview" 637 return p.executeProfile("user/following", w, params) 638} 639 640type FollowFragmentParams struct { 641 UserDid string 642 FollowStatus models.FollowStatus 643 FollowersCount int64 644} 645 646func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 647 return p.executePlain("user/fragments/follow-oob", w, params) 648} 649 650type EditBioParams struct { 651 LoggedInUser *oauth.MultiAccountUser 652 Profile *models.Profile 653} 654 655func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 656 return p.executePlain("user/fragments/editBio", w, params) 657} 658 659type EditPinsParams struct { 660 LoggedInUser *oauth.MultiAccountUser 661 Profile *models.Profile 662 AllRepos []PinnedRepo 663} 664 665type PinnedRepo struct { 666 IsPinned bool 667 models.Repo 668} 669 670func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 671 return p.executePlain("user/fragments/editPins", w, params) 672} 673 674type StarBtnFragmentParams struct { 675 IsStarred bool 676 SubjectAt syntax.ATURI 677 StarCount int 678 HxSwapOob bool 679} 680 681func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 682 params.HxSwapOob = true 683 return p.executePlain("fragments/starBtn", w, params) 684} 685 686type RepoIndexParams struct { 687 LoggedInUser *oauth.MultiAccountUser 688 RepoInfo repoinfo.RepoInfo 689 Active string 690 TagMap map[string][]string 691 CommitsTrunc []types.Commit 692 TagsTrunc []*types.TagReference 693 BranchesTrunc []types.Branch 694 // ForkInfo *types.ForkInfo 695 HTMLReadme template.HTML 696 Raw bool 697 EmailToDid map[string]string 698 VerifiedCommits commitverify.VerifiedCommits 699 Languages []types.RepoLanguageDetails 700 Pipelines map[string]models.Pipeline 701 NeedsKnotUpgrade bool 702 KnotUnreachable bool 703 types.RepoIndexResponse 704} 705 706func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 707 params.Active = "overview" 708 if params.IsEmpty { 709 return p.executeRepo("repo/empty", w, params) 710 } 711 712 if params.NeedsKnotUpgrade { 713 return p.executeRepo("repo/needsUpgrade", w, params) 714 } 715 716 if params.KnotUnreachable { 717 return p.executeRepo("repo/knotUnreachable", w, params) 718 } 719 720 p.rctx.RepoInfo = params.RepoInfo 721 p.rctx.RepoInfo.Ref = params.Ref 722 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 723 724 if params.ReadmeFileName != "" { 725 ext := filepath.Ext(params.ReadmeFileName) 726 switch ext { 727 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 728 params.Raw = false 729 htmlString := p.rctx.RenderMarkdown(params.Readme) 730 sanitized := p.rctx.SanitizeDefault(htmlString) 731 params.HTMLReadme = template.HTML(sanitized) 732 default: 733 params.Raw = true 734 } 735 } 736 737 return p.executeRepo("repo/index", w, params) 738} 739 740type RepoLogParams struct { 741 LoggedInUser *oauth.MultiAccountUser 742 RepoInfo repoinfo.RepoInfo 743 TagMap map[string][]string 744 Active string 745 EmailToDid map[string]string 746 VerifiedCommits commitverify.VerifiedCommits 747 Pipelines map[string]models.Pipeline 748 749 types.RepoLogResponse 750} 751 752func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 753 params.Active = "overview" 754 return p.executeRepo("repo/log", w, params) 755} 756 757type RepoCommitParams struct { 758 LoggedInUser *oauth.MultiAccountUser 759 RepoInfo repoinfo.RepoInfo 760 Active string 761 EmailToDid map[string]string 762 Pipeline *models.Pipeline 763 DiffOpts types.DiffOpts 764 765 // singular because it's always going to be just one 766 VerifiedCommit commitverify.VerifiedCommits 767 768 types.RepoCommitResponse 769} 770 771func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 772 params.Active = "overview" 773 return p.executeRepo("repo/commit", w, params) 774} 775 776type RepoTreeParams struct { 777 LoggedInUser *oauth.MultiAccountUser 778 RepoInfo repoinfo.RepoInfo 779 Active string 780 BreadCrumbs [][]string 781 TreePath string 782 Raw bool 783 HTMLReadme template.HTML 784 EmailToDid map[string]string 785 LastCommitInfo *types.LastCommitInfo 786 types.RepoTreeResponse 787} 788 789type RepoTreeStats struct { 790 NumFolders uint64 791 NumFiles uint64 792} 793 794func (r RepoTreeParams) TreeStats() RepoTreeStats { 795 numFolders, numFiles := 0, 0 796 for _, f := range r.Files { 797 if !f.IsFile() { 798 numFolders += 1 799 } else if f.IsFile() { 800 numFiles += 1 801 } 802 } 803 804 return RepoTreeStats{ 805 NumFolders: uint64(numFolders), 806 NumFiles: uint64(numFiles), 807 } 808} 809 810func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 811 params.Active = "overview" 812 813 p.rctx.RepoInfo = params.RepoInfo 814 p.rctx.RepoInfo.Ref = params.Ref 815 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 816 817 if params.ReadmeFileName != "" { 818 ext := filepath.Ext(params.ReadmeFileName) 819 switch ext { 820 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 821 params.Raw = false 822 htmlString := p.rctx.RenderMarkdown(params.Readme) 823 sanitized := p.rctx.SanitizeDefault(htmlString) 824 params.HTMLReadme = template.HTML(sanitized) 825 default: 826 params.Raw = true 827 } 828 } 829 830 return p.executeRepo("repo/tree", w, params) 831} 832 833type RepoBranchesParams struct { 834 LoggedInUser *oauth.MultiAccountUser 835 RepoInfo repoinfo.RepoInfo 836 Active string 837 types.RepoBranchesResponse 838} 839 840func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 841 params.Active = "overview" 842 return p.executeRepo("repo/branches", w, params) 843} 844 845type RepoTagsParams struct { 846 LoggedInUser *oauth.MultiAccountUser 847 RepoInfo repoinfo.RepoInfo 848 Active string 849 types.RepoTagsResponse 850 ArtifactMap map[plumbing.Hash][]models.Artifact 851 DanglingArtifacts []models.Artifact 852} 853 854func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 855 params.Active = "overview" 856 return p.executeRepo("repo/tags", w, params) 857} 858 859type RepoTagParams struct { 860 LoggedInUser *oauth.MultiAccountUser 861 RepoInfo repoinfo.RepoInfo 862 Active string 863 types.RepoTagResponse 864 ArtifactMap map[plumbing.Hash][]models.Artifact 865 DanglingArtifacts []models.Artifact 866} 867 868func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 869 params.Active = "overview" 870 return p.executeRepo("repo/tag", w, params) 871} 872 873type RepoArtifactParams struct { 874 LoggedInUser *oauth.MultiAccountUser 875 RepoInfo repoinfo.RepoInfo 876 Artifact models.Artifact 877} 878 879func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 880 return p.executePlain("repo/fragments/artifact", w, params) 881} 882 883type RepoBlobParams struct { 884 LoggedInUser *oauth.MultiAccountUser 885 RepoInfo repoinfo.RepoInfo 886 Active string 887 BreadCrumbs [][]string 888 BlobView models.BlobView 889 EmailToDid map[string]string 890 LastCommitInfo *types.LastCommitInfo 891 *tangled.RepoBlob_Output 892} 893 894func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 895 switch params.BlobView.ContentType { 896 case models.BlobContentTypeMarkup: 897 p.rctx.RepoInfo = params.RepoInfo 898 } 899 900 params.Active = "overview" 901 return p.executeRepo("repo/blob", w, params) 902} 903 904type Collaborator struct { 905 Did string 906 Role string 907} 908 909type RepoSettingsParams struct { 910 LoggedInUser *oauth.MultiAccountUser 911 RepoInfo repoinfo.RepoInfo 912 Collaborators []Collaborator 913 Active string 914 Branches []types.Branch 915 Spindles []string 916 CurrentSpindle string 917 Secrets []*tangled.RepoListSecrets_Secret 918 919 // TODO: use repoinfo.roles 920 IsCollaboratorInviteAllowed bool 921} 922 923func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 924 params.Active = "settings" 925 return p.executeRepo("repo/settings", w, params) 926} 927 928type RepoGeneralSettingsParams struct { 929 LoggedInUser *oauth.MultiAccountUser 930 RepoInfo repoinfo.RepoInfo 931 Labels []models.LabelDefinition 932 DefaultLabels []models.LabelDefinition 933 SubscribedLabels map[string]struct{} 934 ShouldSubscribeAll bool 935 Active string 936 Tab string 937 Branches []types.Branch 938} 939 940func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 941 params.Active = "settings" 942 params.Tab = "general" 943 return p.executeRepo("repo/settings/general", w, params) 944} 945 946type RepoAccessSettingsParams struct { 947 LoggedInUser *oauth.MultiAccountUser 948 RepoInfo repoinfo.RepoInfo 949 Active string 950 Tab string 951 Collaborators []Collaborator 952} 953 954func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 955 params.Active = "settings" 956 params.Tab = "access" 957 return p.executeRepo("repo/settings/access", w, params) 958} 959 960type RepoPipelineSettingsParams struct { 961 LoggedInUser *oauth.MultiAccountUser 962 RepoInfo repoinfo.RepoInfo 963 Active string 964 Tab string 965 Spindles []string 966 CurrentSpindle string 967 Secrets []map[string]any 968} 969 970func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 971 params.Active = "settings" 972 params.Tab = "pipelines" 973 return p.executeRepo("repo/settings/pipelines", w, params) 974} 975 976type RepoWebhooksSettingsParams struct { 977 LoggedInUser *oauth.MultiAccountUser 978 RepoInfo repoinfo.RepoInfo 979 Active string 980 Tab string 981 Webhooks []models.Webhook 982 WebhookDeliveries map[int64][]models.WebhookDelivery 983} 984 985func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 986 params.Active = "settings" 987 params.Tab = "hooks" 988 return p.executeRepo("repo/settings/hooks", w, params) 989} 990 991type WebhookDeliveriesListParams struct { 992 LoggedInUser *oauth.MultiAccountUser 993 RepoInfo repoinfo.RepoInfo 994 Webhook *models.Webhook 995 Deliveries []models.WebhookDelivery 996} 997 998func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 999 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1000 if err != nil { 1001 return err 1002 } 1003 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1004} 1005 1006type RepoIssuesParams struct { 1007 LoggedInUser *oauth.MultiAccountUser 1008 RepoInfo repoinfo.RepoInfo 1009 Active string 1010 Issues []models.Issue 1011 IssueCount int 1012 LabelDefs map[string]*models.LabelDefinition 1013 Page pagination.Page 1014 FilterState string 1015 FilterQuery string 1016} 1017 1018func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1019 params.Active = "issues" 1020 return p.executeRepo("repo/issues/issues", w, params) 1021} 1022 1023type RepoSingleIssueParams struct { 1024 LoggedInUser *oauth.MultiAccountUser 1025 RepoInfo repoinfo.RepoInfo 1026 Active string 1027 Issue *models.Issue 1028 CommentList []models.CommentListItem 1029 Backlinks []models.RichReferenceLink 1030 LabelDefs map[string]*models.LabelDefinition 1031 1032 Reactions map[models.ReactionKind]models.ReactionDisplayData 1033 UserReacted map[models.ReactionKind]bool 1034} 1035 1036func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1037 params.Active = "issues" 1038 return p.executeRepo("repo/issues/issue", w, params) 1039} 1040 1041type EditIssueParams struct { 1042 LoggedInUser *oauth.MultiAccountUser 1043 RepoInfo repoinfo.RepoInfo 1044 Issue *models.Issue 1045 Action string 1046} 1047 1048func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1049 params.Action = "edit" 1050 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1051} 1052 1053type ThreadReactionFragmentParams struct { 1054 ThreadAt syntax.ATURI 1055 Kind models.ReactionKind 1056 Count int 1057 Users []string 1058 IsReacted bool 1059} 1060 1061func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1062 return p.executePlain("repo/fragments/reaction", w, params) 1063} 1064 1065type RepoNewIssueParams struct { 1066 LoggedInUser *oauth.MultiAccountUser 1067 RepoInfo repoinfo.RepoInfo 1068 Issue *models.Issue // existing issue if any -- passed when editing 1069 Active string 1070 Action string 1071} 1072 1073func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1074 params.Active = "issues" 1075 params.Action = "create" 1076 return p.executeRepo("repo/issues/new", w, params) 1077} 1078 1079type EditIssueCommentParams struct { 1080 LoggedInUser *oauth.MultiAccountUser 1081 RepoInfo repoinfo.RepoInfo 1082 Issue *models.Issue 1083 Comment *models.IssueComment 1084} 1085 1086func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1087 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1088} 1089 1090type ReplyIssueCommentPlaceholderParams struct { 1091 LoggedInUser *oauth.MultiAccountUser 1092 RepoInfo repoinfo.RepoInfo 1093 Issue *models.Issue 1094 Comment *models.IssueComment 1095} 1096 1097func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1098 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1099} 1100 1101type ReplyIssueCommentParams struct { 1102 LoggedInUser *oauth.MultiAccountUser 1103 RepoInfo repoinfo.RepoInfo 1104 Issue *models.Issue 1105 Comment *models.IssueComment 1106} 1107 1108func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1109 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1110} 1111 1112type IssueCommentBodyParams struct { 1113 LoggedInUser *oauth.MultiAccountUser 1114 RepoInfo repoinfo.RepoInfo 1115 Issue *models.Issue 1116 Comment *models.IssueComment 1117} 1118 1119func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1120 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1121} 1122 1123type RepoNewPullParams struct { 1124 LoggedInUser *oauth.MultiAccountUser 1125 RepoInfo repoinfo.RepoInfo 1126 Branches []types.Branch 1127 Strategy string 1128 SourceBranch string 1129 TargetBranch string 1130 Title string 1131 Body string 1132 Active string 1133} 1134 1135func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1136 params.Active = "pulls" 1137 return p.executeRepo("repo/pulls/new", w, params) 1138} 1139 1140type RepoPullsParams struct { 1141 LoggedInUser *oauth.MultiAccountUser 1142 RepoInfo repoinfo.RepoInfo 1143 Pulls []*models.Pull 1144 Active string 1145 FilterState string 1146 FilterQuery string 1147 Stacks map[string]models.Stack 1148 Pipelines map[string]models.Pipeline 1149 LabelDefs map[string]*models.LabelDefinition 1150 Page pagination.Page 1151 PullCount int 1152} 1153 1154func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1155 params.Active = "pulls" 1156 return p.executeRepo("repo/pulls/pulls", w, params) 1157} 1158 1159type ResubmitResult uint64 1160 1161const ( 1162 ShouldResubmit ResubmitResult = iota 1163 ShouldNotResubmit 1164 Unknown 1165) 1166 1167func (r ResubmitResult) Yes() bool { 1168 return r == ShouldResubmit 1169} 1170func (r ResubmitResult) No() bool { 1171 return r == ShouldNotResubmit 1172} 1173func (r ResubmitResult) Unknown() bool { 1174 return r == Unknown 1175} 1176 1177type RepoSinglePullParams struct { 1178 LoggedInUser *oauth.MultiAccountUser 1179 RepoInfo repoinfo.RepoInfo 1180 Active string 1181 Pull *models.Pull 1182 Stack models.Stack 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}