From c44574f06f0eaaa749e3c73ea8b995ecbd224f89703cdfc98bac5f4426b9366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Mar=C3=ADn?= Date: Sat, 24 Jan 2026 18:37:35 +0100 Subject: [PATCH 1/5] Implement default page (a link builder) (#114) --- obs-status-service/main.go | 14 ++++++-- obs-status-service/static/index.html | 54 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 obs-status-service/static/index.html diff --git a/obs-status-service/main.go b/obs-status-service/main.go index 80446fe..080d26f 100644 --- a/obs-status-service/main.go +++ b/obs-status-service/main.go @@ -271,8 +271,18 @@ func main() { 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/index.html b/obs-status-service/static/index.html new file mode 100644 index 0000000..2f79c95 --- /dev/null +++ b/obs-status-service/static/index.html @@ -0,0 +1,54 @@ + + + openSUSE Build Results + + + + +

Build your own Build Results link

+ + +
+
+
+
+ + + +
+ + +
+
+
+

Preview

+ + +
+
+ +
+

Link

+ + + https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64 + + +

Markdown

+ +
+
+![br](https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64)
+            
+
+ + -- 2.51.1 From 59b1044846f1ca99efcae5e5d304f2a707ad5d31206b025c4b6384c985e3122a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Mar=C3=ADn?= Date: Sun, 25 Jan 2026 12:41:29 +0100 Subject: [PATCH 2/5] feat: Implement interactive logic and styling for link builder - Implement JavaScript logic to dynamically update link and markdown. - Update HTML content to align with the README documentation. - Modified CSS style. --- obs-status-service/main.go | 3 + obs-status-service/static/index.html | 243 ++++++++++++++++---- obs-status-service/static/script.js | 83 +++++++ obs-status-service/static/style.css | 324 +++++++++++++++++++++++++++ 4 files changed, 609 insertions(+), 44 deletions(-) create mode 100644 obs-status-service/static/script.js create mode 100644 obs-status-service/static/style.css diff --git a/obs-status-service/main.go b/obs-status-service/main.go index 080d26f..11f92df 100644 --- a/obs-status-service/main.go +++ b/obs-status-service/main.go @@ -266,6 +266,9 @@ func main() { } }() + // Serve static files (script.js, etc.) + http.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + http.HandleFunc("GET /", func(res http.ResponseWriter, req *http.Request) { if rescanRepoError != nil { res.WriteHeader(500) diff --git a/obs-status-service/static/index.html b/obs-status-service/static/index.html index 2f79c95..b44c3b2 100644 --- a/obs-status-service/static/index.html +++ b/obs-status-service/static/index.html @@ -1,54 +1,209 @@ - - - openSUSE Build Results - - + + - -

Build your own Build Results link

+ + + + + 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

+
+
+
- - -
-
-

Preview

+ - -
-
+
+

Markdown

+
+

+                    
+                
+
+ -
-

Link

+
- - https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64 - +
+

Usage

-

Markdown

+

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}
-![br](https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64) -
-
- - +

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
+ + +
+ +
+

Target Usage

+
    +
  • Inside README.md of package git or project git
  • +
  • Comment section of a Gitea PR
  • +
  • Automated build result processing
  • +
+
+ +
+ +
+

Configuration

+

Default parameters can be changed by environment variables:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDefaultDescription
OBS_STATUS_SERVICE_OBS_URLhttps://build.opensuse.orgLocation for creating build logs and monitor page build results
OBS_STATUS_SERVICE_LISTEN[::1]:8080Listening address and port
OBS_STATUS_SERVICE_CERT/run/obs-status-service.pemLocation of certificate file for service
OBS_STATUS_SERVICE_KEY/run/obs-status-service.pemLocation of key file for service
REDIS(required)URL of the OBS Redis instance with login information
+
+ +
+ +
+

Areas of Responsibility

+
    +
  • Fetch and cache internal data from OBS and present it in usable format: +
      +
    • Generate SVG output for specific OBS project or package
    • +
    • Generate JSON/XML output for automated processing
    • +
    +
  • +
  • Low-overhead
  • +
+
+ +
+ +
+

Caching

+

Repository results (build results) are cached for 10 seconds and repository listing for + OBS instance are cached for 5 minutes — new repositories take up to 5 minutes to be + visible.

+
+ + +
+

Powered by Autogits | openSUSE Project

