From 49ff625ac46dca38f451418648e777addab07668 Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 10 Mar 2026 01:09:15 +0100 Subject: [PATCH] fix: add missing Makefile targets (docker, hooks) and test timeout (#159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes - Add `docker` target (`docker build .`) - Add `hooks` target (installs pre-commit hook running `make check`) - Add 30-second timeout to `test` target (`-timeout 30s`) - Update `.PHONY` to include new targets - Update README to document all Makefile targets (`fmt-check`, `docker`, `hooks`) - Run `make fmt` to fix JS formatting via prettier `docker build .` passes ✅ closes https://git.eeqj.de/sneak/upaas/issues/136, closes https://git.eeqj.de/sneak/upaas/issues/137 Co-authored-by: clawbot Co-authored-by: Jeffrey Paul Reviewed-on: https://git.eeqj.de/sneak/upaas/pulls/159 Co-authored-by: clawbot Co-committed-by: clawbot --- Makefile | 14 +- README.md | 13 +- static/js/app-detail.js | 352 +++++++++++++++++++++------------------- static/js/components.js | 118 +++++++------- static/js/dashboard.js | 27 +-- static/js/deployment.js | 299 +++++++++++++++++----------------- static/js/utils.js | 239 ++++++++++++++------------- 7 files changed, 558 insertions(+), 504 deletions(-) diff --git a/Makefile b/Makefile index de240d0..31ee1f5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build lint fmt fmt-check test check clean +.PHONY: all build lint fmt fmt-check test check clean docker hooks BINARY := upaasd VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") @@ -22,12 +22,22 @@ fmt-check: @test -z "$$(gofmt -l .)" || (echo "Files not formatted:" && gofmt -l . && exit 1) test: - go test -v -race -cover ./... + go test -v -race -cover -timeout 30s ./... # Check runs all validation without making changes # Used by CI and Docker build - fails if anything is wrong check: fmt-check lint test @echo "==> All checks passed!" +docker: + docker build . + +hooks: + @echo "Installing pre-commit hook..." + @mkdir -p .git/hooks + @printf '#!/bin/sh\nmake check\n' > .git/hooks/pre-commit + @chmod +x .git/hooks/pre-commit + @echo "Pre-commit hook installed." + clean: rm -rf bin/ diff --git a/README.md b/README.md index 8e86720..91877d7 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,14 @@ chi Router ──► Middleware Stack ──► Handler ### Commands ```bash -make fmt # Format code -make lint # Run comprehensive linting -make test # Run tests with race detection -make check # Verify everything passes (lint, test, build, format) -make build # Build binary +make fmt # Format code +make fmt-check # Check formatting (read-only, fails if unformatted) +make lint # Run comprehensive linting +make test # Run tests with race detection (30s timeout) +make check # Verify everything passes (fmt-check, lint, test) +make build # Build binary +make docker # Build Docker image +make hooks # Install pre-commit hook (runs make check) ``` ### Commit Requirements diff --git a/static/js/app-detail.js b/static/js/app-detail.js index 406763f..9778ea8 100644 --- a/static/js/app-detail.js +++ b/static/js/app-detail.js @@ -6,189 +6,215 @@ */ document.addEventListener("alpine:init", () => { - Alpine.data("appDetail", (config) => ({ - appId: config.appId, - currentDeploymentId: config.initialDeploymentId, - appStatus: config.initialStatus || "unknown", - containerLogs: "Loading container logs...", - containerStatus: "unknown", - buildLogs: config.initialDeploymentId - ? "Loading build logs..." - : "No deployments yet", - buildStatus: config.initialBuildStatus || "unknown", - showBuildLogs: !!config.initialDeploymentId, - deploying: false, - deployments: [], - // Track whether user wants auto-scroll (per log pane) - _containerAutoScroll: true, - _buildAutoScroll: true, - _pollTimer: null, + Alpine.data("appDetail", (config) => ({ + appId: config.appId, + currentDeploymentId: config.initialDeploymentId, + appStatus: config.initialStatus || "unknown", + containerLogs: "Loading container logs...", + containerStatus: "unknown", + buildLogs: config.initialDeploymentId + ? "Loading build logs..." + : "No deployments yet", + buildStatus: config.initialBuildStatus || "unknown", + showBuildLogs: !!config.initialDeploymentId, + deploying: false, + deployments: [], + // Track whether user wants auto-scroll (per log pane) + _containerAutoScroll: true, + _buildAutoScroll: true, + _pollTimer: null, - init() { - this.deploying = Alpine.store("utils").isDeploying(this.appStatus); - this.fetchAll(); - this._schedulePoll(); + init() { + this.deploying = Alpine.store("utils").isDeploying(this.appStatus); + this.fetchAll(); + this._schedulePoll(); - // Set up scroll listeners after DOM is ready - this.$nextTick(() => { - this._initScrollTracking(this.$refs.containerLogsWrapper, '_containerAutoScroll'); - this._initScrollTracking(this.$refs.buildLogsWrapper, '_buildAutoScroll'); - }); - }, + // Set up scroll listeners after DOM is ready + this.$nextTick(() => { + this._initScrollTracking( + this.$refs.containerLogsWrapper, + "_containerAutoScroll", + ); + this._initScrollTracking( + this.$refs.buildLogsWrapper, + "_buildAutoScroll", + ); + }); + }, - _schedulePoll() { - if (this._pollTimer) clearTimeout(this._pollTimer); - const interval = Alpine.store("utils").isDeploying(this.appStatus) ? 1000 : 10000; - this._pollTimer = setTimeout(() => { - this.fetchAll(); - this._schedulePoll(); - }, interval); - }, + _schedulePoll() { + if (this._pollTimer) clearTimeout(this._pollTimer); + const interval = Alpine.store("utils").isDeploying(this.appStatus) + ? 1000 + : 10000; + this._pollTimer = setTimeout(() => { + this.fetchAll(); + this._schedulePoll(); + }, interval); + }, - _initScrollTracking(el, flag) { - if (!el) return; - el.addEventListener('scroll', () => { - this[flag] = Alpine.store("utils").isScrolledToBottom(el); - }, { passive: true }); - }, + _initScrollTracking(el, flag) { + if (!el) return; + el.addEventListener( + "scroll", + () => { + this[flag] = Alpine.store("utils").isScrolledToBottom(el); + }, + { passive: true }, + ); + }, - fetchAll() { - this.fetchAppStatus(); - // Only fetch logs when the respective pane is visible - if (this.$refs.containerLogsWrapper && this._isElementVisible(this.$refs.containerLogsWrapper)) { - this.fetchContainerLogs(); - } - if (this.showBuildLogs && this.$refs.buildLogsWrapper && this._isElementVisible(this.$refs.buildLogsWrapper)) { - this.fetchBuildLogs(); - } - this.fetchRecentDeployments(); - }, + fetchAll() { + this.fetchAppStatus(); + // Only fetch logs when the respective pane is visible + if ( + this.$refs.containerLogsWrapper && + this._isElementVisible(this.$refs.containerLogsWrapper) + ) { + this.fetchContainerLogs(); + } + if ( + this.showBuildLogs && + this.$refs.buildLogsWrapper && + this._isElementVisible(this.$refs.buildLogsWrapper) + ) { + this.fetchBuildLogs(); + } + this.fetchRecentDeployments(); + }, - _isElementVisible(el) { - if (!el) return false; - // Check if element is in viewport (roughly) - const rect = el.getBoundingClientRect(); - return rect.bottom > 0 && rect.top < window.innerHeight; - }, + _isElementVisible(el) { + if (!el) return false; + // Check if element is in viewport (roughly) + const rect = el.getBoundingClientRect(); + return rect.bottom > 0 && rect.top < window.innerHeight; + }, - async fetchAppStatus() { - try { - const res = await fetch(`/apps/${this.appId}/status`); - const data = await res.json(); - const wasDeploying = this.deploying; - this.appStatus = data.status; - this.deploying = Alpine.store("utils").isDeploying(data.status); + async fetchAppStatus() { + try { + const res = await fetch(`/apps/${this.appId}/status`); + const data = await res.json(); + const wasDeploying = this.deploying; + this.appStatus = data.status; + this.deploying = Alpine.store("utils").isDeploying(data.status); - // Re-schedule polling when deployment state changes - if (this.deploying !== wasDeploying) { - this._schedulePoll(); - } + // Re-schedule polling when deployment state changes + if (this.deploying !== wasDeploying) { + this._schedulePoll(); + } - if ( - data.latestDeploymentID && - data.latestDeploymentID !== this.currentDeploymentId - ) { - this.currentDeploymentId = data.latestDeploymentID; - this.showBuildLogs = true; - this.fetchBuildLogs(); - } - } catch (err) { - console.error("Status fetch error:", err); - } - }, + if ( + data.latestDeploymentID && + data.latestDeploymentID !== this.currentDeploymentId + ) { + this.currentDeploymentId = data.latestDeploymentID; + this.showBuildLogs = true; + this.fetchBuildLogs(); + } + } catch (err) { + console.error("Status fetch error:", err); + } + }, - async fetchContainerLogs() { - try { - const res = await fetch(`/apps/${this.appId}/container-logs`); - const data = await res.json(); - const newLogs = data.logs || "No logs available"; - const changed = newLogs !== this.containerLogs; - this.containerLogs = newLogs; - this.containerStatus = data.status; - if (changed && this._containerAutoScroll) { - this.$nextTick(() => { - Alpine.store("utils").scrollToBottom(this.$refs.containerLogsWrapper); - }); - } - } catch (err) { - this.containerLogs = "Failed to fetch logs"; - } - }, + async fetchContainerLogs() { + try { + const res = await fetch(`/apps/${this.appId}/container-logs`); + const data = await res.json(); + const newLogs = data.logs || "No logs available"; + const changed = newLogs !== this.containerLogs; + this.containerLogs = newLogs; + this.containerStatus = data.status; + if (changed && this._containerAutoScroll) { + this.$nextTick(() => { + Alpine.store("utils").scrollToBottom( + this.$refs.containerLogsWrapper, + ); + }); + } + } catch (err) { + this.containerLogs = "Failed to fetch logs"; + } + }, - async fetchBuildLogs() { - if (!this.currentDeploymentId) return; - try { - const res = await fetch( - `/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`, - ); - const data = await res.json(); - const newLogs = data.logs || "No build logs available"; - const changed = newLogs !== this.buildLogs; - this.buildLogs = newLogs; - this.buildStatus = data.status; - if (changed && this._buildAutoScroll) { - this.$nextTick(() => { - Alpine.store("utils").scrollToBottom(this.$refs.buildLogsWrapper); - }); - } - } catch (err) { - this.buildLogs = "Failed to fetch logs"; - } - }, + async fetchBuildLogs() { + if (!this.currentDeploymentId) return; + try { + const res = await fetch( + `/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`, + ); + const data = await res.json(); + const newLogs = data.logs || "No build logs available"; + const changed = newLogs !== this.buildLogs; + this.buildLogs = newLogs; + this.buildStatus = data.status; + if (changed && this._buildAutoScroll) { + this.$nextTick(() => { + Alpine.store("utils").scrollToBottom( + this.$refs.buildLogsWrapper, + ); + }); + } + } catch (err) { + this.buildLogs = "Failed to fetch logs"; + } + }, - async fetchRecentDeployments() { - try { - const res = await fetch(`/apps/${this.appId}/recent-deployments`); - const data = await res.json(); - this.deployments = data.deployments || []; - } catch (err) { - console.error("Deployments fetch error:", err); - } - }, + async fetchRecentDeployments() { + try { + const res = await fetch( + `/apps/${this.appId}/recent-deployments`, + ); + const data = await res.json(); + this.deployments = data.deployments || []; + } catch (err) { + console.error("Deployments fetch error:", err); + } + }, - submitDeploy() { - this.deploying = true; - }, + submitDeploy() { + this.deploying = true; + }, - get statusBadgeClass() { - return Alpine.store("utils").statusBadgeClass(this.appStatus); - }, + get statusBadgeClass() { + return Alpine.store("utils").statusBadgeClass(this.appStatus); + }, - get statusLabel() { - return Alpine.store("utils").statusLabel(this.appStatus); - }, + get statusLabel() { + return Alpine.store("utils").statusLabel(this.appStatus); + }, - get containerStatusBadgeClass() { - return ( - Alpine.store("utils").statusBadgeClass(this.containerStatus) + - " text-xs" - ); - }, + get containerStatusBadgeClass() { + return ( + Alpine.store("utils").statusBadgeClass(this.containerStatus) + + " text-xs" + ); + }, - get containerStatusLabel() { - return Alpine.store("utils").statusLabel(this.containerStatus); - }, + get containerStatusLabel() { + return Alpine.store("utils").statusLabel(this.containerStatus); + }, - get buildStatusBadgeClass() { - return ( - Alpine.store("utils").statusBadgeClass(this.buildStatus) + " text-xs" - ); - }, + get buildStatusBadgeClass() { + return ( + Alpine.store("utils").statusBadgeClass(this.buildStatus) + + " text-xs" + ); + }, - get buildStatusLabel() { - return Alpine.store("utils").statusLabel(this.buildStatus); - }, + get buildStatusLabel() { + return Alpine.store("utils").statusLabel(this.buildStatus); + }, - deploymentStatusClass(status) { - return Alpine.store("utils").statusBadgeClass(status); - }, + deploymentStatusClass(status) { + return Alpine.store("utils").statusBadgeClass(status); + }, - deploymentStatusLabel(status) { - return Alpine.store("utils").statusLabel(status); - }, + deploymentStatusLabel(status) { + return Alpine.store("utils").statusLabel(status); + }, - formatTime(isoTime) { - return Alpine.store("utils").formatRelativeTime(isoTime); - }, - })); + formatTime(isoTime) { + return Alpine.store("utils").formatRelativeTime(isoTime); + }, + })); }); diff --git a/static/js/components.js b/static/js/components.js index 2563b51..3683f41 100644 --- a/static/js/components.js +++ b/static/js/components.js @@ -6,66 +6,66 @@ */ document.addEventListener("alpine:init", () => { - // ============================================ - // Copy Button Component - // ============================================ - Alpine.data("copyButton", (targetId) => ({ - copied: false, - async copy() { - const target = document.getElementById(targetId); - if (!target) return; - const text = target.textContent || target.value; - const success = await Alpine.store("utils").copyToClipboard(text); - if (success) { - this.copied = true; - setTimeout(() => { - this.copied = false; - }, 2000); - } - }, - })); + // ============================================ + // Copy Button Component + // ============================================ + Alpine.data("copyButton", (targetId) => ({ + copied: false, + async copy() { + const target = document.getElementById(targetId); + if (!target) return; + const text = target.textContent || target.value; + const success = await Alpine.store("utils").copyToClipboard(text); + if (success) { + this.copied = true; + setTimeout(() => { + this.copied = false; + }, 2000); + } + }, + })); - // ============================================ - // Confirm Action Component - // ============================================ - Alpine.data("confirmAction", (message) => ({ - confirm(event) { - if (!window.confirm(message)) { - event.preventDefault(); - } - }, - })); + // ============================================ + // Confirm Action Component + // ============================================ + Alpine.data("confirmAction", (message) => ({ + confirm(event) { + if (!window.confirm(message)) { + event.preventDefault(); + } + }, + })); - // ============================================ - // Auto-dismiss Alert Component - // ============================================ - Alpine.data("autoDismiss", (delay = 5000) => ({ - show: true, - init() { - setTimeout(() => { - this.dismiss(); - }, delay); - }, - dismiss() { - this.show = false; - setTimeout(() => { - this.$el.remove(); - }, 300); - }, - })); + // ============================================ + // Auto-dismiss Alert Component + // ============================================ + Alpine.data("autoDismiss", (delay = 5000) => ({ + show: true, + init() { + setTimeout(() => { + this.dismiss(); + }, delay); + }, + dismiss() { + this.show = false; + setTimeout(() => { + this.$el.remove(); + }, 300); + }, + })); - // ============================================ - // Relative Time Component - // ============================================ - Alpine.data("relativeTime", (isoTime) => ({ - display: "", - init() { - this.update(); - // Update every minute - setInterval(() => this.update(), 60000); - }, - update() { - this.display = Alpine.store("utils").formatRelativeTime(isoTime); - }, - })); + // ============================================ + // Relative Time Component + // ============================================ + Alpine.data("relativeTime", (isoTime) => ({ + display: "", + init() { + this.update(); + // Update every minute + setInterval(() => this.update(), 60000); + }, + update() { + this.display = Alpine.store("utils").formatRelativeTime(isoTime); + }, + })); }); diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 7cbb024..b597ae5 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -5,17 +5,18 @@ */ document.addEventListener("alpine:init", () => { - Alpine.data("dashboard", () => ({ - init() { - // Update relative times every minute - setInterval(() => { - this.$el.querySelectorAll("[data-time]").forEach((el) => { - const time = el.getAttribute("data-time"); - if (time) { - el.textContent = Alpine.store("utils").formatRelativeTime(time); - } - }); - }, 60000); - }, - })); + Alpine.data("dashboard", () => ({ + init() { + // Update relative times every minute + setInterval(() => { + this.$el.querySelectorAll("[data-time]").forEach((el) => { + const time = el.getAttribute("data-time"); + if (time) { + el.textContent = + Alpine.store("utils").formatRelativeTime(time); + } + }); + }, 60000); + }, + })); }); diff --git a/static/js/deployment.js b/static/js/deployment.js index 7f00418..bbac098 100644 --- a/static/js/deployment.js +++ b/static/js/deployment.js @@ -6,171 +6,180 @@ */ document.addEventListener("alpine:init", () => { - // ============================================ - // Deployment Card Component (for individual deployment cards) - // ============================================ - Alpine.data("deploymentCard", (config) => ({ - appId: config.appId, - deploymentId: config.deploymentId, - logs: "", - status: config.status || "", - pollInterval: null, - _autoScroll: true, + // ============================================ + // Deployment Card Component (for individual deployment cards) + // ============================================ + Alpine.data("deploymentCard", (config) => ({ + appId: config.appId, + deploymentId: config.deploymentId, + logs: "", + status: config.status || "", + pollInterval: null, + _autoScroll: true, - init() { - // Read initial logs from script tag (avoids escaping issues) - const initialLogsEl = this.$el.querySelector(".initial-logs"); - this.logs = initialLogsEl?.dataset.logs || "Loading..."; + init() { + // Read initial logs from script tag (avoids escaping issues) + const initialLogsEl = this.$el.querySelector(".initial-logs"); + this.logs = initialLogsEl?.dataset.logs || "Loading..."; - // Set up scroll tracking - this.$nextTick(() => { - const wrapper = this.$refs.logsWrapper; - if (wrapper) { - wrapper.addEventListener('scroll', () => { - this._autoScroll = Alpine.store("utils").isScrolledToBottom(wrapper); - }, { passive: true }); - } - }); + // Set up scroll tracking + this.$nextTick(() => { + const wrapper = this.$refs.logsWrapper; + if (wrapper) { + wrapper.addEventListener( + "scroll", + () => { + this._autoScroll = + Alpine.store("utils").isScrolledToBottom( + wrapper, + ); + }, + { passive: true }, + ); + } + }); - // Only poll if deployment is in progress - if (Alpine.store("utils").isDeploying(this.status)) { - this.fetchLogs(); - this.pollInterval = setInterval(() => this.fetchLogs(), 1000); - } - }, + // Only poll if deployment is in progress + if (Alpine.store("utils").isDeploying(this.status)) { + this.fetchLogs(); + this.pollInterval = setInterval(() => this.fetchLogs(), 1000); + } + }, - destroy() { - if (this.pollInterval) { - clearInterval(this.pollInterval); - } - }, + destroy() { + if (this.pollInterval) { + clearInterval(this.pollInterval); + } + }, - async fetchLogs() { - try { - const res = await fetch( - `/apps/${this.appId}/deployments/${this.deploymentId}/logs`, - ); - const data = await res.json(); - const newLogs = data.logs || "No logs available"; - const logsChanged = newLogs !== this.logs; - this.logs = newLogs; - this.status = data.status; + async fetchLogs() { + try { + const res = await fetch( + `/apps/${this.appId}/deployments/${this.deploymentId}/logs`, + ); + const data = await res.json(); + const newLogs = data.logs || "No logs available"; + const logsChanged = newLogs !== this.logs; + this.logs = newLogs; + this.status = data.status; - // Scroll to bottom only when content changes AND user hasn't scrolled up - if (logsChanged && this._autoScroll) { - this.$nextTick(() => { - Alpine.store("utils").scrollToBottom(this.$refs.logsWrapper); - }); - } + // Scroll to bottom only when content changes AND user hasn't scrolled up + if (logsChanged && this._autoScroll) { + this.$nextTick(() => { + Alpine.store("utils").scrollToBottom( + this.$refs.logsWrapper, + ); + }); + } - // Stop polling if deployment is done - if (!Alpine.store("utils").isDeploying(data.status)) { - if (this.pollInterval) { - clearInterval(this.pollInterval); - this.pollInterval = null; - } - // Reload page to show final state with duration etc - window.location.reload(); - } - } catch (err) { - console.error("Logs fetch error:", err); - } - }, + // Stop polling if deployment is done + if (!Alpine.store("utils").isDeploying(data.status)) { + if (this.pollInterval) { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + // Reload page to show final state with duration etc + window.location.reload(); + } + } catch (err) { + console.error("Logs fetch error:", err); + } + }, - get statusBadgeClass() { - return Alpine.store("utils").statusBadgeClass(this.status); - }, + get statusBadgeClass() { + return Alpine.store("utils").statusBadgeClass(this.status); + }, - get statusLabel() { - return Alpine.store("utils").statusLabel(this.status); - }, - })); + get statusLabel() { + return Alpine.store("utils").statusLabel(this.status); + }, + })); - // ============================================ - // Deployments History Page Component - // ============================================ - Alpine.data("deploymentsPage", (config) => ({ - appId: config.appId, - currentDeploymentId: null, - isDeploying: false, + // ============================================ + // Deployments History Page Component + // ============================================ + Alpine.data("deploymentsPage", (config) => ({ + appId: config.appId, + currentDeploymentId: null, + isDeploying: false, - init() { - // Check for in-progress deployments on page load - const inProgressCard = document.querySelector( - '[data-status="building"], [data-status="deploying"]', - ); - if (inProgressCard) { - this.currentDeploymentId = parseInt( - inProgressCard.getAttribute("data-deployment-id"), - 10, - ); - this.isDeploying = true; - } + init() { + // Check for in-progress deployments on page load + const inProgressCard = document.querySelector( + '[data-status="building"], [data-status="deploying"]', + ); + if (inProgressCard) { + this.currentDeploymentId = parseInt( + inProgressCard.getAttribute("data-deployment-id"), + 10, + ); + this.isDeploying = true; + } - this.fetchAppStatus(); - this._scheduleStatusPoll(); - }, + this.fetchAppStatus(); + this._scheduleStatusPoll(); + }, - _statusPollTimer: null, + _statusPollTimer: null, - _scheduleStatusPoll() { - if (this._statusPollTimer) clearTimeout(this._statusPollTimer); - const interval = this.isDeploying ? 1000 : 10000; - this._statusPollTimer = setTimeout(() => { - this.fetchAppStatus(); - this._scheduleStatusPoll(); - }, interval); - }, + _scheduleStatusPoll() { + if (this._statusPollTimer) clearTimeout(this._statusPollTimer); + const interval = this.isDeploying ? 1000 : 10000; + this._statusPollTimer = setTimeout(() => { + this.fetchAppStatus(); + this._scheduleStatusPoll(); + }, interval); + }, - async fetchAppStatus() { - try { - const res = await fetch(`/apps/${this.appId}/status`); - const data = await res.json(); - // Use deployment status, not app status - it's more reliable during transitions - const deploying = Alpine.store("utils").isDeploying( - data.latestDeploymentStatus, - ); + async fetchAppStatus() { + try { + const res = await fetch(`/apps/${this.appId}/status`); + const data = await res.json(); + // Use deployment status, not app status - it's more reliable during transitions + const deploying = Alpine.store("utils").isDeploying( + data.latestDeploymentStatus, + ); - // Detect new deployment - if ( - data.latestDeploymentID && - data.latestDeploymentID !== this.currentDeploymentId - ) { - // Check if we have a card for this deployment - const hasCard = document.querySelector( - `[data-deployment-id="${data.latestDeploymentID}"]`, - ); + // Detect new deployment + if ( + data.latestDeploymentID && + data.latestDeploymentID !== this.currentDeploymentId + ) { + // Check if we have a card for this deployment + const hasCard = document.querySelector( + `[data-deployment-id="${data.latestDeploymentID}"]`, + ); - if (deploying && !hasCard) { - // New deployment started but no card exists - reload to show it - window.location.reload(); + if (deploying && !hasCard) { + // New deployment started but no card exists - reload to show it + window.location.reload(); - return; - } + return; + } - this.currentDeploymentId = data.latestDeploymentID; - } + this.currentDeploymentId = data.latestDeploymentID; + } - // Update deploying state based on latest deployment status - if (deploying && !this.isDeploying) { - this.isDeploying = true; - this._scheduleStatusPoll(); // Switch to fast polling - } else if (!deploying && this.isDeploying) { - // Deployment finished - reload to show final state - this.isDeploying = false; - window.location.reload(); - } - } catch (err) { - console.error("Status fetch error:", err); - } - }, + // Update deploying state based on latest deployment status + if (deploying && !this.isDeploying) { + this.isDeploying = true; + this._scheduleStatusPoll(); // Switch to fast polling + } else if (!deploying && this.isDeploying) { + // Deployment finished - reload to show final state + this.isDeploying = false; + window.location.reload(); + } + } catch (err) { + console.error("Status fetch error:", err); + } + }, - submitDeploy() { - this.isDeploying = true; - }, + submitDeploy() { + this.isDeploying = true; + }, - formatTime(isoTime) { - return Alpine.store("utils").formatRelativeTime(isoTime); - }, - })); + formatTime(isoTime) { + return Alpine.store("utils").formatRelativeTime(isoTime); + }, + })); }); diff --git a/static/js/utils.js b/static/js/utils.js index d2e6141..3b9e64b 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -5,139 +5,144 @@ */ document.addEventListener("alpine:init", () => { - Alpine.store("utils", { - /** - * Format a date string as relative time (e.g., "5 minutes ago") - */ - formatRelativeTime(dateStr) { - if (!dateStr) return ""; - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now - date; - const diffSec = Math.floor(diffMs / 1000); - const diffMin = Math.floor(diffSec / 60); - const diffHour = Math.floor(diffMin / 60); - const diffDay = Math.floor(diffHour / 24); + Alpine.store("utils", { + /** + * Format a date string as relative time (e.g., "5 minutes ago") + */ + formatRelativeTime(dateStr) { + if (!dateStr) return ""; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now - date; + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHour / 24); - if (diffSec < 60) return "just now"; - if (diffMin < 60) - return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago"); - if (diffHour < 24) - return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); - if (diffDay < 7) - return diffDay + (diffDay === 1 ? " day ago" : " days ago"); - return date.toLocaleDateString(); - }, + if (diffSec < 60) return "just now"; + if (diffMin < 60) + return ( + diffMin + (diffMin === 1 ? " minute ago" : " minutes ago") + ); + if (diffHour < 24) + return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); + if (diffDay < 7) + return diffDay + (diffDay === 1 ? " day ago" : " days ago"); + return date.toLocaleDateString(); + }, - /** - * Get the badge class for a given status - */ - statusBadgeClass(status) { - if (status === "running" || status === "success") return "badge-success"; - if (status === "building" || status === "deploying") - return "badge-warning"; - if (status === "failed" || status === "error") return "badge-error"; - return "badge-neutral"; - }, + /** + * Get the badge class for a given status + */ + statusBadgeClass(status) { + if (status === "running" || status === "success") + return "badge-success"; + if (status === "building" || status === "deploying") + return "badge-warning"; + if (status === "failed" || status === "error") return "badge-error"; + return "badge-neutral"; + }, - /** - * Format status for display (capitalize first letter) - */ - statusLabel(status) { - if (!status) return ""; - return status.charAt(0).toUpperCase() + status.slice(1); - }, + /** + * Format status for display (capitalize first letter) + */ + statusLabel(status) { + if (!status) return ""; + return status.charAt(0).toUpperCase() + status.slice(1); + }, - /** - * Check if status indicates active deployment - */ - isDeploying(status) { - return status === "building" || status === "deploying"; - }, + /** + * Check if status indicates active deployment + */ + isDeploying(status) { + return status === "building" || status === "deploying"; + }, - /** - * Scroll an element to the bottom - */ - scrollToBottom(el) { - if (el) { - requestAnimationFrame(() => { - el.scrollTop = el.scrollHeight; - }); - } - }, + /** + * Scroll an element to the bottom + */ + scrollToBottom(el) { + if (el) { + requestAnimationFrame(() => { + el.scrollTop = el.scrollHeight; + }); + } + }, - /** - * Check if a scrollable element is at (or near) the bottom. - * Tolerance of 30px accounts for rounding and partial lines. - */ - isScrolledToBottom(el, tolerance = 30) { - if (!el) return true; - return el.scrollHeight - el.scrollTop - el.clientHeight <= tolerance; - }, + /** + * Check if a scrollable element is at (or near) the bottom. + * Tolerance of 30px accounts for rounding and partial lines. + */ + isScrolledToBottom(el, tolerance = 30) { + if (!el) return true; + return ( + el.scrollHeight - el.scrollTop - el.clientHeight <= tolerance + ); + }, - /** - * Copy text to clipboard - */ - async copyToClipboard(text, button) { - try { - await navigator.clipboard.writeText(text); - return true; - } catch (err) { - // Fallback for older browsers - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.style.position = "fixed"; - textArea.style.left = "-9999px"; - document.body.appendChild(textArea); - textArea.select(); - try { - document.execCommand("copy"); - document.body.removeChild(textArea); - return true; - } catch (e) { - document.body.removeChild(textArea); - return false; - } - } - }, - }); + /** + * Copy text to clipboard + */ + async copyToClipboard(text, button) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + // Fallback for older browsers + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand("copy"); + document.body.removeChild(textArea); + return true; + } catch (e) { + document.body.removeChild(textArea); + return false; + } + } + }, + }); }); // ============================================ // Legacy support - expose utilities globally // ============================================ window.upaas = { - // These are kept for backwards compatibility but templates should use Alpine.js - formatRelativeTime(dateStr) { - if (!dateStr) return ""; - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now - date; - const diffSec = Math.floor(diffMs / 1000); - const diffMin = Math.floor(diffSec / 60); - const diffHour = Math.floor(diffMin / 60); - const diffDay = Math.floor(diffHour / 24); + // These are kept for backwards compatibility but templates should use Alpine.js + formatRelativeTime(dateStr) { + if (!dateStr) return ""; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now - date; + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHour / 24); - if (diffSec < 60) return "just now"; - if (diffMin < 60) - return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago"); - if (diffHour < 24) - return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); - if (diffDay < 7) - return diffDay + (diffDay === 1 ? " day ago" : " days ago"); - return date.toLocaleDateString(); - }, - // Placeholder functions - templates should migrate to Alpine.js - initAppDetailPage() {}, - initDeploymentsPage() {}, + if (diffSec < 60) return "just now"; + if (diffMin < 60) + return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago"); + if (diffHour < 24) + return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); + if (diffDay < 7) + return diffDay + (diffDay === 1 ? " day ago" : " days ago"); + return date.toLocaleDateString(); + }, + // Placeholder functions - templates should migrate to Alpine.js + initAppDetailPage() {}, + initDeploymentsPage() {}, }; // Update relative times on page load for non-Alpine elements document.addEventListener("DOMContentLoaded", () => { - document.querySelectorAll(".relative-time[data-time]").forEach((el) => { - const time = el.getAttribute("data-time"); - if (time) { - el.textContent = window.upaas.formatRelativeTime(time); - } - }); + document.querySelectorAll(".relative-time[data-time]").forEach((el) => { + const time = el.getAttribute("data-time"); + if (time) { + el.textContent = window.upaas.formatRelativeTime(time); + } + }); });