diff --git a/obs-status-service/main.go b/obs-status-service/main.go index 80446fe..1884640 100644 --- a/obs-status-service/main.go +++ b/obs-status-service/main.go @@ -266,13 +266,37 @@ func main() { } }() + // Serve static files (CSS, JS, images) + http.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + // Serve favicon + http.HandleFunc("GET /favicon.ico", func(res http.ResponseWriter, req *http.Request) { + data, err := os.ReadFile("static/favicon.svg") + if err != nil { + res.WriteHeader(404) + return + } + res.Header().Set("content-type", "image/svg+xml") + res.Write(data) + }) + http.HandleFunc("GET /", func(res http.ResponseWriter, req *http.Request) { if rescanRepoError != nil { res.WriteHeader(500) return } - res.WriteHeader(404) - res.Write([]byte("404 page not found\n")) + + path := "static/index.html" + data, err := os.ReadFile(path) + if err != nil { + res.WriteHeader(404) + res.Write([]byte("404 page not found\n")) + return + } + + res.Header().Add("content-type", "text/html") + res.Write(data) + }) http.HandleFunc("GET /status/{Project}", func(res http.ResponseWriter, req *http.Request) { mime := ParseMimeHeader(req) diff --git a/obs-status-service/static/favicon.svg b/obs-status-service/static/favicon.svg new file mode 100644 index 0000000..b534347 --- /dev/null +++ b/obs-status-service/static/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/obs-status-service/static/index.html b/obs-status-service/static/index.html new file mode 100644 index 0000000..b53af8a --- /dev/null +++ b/obs-status-service/static/index.html @@ -0,0 +1,129 @@ + + + + + + + OBS Status Service + + + + + + + + +
+

openSUSE Logo OBS Status Service

+

Generate embeddable SVG badges showing build status from Open Build Service.

+
+ +
+
+

Get your Build Result Image

+
+
+
+ + + +
+
+
+

Preview

+
+ +

Enter a project name to see the preview

+
+
+
+ + + +
+

Markdown

+
+

+                    
+                
+
+
+ +
+ +
+

Usage

+ +

Request Formats

+

Requests for individual build results:

+
/status/{project}/{package}/{repo}/{arch}
+

Where package, repo and arch are optional parameters.

+ +

Requests for project results:

+
/status/{project}
+ +

Output Formats

+

By default, SVG output is generated. JSON and XML output is possible by setting the Accept + request header:

