ui improovements

This commit is contained in:
pavel 2026-05-14 22:52:59 +02:00
commit 950139010e
7 changed files with 67 additions and 0 deletions

View file

@ -80,6 +80,27 @@ func (a *App) ensureCaddyRoute(ctx context.Context, p Project) error {
return nil return nil
} }
func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.CaddyAdminURL+"/config/", nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("caddy config request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("caddy config error: status=%d body=%s", resp.StatusCode, string(body))
}
var out map[string]any
if err := json.Unmarshal(body, &out); err != nil {
return nil, fmt.Errorf("caddy config parse failed: %w", err)
}
return out, nil
}
func (a *App) ensureForgejoDeployWebhook(ctx context.Context, p Project) error { func (a *App) ensureForgejoDeployWebhook(ctx context.Context, p Project) error {
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" || p.RepoURL == "" { if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" || p.RepoURL == "" {
return nil return nil

View file

@ -24,6 +24,7 @@ func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error
return nil, err return nil, err
} }
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken) p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
p.AppURL = a.appURL(p.RouteHost)
out = append(out, p) out = append(out, p)
} }
return out, rows.Err() return out, rows.Err()
@ -70,6 +71,7 @@ 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)
p.AppURL = a.appURL(p.RouteHost)
return p, a.provisionProject(ctx, &p) return p, a.provisionProject(ctx, &p)
} }
@ -152,6 +154,7 @@ func (a *App) getProject(ctx context.Context, userID string, projectID int64) (P
return Project{}, err return Project{}, err
} }
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken) p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
p.AppURL = a.appURL(p.RouteHost)
return p, nil return p, nil
} }

View file

@ -15,6 +15,7 @@ func (a *App) routes() http.Handler {
mux.HandleFunc("/api/workspaces", a.withAuth(a.handleWorkspaces)) mux.HandleFunc("/api/workspaces", a.withAuth(a.handleWorkspaces))
mux.HandleFunc("/api/projects", a.withAuth(a.handleProjects)) mux.HandleFunc("/api/projects", a.withAuth(a.handleProjects))
mux.HandleFunc("/api/projects/", a.withAuth(a.handleProjectSubroutes)) mux.HandleFunc("/api/projects/", a.withAuth(a.handleProjectSubroutes))
mux.HandleFunc("/api/caddy/config", a.withAuth(a.handleCaddyConfig))
mux.HandleFunc("/api/webhooks/deploy/", a.handleDeployWebhook) mux.HandleFunc("/api/webhooks/deploy/", a.handleDeployWebhook)
return loggingMiddleware(mux) return loggingMiddleware(mux)
} }
@ -268,3 +269,16 @@ func (a *App) handleProjectLogs(w http.ResponseWriter, r *http.Request, user Use
} }
writeJSON(w, http.StatusOK, map[string]any{"lines": lines, "logs": logs}) writeJSON(w, http.StatusOK, map[string]any{"lines": lines, "logs": logs})
} }
func (a *App) handleCaddyConfig(w http.ResponseWriter, r *http.Request, _ User) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
cfg, err := a.fetchCaddyConfig(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
writeJSON(w, http.StatusOK, cfg)
}

View file

@ -47,6 +47,7 @@ type Project struct {
RepoPrivate bool `json:"repo_private"` 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"`
AppURL string `json:"app_url"`
TargetPort int `json:"target_port"` TargetPort int `json:"target_port"`
WorkspaceID int64 `json:"workspace_id"` WorkspaceID int64 `json:"workspace_id"`
Workspace string `json:"workspace"` Workspace string `json:"workspace"`

View file

@ -50,6 +50,14 @@ func (a *App) deployWebhookURL(projectID int64, token string) string {
return path return path
} }
func (a *App) appURL(routeHost string) string {
routeHost = strings.TrimSpace(routeHost)
if routeHost == "" {
return ""
}
return "https://" + routeHost
}
func newDeployToken() (string, error) { func newDeployToken() (string, error) {
buf := make([]byte, 24) buf := make([]byte, 24)
if _, err := rand.Read(buf); err != nil { if _, err := rand.Read(buf); err != nil {

View file

@ -15,8 +15,10 @@ async function init() {
document.getElementById('workspaceForm').addEventListener('submit', createWorkspace); document.getElementById('workspaceForm').addEventListener('submit', createWorkspace);
document.getElementById('createForm').addEventListener('submit', createProject); document.getElementById('createForm').addEventListener('submit', createProject);
document.getElementById('refreshCaddyBtn').addEventListener('click', loadCaddyConfig);
await refreshWorkspaces(); await refreshWorkspaces();
await refreshProjects(); await refreshProjects();
await loadCaddyConfig();
} }
async function refreshWorkspaces() { async function refreshWorkspaces() {
@ -52,6 +54,7 @@ function renderProjects() {
<p class="meta">workspace: ${escapeHtml(p.workspace)} (${escapeHtml(p.unix_user)})</p> <p class="meta">workspace: ${escapeHtml(p.workspace)} (${escapeHtml(p.unix_user)})</p>
<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">app url: ${p.app_url ? `<a class="link" href="${escapeHtml(p.app_url)}" target="_blank" rel="noreferrer">${escapeHtml(p.app_url)}</a>` : '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 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>
@ -194,6 +197,17 @@ async function createWorkspace(e) {
} }
} }
async function loadCaddyConfig() {
const out = document.getElementById('caddyConfigOut');
out.textContent = 'Loading Caddy config...';
try {
const cfg = await api('/api/caddy/config');
out.textContent = JSON.stringify(cfg, null, 2);
} catch (err) {
out.textContent = `Error: ${err.message}`;
}
}
function escapeHtml(s) { function escapeHtml(s) {
return String(s) return String(s)
.replaceAll('&', '&amp;') .replaceAll('&', '&amp;')

View file

@ -45,6 +45,12 @@
<h2>Your Projects</h2> <h2>Your Projects</h2>
<div id="projects" class="projects"></div> <div id="projects" class="projects"></div>
</section> </section>
<section class="card">
<h2>Caddy Config</h2>
<button id="refreshCaddyBtn" type="button">Refresh Caddy Config</button>
<pre id="caddyConfigOut" class="meta" style="white-space: pre-wrap; max-height: 320px; overflow: auto;">Not loaded.</pre>
</section>
</main> </main>
<script src="/app.js" defer></script> <script src="/app.js" defer></script>
</body> </body>