+
+ + + \ No newline at end of file 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 -- 2.51.1 From 93b23e4340c75d1364fc176a36b505bf4d19b2586e108cb72b7925239e97d4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Mar=C3=ADn?= Date: Sun, 25 Jan 2026 12:43:10 +0100 Subject: [PATCH 3/5] Revert "feat: Implement interactive logic and styling for link builder" This reverts commit 59b1044846f1ca99efcae5e5d304f2a707ad5d31206b025c4b6384c985e3122a. --- obs-status-service/main.go | 3 - obs-status-service/static/index.html | 243 ++++---------------- obs-status-service/static/script.js | 83 ------- obs-status-service/static/style.css | 324 --------------------------- 4 files changed, 44 insertions(+), 609 deletions(-) delete mode 100644 obs-status-service/static/script.js delete mode 100644 obs-status-service/static/style.css diff --git a/obs-status-service/main.go b/obs-status-service/main.go index 11f92df..080d26f 100644 --- a/obs-status-service/main.go +++ b/obs-status-service/main.go @@ -266,9 +266,6 @@ func main() { } }() - // Serve static files (script.js, etc.) - http.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - http.HandleFunc("GET /", func(res http.ResponseWriter, req *http.Request) { if rescanRepoError != nil { res.WriteHeader(500) diff --git a/obs-status-service/static/index.html b/obs-status-service/static/index.html index b44c3b2..2f79c95 100644 --- a/obs-status-service/static/index.html +++ b/obs-status-service/static/index.html @@ -1,209 +1,54 @@ - - + + + openSUSE Build Results + + - - - - - OBS Status Service - - - - - + +

Build your own Build Results link

- -
-

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

-
-
-
+
+
+
+
+ + + +
- + +
+
+
+

Preview

-
-

Markdown

-
-

-                    
-                
-
-
+ + + -
+
+

Link

-
-

Usage

+ + https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64 + -

Request Formats

-

Requests for individual build results:

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

Where package, repo and arch are optional parameters.

+

Markdown

-

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
-
- -
- -
-

Target Usage

-
    -
  • Inside README.md of package git or project git
  • -
  • Comment section of a Gitea PR
  • -
  • Automated build result processing
  • -
-
- -
- -
-

Configuration

-

Default parameters can be changed by environment variables:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
VariableDefaultDescription
OBS_STATUS_SERVICE_OBS_URLhttps://build.opensuse.orgLocation for creating build logs and monitor page build results
OBS_STATUS_SERVICE_LISTEN[::1]:8080Listening address and port
OBS_STATUS_SERVICE_CERT/run/obs-status-service.pemLocation of certificate file for service
OBS_STATUS_SERVICE_KEY/run/obs-status-service.pemLocation of key file for service
REDIS(required)URL of the OBS Redis instance with login information
-
- -
- -
-

Areas of Responsibility

-
    -
  • Fetch and cache internal data from OBS and present it in usable format: -
      -
    • Generate SVG output for specific OBS project or package
    • -
    • Generate JSON/XML output for automated processing
    • -
    -
  • -
  • Low-overhead
  • -
-
- -
- -
-

Caching

-

Repository results (build results) are cached for 10 seconds and repository listing for - OBS instance are cached for 5 minutes — new repositories take up to 5 minutes to be - visible.

-
-
- -
-

Powered by Autogits | openSUSE Project

