--- apply: always --- # 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 ``` ## Workflows ### Defining Steps - Define a workflow step using `NewStep` and `StepParams`. - Each step requires a `Name`, `Identifier`, and a `Handler` function. - The `Handler` function processes the job and returns a result and the next step (if any). ```go lastStep := NewStep(&StepParams{ Name: "Last step", Identifier: "last_step", Description: "Last step", Client: client, MaxRetries: 3, Handler: func(ctx context.Context, currentStep *Step, job queue.GeneralQueueJob) (queue.GeneralQueueHandlerResult, *Step, error) { return queue.GeneralQueueHandlerResult{ ResultPayload: map[string]interface{}{ "lastStep": "hello", }, }, nil, nil }, }) firstStep := NewStep(&StepParams{ Name: "Initial step", Identifier: "initial_step", Description: "Initial step", Client: client, MaxRetries: 3, Handler: func(ctx context.Context, currentStep *Step, job queue.GeneralQueueJob) (queue.GeneralQueueHandlerResult, *Step, error) { return queue.GeneralQueueHandlerResult{ ResultPayload: map[string]interface{}{ "firstStep": "hello", }, }, lastStep, nil // Return the next step to execute }, }) ``` ### Creating Workflows - Define a workflow using `NewWorkflow`, providing a name, identifier, starting step, and all steps in the workflow. ```go w := NewWorkflow("Example", "example", firstStep, []*Step{firstStep, lastStep}) ``` ### Executing Workflows - To start a workflow, call the `Execute` method with an initial payload. - A unique `workflowId` is automatically generated and passed through each step. ```go err := workflow.Execute(ctx, map[string]interface{}{ "input": "start data", }) ``` ### Scheduled Workflows - Use `NewCronTrigger` to run a workflow at regular intervals. ```go NewCronTrigger(ctx, workflow, 1 * time.Hour) ``` ## 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.