Files
upaas/static/js/deployment.js
clawbot 49ff625ac4
All checks were successful
Check / check (push) Successful in 4s
fix: add missing Makefile targets (docker, hooks) and test timeout (#159)
## 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 #136, closes #137

<!-- session: agent:sdlc-manager:subagent:44375174-444b-43bf-a341-2def7ebb9fdf -->

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #159
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-10 01:09:15 +01:00

186 lines
6.5 KiB
JavaScript

/**
* upaas - Deployment Components
*
* Deployment card (individual deployment log viewer) and
* deployments history page (list of all deployments).
*/
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,
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 },
);
}
});
// 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);
}
},
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,
);
});
}
// 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 statusLabel() {
return Alpine.store("utils").statusLabel(this.status);
},
}));
// ============================================
// 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;
}
this.fetchAppStatus();
this._scheduleStatusPoll();
},
_statusPollTimer: null,
_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,
);
// 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();
return;
}
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);
}
},
submitDeploy() {
this.isDeploying = true;
},
formatTime(isoTime) {
return Alpine.store("utils").formatRelativeTime(isoTime);
},
}));
});