retry repo create
This commit is contained in:
parent
4e6ad3cd26
commit
515d90cc9e
7 changed files with 88 additions and 19 deletions
|
|
@ -91,6 +91,7 @@ Notes:
|
||||||
- Per-project runtime endpoints:
|
- Per-project runtime endpoints:
|
||||||
- `GET /api/projects/:id/status`
|
- `GET /api/projects/:id/status`
|
||||||
- `GET /api/projects/:id/logs?lines=200`
|
- `GET /api/projects/:id/logs?lines=200`
|
||||||
|
- `POST /api/projects/:id/reprovision` (retry provisioning for failed/pending projects)
|
||||||
- Status is read via `systemctl --user show <service>`.
|
- Status is read via `systemctl --user show <service>`.
|
||||||
- Logs are read via `journalctl --user -u <service>`.
|
- Logs are read via `journalctl --user -u <service>`.
|
||||||
- Both commands are executed as the workspace Unix user via `sudo -n -u <unix_user> ...`.
|
- Both commands are executed as the workspace Unix user via `sudo -n -u <unix_user> ...`.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadConfig() (Config, error) {
|
func loadConfig() (Config, error) {
|
||||||
|
projectRoot := expandPath(getenv("PROJECTS_ROOT", "$HOME/projects"))
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
DatabaseURL: getenv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/box?sslmode=disable"),
|
DatabaseURL: getenv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/box?sslmode=disable"),
|
||||||
ListenAddr: getenv("LISTEN_ADDR", ":8080"),
|
ListenAddr: getenv("LISTEN_ADDR", ":8080"),
|
||||||
|
|
@ -21,7 +22,7 @@ func loadConfig() (Config, error) {
|
||||||
ForgejoBaseURL: strings.TrimRight(getenv("FORGEJO_BASE_URL", ""), "/"),
|
ForgejoBaseURL: strings.TrimRight(getenv("FORGEJO_BASE_URL", ""), "/"),
|
||||||
ForgejoToken: getenv("FORGEJO_TOKEN", ""),
|
ForgejoToken: getenv("FORGEJO_TOKEN", ""),
|
||||||
ForgejoOrg: getenv("FORGEJO_ORG", ""),
|
ForgejoOrg: getenv("FORGEJO_ORG", ""),
|
||||||
ProjectRoot: getenv("PROJECTS_ROOT", filepath.Join(userHomeOrDot(), "projects")),
|
ProjectRoot: projectRoot,
|
||||||
DefaultRunCommand: getenv("DEFAULT_RUN_COMMAND", "go run ."),
|
DefaultRunCommand: getenv("DEFAULT_RUN_COMMAND", "go run ."),
|
||||||
CaddyAdminURL: strings.TrimRight(getenv("CADDY_ADMIN_URL", "http://localhost:2019"), "/"),
|
CaddyAdminURL: strings.TrimRight(getenv("CADDY_ADMIN_URL", "http://localhost:2019"), "/"),
|
||||||
CaddyServerID: getenv("CADDY_SERVER_ID", "srv0"),
|
CaddyServerID: getenv("CADDY_SERVER_ID", "srv0"),
|
||||||
|
|
@ -35,6 +36,23 @@ func loadConfig() (Config, error) {
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expandPath(p string) string {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
return filepath.Join(userHomeOrDot(), "projects")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(p, "~/") {
|
||||||
|
p = filepath.Join(userHomeOrDot(), strings.TrimPrefix(p, "~/"))
|
||||||
|
}
|
||||||
|
p = os.ExpandEnv(p)
|
||||||
|
if !filepath.IsAbs(p) {
|
||||||
|
if abs, err := filepath.Abs(p); err == nil {
|
||||||
|
p = abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filepath.Clean(p)
|
||||||
|
}
|
||||||
|
|
||||||
func userHomeOrDot() string {
|
func userHomeOrDot() string {
|
||||||
h, err := os.UserHomeDir()
|
h, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ func (a *App) migrate(ctx context.Context) error {
|
||||||
slug TEXT NOT NULL,
|
slug TEXT NOT NULL,
|
||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
repo_url TEXT NOT NULL DEFAULT '',
|
repo_url TEXT NOT NULL DEFAULT '',
|
||||||
|
repo_private BOOLEAN NOT NULL DEFAULT true,
|
||||||
service_name TEXT NOT NULL,
|
service_name TEXT NOT NULL,
|
||||||
route_host TEXT NOT NULL DEFAULT '',
|
route_host TEXT NOT NULL DEFAULT '',
|
||||||
target_port INT NOT NULL DEFAULT 8000,
|
target_port INT NOT NULL DEFAULT 8000,
|
||||||
|
|
@ -47,6 +48,7 @@ func (a *App) migrate(ctx context.Context) error {
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS target_port INT NOT NULL DEFAULT 8000;`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS target_port INT NOT NULL DEFAULT 8000;`,
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS workspace_id BIGINT REFERENCES workspaces(id) ON DELETE RESTRICT;`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS workspace_id BIGINT REFERENCES workspaces(id) ON DELETE RESTRICT;`,
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS deploy_token TEXT NOT NULL DEFAULT '';`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS deploy_token TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS repo_private BOOLEAN NOT NULL DEFAULT true;`,
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_state TEXT NOT NULL DEFAULT 'provisioned';`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_state TEXT NOT NULL DEFAULT 'provisioned';`,
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_error TEXT NOT NULL DEFAULT '';`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_error TEXT NOT NULL DEFAULT '';`,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error) {
|
func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error) {
|
||||||
rows, err := a.db.QueryContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.created_at
|
rows, err := a.db.QueryContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.created_at
|
||||||
FROM projects p
|
FROM projects p
|
||||||
JOIN workspaces w ON w.id = p.workspace_id
|
JOIN workspaces w ON w.id = p.workspace_id
|
||||||
WHERE p.user_id=$1 ORDER BY p.created_at DESC`, userID)
|
WHERE p.user_id=$1 ORDER BY p.created_at DESC`, userID)
|
||||||
|
|
@ -20,7 +20,7 @@ func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error
|
||||||
var out []Project
|
var out []Project
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var p Project
|
var p Project
|
||||||
if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt); err != nil {
|
if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
||||||
|
|
@ -54,9 +54,9 @@ func (a *App) createProject(ctx context.Context, user User, workspaceID int64, n
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
err = tx.QueryRowContext(ctx, `INSERT INTO projects (user_id, workspace_id, name, slug, description, repo_url, service_name, route_host, target_port, deploy_token, provision_state, provision_error) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'pending','') RETURNING id, user_id, name, slug, description, repo_url, service_name, route_host, target_port, workspace_id, deploy_token, provision_state, provision_error, created_at`,
|
err = tx.QueryRowContext(ctx, `INSERT INTO projects (user_id, workspace_id, name, slug, description, repo_url, repo_private, service_name, route_host, target_port, deploy_token, provision_state, provision_error) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'pending','') RETURNING id, user_id, name, slug, description, repo_url, repo_private, service_name, route_host, target_port, workspace_id, deploy_token, provision_state, provision_error, created_at`,
|
||||||
user.Username, workspaceID, name, slug, description, repoURL, serviceName, routeHost, targetPort, deployToken,
|
user.Username, workspaceID, name, slug, description, repoURL, private, serviceName, routeHost, targetPort, deployToken,
|
||||||
).Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
).Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
return Project{}, errors.New("project with this name already exists")
|
return Project{}, errors.New("project with this name already exists")
|
||||||
|
|
@ -70,57 +70,67 @@ func (a *App) createProject(ctx context.Context, user User, workspaceID int64, n
|
||||||
p.Workspace = workspace.Name
|
p.Workspace = workspace.Name
|
||||||
p.UnixUser = workspace.UnixUser
|
p.UnixUser = workspace.UnixUser
|
||||||
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
||||||
|
return p, a.provisionProject(ctx, &p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) provisionProject(ctx context.Context, p *Project) error {
|
||||||
if p.RepoURL == "" {
|
if p.RepoURL == "" {
|
||||||
repoURL, err = a.createForgejoRepo(ctx, slug, description, private)
|
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" {
|
||||||
|
err := errors.New("repo_url is empty and Forgejo is not configured; set FORGEJO_BASE_URL/FORGEJO_TOKEN or provide repo_url")
|
||||||
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||||
|
p.ProvisionState = "failed"
|
||||||
|
p.ProvisionError = err.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
repoURL, err := a.createForgejoRepo(ctx, p.Slug, p.Description, p.RepoPrivate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||||
p.ProvisionState = "failed"
|
p.ProvisionState = "failed"
|
||||||
p.ProvisionError = err.Error()
|
p.ProvisionError = err.Error()
|
||||||
return p, err
|
return err
|
||||||
}
|
}
|
||||||
p.RepoURL = repoURL
|
p.RepoURL = repoURL
|
||||||
if _, err := a.db.ExecContext(ctx, `UPDATE projects SET repo_url=$1 WHERE id=$2`, repoURL, p.ID); err != nil {
|
if _, err := a.db.ExecContext(ctx, `UPDATE projects SET repo_url=$1 WHERE id=$2`, repoURL, p.ID); err != nil {
|
||||||
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||||
p.ProvisionState = "failed"
|
p.ProvisionState = "failed"
|
||||||
p.ProvisionError = err.Error()
|
p.ProvisionError = err.Error()
|
||||||
return p, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := a.ensureForgejoDeployWebhook(ctx, p); err != nil {
|
if err := a.ensureForgejoDeployWebhook(ctx, *p); err != nil {
|
||||||
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||||
p.ProvisionState = "failed"
|
p.ProvisionState = "failed"
|
||||||
p.ProvisionError = err.Error()
|
p.ProvisionError = err.Error()
|
||||||
return p, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.renderSystemdForProject(ctx, p); err != nil {
|
if err := a.renderSystemdForProject(ctx, *p); err != nil {
|
||||||
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||||
p.ProvisionState = "failed"
|
p.ProvisionState = "failed"
|
||||||
p.ProvisionError = err.Error()
|
p.ProvisionError = err.Error()
|
||||||
return p, err
|
return err
|
||||||
}
|
}
|
||||||
if err := a.ensureCaddyRoute(ctx, p); err != nil {
|
if err := a.ensureCaddyRoute(ctx, *p); err != nil {
|
||||||
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||||
p.ProvisionState = "failed"
|
p.ProvisionState = "failed"
|
||||||
p.ProvisionError = err.Error()
|
p.ProvisionError = err.Error()
|
||||||
return p, err
|
return err
|
||||||
}
|
}
|
||||||
if err := a.updateProvisioningState(ctx, p.ID, "provisioned", ""); err != nil {
|
if err := a.updateProvisioningState(ctx, p.ID, "provisioned", ""); err != nil {
|
||||||
p.ProvisionState = "failed"
|
p.ProvisionState = "failed"
|
||||||
p.ProvisionError = err.Error()
|
p.ProvisionError = err.Error()
|
||||||
return p, err
|
return err
|
||||||
}
|
}
|
||||||
p.ProvisionState = "provisioned"
|
p.ProvisionState = "provisioned"
|
||||||
p.ProvisionError = ""
|
p.ProvisionError = ""
|
||||||
return p, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) getProject(ctx context.Context, userID string, projectID int64) (Project, error) {
|
func (a *App) getProject(ctx context.Context, userID string, projectID int64) (Project, error) {
|
||||||
var p Project
|
var p Project
|
||||||
err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.created_at
|
err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.created_at
|
||||||
FROM projects p JOIN workspaces w ON w.id=p.workspace_id WHERE p.user_id=$1 AND p.id=$2`, userID, projectID).
|
FROM projects p JOIN workspaces w ON w.id=p.workspace_id WHERE p.user_id=$1 AND p.id=$2`, userID, projectID).
|
||||||
Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return Project{}, errors.New("project not found")
|
return Project{}, errors.New("project not found")
|
||||||
|
|
@ -136,6 +146,22 @@ func (a *App) updateProvisioningState(ctx context.Context, projectID int64, stat
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) reprovisionProject(ctx context.Context, userID string, projectID int64) (Project, error) {
|
||||||
|
p, err := a.getProject(ctx, userID, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return Project{}, err
|
||||||
|
}
|
||||||
|
if err := a.updateProvisioningState(ctx, p.ID, "pending", ""); err != nil {
|
||||||
|
return Project{}, err
|
||||||
|
}
|
||||||
|
p.ProvisionState = "pending"
|
||||||
|
p.ProvisionError = ""
|
||||||
|
if err := a.provisionProject(ctx, &p); err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) listEnvVars(ctx context.Context, userID string, projectID int64) ([]EnvVar, error) {
|
func (a *App) listEnvVars(ctx context.Context, userID string, projectID int64) ([]EnvVar, error) {
|
||||||
if _, err := a.getProject(ctx, userID, projectID); err != nil {
|
if _, err := a.getProject(ctx, userID, projectID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,8 @@ func (a *App) handleProjectSubroutes(w http.ResponseWriter, r *http.Request, use
|
||||||
a.handleProjectEnv(w, r, user, projectID)
|
a.handleProjectEnv(w, r, user, projectID)
|
||||||
case "service":
|
case "service":
|
||||||
a.handleServiceRegenerate(w, r, user, projectID)
|
a.handleServiceRegenerate(w, r, user, projectID)
|
||||||
|
case "reprovision":
|
||||||
|
a.handleProjectReprovision(w, r, user, projectID)
|
||||||
case "status":
|
case "status":
|
||||||
a.handleProjectStatus(w, r, user, projectID)
|
a.handleProjectStatus(w, r, user, projectID)
|
||||||
case "logs":
|
case "logs":
|
||||||
|
|
@ -145,6 +147,19 @@ func (a *App) handleProjectSubroutes(w http.ResponseWriter, r *http.Request, use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) handleProjectReprovision(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, err := a.reprovisionProject(r.Context(), user.Username, projectID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, p)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) handleProjectEnv(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
func (a *App) handleProjectEnv(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ type Project struct {
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
RepoURL string `json:"repo_url"`
|
RepoURL string `json:"repo_url"`
|
||||||
|
RepoPrivate bool `json:"repo_private"`
|
||||||
ServiceName string `json:"service_name"`
|
ServiceName string `json:"service_name"`
|
||||||
RouteHost string `json:"route_host"`
|
RouteHost string `json:"route_host"`
|
||||||
TargetPort int `json:"target_port"`
|
TargetPort int `json:"target_port"`
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ function renderProjects() {
|
||||||
<p class="meta">slug: ${escapeHtml(p.slug)} | service: ${escapeHtml(p.service_name)}</p>
|
<p class="meta">slug: ${escapeHtml(p.slug)} | service: ${escapeHtml(p.service_name)}</p>
|
||||||
<p class="meta">app target: 127.0.0.1:${escapeHtml(p.target_port)} | route host: ${escapeHtml(p.route_host || 'not set')}</p>
|
<p class="meta">app target: 127.0.0.1:${escapeHtml(p.target_port)} | route host: ${escapeHtml(p.route_host || 'not set')}</p>
|
||||||
<p class="meta">deploy webhook: <code>${escapeHtml(p.webhook_url || 'n/a')}</code></p>
|
<p class="meta">deploy webhook: <code>${escapeHtml(p.webhook_url || 'n/a')}</code></p>
|
||||||
|
<p class="meta">provisioning: ${escapeHtml(p.provision_state || 'unknown')}${p.provision_error ? ` | error: ${escapeHtml(p.provision_error)}` : ''}</p>
|
||||||
<p>${escapeHtml(p.description || '')}</p>
|
<p>${escapeHtml(p.description || '')}</p>
|
||||||
${p.repo_url ? `<p><a class="link" href="${p.repo_url}" target="_blank" rel="noreferrer">Open Repository</a></p>` : '<p class="muted">No repository URL set.</p>'}
|
${p.repo_url ? `<p><a class="link" href="${p.repo_url}" target="_blank" rel="noreferrer">Open Repository</a></p>` : '<p class="muted">No repository URL set.</p>'}
|
||||||
<div class="env-row">
|
<div class="env-row">
|
||||||
|
|
@ -65,6 +66,7 @@ function renderProjects() {
|
||||||
<div class="env-row">
|
<div class="env-row">
|
||||||
<button class="small" data-status>Refresh Status</button>
|
<button class="small" data-status>Refresh Status</button>
|
||||||
<button class="small" data-logs>Load Logs</button>
|
<button class="small" data-logs>Load Logs</button>
|
||||||
|
<button class="small" data-reprovision>Reprovision</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta" data-status-out>status: loading...</div>
|
<div class="meta" data-status-out>status: loading...</div>
|
||||||
<pre class="meta" data-logs-out style="white-space: pre-wrap; max-height: 220px; overflow: auto;">logs: not loaded</pre>
|
<pre class="meta" data-logs-out style="white-space: pre-wrap; max-height: 220px; overflow: auto;">logs: not loaded</pre>
|
||||||
|
|
@ -90,6 +92,10 @@ function renderProjects() {
|
||||||
div.querySelector('[data-logs]').addEventListener('click', async () => {
|
div.querySelector('[data-logs]').addEventListener('click', async () => {
|
||||||
await loadLogs(div, p.id);
|
await loadLogs(div, p.id);
|
||||||
});
|
});
|
||||||
|
div.querySelector('[data-reprovision]').addEventListener('click', async () => {
|
||||||
|
await api(`/api/projects/${p.id}/reprovision`, { method: 'POST' });
|
||||||
|
await refreshProjects();
|
||||||
|
});
|
||||||
|
|
||||||
root.appendChild(div);
|
root.appendChild(div);
|
||||||
loadEnvList(div, p.id);
|
loadEnvList(div, p.id);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue