From c9fe4f4bf1fb738734ea2d9b24740e18112a3d89 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 22 Feb 2026 03:40:57 -0800 Subject: [PATCH] rework: address review feedback on PR #126 Changes per sneak's review: - Delete docker-compose.yml, add example stanza to README - Define custom domain types: ImageID, ContainerID, UnparsedURL - Use custom types in all function signatures throughout codebase - Restore imageID parameter (as domain.ImageID) in deploy pipeline - buildContainerOptions now takes ImageID directly instead of constructing image tag from deploymentID - Fix pre-existing JS formatting (prettier) make check passes with zero failures. --- README.md | 23 + docker-compose.yml | 18 - internal/docker/client.go | 63 +- internal/domain/types.go | 16 + internal/service/deploy/deploy.go | 49 +- internal/service/webhook/webhook.go | 29 +- static/js/alpine.min.js | 3047 ++++++++++++++++++++++++++- static/js/app.js | 1053 ++++----- 8 files changed, 3702 insertions(+), 596 deletions(-) delete mode 100644 docker-compose.yml create mode 100644 internal/domain/types.go diff --git a/README.md b/README.md index 2583fee..7e2f045 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,29 @@ docker run -d \ upaas ``` +### Docker Compose + +```yaml +services: + upaas: + build: . + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${HOST_DATA_DIR:-./data}:/var/lib/upaas + environment: + - UPAAS_HOST_DATA_DIR=${HOST_DATA_DIR:-./data} + # Optional: uncomment to enable debug logging + # - DEBUG=true + # Optional: Sentry error reporting + # - SENTRY_DSN=https://... + # Optional: Prometheus metrics auth + # - METRICS_USERNAME=prometheus + # - METRICS_PASSWORD=secret +``` + **Important**: When running µPaaS inside a container, set `UPAAS_HOST_DATA_DIR` to the host path that maps to `UPAAS_DATA_DIR`. This is required for Docker bind mounts during builds to work correctly. diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 4337a0d..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - upaas: - build: . - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ${HOST_DATA_DIR:-./data}:/var/lib/upaas - environment: - - UPAAS_HOST_DATA_DIR=${HOST_DATA_DIR:-./data} - # Optional: uncomment to enable debug logging - # - DEBUG=true - # Optional: Sentry error reporting - # - SENTRY_DSN=https://... - # Optional: Prometheus metrics auth - # - METRICS_USERNAME=prometheus - # - METRICS_PASSWORD=secret diff --git a/internal/docker/client.go b/internal/docker/client.go index 38cc198..6e56b98 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -14,7 +14,7 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types" + dockertypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" @@ -26,6 +26,7 @@ import ( "go.uber.org/fx" "git.eeqj.de/sneak/upaas/internal/config" + "git.eeqj.de/sneak/upaas/internal/domain" "git.eeqj.de/sneak/upaas/internal/logger" ) @@ -116,7 +117,7 @@ type BuildImageOptions struct { func (c *Client) BuildImage( ctx context.Context, opts BuildImageOptions, -) (string, error) { +) (domain.ImageID, error) { if c.docker == nil { return "", ErrNotConnected } @@ -188,7 +189,7 @@ func buildPortConfig(ports []PortMapping) (nat.PortSet, nat.PortMap) { func (c *Client) CreateContainer( ctx context.Context, opts CreateContainerOptions, -) (string, error) { +) (domain.ContainerID, error) { if c.docker == nil { return "", ErrNotConnected } @@ -241,18 +242,18 @@ func (c *Client) CreateContainer( return "", fmt.Errorf("failed to create container: %w", err) } - return resp.ID, nil + return domain.ContainerID(resp.ID), nil } // StartContainer starts a container. -func (c *Client) StartContainer(ctx context.Context, containerID string) error { +func (c *Client) StartContainer(ctx context.Context, containerID domain.ContainerID) error { if c.docker == nil { return ErrNotConnected } c.log.Info("starting container", "id", containerID) - err := c.docker.ContainerStart(ctx, containerID, container.StartOptions{}) + err := c.docker.ContainerStart(ctx, string(containerID), container.StartOptions{}) if err != nil { return fmt.Errorf("failed to start container: %w", err) } @@ -261,7 +262,7 @@ func (c *Client) StartContainer(ctx context.Context, containerID string) error { } // StopContainer stops a container. -func (c *Client) StopContainer(ctx context.Context, containerID string) error { +func (c *Client) StopContainer(ctx context.Context, containerID domain.ContainerID) error { if c.docker == nil { return ErrNotConnected } @@ -270,7 +271,7 @@ func (c *Client) StopContainer(ctx context.Context, containerID string) error { timeout := stopTimeoutSeconds - err := c.docker.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) + err := c.docker.ContainerStop(ctx, string(containerID), container.StopOptions{Timeout: &timeout}) if err != nil { return fmt.Errorf("failed to stop container: %w", err) } @@ -281,7 +282,7 @@ func (c *Client) StopContainer(ctx context.Context, containerID string) error { // RemoveContainer removes a container. func (c *Client) RemoveContainer( ctx context.Context, - containerID string, + containerID domain.ContainerID, force bool, ) error { if c.docker == nil { @@ -290,7 +291,7 @@ func (c *Client) RemoveContainer( c.log.Info("removing container", "id", containerID, "force", force) - err := c.docker.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force}) + err := c.docker.ContainerRemove(ctx, string(containerID), container.RemoveOptions{Force: force}) if err != nil { return fmt.Errorf("failed to remove container: %w", err) } @@ -301,7 +302,7 @@ func (c *Client) RemoveContainer( // ContainerLogs returns the logs for a container. func (c *Client) ContainerLogs( ctx context.Context, - containerID string, + containerID domain.ContainerID, tail string, ) (string, error) { if c.docker == nil { @@ -314,7 +315,7 @@ func (c *Client) ContainerLogs( Tail: tail, } - reader, err := c.docker.ContainerLogs(ctx, containerID, opts) + reader, err := c.docker.ContainerLogs(ctx, string(containerID), opts) if err != nil { return "", fmt.Errorf("failed to get container logs: %w", err) } @@ -337,13 +338,13 @@ func (c *Client) ContainerLogs( // IsContainerRunning checks if a container is running. func (c *Client) IsContainerRunning( ctx context.Context, - containerID string, + containerID domain.ContainerID, ) (bool, error) { if c.docker == nil { return false, ErrNotConnected } - inspect, err := c.docker.ContainerInspect(ctx, containerID) + inspect, err := c.docker.ContainerInspect(ctx, string(containerID)) if err != nil { return false, fmt.Errorf("failed to inspect container: %w", err) } @@ -354,13 +355,13 @@ func (c *Client) IsContainerRunning( // IsContainerHealthy checks if a container is healthy. func (c *Client) IsContainerHealthy( ctx context.Context, - containerID string, + containerID domain.ContainerID, ) (bool, error) { if c.docker == nil { return false, ErrNotConnected } - inspect, err := c.docker.ContainerInspect(ctx, containerID) + inspect, err := c.docker.ContainerInspect(ctx, string(containerID)) if err != nil { return false, fmt.Errorf("failed to inspect container: %w", err) } @@ -378,7 +379,7 @@ const LabelUpaasID = "upaas.id" // ContainerInfo contains basic information about a container. type ContainerInfo struct { - ID string + ID domain.ContainerID Running bool } @@ -413,7 +414,7 @@ func (c *Client) FindContainerByAppID( ctr := containers[0] return &ContainerInfo{ - ID: ctr.ID, + ID: domain.ContainerID(ctr.ID), Running: ctr.State == "running", }, nil } @@ -482,8 +483,8 @@ func (c *Client) CloneRepo( // RemoveImage removes a Docker image by ID or tag. // It returns nil if the image was successfully removed or does not exist. -func (c *Client) RemoveImage(ctx context.Context, imageID string) error { - _, err := c.docker.ImageRemove(ctx, imageID, image.RemoveOptions{ +func (c *Client) RemoveImage(ctx context.Context, imageID domain.ImageID) error { + _, err := c.docker.ImageRemove(ctx, string(imageID), image.RemoveOptions{ Force: true, PruneChildren: true, }) @@ -497,7 +498,7 @@ func (c *Client) RemoveImage(ctx context.Context, imageID string) error { func (c *Client) performBuild( ctx context.Context, opts BuildImageOptions, -) (string, error) { +) (domain.ImageID, error) { // Create tar archive of build context tarArchive, err := archive.TarWithOptions(opts.ContextDir, &archive.TarOptions{}) if err != nil { @@ -512,7 +513,7 @@ func (c *Client) performBuild( }() // Build image - resp, err := c.docker.ImageBuild(ctx, tarArchive, types.ImageBuildOptions{ + resp, err := c.docker.ImageBuild(ctx, tarArchive, dockertypes.ImageBuildOptions{ Dockerfile: opts.DockerfilePath, Tags: opts.Tags, Remove: true, @@ -542,7 +543,7 @@ func (c *Client) performBuild( return "", fmt.Errorf("failed to inspect image: %w", inspectErr) } - return inspect.ID, nil + return domain.ImageID(inspect.ID), nil } return "", nil @@ -603,22 +604,22 @@ func (c *Client) performClone(ctx context.Context, cfg *cloneConfig) (*CloneResu } }() - containerID, err := c.createGitContainer(ctx, cfg) + gitContainerID, err := c.createGitContainer(ctx, cfg) if err != nil { return nil, err } defer func() { - _ = c.docker.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) + _ = c.docker.ContainerRemove(ctx, string(gitContainerID), container.RemoveOptions{Force: true}) }() - return c.runGitClone(ctx, containerID) + return c.runGitClone(ctx, gitContainerID) } func (c *Client) createGitContainer( ctx context.Context, cfg *cloneConfig, -) (string, error) { +) (domain.ContainerID, error) { gitSSHCmd := "ssh -i /keys/deploy_key -o StrictHostKeyChecking=no" // Build the git command using environment variables to avoid shell injection. @@ -675,16 +676,16 @@ func (c *Client) createGitContainer( return "", fmt.Errorf("failed to create git container: %w", err) } - return resp.ID, nil + return domain.ContainerID(resp.ID), nil } -func (c *Client) runGitClone(ctx context.Context, containerID string) (*CloneResult, error) { - err := c.docker.ContainerStart(ctx, containerID, container.StartOptions{}) +func (c *Client) runGitClone(ctx context.Context, containerID domain.ContainerID) (*CloneResult, error) { + err := c.docker.ContainerStart(ctx, string(containerID), container.StartOptions{}) if err != nil { return nil, fmt.Errorf("failed to start git container: %w", err) } - statusCh, errCh := c.docker.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) + statusCh, errCh := c.docker.ContainerWait(ctx, string(containerID), container.WaitConditionNotRunning) select { case err := <-errCh: diff --git a/internal/domain/types.go b/internal/domain/types.go new file mode 100644 index 0000000..32c3ae8 --- /dev/null +++ b/internal/domain/types.go @@ -0,0 +1,16 @@ +// Package domain defines domain-specific string types for compile-time safety. +// Using named types prevents accidentally passing the wrong string argument +// (e.g. a container ID where an image ID is expected). +package domain + +// ImageID is a Docker image identifier (ID or tag). +type ImageID string + +// ContainerID is a Docker container identifier. +type ContainerID string + +// UnparsedURL is a URL stored as a plain string without parsing. +// Use this instead of string when the value is known to be a URL +// but should not be parsed into a net/url.URL (e.g. webhook URLs, +// compare URLs from external payloads). +type UnparsedURL string diff --git a/internal/service/deploy/deploy.go b/internal/service/deploy/deploy.go index 0e47f99..8ab981c 100644 --- a/internal/service/deploy/deploy.go +++ b/internal/service/deploy/deploy.go @@ -20,6 +20,7 @@ import ( "git.eeqj.de/sneak/upaas/internal/config" "git.eeqj.de/sneak/upaas/internal/database" "git.eeqj.de/sneak/upaas/internal/docker" + "git.eeqj.de/sneak/upaas/internal/domain" "git.eeqj.de/sneak/upaas/internal/logger" "git.eeqj.de/sneak/upaas/internal/models" "git.eeqj.de/sneak/upaas/internal/service/notify" @@ -417,15 +418,13 @@ func (svc *Service) executeRollback( svc.removeOldContainer(ctx, app, deployment) - rollbackOpts, err := svc.buildContainerOptions(ctx, app, deployment.ID) + rollbackOpts, err := svc.buildContainerOptions(ctx, app, domain.ImageID(previousImageID)) if err != nil { svc.failDeployment(bgCtx, app, deployment, err) return fmt.Errorf("failed to build container options: %w", err) } - rollbackOpts.Image = previousImageID - containerID, err := svc.docker.CreateContainer(ctx, rollbackOpts) if err != nil { svc.failDeployment(bgCtx, app, deployment, fmt.Errorf("failed to create rollback container: %w", err)) @@ -433,8 +432,8 @@ func (svc *Service) executeRollback( return fmt.Errorf("failed to create rollback container: %w", err) } - deployment.ContainerID = sql.NullString{String: containerID, Valid: true} - _ = deployment.AppendLog(bgCtx, "Rollback container created: "+containerID) + deployment.ContainerID = sql.NullString{String: string(containerID), Valid: true} + _ = deployment.AppendLog(bgCtx, "Rollback container created: "+string(containerID)) startErr := svc.docker.StartContainer(ctx, containerID) if startErr != nil { @@ -484,7 +483,7 @@ func (svc *Service) runBuildAndDeploy( svc.notify.NotifyBuildSuccess(bgCtx, app, deployment) // Deploy phase with timeout - err = svc.deployContainerWithTimeout(deployCtx, app, deployment) + err = svc.deployContainerWithTimeout(deployCtx, app, deployment, imageID) if err != nil { cancelErr := svc.checkCancelled(deployCtx, bgCtx, app, deployment, imageID) if cancelErr != nil { @@ -516,7 +515,7 @@ func (svc *Service) buildImageWithTimeout( ctx context.Context, app *models.App, deployment *models.Deployment, -) (string, error) { +) (domain.ImageID, error) { buildCtx, cancel := context.WithTimeout(ctx, buildTimeout) defer cancel() @@ -541,6 +540,7 @@ func (svc *Service) deployContainerWithTimeout( ctx context.Context, app *models.App, deployment *models.Deployment, + imageID domain.ImageID, ) error { deployCtx, cancel := context.WithTimeout(ctx, deployTimeout) defer cancel() @@ -554,7 +554,7 @@ func (svc *Service) deployContainerWithTimeout( svc.removeOldContainer(deployCtx, app, deployment) // Create and start the new container - _, err = svc.createAndStartContainer(deployCtx, app, deployment) + _, err = svc.createAndStartContainer(deployCtx, app, deployment, imageID) if err != nil { if errors.Is(deployCtx.Err(), context.DeadlineExceeded) { timeoutErr := fmt.Errorf("%w after %v", ErrDeployTimeout, deployTimeout) @@ -668,7 +668,7 @@ func (svc *Service) checkCancelled( bgCtx context.Context, app *models.App, deployment *models.Deployment, - imageID string, + imageID domain.ImageID, ) error { if !errors.Is(deployCtx.Err(), context.Canceled) { return nil @@ -688,7 +688,7 @@ func (svc *Service) cleanupCancelledDeploy( ctx context.Context, app *models.App, deployment *models.Deployment, - imageID string, + imageID domain.ImageID, ) { // Clean up the intermediate Docker image if one was built if imageID != "" { @@ -696,11 +696,11 @@ func (svc *Service) cleanupCancelledDeploy( if removeErr != nil { svc.log.Error("failed to remove image from cancelled deploy", "error", removeErr, "app", app.Name, "image", imageID) - _ = deployment.AppendLog(ctx, "WARNING: failed to clean up image "+imageID+": "+removeErr.Error()) + _ = deployment.AppendLog(ctx, "WARNING: failed to clean up image "+string(imageID)+": "+removeErr.Error()) } else { svc.log.Info("cleaned up image from cancelled deploy", "app", app.Name, "image", imageID) - _ = deployment.AppendLog(ctx, "Cleaned up intermediate image: "+imageID) + _ = deployment.AppendLog(ctx, "Cleaned up intermediate image: "+string(imageID)) } } @@ -817,7 +817,7 @@ func (svc *Service) buildImage( ctx context.Context, app *models.App, deployment *models.Deployment, -) (string, error) { +) (domain.ImageID, error) { workDir, cleanup, err := svc.cloneRepository(ctx, app, deployment) if err != nil { return "", err @@ -851,8 +851,8 @@ func (svc *Service) buildImage( return "", fmt.Errorf("failed to build image: %w", err) } - deployment.ImageID = sql.NullString{String: imageID, Valid: true} - _ = deployment.AppendLog(ctx, "Image built: "+imageID) + deployment.ImageID = sql.NullString{String: string(imageID), Valid: true} + _ = deployment.AppendLog(ctx, "Image built: "+string(imageID)) return imageID, nil } @@ -1010,15 +1010,16 @@ func (svc *Service) removeOldContainer( svc.log.Warn("failed to remove old container", "error", removeErr) } - _ = deployment.AppendLog(ctx, "Old container removed: "+containerInfo.ID[:12]) + _ = deployment.AppendLog(ctx, "Old container removed: "+string(containerInfo.ID[:12])) } func (svc *Service) createAndStartContainer( ctx context.Context, app *models.App, deployment *models.Deployment, -) (string, error) { - containerOpts, err := svc.buildContainerOptions(ctx, app, deployment.ID) + imageID domain.ImageID, +) (domain.ContainerID, error) { + containerOpts, err := svc.buildContainerOptions(ctx, app, imageID) if err != nil { svc.failDeployment(ctx, app, deployment, err) @@ -1038,8 +1039,8 @@ func (svc *Service) createAndStartContainer( return "", fmt.Errorf("failed to create container: %w", err) } - deployment.ContainerID = sql.NullString{String: containerID, Valid: true} - _ = deployment.AppendLog(ctx, "Container created: "+containerID) + deployment.ContainerID = sql.NullString{String: string(containerID), Valid: true} + _ = deployment.AppendLog(ctx, "Container created: "+string(containerID)) startErr := svc.docker.StartContainer(ctx, containerID) if startErr != nil { @@ -1062,7 +1063,7 @@ func (svc *Service) createAndStartContainer( func (svc *Service) buildContainerOptions( ctx context.Context, app *models.App, - deploymentID int64, + imageID domain.ImageID, ) (docker.CreateContainerOptions, error) { envVars, err := app.GetEnvVars(ctx) if err != nil { @@ -1096,7 +1097,7 @@ func (svc *Service) buildContainerOptions( return docker.CreateContainerOptions{ Name: "upaas-" + app.Name, - Image: fmt.Sprintf("upaas-%s:%d", app.Name, deploymentID), + Image: string(imageID), Env: envMap, Labels: buildLabelMap(app, labels), Volumes: buildVolumeMounts(volumes), @@ -1146,9 +1147,9 @@ func buildPortMappings(ports []*models.Port) []docker.PortMapping { func (svc *Service) updateAppRunning( ctx context.Context, app *models.App, - imageID string, + imageID domain.ImageID, ) error { - app.ImageID = sql.NullString{String: imageID, Valid: true} + app.ImageID = sql.NullString{String: string(imageID), Valid: true} app.Status = models.AppStatusRunning saveErr := app.Save(ctx) diff --git a/internal/service/webhook/webhook.go b/internal/service/webhook/webhook.go index dbdd23d..ed7fb33 100644 --- a/internal/service/webhook/webhook.go +++ b/internal/service/webhook/webhook.go @@ -11,6 +11,7 @@ import ( "go.uber.org/fx" "git.eeqj.de/sneak/upaas/internal/database" + "git.eeqj.de/sneak/upaas/internal/domain" "git.eeqj.de/sneak/upaas/internal/logger" "git.eeqj.de/sneak/upaas/internal/models" "git.eeqj.de/sneak/upaas/internal/service/deploy" @@ -47,24 +48,24 @@ func New(_ fx.Lifecycle, params ServiceParams) (*Service, error) { // //nolint:tagliatelle // Field names match Gitea API (snake_case) type GiteaPushPayload struct { - Ref string `json:"ref"` - Before string `json:"before"` - After string `json:"after"` - CompareURL string `json:"compare_url"` + Ref string `json:"ref"` + Before string `json:"before"` + After string `json:"after"` + CompareURL domain.UnparsedURL `json:"compare_url"` Repository struct { - FullName string `json:"full_name"` - CloneURL string `json:"clone_url"` - SSHURL string `json:"ssh_url"` - HTMLURL string `json:"html_url"` + FullName string `json:"full_name"` + CloneURL domain.UnparsedURL `json:"clone_url"` + SSHURL string `json:"ssh_url"` + HTMLURL domain.UnparsedURL `json:"html_url"` } `json:"repository"` Pusher struct { Username string `json:"username"` Email string `json:"email"` } `json:"pusher"` Commits []struct { - ID string `json:"id"` - URL string `json:"url"` - Message string `json:"message"` + ID string `json:"id"` + URL domain.UnparsedURL `json:"url"` + Message string `json:"message"` Author struct { Name string `json:"name"` Email string `json:"email"` @@ -104,7 +105,7 @@ func (svc *Service) HandleWebhook( event.EventType = eventType event.Branch = branch event.CommitSHA = sql.NullString{String: commitSHA, Valid: commitSHA != ""} - event.CommitURL = sql.NullString{String: commitURL, Valid: commitURL != ""} + event.CommitURL = sql.NullString{String: string(commitURL), Valid: commitURL != ""} event.Payload = sql.NullString{String: string(payload), Valid: true} event.Matched = matched event.Processed = false @@ -168,7 +169,7 @@ func extractBranch(ref string) string { // extractCommitURL extracts the commit URL from the webhook payload. // Prefers the URL from the head commit, falls back to constructing from repo URL. -func extractCommitURL(payload GiteaPushPayload) string { +func extractCommitURL(payload GiteaPushPayload) domain.UnparsedURL { // Try to find the URL from the head commit (matching After SHA) for _, commit := range payload.Commits { if commit.ID == payload.After && commit.URL != "" { @@ -178,7 +179,7 @@ func extractCommitURL(payload GiteaPushPayload) string { // Fall back to constructing URL from repo HTML URL if payload.Repository.HTMLURL != "" && payload.After != "" { - return payload.Repository.HTMLURL + "/commit/" + payload.After + return domain.UnparsedURL(string(payload.Repository.HTMLURL) + "/commit/" + payload.After) } return "" diff --git a/static/js/alpine.min.js b/static/js/alpine.min.js index 2a6849c..6bde2c4 100644 --- a/static/js/alpine.min.js +++ b/static/js/alpine.min.js @@ -1,5 +1,3046 @@ -(()=>{var nt=!1,it=!1,G=[],ot=-1;function Ut(e){In(e)}function In(e){G.includes(e)||G.push(e),$n()}function Wt(e){let t=G.indexOf(e);t!==-1&&t>ot&&G.splice(t,1)}function $n(){!it&&!nt&&(nt=!0,queueMicrotask(Ln))}function Ln(){nt=!1,it=!0;for(let e=0;ee.effect(t,{scheduler:r=>{st?Ut(r):r()}}),at=e.raw}function ct(e){N=e}function Yt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),F(i))},i},()=>{t()}]}function Oe(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>F(i)}var Xt=[],Zt=[],Qt=[];function er(e){Qt.push(e)}function re(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Zt.push(t))}function Re(e){Xt.push(e)}function Te(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function tr(e){for(e._x_effects?.forEach(Wt);e._x_cleanups?.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function pe(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){jn(),ut.disconnect(),ft=!1}var de=[];function jn(){let e=ut.takeRecords();de.push(()=>e.length>0&&mt(e));let t=de.length;queueMicrotask(()=>{if(de.length===t)for(;de.length>0;)de.shift()()})}function m(e){if(!ft)return e();dt();let t=e();return pe(),t}var pt=!1,Ce=[];function rr(){pt=!0}function nr(){pt=!1,mt(Ce),Ce=[]}function mt(e){if(pt){Ce=Ce.concat(e);return}let t=[],r=new Set,n=new Map,i=new Map;for(let o=0;o{s.nodeType===1&&s._x_marker&&r.add(s)}),e[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||t.push(s)}})),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{Xt.forEach(a=>a(s,o))});for(let o of r)t.some(s=>s.contains(o))||Zt.forEach(s=>s(o));for(let o of t)o.isConnected&&Qt.forEach(s=>s(o));t=null,r=null,n=null,i=null}function Me(e){return k(B(e))}function D(e,t,r){return e._x_dataStack=[t,...B(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function B(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?B(e.host):e.parentNode?B(e.parentNode):[]}function k(e){return new Proxy({objects:e},Fn)}var Fn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Bn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Bn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function ne(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Ne(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>zn(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function zn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ir={};function y(e,t){ir[e]=t}function K(e,t){let r=Hn(t);return Object.entries(ir).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function Hn(e){let[t,r]=_t(e),n={interceptor:Ne,...t};return re(e,r),n}function or(e,t,r,...n){try{return r(...n)}catch(i){ie(i,e,t)}}function ie(...e){return sr(...e)}var sr=Kn;function ar(e){sr=e}function Kn(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} +(() => { + var nt = !1, + it = !1, + G = [], + ot = -1; + function Ut(e) { + In(e); + } + function In(e) { + (G.includes(e) || G.push(e), $n()); + } + function Wt(e) { + let t = G.indexOf(e); + t !== -1 && t > ot && G.splice(t, 1); + } + function $n() { + !it && !nt && ((nt = !0), queueMicrotask(Ln)); + } + function Ln() { + ((nt = !1), (it = !0)); + for (let e = 0; e < G.length; e++) (G[e](), (ot = e)); + ((G.length = 0), (ot = -1), (it = !1)); + } + var R, + N, + F, + at, + st = !0; + function Gt(e) { + ((st = !1), e(), (st = !0)); + } + function Jt(e) { + ((R = e.reactive), + (F = e.release), + (N = (t) => + e.effect(t, { + scheduler: (r) => { + st ? Ut(r) : r(); + }, + })), + (at = e.raw)); + } + function ct(e) { + N = e; + } + function Yt(e) { + let t = () => {}; + return [ + (n) => { + let i = N(n); + return ( + e._x_effects || + ((e._x_effects = new Set()), + (e._x_runEffects = () => { + e._x_effects.forEach((o) => o()); + })), + e._x_effects.add(i), + (t = () => { + i !== void 0 && (e._x_effects.delete(i), F(i)); + }), + i + ); + }, + () => { + t(); + }, + ]; + } + function Oe(e, t) { + let r = !0, + n, + i = N(() => { + let o = e(); + (JSON.stringify(o), + r + ? (n = o) + : queueMicrotask(() => { + (t(o, n), (n = o)); + }), + (r = !1)); + }); + return () => F(i); + } + var Xt = [], + Zt = [], + Qt = []; + function er(e) { + Qt.push(e); + } + function re(e, t) { + typeof t == "function" + ? (e._x_cleanups || (e._x_cleanups = []), e._x_cleanups.push(t)) + : ((t = e), Zt.push(t)); + } + function Re(e) { + Xt.push(e); + } + function Te(e, t, r) { + (e._x_attributeCleanups || (e._x_attributeCleanups = {}), + e._x_attributeCleanups[t] || (e._x_attributeCleanups[t] = []), + e._x_attributeCleanups[t].push(r)); + } + function lt(e, t) { + e._x_attributeCleanups && + Object.entries(e._x_attributeCleanups).forEach(([r, n]) => { + (t === void 0 || t.includes(r)) && + (n.forEach((i) => i()), delete e._x_attributeCleanups[r]); + }); + } + function tr(e) { + for (e._x_effects?.forEach(Wt); e._x_cleanups?.length; ) + e._x_cleanups.pop()(); + } + var ut = new MutationObserver(mt), + ft = !1; + function pe() { + (ut.observe(document, { + subtree: !0, + childList: !0, + attributes: !0, + attributeOldValue: !0, + }), + (ft = !0)); + } + function dt() { + (jn(), ut.disconnect(), (ft = !1)); + } + var de = []; + function jn() { + let e = ut.takeRecords(); + de.push(() => e.length > 0 && mt(e)); + let t = de.length; + queueMicrotask(() => { + if (de.length === t) for (; de.length > 0; ) de.shift()(); + }); + } + function m(e) { + if (!ft) return e(); + dt(); + let t = e(); + return (pe(), t); + } + var pt = !1, + Ce = []; + function rr() { + pt = !0; + } + function nr() { + ((pt = !1), mt(Ce), (Ce = [])); + } + function mt(e) { + if (pt) { + Ce = Ce.concat(e); + return; + } + let t = [], + r = new Set(), + n = new Map(), + i = new Map(); + for (let o = 0; o < e.length; o++) + if ( + !e[o].target._x_ignoreMutationObserver && + (e[o].type === "childList" && + (e[o].removedNodes.forEach((s) => { + s.nodeType === 1 && s._x_marker && r.add(s); + }), + e[o].addedNodes.forEach((s) => { + if (s.nodeType === 1) { + if (r.has(s)) { + r.delete(s); + return; + } + s._x_marker || t.push(s); + } + })), + e[o].type === "attributes") + ) { + let s = e[o].target, + a = e[o].attributeName, + c = e[o].oldValue, + l = () => { + (n.has(s) || n.set(s, []), + n + .get(s) + .push({ name: a, value: s.getAttribute(a) })); + }, + u = () => { + (i.has(s) || i.set(s, []), i.get(s).push(a)); + }; + s.hasAttribute(a) && c === null + ? l() + : s.hasAttribute(a) + ? (u(), l()) + : u(); + } + (i.forEach((o, s) => { + lt(s, o); + }), + n.forEach((o, s) => { + Xt.forEach((a) => a(s, o)); + })); + for (let o of r) + t.some((s) => s.contains(o)) || Zt.forEach((s) => s(o)); + for (let o of t) o.isConnected && Qt.forEach((s) => s(o)); + ((t = null), (r = null), (n = null), (i = null)); + } + function Me(e) { + return k(B(e)); + } + function D(e, t, r) { + return ( + (e._x_dataStack = [t, ...B(r || e)]), + () => { + e._x_dataStack = e._x_dataStack.filter((n) => n !== t); + } + ); + } + function B(e) { + return e._x_dataStack + ? e._x_dataStack + : typeof ShadowRoot == "function" && e instanceof ShadowRoot + ? B(e.host) + : e.parentNode + ? B(e.parentNode) + : []; + } + function k(e) { + return new Proxy({ objects: e }, Fn); + } + var Fn = { + ownKeys({ objects: e }) { + return Array.from(new Set(e.flatMap((t) => Object.keys(t)))); + }, + has({ objects: e }, t) { + return t == Symbol.unscopables + ? !1 + : e.some( + (r) => + Object.prototype.hasOwnProperty.call(r, t) || + Reflect.has(r, t), + ); + }, + get({ objects: e }, t, r) { + return t == "toJSON" + ? Bn + : Reflect.get(e.find((n) => Reflect.has(n, t)) || {}, t, r); + }, + set({ objects: e }, t, r, n) { + let i = + e.find((s) => Object.prototype.hasOwnProperty.call(s, t)) || + e[e.length - 1], + o = Object.getOwnPropertyDescriptor(i, t); + return o?.set && o?.get + ? o.set.call(n, r) || !0 + : Reflect.set(i, t, r); + }, + }; + function Bn() { + return Reflect.ownKeys(this).reduce( + (t, r) => ((t[r] = Reflect.get(this, r)), t), + {}, + ); + } + function ne(e) { + let t = (n) => typeof n == "object" && !Array.isArray(n) && n !== null, + r = (n, i = "") => { + Object.entries(Object.getOwnPropertyDescriptors(n)).forEach( + ([o, { value: s, enumerable: a }]) => { + if ( + a === !1 || + s === void 0 || + (typeof s == "object" && s !== null && s.__v_skip) + ) + return; + let c = i === "" ? o : `${i}.${o}`; + typeof s == "object" && s !== null && s._x_interceptor + ? (n[o] = s.initialize(e, c, o)) + : t(s) && + s !== n && + !(s instanceof Element) && + r(s, c); + }, + ); + }; + return r(e); + } + function Ne(e, t = () => {}) { + let r = { + initialValue: void 0, + _x_interceptor: !0, + initialize(n, i, o) { + return e( + this.initialValue, + () => zn(n, i), + (s) => ht(n, i, s), + i, + o, + ); + }, + }; + return ( + t(r), + (n) => { + if (typeof n == "object" && n !== null && n._x_interceptor) { + let i = r.initialize.bind(r); + r.initialize = (o, s, a) => { + let c = n.initialize(o, s, a); + return ((r.initialValue = c), i(o, s, a)); + }; + } else r.initialValue = n; + return r; + } + ); + } + function zn(e, t) { + return t.split(".").reduce((r, n) => r[n], e); + } + function ht(e, t, r) { + if ((typeof t == "string" && (t = t.split(".")), t.length === 1)) + e[t[0]] = r; + else { + if (t.length === 0) throw error; + return (e[t[0]] || (e[t[0]] = {}), ht(e[t[0]], t.slice(1), r)); + } + } + var ir = {}; + function y(e, t) { + ir[e] = t; + } + function K(e, t) { + let r = Hn(t); + return ( + Object.entries(ir).forEach(([n, i]) => { + Object.defineProperty(e, `$${n}`, { + get() { + return i(t, r); + }, + enumerable: !1, + }); + }), + e + ); + } + function Hn(e) { + let [t, r] = _t(e), + n = { interceptor: Ne, ...t }; + return (re(e, r), n); + } + function or(e, t, r, ...n) { + try { + return r(...n); + } catch (i) { + ie(i, e, t); + } + } + function ie(...e) { + return sr(...e); + } + var sr = Kn; + function ar(e) { + sr = e; + } + function Kn(e, t, r = void 0) { + ((e = Object.assign(e ?? { message: "No error message given." }, { + el: t, + expression: r, + })), + console.warn( + `Alpine Expression Error: ${e.message} -${r?'Expression: "'+r+`" +${ + r + ? 'Expression: "' + + r + + `" -`:""}`,t),setTimeout(()=>{throw e},0)}var oe=!0;function De(e){let t=oe;oe=!1;let r=e();return oe=t,r}function T(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return cr(...e)}var cr=xt;function lr(e){cr=e}var ur;function fr(e){ur=e}function xt(e,t){let r={};K(r,e);let n=[r,...B(e)],i=typeof t=="function"?Vn(n,t):Un(n,t,e);return or.bind(null,e,t,i)}function Vn(e,t){return(r=()=>{},{scope:n={},params:i=[],context:o}={})=>{if(!oe){me(r,t,k([n,...e]),i);return}let s=t.apply(k([n,...e]),i);me(r,s)}}var gt={};function qn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return ie(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Un(e,t,r){let n=qn(t,r);return(i=()=>{},{scope:o={},params:s=[],context:a}={})=>{n.result=void 0,n.finished=!1;let c=k([o,...e]);if(typeof n=="function"){let l=n.call(a,n,c).catch(u=>ie(u,r,t));n.finished?(me(i,n.result,c,s,r),n.result=void 0):l.then(u=>{me(i,u,c,s,r)}).catch(u=>ie(u,r,t)).finally(()=>n.result=void 0)}}}function me(e,t,r,n,i){if(oe&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>me(e,s,r,n)).catch(s=>ie(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}function dr(...e){return ur(...e)}function pr(e,t,r={}){let n={};K(n,e);let i=[n,...B(e)],o=k([r.scope??{},...i]),s=r.params??[];if(t.includes("await")){let a=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t;return new a(["scope"],`with (scope) { let __result = ${c}; return __result }`).call(r.context,o)}else{let a=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(()=>{ ${t} })()`:t,l=new Function(["scope"],`with (scope) { let __result = ${a}; return __result }`).call(r.context,o);return typeof l=="function"&&oe?l.apply(o,s):l}}var wt="x-";function C(e=""){return wt+e}function mr(e){wt=e}var ke={};function d(e,t){return ke[e]=t,{before(r){if(!ke[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=J.indexOf(r);J.splice(n>=0?n:J.indexOf("DEFAULT"),0,e)}}}function hr(e){return Object.keys(ke).includes(e)}function _e(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=Et(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(xr((o,s)=>n[o]=s)).filter(br).map(Gn(n,r)).sort(Jn).map(o=>Wn(e,o))}function Et(e){return Array.from(e).map(xr()).filter(t=>!br(t))}var yt=!1,he=new Map,_r=Symbol();function gr(e){yt=!0;let t=Symbol();_r=t,he.set(t,[]);let r=()=>{for(;he.get(t).length;)he.get(t).shift()();he.delete(t)},n=()=>{yt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Yt(e);return t.push(i),[{Alpine:z,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:T.bind(T,e)},()=>t.forEach(a=>a())]}function Wn(e,t){let r=()=>{},n=ke[t.type]||r,[i,o]=_t(e);Te(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),yt?he.get(_r).push(n):n())};return s.runCleanups=o,s}var Pe=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Ie=e=>e;function xr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=yr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var yr=[];function se(e){yr.push(e)}function br({name:e}){return wr().test(e)}var wr=()=>new RegExp(`^${wt}([^:^.]+)\\b`);function Gn(e,t){return({name:r,value:n})=>{let i=r.match(wr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var bt="DEFAULT",J=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",bt,"teleport"];function Jn(e,t){let r=J.indexOf(e.type)===-1?bt:e.type,n=J.indexOf(t.type)===-1?bt:t.type;return J.indexOf(r)-J.indexOf(n)}function Y(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function P(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>P(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)P(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var Er=!1;function vr(){Er&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Er=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `