From 9e861f050f3816f3ea4621ee21dbfb4acd2ca32e Mon Sep 17 00:00:00 2001 From: Achim Rohn Date: Wed, 7 Jan 2026 20:38:24 +0100 Subject: [PATCH] Add and copy AGENTS.md for starter --- authentication/auth.go | 3 +- starter/.air.toml | 2 +- starter/AGENTS.md | 316 +++++++++++++++++++++++++++++++++++++++ starter/create/create.go | 6 +- 4 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 starter/AGENTS.md diff --git a/authentication/auth.go b/authentication/auth.go index 83adc5e..6783c63 100644 --- a/authentication/auth.go +++ b/authentication/auth.go @@ -1,10 +1,11 @@ package authentication import ( - . "git.gorlug.de/code/ersteller" "net/http" "strings" + . "git.gorlug.de/code/ersteller" + "github.com/gorilla/sessions" "golang.org/x/net/context" ) diff --git a/starter/.air.toml b/starter/.air.toml index 6c7b8c1..82c20c0 100644 --- a/starter/.air.toml +++ b/starter/.air.toml @@ -7,7 +7,7 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "time go build -gcflags=\"all=-l\" -ldflags=\"-w -s\" -o ./tmp/main main.go" delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata", "cdk", "dist", "db", "static"] + exclude_dir = ["assets", "tmp", "vendor", "testdata", "cdk", "dist", "db", "static", "docker"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/starter/AGENTS.md b/starter/AGENTS.md new file mode 100644 index 0000000..040cfe2 --- /dev/null +++ b/starter/AGENTS.md @@ -0,0 +1,316 @@ +# Development Guidelines + +## Styling + +Every page has styling in a separate CSS file. + +- Page-specific styles should be placed in separate CSS files in the `static/` directory +- Example: The media page has its styles in `static/media.css` +- General/shared styles are in `static/styles.css` + +## Single Page Implementation + +### Path constants + +- Define path constants for the root paths of the page only (not for sub-routes). + +```go +const MediaPath = "/media" +const MediaPathDe = "/medien" +``` + +### Page struct and initializer + +- Define a `Page` struct that contains all routes and actions as fields. +- Initialize routes in the `NewPage` constructor function. + +```go +type Page struct { + // Routes + ViewRoute HtmxRoute + StartRoute HtmxRoute + StopRoute HtmxRoute + +} + +func NewPage(server *Server) *Page { + p := &Page{} + + // Initialize routes + p.ViewRoute = NewHtmxGetRoute(p.View, LanguagePaths{En: MediaPath, De: MediaPathDe}).SetActivePath(MediaPath) + p.StartRoute = NewHtmxPostRoute(p.Start, LanguagePaths{En: MediaPath + "/start", De: MediaPathDe + "/start"}) + p.StopRoute = NewHtmxPostRoute(p.Stop, LanguagePaths{En: MediaPath + "/stop", De: MediaPathDe + "/stop"}) + + // Register routes + p.ViewRoute.Add(server) + p.StartRoute.Add(server) + p.StopRoute.Add(server) + + return p +} +``` + +### Multilingual content (`I18nText`) + +- Declare user-facing text with `I18nText`; avoid hardcoded strings. +- Create a separate `Texts` struct type and initialize it in a `createTexts()` function. + +```go +var texts *Texts + +type Texts struct { + PageTitle I18nText + PageDescription I18nText + HeroTitle I18nText + StartProcessing I18nText +} + +func createTexts() { + texts = &Texts{ + PageTitle: NewI18nText(map[Language]string{En: "Media Processing", De: "Medienverarbeitung"}), + PageDescription: NewI18nText(map[Language]string{En: "Process and organize your media files", De: "Verarbeiten und organisieren Sie Ihre Mediendateien"}), + HeroTitle: NewI18nText(map[Language]string{En: "Media Processing", De: "Medienverarbeitung"}), + StartProcessing: NewI18nText(map[Language]string{En: "Start Processing", De: "Verarbeitung starten"}), + } +} + +func (p *Page) getMetaData() PageWebsiteMetaData { + return PageWebsiteMetaData{ + Title: texts.PageTitle, + Lang: En, + Description: texts.PageDescription, + StyleSrcs: []string{"/static/media.css"}, + } +} + +func (p *Page) View(c HtmxContext) { + lang := c.GetLanguage() + content := Group{ + Div(Class("hero"), + H1(Text(texts.HeroTitle.FromLang(lang))), + ), + } + p.createPage(c, p.getMetaData(), content) +} +``` + +### Language-aware URLs and hx attributes + +- Derive URLs from routes with the active language for all hx attributes. + +```go +lang := c.GetLanguage() + +Button( + Text(texts.StartProcessing.FromLang(lang)), + p.StartRoute.GetHtmx(lang), + hx.Target("body"), +) + +// Pagination example with hx.PushURL to keep browser URL in sync +Button( + Text(texts.Next.FromLang(lang)), + hx.Get(p.RefreshRoute.ToUrlFromContext(c, lang)), + hx.PushURL(p.ViewRoute.ToUrlFromContext(c, lang)), + hx.Target("#queue-contents"), + hx.Swap("innerHTML"), +) +``` + +### Page assembly and styles + +- Build pages with `createPage` and supply metadata plus styles. +- Create a `getMetaData()` function to centralize page metadata configuration. +- Keep shared CSS in `static/styles.css` and page-specific CSS in `static/.css`; include via `StyleSrcs`. + +```go +func (p *Page) getMetaData() PageWebsiteMetaData { + return PageWebsiteMetaData{ + Title: texts.PageTitle, + Lang: En, + Description: texts.PageDescription, + StyleSrcs: []string{"/static/media.css"}, + } +} + +func (p *Page) View(c HtmxContext) { + lang := c.GetLanguage() + content := Group{ + Div(Class("hero"), H1(Text(texts.HeroTitle.FromLang(lang)))), + // ... more components ... + } + + p.createPage(c, p.getMetaData(), content) +} +``` + +### HTMX partials and polling + +- Use HTMX forms and partial refreshes; combine `hx.Target`, `hx.Swap`, and periodic polling (`hx.Trigger("every 5s")`) with language-aware URLs. + +```go +// Partial that refreshes every 5s when running +Group{ + hx.Get(p.RefreshStatusRoute.ToUrlFromContext(c, lang)), + hx.Trigger("every 5s"), + hx.Target("#queue-status"), + hx.Swap("innerHTML"), +} + +Form( + p.SaveSettingsRoute.GetHtmx(lang), + hx.Target("body"), + // form fields... +) + +// Use GetHtmx() for form actions to ensure proper HTMX attributes +Form( + p.localLoginRoute.GetHtmx(language), + hx.Target("body"), + // form fields... +) +``` + +## HTMX Routes + +**ALWAYS** prefer using `p.Route.GetHtmx()` over directly calling `hx.Get(p.Route.ToUrlFromContext())`. + +**Example:** +- ✅ Preferred: `p.RefreshStatusRoute.GetHtmx(c, lang)` +- ❌ Avoid: `hx.Get(p.RefreshStatusRoute.ToUrlFromContext(c, lang))` + +## Persisting Data with Ent Schemas + +### Schema Definition + +- Define schemas in `ent/schema/.go` +- Each schema is a struct that embeds `ent.Schema` +- Use the `TimeMixin` to automatically add `created_at` and `updated_at` fields + +```go +package schema + +import ( + "entgo.io/ent" + "entgo.io/ent/schema/field" + "entgo.io/ent/schema/edge" +) + +type Media struct { + ent.Schema +} + +// Apply TimeMixin for automatic timestamp fields +func (Media) Mixin() []ent.Mixin { + return []ent.Mixin{ + TimeMixin{}, + } +} + +// Define fields +func (Media) Fields() []ent.Field { + return []ent.Field{ + field.String("filename"), + field.String("hash"), + field.String("description").Default(""), + field.Time("created_date").Optional().Nillable(), + field.Int("year").Optional().Nillable(), + field.Float("gps_latitude").Optional().Nillable(), + } +} + +// Define relationships +func (Media) Edges() []ent.Edge { + return []ent.Edge{ + edge.To("tags", Tag.Type), // Many-to-Many + } +} +``` + +### Field Types and Options + +**Common field types:** +- `field.String("name")` - string field +- `field.Int("count")` - integer field +- `field.Float("latitude")` - float64 field +- `field.Bool("active")` - boolean field +- `field.Time("date")` - time.Time field + +**Field modifiers:** +- `.Default(value)` - set default value +- `.Optional()` - field can be omitted on creation +- `.Nillable()` - field can be nil (must be used with Optional) +- `.Immutable()` - field cannot be updated after creation +- `.Unique()` - field value must be unique + +### Generating Code + +After modifying schemas, regenerate the ent code: + +```bash +go run generate_schema.go +``` + +### Usage in Handlers + +**Create:** +```go +media, err := server.Client.Media.Create(). + SetFilename("photo.jpg"). + SetHash("abc123"). + SetDescription("Vacation photo"). + Save(c.Request.Context()) +``` + +**Query:** +```go +// Get by ID +media, err := server.Client.Media.Get(c.Request.Context(), id) + +// Query with filters +medias, err := server.Client.Media.Query(). + Where(media.YearEQ(2024)). + All(c.Request.Context()) + +// Query with edges +medias, err := server.Client.Media.Query(). + WithTags(). // Load tags relationship + All(c.Request.Context()) +``` + +**Update:** +```go +err := server.Client.Media.UpdateOneID(id). + SetDescription("Updated description"). + Save(c.Request.Context()) +``` + +**Delete:** +```go +err := server.Client.Media.DeleteOneID(id). + Exec(c.Request.Context()) +``` + +### Relationships (Edges) + +**One-to-Many:** +```go +edge.To("posts", Post.Type) // User has many posts +edge.From("owner", User.Type).Ref("posts").Unique() // Post belongs to one user +``` + +**Many-to-Many:** +```go +edge.To("tags", Tag.Type) // Media has many tags +edge.From("media", Media.Type).Ref("tags") // Tag has many media +``` + +## Important Instructions + +Do what has been asked; nothing more, nothing less. + +**NEVER** create files unless they're absolutely necessary for achieving your goal. + +**ALWAYS** prefer editing an existing file to creating a new one. + +**NEVER** proactively create documentation files (`*.md`) or README files. Only create documentation files if explicitly requested by the User. diff --git a/starter/create/create.go b/starter/create/create.go index 10d7357..cc6e2c8 100644 --- a/starter/create/create.go +++ b/starter/create/create.go @@ -2,8 +2,6 @@ package create import ( "fmt" - . "git.gorlug.de/code/ersteller" - "git.gorlug.de/code/ersteller/starter/env" "log" "os" "os/exec" @@ -11,6 +9,9 @@ import ( "path/filepath" "runtime" "strings" + + . "git.gorlug.de/code/ersteller" + "git.gorlug.de/code/ersteller/starter/env" ) type DatabaseType string @@ -97,6 +98,7 @@ func (s StarterCreator) Create() { } s.copyFile("../.gitignore", ".gitignore") + s.copyFile("../AGENTS.md", "AGENTS.md") s.createEnvironment() directories := []string{