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