+ + + + + + + + + + + + + + + + + + + + + +
Accept HeaderOutput Format
(default)SVG image
application/jsonJSON data
application/obs+xmlXML output
+
+
+ + + + \ No newline at end of file diff --git a/obs-status-service/static/logo.svg b/obs-status-service/static/logo.svg new file mode 100644 index 0000000..65b332b --- /dev/null +++ b/obs-status-service/static/logo.svg @@ -0,0 +1 @@ +openSUSE icon diff --git a/obs-status-service/static/script.js b/obs-status-service/static/script.js new file mode 100644 index 0000000..0a1c4c3 --- /dev/null +++ b/obs-status-service/static/script.js @@ -0,0 +1,83 @@ +function updatePreview() { + const project = document.getElementById('project').value.trim(); + const pkg = document.getElementById('package').value.trim(); + const repo = document.getElementById('repo').value.trim(); + const hint = document.getElementById('preview-hint'); + const preview = document.getElementById('preview'); + const link = document.getElementById('result-link'); + const markdown = document.getElementById('result-markdown'); + const linkSection = document.getElementById('link-section'); + const markdownSection = document.getElementById('markdown-section'); + + if (!project) { + hint.style.display = 'block'; + preview.style.display = 'none'; + preview.data = ''; + link.value = ''; + markdown.textContent = ''; + linkSection.classList.remove('visible'); + markdownSection.classList.remove('visible'); + return; + } + + // Build the path correctly with all components + let path = '/status/' + encodeURIComponent(project); + if (pkg) { + path += '/' + encodeURIComponent(pkg); + } + if (repo) { + path += '/' + repo; // repo already contains / for arch + } + + const fullUrl = window.location.origin + path; + + hint.style.display = 'none'; + preview.style.display = 'block'; + + // Force reload of object to ensure it updates even if path is similar or cached + const newPreview = preview.cloneNode(true); + newPreview.data = path; + preview.parentNode.replaceChild(newPreview, preview); + // Update reference if we were using it elsewhere, though here we just exit + + link.value = fullUrl; + markdown.textContent = '![Build Status](' + fullUrl + ')'; + + // Show result sections with animation + linkSection.classList.add('visible'); + markdownSection.classList.add('visible'); +} + +function copyResult(elementId, btnId) { + let textToCopy = ''; + const element = document.getElementById(elementId); + + if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { + textToCopy = element.value; + } else { + textToCopy = element.textContent; + } + + const btn = document.getElementById(btnId); + + if (!textToCopy) return; + + navigator.clipboard.writeText(textToCopy).then(() => { + const originalHTML = btn.innerHTML; + btn.textContent = '✓'; + btn.classList.add('copied'); + + setTimeout(() => { + btn.innerHTML = originalHTML; + btn.classList.remove('copied'); + }, 2000); + }).catch(err => { + console.error('Failed to copy:', err); + btn.textContent = 'Error'; + }); +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', () => { + updatePreview(); +}); diff --git a/obs-status-service/static/style.css b/obs-status-service/static/style.css new file mode 100644 index 0000000..73f863e --- /dev/null +++ b/obs-status-service/static/style.css @@ -0,0 +1,324 @@ +/* OBS Status Service - Custom Styles */ + +/* openSUSE Brand Colors */ +:root { + --opensuse-green: #73ba25; + --opensuse-dark-green: #6da741; + --opensuse-darker: #35b4a1; + --bg-dark: #1a1a2e; + --bg-card: #16213e; + --text-muted: #8892b0; + --border-color: #233554; + + /* Override Pico primary color */ + --pico-primary: var(--opensuse-green); + --pico-primary-hover: var(--opensuse-dark-green); + + --font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif; +} + +/* Dark theme */ +html { + background: linear-gradient(135deg, var(--bg-dark) 0%, #0f0f1a 100%); + min-height: 100vh; + font-family: var(--font-family); +} + +body.container { + max-width: 900px; + padding-top: 2rem; + padding-bottom: 3rem; +} + +/* Header styling */ +h1 { + color: var(--opensuse-green); + font-size: 2.5rem; + font-weight: 700; + display: flex; + align-items: center; + gap: 0.75rem; +} + +/* Removed emoji ::before content */ + +.logo-icon { + height: 2.5rem !important; + /* Adjusted to match text size */ + width: auto; +} + +h2 { + color: #e6f1ff; + border-bottom: 2px solid var(--opensuse-green); + padding-bottom: 0.5rem; + margin-top: 2rem; +} + +h3, +h4 { + color: #ccd6f6; +} + +/* Card styling */ +article { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +/* Form inputs */ +fieldset { + border: none; + padding: 0; +} + +input { + background: rgba(255, 255, 255, 0.05) !important; + border: 1px solid var(--border-color) !important; + border-radius: 8px !important; + color: #e6f1ff !important; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + font-family: inherit; +} + +input:focus { + border-color: var(--opensuse-green) !important; + box-shadow: 0 0 0 3px rgba(115, 186, 37, 0.2) !important; +} + +input::placeholder { + color: var(--text-muted) !important; + opacity: 0.7; +} + +label { + color: #a8b2d1; + font-weight: 500; + font-size: 0.9rem; +} + +/* Preview section */ +#preview-container { + min-height: 150px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#preview { + max-width: 100%; + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); + min-height: 80px; +} + +#preview-hint { + color: var(--text-muted); + text-align: center; + padding: 2rem; + border: 2px dashed var(--border-color); + border-radius: 8px; + margin: 0; +} + +/* Results section - hidden by default */ +.result-section { + margin-top: 1.5rem; + opacity: 0; + max-height: 0; + overflow: hidden; + transition: opacity 0.3s ease, max-height 0.3s ease; +} + +.result-section.visible { + opacity: 1; + max-height: 200px; + overflow: visible; + /* Allow copy button to be interactive */ +} + +.result-section h3 { + margin-bottom: 0.5rem; + font-size: 1rem; + color: var(--opensuse-green); +} + +/* Input Groups for Result Sections */ +.input-group, +.markdown-container { + position: relative; + display: flex; + align-items: center; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 0.5rem; +} + +.input-group input { + background: transparent !important; + border: none !important; + box-shadow: none !important; + width: 100%; + padding: 0.5rem; + margin-bottom: 0; + font-family: 'Fira Code', 'Monaco', monospace; + font-size: 0.9rem; + color: var(--opensuse-green) !important; +} + +.input-group input:focus { + box-shadow: none !important; +} + +/* Markdown specific container override */ +.markdown-container { + padding: 1rem; + padding-right: 4rem; + /* Space for button */ + display: block; + /* Use block for pre tag */ +} + +#result-markdown { + margin: 0; + font-family: 'Fira Code', 'Monaco', monospace; + font-size: 0.85rem; + color: #a8b2d1; + word-break: break-all; + white-space: pre-wrap; + border: none; + background: transparent !important; +} + +/* Copy button */ +.copy-btn { + position: absolute; + top: 50%; + right: 0.5rem; + transform: translateY(-50%); + background: transparent; + color: var(--opensuse-green); + border: 1px solid var(--opensuse-green); + border-radius: 6px; + padding: 0.4rem 0.8rem; + cursor: pointer; + font-size: 0.8rem; + transition: all 0.2s ease; + z-index: 10; + font-weight: bold; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.copy-icon { + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.copy-btn:hover { + background: var(--opensuse-green); + color: #fff; +} + +/* .markdown-container .copy-btn specific overrides removed to allow vertical centering */ + +/* Hover state handled above */ + +.copy-btn:active { + transform: translateY(-50%) scale(0.95); +} + +.markdown-container .copy-btn:active { + transform: translateY(-50%) scale(0.95); +} + +.copy-btn.copied { + background: #28a745; +} + +/* Horizontal rules */ +hr { + border: none; + height: 1px; + background: linear-gradient(to right, transparent, var(--border-color), transparent); + margin: 2.5rem 0; +} + +/* Code blocks */ +pre, +code { + background: rgba(0, 0, 0, 0.3) !important; + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--opensuse-green) !important; + font-family: 'Fira Code', 'Monaco', monospace; +} + +pre { + padding: 1rem; + overflow-x: auto; +} + +code { + padding: 0.2rem 0.5rem; +} + +/* Table styling */ +table { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; +} + +thead { + background: rgba(115, 186, 37, 0.1); +} + +th { + color: var(--opensuse-green) !important; + font-weight: 600; +} + +td, +th { + border-color: var(--border-color) !important; +} + +/* Lists */ +ul { + color: #a8b2d1; +} + +li { + margin-bottom: 0.5rem; +} + +/* Paragraphs */ +p { + color: #8892b0; + line-height: 1.7; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + h1 { + font-size: 1.8rem; + } + + .logo-icon { + height: 2rem !important; + } + + .grid { + grid-template-columns: 1fr !important; + } +} \ No newline at end of file