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
+
+
+
+
+
+
+
+
+
+
+
+
+ Get your Build Result Image
+
+
+
+
+
+
+
+
+
+
+ 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 Header |
+ Output Format |
+
+
+
+
+ | (default) |
+ SVG image |
+
+
+ application/json |
+ JSON data |
+
+
+ application/obs+xml |
+ XML 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 @@
+
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 = '';
+
+ // 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