Skip to content
Merged
35 changes: 33 additions & 2 deletions gearbox/internal/framework/agent/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1109,14 +1109,45 @@ func (c *Client) GetFirewallConfig() (*FirewallConfigResponse, error) {
}

// UpdateFirewallConfig updates the firewall configuration.
// Note: Returns a response even on validation failure (400) - check resp.Success field.
// The agent returns 400 with a valid JSON body containing validation details
// (nft -c -f output, expected-SHA mismatch, etc.) rather than a generic error.
func (c *Client) UpdateFirewallConfig(req *FirewallConfigUpdateRequest) (*FirewallConfigUpdateResponse, error) {
body, err := c.doRequestWithBody("POST", "/api/v1/firewall/config", req)
fullURL := c.baseURL + "/api/v1/firewall/config"

jsonBody, err := json.Marshal(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}

httpReq, err := http.NewRequest("POST", fullURL, strings.NewReader(string(jsonBody)))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Content-Type", "application/json")

httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer httpResp.Body.Close()

body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

var resp FirewallConfigUpdateResponse
if err := json.Unmarshal(body, &resp); err != nil {
if httpResp.StatusCode >= 400 {
return nil, &APIError{
StatusCode: httpResp.StatusCode,
Message: fmt.Sprintf("HTTP %d: %s", httpResp.StatusCode, http.StatusText(httpResp.StatusCode)),
}
}
return nil, fmt.Errorf("failed to parse firewall config update response: %w", err)
}

Expand Down
2 changes: 2 additions & 0 deletions gearbox/internal/framework/handler/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ func (h *Handler) APIFirewallConfigSave(w http.ResponseWriter, r *http.Request)
boxID := chi.URLParam(r, "boxID")
server, err := h.db.GetBoxByBoxID(boxID)
if err != nil || server == nil {
h.logger.Warn("firewall config save: box lookup failed", "box_id", boxID, "err", err, "server_nil", server == nil)
h.jsonError(w, "Server not found", http.StatusNotFound)
return
}
Expand Down Expand Up @@ -645,6 +646,7 @@ func (h *Handler) APIFirewallConfigValidate(w http.ResponseWriter, r *http.Reque
boxID := chi.URLParam(r, "boxID")
server, err := h.db.GetBoxByBoxID(boxID)
if err != nil || server == nil {
h.logger.Warn("firewall config validate: box lookup failed", "box_id", boxID, "err", err, "server_nil", server == nil)
h.jsonError(w, "Server not found", http.StatusNotFound)
return
}
Expand Down
4 changes: 4 additions & 0 deletions gearbox/internal/framework/templates/layouts/base.templ
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ func gearLabelForPath(path string) string {
return "Alerts"
case path == "/os-updates" || strings.HasPrefix(path, "/os-updates/"):
return "OS Updates"
case path == "/config/firewall" || strings.HasPrefix(path, "/config/firewall/"):
return "Firewall Configuration"
case path == "/config/haproxy" || strings.HasPrefix(path, "/config/haproxy/"):
return "HAProxy Configuration"
case path == "/settings" || strings.HasPrefix(path, "/settings/"):
return "Settings"
case path == "/welcome":
Expand Down
28 changes: 0 additions & 28 deletions gearbox/internal/framework/templates/pages/backup.templ
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,6 @@ import (
// BackupPage renders the database backup management page.
templ BackupPage(user *models.User, backups []database.BackupInfo, backupDir string, successMsg, errorMsg string) {
@layouts.Base("Database Backups", user, "/settings") {
<!-- Hidden container for page header content -->
<div id="page-header-source" class="hidden">
<div class="flex items-center">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100">Database Backups</h2>
</div>
<div class="flex-1"></div>
<a href="/settings" class="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Back to Settings
</a>
</div>

<div class="max-w-6xl mx-auto px-4 py-8">
if successMsg != "" {
@ui.ToastOnLoad(successMsg, "success")
Expand Down Expand Up @@ -160,18 +146,6 @@ templ BackupPage(user *models.User, backups []database.BackupInfo, backupDir str
}

<script>
// Move page header content to main header
function setupPageHeader() {
const source = document.getElementById('page-header-source');
const target = document.getElementById('header-page-content');
if (source && target) {
while (source.firstChild) {
target.appendChild(source.firstChild);
}
source.remove();
}
}

async function createBackup() {
try {
const response = await fetch('/api/backup/create', {
Expand Down Expand Up @@ -289,8 +263,6 @@ templ BackupPage(user *models.User, backups []database.BackupInfo, backupDir str
function downloadBackup(backupPath) {
window.location.href = '/api/backup/download/' + encodeURIComponent(backupPath);
}

document.addEventListener('DOMContentLoaded', setupPageHeader);
</script>
</div>
}
Expand Down
Loading
Loading