fix: while connected to MySQL if internet was lost no qso would be logged anymore

This commit is contained in:
2026-07-12 18:01:03 +02:00
parent 7398261c50
commit a00817b93e
16 changed files with 850 additions and 9 deletions
+64 -3
View File
@@ -21,6 +21,12 @@ const clublogRealtimeURL = "https://clublog.org/realtime.php"
// N QSOs is one HTTP request instead of N realtime.php calls.
const clublogBatchURL = "https://clublog.org/putlogs.php"
// clublogUserAgent identifies OpsLog to Club Log. Go's default
// "Go-http-client/1.1" User-Agent is blocked by Club Log's web front end (nginx
// returns 403 Forbidden before the request reaches the app), so every request
// must send a real, app-identifying User-Agent.
const clublogUserAgent = "OpsLog/1.0 (+https://github.com/GregTroar/OpsLog)"
// clublogAppAPIKey is OpsLog's Club Log *application* API key. Club Log
// requires an api parameter that identifies the client software (not the
// user) — the same way Log4OM embeds its own key — so we ship it baked in
@@ -122,6 +128,7 @@ func UploadClublogADIF(ctx context.Context, client *http.Client, cfg ServiceConf
return UploadResult{}, fmt.Errorf("clublog: build request: %w", err)
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("User-Agent", clublogUserAgent)
if client == nil {
client = &http.Client{Timeout: 120 * time.Second}
}
@@ -135,10 +142,63 @@ func UploadClublogADIF(ctx context.Context, client *http.Client, cfg ServiceConf
if resp.StatusCode == http.StatusOK {
return UploadResult{OK: true, Message: msg}, nil
}
if msg == "" {
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
diag := clublogFailureDiag(resp, msg)
return UploadResult{OK: false, Message: diag}, fmt.Errorf("clublog: batch upload failed: %s", diag)
}
// clublogFailureDiag turns a non-200 response into a readable one-liner that
// names WHO blocked the request — Club Log's app vs. an intermediary (Cloudflare,
// Sucuri, a corporate proxy, an antivirus TLS shim). A bare "403 Forbidden nginx"
// page means the request never reached the app; the fingerprint headers below
// tell the operator whether it's Club Log's own WAF or something on their side.
func clublogFailureDiag(resp *http.Response, body string) string {
server := strings.TrimSpace(resp.Header.Get("Server"))
// Notable intermediary fingerprints — presence points at the culprit.
fp := []string{}
for _, h := range []string{"CF-RAY", "CF-Mitigated", "X-Sucuri-ID", "X-Sucuri-Block", "X-Squid-Error", "Via", "X-Cache", "Retry-After"} {
if v := strings.TrimSpace(resp.Header.Get(h)); v != "" {
fp = append(fp, h+"="+v)
}
}
return UploadResult{OK: false, Message: msg}, fmt.Errorf("clublog: batch upload failed: %s", msg)
// Collapse the HTML error page to something short.
summary := stripHTMLBrief(body)
if summary == "" {
summary = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
out := fmt.Sprintf("HTTP %d — %s", resp.StatusCode, summary)
if server != "" {
out += " [server=" + server + "]"
}
if len(fp) > 0 {
out += " [" + strings.Join(fp, " ") + "]"
}
return out
}
// stripHTMLBrief removes tags and collapses whitespace, returning the first ~160
// chars of visible text — enough to read "403 Forbidden" without the markup.
func stripHTMLBrief(s string) string {
var b strings.Builder
depth := 0
for _, r := range s {
switch r {
case '<':
depth++
case '>':
if depth > 0 {
depth--
}
default:
if depth == 0 {
b.WriteRune(r)
}
}
}
out := strings.Join(strings.Fields(b.String()), " ")
if len(out) > 160 {
out = out[:160] + "…"
}
return out
}
// TestClublog validates the configured credentials by attempting a no-op
@@ -164,6 +224,7 @@ func clublogPost(ctx context.Context, client *http.Client, endpoint string, form
return UploadResult{}, fmt.Errorf("clublog: build request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", clublogUserAgent)
if client == nil {
client = &http.Client{Timeout: 20 * time.Second}
}