From 7381cda7b8bec00906ae70571b65ae26b1683b58 Mon Sep 17 00:00:00 2001 From: Adam French Date: Tue, 17 Mar 2026 00:14:59 +0000 Subject: [PATCH] Move Gitea feed from frontend to backend with cached GraphQL proxy Replaces direct browser-to-Gitea API calls with a backend service that proxies and caches the feed (1-min TTL), served via the existing GraphQL HomeData query. Commit message parsing now happens server-side. Co-Authored-By: Claude Sonnet 4.6 --- backend/graph/generated.go | 364 +++++++++++++++++- backend/graph/gitea_helpers.go | 17 + backend/graph/model/models_gen.go | 9 + backend/graph/schema.resolvers.go | 22 +- backend/graph/schema/gitea.graphql | 8 + backend/graph/schema/schema.graphql | 1 + backend/handlers/store.go | 12 + backend/main.go | 5 +- backend/services/gitea.go | 72 ++++ .../vue/src/components/util/CommitHistory.vue | 45 +-- nginx/vue/src/stores/homeData.js | 12 +- 11 files changed, 520 insertions(+), 47 deletions(-) create mode 100644 backend/graph/gitea_helpers.go create mode 100644 backend/graph/schema/gitea.graphql create mode 100644 backend/services/gitea.go diff --git a/backend/graph/generated.go b/backend/graph/generated.go index e1e8c35..d839582 100644 --- a/backend/graph/generated.go +++ b/backend/graph/generated.go @@ -66,6 +66,15 @@ type ComplexityRoot struct { UpdatedAt func(childComplexity int) int } + GiteaFeedItem struct { + AvatarURL func(childComplexity int) int + CommitMessage func(childComplexity int) int + CreatedAt func(childComplexity int) int + OpType func(childComplexity int) int + RepoName func(childComplexity int) int + RepoURL func(childComplexity int) int + } + Message struct { AuthorID func(childComplexity int) int Content func(childComplexity int) int @@ -100,6 +109,7 @@ type ComplexityRoot struct { Query struct { Activities func(childComplexity int) int Favorites func(childComplexity int) int + GiteaFeed func(childComplexity int) int Me func(childComplexity int) int Messages func(childComplexity int) int Post func(childComplexity int, id int) int @@ -197,6 +207,7 @@ type QueryResolver interface { Messages(ctx context.Context) ([]*models.Message, error) SpotifyListening(ctx context.Context) (*model.SpotifyPlaying, error) SpotifyRecent(ctx context.Context) ([]*model.SpotifyRecentItem, error) + GiteaFeed(ctx context.Context) (*model.GiteaFeedItem, error) Me(ctx context.Context) (*models.User, error) } type RowingResolver interface { @@ -304,6 +315,43 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.Favorite.UpdatedAt(childComplexity), true + case "GiteaFeedItem.avatarUrl": + if e.ComplexityRoot.GiteaFeedItem.AvatarURL == nil { + break + } + + return e.ComplexityRoot.GiteaFeedItem.AvatarURL(childComplexity), true + case "GiteaFeedItem.commitMessage": + if e.ComplexityRoot.GiteaFeedItem.CommitMessage == nil { + break + } + + return e.ComplexityRoot.GiteaFeedItem.CommitMessage(childComplexity), true + case "GiteaFeedItem.createdAt": + if e.ComplexityRoot.GiteaFeedItem.CreatedAt == nil { + break + } + + return e.ComplexityRoot.GiteaFeedItem.CreatedAt(childComplexity), true + case "GiteaFeedItem.opType": + if e.ComplexityRoot.GiteaFeedItem.OpType == nil { + break + } + + return e.ComplexityRoot.GiteaFeedItem.OpType(childComplexity), true + case "GiteaFeedItem.repoName": + if e.ComplexityRoot.GiteaFeedItem.RepoName == nil { + break + } + + return e.ComplexityRoot.GiteaFeedItem.RepoName(childComplexity), true + case "GiteaFeedItem.repoUrl": + if e.ComplexityRoot.GiteaFeedItem.RepoURL == nil { + break + } + + return e.ComplexityRoot.GiteaFeedItem.RepoURL(childComplexity), true + case "Message.authorId": if e.ComplexityRoot.Message.AuthorID == nil { break @@ -496,6 +544,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.Query.Favorites(childComplexity), true + case "Query.giteaFeed": + if e.ComplexityRoot.Query.GiteaFeed == nil { + break + } + + return e.ComplexityRoot.Query.GiteaFeed(childComplexity), true case "Query.me": if e.ComplexityRoot.Query.Me == nil { @@ -796,7 +850,7 @@ func newExecutionContext( } } -//go:embed "schema/activity.graphql" "schema/auth.graphql" "schema/favorite.graphql" "schema/message.graphql" "schema/post.graphql" "schema/rowing.graphql" "schema/schema.graphql" "schema/spotify.graphql" "schema/user.graphql" +//go:embed "schema/activity.graphql" "schema/auth.graphql" "schema/favorite.graphql" "schema/gitea.graphql" "schema/message.graphql" "schema/post.graphql" "schema/rowing.graphql" "schema/schema.graphql" "schema/spotify.graphql" "schema/user.graphql" var sourcesFS embed.FS func sourceData(filename string) string { @@ -811,6 +865,7 @@ var sources = []*ast.Source{ {Name: "schema/activity.graphql", Input: sourceData("schema/activity.graphql"), BuiltIn: false}, {Name: "schema/auth.graphql", Input: sourceData("schema/auth.graphql"), BuiltIn: false}, {Name: "schema/favorite.graphql", Input: sourceData("schema/favorite.graphql"), BuiltIn: false}, + {Name: "schema/gitea.graphql", Input: sourceData("schema/gitea.graphql"), BuiltIn: false}, {Name: "schema/message.graphql", Input: sourceData("schema/message.graphql"), BuiltIn: false}, {Name: "schema/post.graphql", Input: sourceData("schema/post.graphql"), BuiltIn: false}, {Name: "schema/rowing.graphql", Input: sourceData("schema/rowing.graphql"), BuiltIn: false}, @@ -1407,6 +1462,180 @@ func (ec *executionContext) fieldContext_Favorite_link(_ context.Context, field return fc, nil } +func (ec *executionContext) _GiteaFeedItem_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_GiteaFeedItem_avatarUrl, + func(ctx context.Context) (any, error) { + return obj.AvatarURL, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_GiteaFeedItem_avatarUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GiteaFeedItem", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GiteaFeedItem_repoUrl(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_GiteaFeedItem_repoUrl, + func(ctx context.Context) (any, error) { + return obj.RepoURL, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_GiteaFeedItem_repoUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GiteaFeedItem", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GiteaFeedItem_repoName(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_GiteaFeedItem_repoName, + func(ctx context.Context) (any, error) { + return obj.RepoName, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_GiteaFeedItem_repoName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GiteaFeedItem", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GiteaFeedItem_opType(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_GiteaFeedItem_opType, + func(ctx context.Context) (any, error) { + return obj.OpType, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_GiteaFeedItem_opType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GiteaFeedItem", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GiteaFeedItem_commitMessage(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_GiteaFeedItem_commitMessage, + func(ctx context.Context) (any, error) { + return obj.CommitMessage, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_GiteaFeedItem_commitMessage(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GiteaFeedItem", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _GiteaFeedItem_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.GiteaFeedItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_GiteaFeedItem_createdAt, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + ec.marshalNTime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_GiteaFeedItem_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GiteaFeedItem", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Time does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Message_id(ctx context.Context, field graphql.CollectedField, obj *models.Message) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -2713,6 +2942,49 @@ func (ec *executionContext) fieldContext_Query_spotifyRecent(_ context.Context, return fc, nil } +func (ec *executionContext) _Query_giteaFeed(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Query_giteaFeed, + func(ctx context.Context) (any, error) { + return ec.Resolvers.Query().GiteaFeed(ctx) + }, + nil, + ec.marshalOGiteaFeedItem2ᚖadamᚑfrenchᚗcoᚗukᚋbackendᚋgraphᚋmodelᚐGiteaFeedItem, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Query_giteaFeed(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "avatarUrl": + return ec.fieldContext_GiteaFeedItem_avatarUrl(ctx, field) + case "repoUrl": + return ec.fieldContext_GiteaFeedItem_repoUrl(ctx, field) + case "repoName": + return ec.fieldContext_GiteaFeedItem_repoName(ctx, field) + case "opType": + return ec.fieldContext_GiteaFeedItem_opType(ctx, field) + case "commitMessage": + return ec.fieldContext_GiteaFeedItem_commitMessage(ctx, field) + case "createdAt": + return ec.fieldContext_GiteaFeedItem_createdAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type GiteaFeedItem", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -5472,6 +5744,70 @@ func (ec *executionContext) _Favorite(ctx context.Context, sel ast.SelectionSet, return out } +var giteaFeedItemImplementors = []string{"GiteaFeedItem"} + +func (ec *executionContext) _GiteaFeedItem(ctx context.Context, sel ast.SelectionSet, obj *model.GiteaFeedItem) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, giteaFeedItemImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("GiteaFeedItem") + case "avatarUrl": + out.Values[i] = ec._GiteaFeedItem_avatarUrl(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "repoUrl": + out.Values[i] = ec._GiteaFeedItem_repoUrl(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "repoName": + out.Values[i] = ec._GiteaFeedItem_repoName(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "opType": + out.Values[i] = ec._GiteaFeedItem_opType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "commitMessage": + out.Values[i] = ec._GiteaFeedItem_commitMessage(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdAt": + out.Values[i] = ec._GiteaFeedItem_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var messageImplementors = []string{"Message"} func (ec *executionContext) _Message(ctx context.Context, sel ast.SelectionSet, obj *models.Message) graphql.Marshaler { @@ -6030,6 +6366,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "giteaFeed": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_giteaFeed(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "me": field := field @@ -7505,6 +7860,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } +func (ec *executionContext) marshalOGiteaFeedItem2ᚖadamᚑfrenchᚗcoᚗukᚋbackendᚋgraphᚋmodelᚐGiteaFeedItem(ctx context.Context, sel ast.SelectionSet, v *model.GiteaFeedItem) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._GiteaFeedItem(ctx, sel, v) +} + func (ec *executionContext) marshalOPost2ᚖadamᚑfrenchᚗcoᚗukᚋbackendᚋmodelsᚐPost(ctx context.Context, sel ast.SelectionSet, v *models.Post) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/backend/graph/gitea_helpers.go b/backend/graph/gitea_helpers.go new file mode 100644 index 0000000..95fad87 --- /dev/null +++ b/backend/graph/gitea_helpers.go @@ -0,0 +1,17 @@ +package graph + +import ( + "adam-french.co.uk/backend/graph/model" + "adam-french.co.uk/backend/services" +) + +func mapGiteaFeed(feed *services.GiteaFeedResponse) *model.GiteaFeedItem { + return &model.GiteaFeedItem{ + AvatarURL: feed.ActUser.AvatarURL, + RepoURL: feed.Repo.HTMLURL, + RepoName: feed.Repo.FullName, + OpType: feed.OpType, + CommitMessage: services.ParseCommitMessage(feed.Content), + CreatedAt: feed.Created, + } +} diff --git a/backend/graph/model/models_gen.go b/backend/graph/model/models_gen.go index 77b28df..1954b66 100644 --- a/backend/graph/model/models_gen.go +++ b/backend/graph/model/models_gen.go @@ -34,6 +34,15 @@ type CreateUserInput struct { Password string `json:"password"` } +type GiteaFeedItem struct { + AvatarURL string `json:"avatarUrl"` + RepoURL string `json:"repoUrl"` + RepoName string `json:"repoName"` + OpType string `json:"opType"` + CommitMessage string `json:"commitMessage"` + CreatedAt time.Time `json:"createdAt"` +} + type LoginInput struct { Username string `json:"username"` Password string `json:"password"` diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 2d86da3..934cf8a 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -13,6 +13,7 @@ import ( "adam-french.co.uk/backend/graph/model" "adam-french.co.uk/backend/models" + "adam-french.co.uk/backend/services" spotify "github.com/zmb3/spotify/v2" "golang.org/x/crypto/bcrypt" ) @@ -429,6 +430,26 @@ func (r *queryResolver) SpotifyRecent(ctx context.Context) ([]*model.SpotifyRece return mapRecentItems(played), nil } +// GiteaFeed is the resolver for the giteaFeed field. +func (r *queryResolver) GiteaFeed(ctx context.Context) (*model.GiteaFeedItem, error) { + if r.Store.GiteaFeedFresh() { + return mapGiteaFeed(r.Store.GiteaFeed), nil + } + + feed, err := services.FetchLatestFeed(r.Store.GiteaHost, r.Store.GiteaPort) + if err != nil { + return nil, err + } + if feed == nil { + return nil, nil + } + + r.Store.GiteaFeed = feed + r.Store.GiteaFeedFetchedAt = time.Now() + + return mapGiteaFeed(feed), nil +} + // Me is the resolver for the me field. func (r *queryResolver) Me(ctx context.Context) (*models.User, error) { userID, ok := UserIDFromCtx(ctx) @@ -453,4 +474,3 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } - diff --git a/backend/graph/schema/gitea.graphql b/backend/graph/schema/gitea.graphql new file mode 100644 index 0000000..1749736 --- /dev/null +++ b/backend/graph/schema/gitea.graphql @@ -0,0 +1,8 @@ +type GiteaFeedItem { + avatarUrl: String! + repoUrl: String! + repoName: String! + opType: String! + commitMessage: String! + createdAt: Time! +} diff --git a/backend/graph/schema/schema.graphql b/backend/graph/schema/schema.graphql index 93bdca1..43cd9c3 100644 --- a/backend/graph/schema/schema.graphql +++ b/backend/graph/schema/schema.graphql @@ -11,6 +11,7 @@ type Query { messages: [Message!]! spotifyListening: SpotifyPlaying spotifyRecent: [SpotifyRecentItem!]! + giteaFeed: GiteaFeedItem me: User } diff --git a/backend/handlers/store.go b/backend/handlers/store.go index 9ed6f47..74e2eea 100644 --- a/backend/handlers/store.go +++ b/backend/handlers/store.go @@ -20,4 +20,16 @@ type Store struct { RecentSongs *[]spotify.RecentlyPlayedItem RecentSongsFetchedAt time.Time + + GiteaHost string + GiteaPort string + GiteaFeed *services.GiteaFeedResponse + GiteaFeedFetchedAt time.Time +} + +func (s *Store) GiteaFeedFresh() bool { + if s.GiteaFeed == nil { + return false + } + return time.Since(s.GiteaFeedFetchedAt) < time.Minute } diff --git a/backend/main.go b/backend/main.go index bacb9b7..cc8f2d2 100644 --- a/backend/main.go +++ b/backend/main.go @@ -71,7 +71,10 @@ func main() { notesConfig := services.NotesConfig{Dir: notesDir} notes := services.InitNotes(¬esConfig) - store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes} + giteaHost := os.Getenv("GITEA_HOST") + giteaPort := os.Getenv("GITEA_PORT") + + store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes, GiteaHost: giteaHost, GiteaPort: giteaPort} protected := r.Group("/", store.AuthMiddlewear) admin := r.Group("/", store.AuthMiddlewear, store.AdminMiddleware) diff --git a/backend/services/gitea.go b/backend/services/gitea.go new file mode 100644 index 0000000..99c3903 --- /dev/null +++ b/backend/services/gitea.go @@ -0,0 +1,72 @@ +package services + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type GiteaFeedCommit struct { + Message string `json:"Message"` +} + +type GiteaFeedContent struct { + Commits []GiteaFeedCommit `json:"Commits"` +} + +type GiteaFeedUser struct { + AvatarURL string `json:"avatar_url"` +} + +type GiteaFeedRepo struct { + FullName string `json:"full_name"` + HTMLURL string `json:"html_url"` +} + +type GiteaFeedResponse struct { + ActUser GiteaFeedUser `json:"act_user"` + Repo GiteaFeedRepo `json:"repo"` + OpType string `json:"op_type"` + Content string `json:"content"` + Created time.Time `json:"created"` +} + +func FetchLatestFeed(host, port string) (*GiteaFeedResponse, error) { + url := fmt.Sprintf("http://%s:%s/api/v1/users/adamf/activities/feeds?limit=1", host, port) + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var items []GiteaFeedResponse + if err := json.Unmarshal(body, &items); err != nil { + return nil, err + } + + if len(items) == 0 { + return nil, nil + } + + return &items[0], nil +} + +func ParseCommitMessage(content string) string { + var c GiteaFeedContent + if err := json.Unmarshal([]byte(content), &c); err != nil { + return "" + } + if len(c.Commits) == 0 { + return "" + } + return c.Commits[0].Message +} diff --git a/nginx/vue/src/components/util/CommitHistory.vue b/nginx/vue/src/components/util/CommitHistory.vue index 163170f..63a26b8 100644 --- a/nginx/vue/src/components/util/CommitHistory.vue +++ b/nginx/vue/src/components/util/CommitHistory.vue @@ -1,59 +1,36 @@