-
- - - \ No newline at end of file +![br](https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64) + + + + diff --git a/obs-status-service/static/script.js b/obs-status-service/static/script.js deleted file mode 100644 index 0a1c4c3..0000000 --- a/obs-status-service/static/script.js +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 73f863e..0000000 --- a/obs-status-service/static/style.css +++ /dev/null @@ -1,324 +0,0 @@ -/* 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 -- 2.51.1 From 5393de0a44bafa90679f135f0fd0ea8bad127fa8811969d6f0021686aef0e636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Mar=C3=ADn?= Date: Sun, 25 Jan 2026 12:52:04 +0100 Subject: [PATCH 4/5] feat: Implement interactive logic and styling for link builder - Implement JavaScript logic to dynamically update link and markdown. - Update HTML content to align with the README documentation. - Modified CSS style. --- obs-status-service/main.go | 14 ++ obs-status-service/static/favicon.svg | 1 + obs-status-service/static/index.html | 243 +++++++++++++++---- obs-status-service/static/logo.svg | 1 + obs-status-service/static/script.js | 83 +++++++ obs-status-service/static/style.css | 324 ++++++++++++++++++++++++++ 6 files changed, 622 insertions(+), 44 deletions(-) create mode 100644 obs-status-service/static/favicon.svg create mode 100644 obs-status-service/static/logo.svg create mode 100644 obs-status-service/static/script.js create mode 100644 obs-status-service/static/style.css diff --git a/obs-status-service/main.go b/obs-status-service/main.go index 080d26f..1884640 100644 --- a/obs-status-service/main.go +++ b/obs-status-service/main.go @@ -266,6 +266,20 @@ 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) 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 index 2f79c95..093c7aa 100644 --- a/obs-status-service/static/index.html +++ b/obs-status-service/static/index.html @@ -1,54 +1,209 @@ - - - openSUSE Build Results - - + + - -

Build your own Build Results link

+ + + + 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

+
+
+
- - -
-
-

Preview

+ - -
-
+
+

Markdown

+
+

+                    
+                
+
+ -
-

Link

+
- - https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64 - +
+

Usage

-

Markdown

+

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}
-![br](https://br.opensuse.org/status/devel:languages:python:Factory/python313/openSUSE_Tumbleweed/x86_64) -
-
- - +

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
+ + +
+ +
+

Target Usage

+
    +
  • Inside README.md of package git or project git
  • +
  • Comment section of a Gitea PR
  • +
  • Automated build result processing
  • +
+
+ +
+ +
+

Configuration

+

Default parameters can be changed by environment variables:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDefaultDescription
OBS_STATUS_SERVICE_OBS_URLhttps://build.opensuse.orgLocation for creating build logs and monitor page build results
OBS_STATUS_SERVICE_LISTEN[::1]:8080Listening address and port
OBS_STATUS_SERVICE_CERT/run/obs-status-service.pemLocation of certificate file for service
OBS_STATUS_SERVICE_KEY/run/obs-status-service.pemLocation of key file for service
REDIS(required)URL of the OBS Redis instance with login information
+
+ +
+ +
+

Areas of Responsibility

+
    +
  • Fetch and cache internal data from OBS and present it in usable format: +
      +
    • Generate SVG output for specific OBS project or package
    • +
    • Generate JSON/XML output for automated processing
    • +
    +
  • +
  • Low-overhead
  • +
+
+ +
+ +
+

Caching

+

Repository results (build results) are cached for 10 seconds and repository listing for + OBS instance are cached for 5 minutes — new repositories take up to 5 minutes to be + visible.

+
+ + +
+

Powered by Autogits | openSUSE Project

+
+ + + \ 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 -- 2.51.1 From 339b189c504dc79bf9cbd07b1adc131d6b6fadc9cfa14dec3443c9e7d3baf632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Mar=C3=ADn?= Date: Wed, 28 Jan 2026 22:49:33 +0100 Subject: [PATCH 5/5] fix: Remove redundant documentation sections from HTML --- obs-status-service/static/index.html | 82 +--------------------------- 1 file changed, 1 insertion(+), 81 deletions(-) diff --git a/obs-status-service/static/index.html b/obs-status-service/static/index.html index 093c7aa..b53af8a 100644 --- a/obs-status-service/static/index.html +++ b/obs-status-service/static/index.html @@ -120,89 +120,9 @@ - -
- -
-

Target Usage

-
    -
  • Inside README.md of package git or project git
  • -
  • Comment section of a Gitea PR
  • -
  • Automated build result processing
  • -
-
- -
- -
-

Configuration

-

Default parameters can be changed by environment variables:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
VariableDefaultDescription
OBS_STATUS_SERVICE_OBS_URLhttps://build.opensuse.orgLocation for creating build logs and monitor page build results
OBS_STATUS_SERVICE_LISTEN[::1]:8080Listening address and port
OBS_STATUS_SERVICE_CERT/run/obs-status-service.pemLocation of certificate file for service
OBS_STATUS_SERVICE_KEY/run/obs-status-service.pemLocation of key file for service
REDIS(required)URL of the OBS Redis instance with login information
-
- -
- -
-

Areas of Responsibility

-
    -
  • Fetch and cache internal data from OBS and present it in usable format: -
      -
    • Generate SVG output for specific OBS project or package
    • -
    • Generate JSON/XML output for automated processing
    • -
    -
  • -
  • Low-overhead
  • -
-
- -
- -
-

Caching

-

Repository results (build results) are cached for 10 seconds and repository listing for - OBS instance are cached for 5 minutes — new repositories take up to 5 minutes to be - visible.

-
- -- 2.51.1