SHA256
1
0

5 Commits

Author SHA256 Message Date
339b189c50 fix: Remove redundant documentation sections from HTML 2026-01-28 22:49:33 +01:00
5393de0a44 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.
2026-01-25 12:52:04 +01:00
93b23e4340 Revert "feat: Implement interactive logic and styling for link builder"
This reverts commit 59b1044846.
2026-01-25 12:43:10 +01:00
59b1044846 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.
2026-01-25 12:41:29 +01:00
c44574f06f Implement default page (a link builder) (#114) 2026-01-24 18:37:35 +01:00
6 changed files with 564 additions and 2 deletions

View File

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

View File

@@ -0,0 +1 @@
<svg fill="#73BA25" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 458 512"><path d="M201.741 0A256.473 256.473 0 0089.477 26.06 256.206 256.206 0 000 98.632c31.256 8.342 53.38 15.512 60.005 17.736.104-4.061.778-40.382.778-40.382s.086-.832.53-1.265c.572-.558 1.398-.39 1.398-.39 8.221 1.19 183.695 27.173 257.658 70.23 9.134 5.339 13.643 11.014 19.276 16.768 20.446 21.125 47.462 108.961 50.363 127.074.114.712-.766 1.485-1.142 1.778h-.01c-2.101 1.637-4.388 3.34-6.679 4.861-17.501 11.731-57.82 39.926-109.549 35.322-46.465-4.106-107.171-30.732-180.354-78.903 7.195 16.83 14.285 33.712 21.378 50.585 10.598 5.499 112.893 57.571 163.379 56.554 40.664-.846 84.155-20.66 101.554-31.121 0 0 3.823-2.302 5.487-1.017 1.82 1.404 1.317 3.557.886 5.754-1.071 4.99-3.507 14.093-5.165 18.414l-1.399 3.522c-1.991 5.33-3.903 10.285-7.589 13.336-10.25 9.311-26.609 16.718-52.242 27.848-39.621 17.314-103.904 28.325-163.586 27.946-21.376-.475-42.028-2.844-60.162-4.961-37.216-4.197-67.497-7.602-85.96 5.739a256.232 256.232 0 0086.911 64.833A256.481 256.481 0 00201.741 512c33.652 0 66.975-6.621 98.066-19.487a256.285 256.285 0 0083.137-55.493 255.99 255.99 0 0055.55-83.053 255.776 255.776 0 000-195.934 255.981 255.981 0 00-55.55-83.052 256.277 256.277 0 00-83.137-55.494A256.48 256.48 0 00201.741 0zm57.158 148.539c-20.058-.639-39.184 6.432-53.847 20.108-14.657 13.631-23.044 32.215-23.787 52.239-1.386 41.309 31.08 76.116 72.395 77.638 20.149.676 39.234-6.4 53.897-20.157 14.619-13.589 23.007-32.173 23.787-52.198 1.42-41.272-31.085-76.152-72.445-77.63zm-.629 22.093c28.905 1.024 51.535 25.281 50.562 54.148-.447 13.922-6.325 26.826-16.537 36.397-10.226 9.526-23.593 14.479-37.617 14.056-28.838-1.063-51.469-25.348-50.495-54.223.424-13.973 6.419-26.879 16.586-36.405 10.167-9.526 23.469-14.478 37.501-13.973zm8.566 26.251c-12.842 0-23.215 6.905-23.215 15.495 0 8.509 10.373 15.453 23.215 15.453 12.836 0 23.249-6.944 23.249-15.453 0-8.59-10.406-15.495-23.249-15.495z" fill="#73BA25"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OBS Status Service</title>
<meta name="description" content="Generate embeddable SVG badges showing build status from Open Build Service">
<link rel="icon" type="image/svg+xml" href="/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" />
<link rel="stylesheet" href="/static/style.css" />
<script src="/static/script.js" defer></script>
</head>
<body class="container">
<header>
<h1><img src="/static/logo.svg" alt="openSUSE Logo" class="logo-icon"> OBS Status Service</h1>
<p>Generate embeddable SVG badges showing build status from Open Build Service.</p>
</header>
<main>
<section>
<h2>Get your Build Result Image</h2>
<div class="grid">
<article>
<fieldset>
<label>
Project <small>(required)</small>
<input id="project" name="project" placeholder="devel:languages:python:Factory"
oninput="updatePreview()" autocomplete="off" />
</label>
<label>
Package <small>(optional)</small>
<input id="package" name="package" placeholder="python313" oninput="updatePreview()"
autocomplete="off" />
</label>
<label>
Repository / Architecture <small>(optional)</small>
<input id="repo" name="repo" placeholder="openSUSE_Tumbleweed/x86_64"
oninput="updatePreview()" autocomplete="off" />
</label>
</fieldset>
</article>
<article>
<h3>Preview</h3>
<div id="preview-container">
<object id="preview" data="" type="image/svg+xml" aria-label="Build Result"
style="display: none;"></object>
<p id="preview-hint"><small>Enter a project name to see the preview</small></p>
</div>
</article>
</div>
<div id="link-section" class="result-section">
<h3>Link</h3>
<div class="input-group">
<input id="result-link" readonly />
<button id="copy-link-btn" class="copy-btn" onclick="copyResult('result-link', 'copy-link-btn')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
class="copy-icon">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
</button>
</div>
</div>
<div id="markdown-section" class="result-section">
<h3>Markdown</h3>
<div class="markdown-container">
<pre id="result-markdown"></pre>
<button id="copy-markdown-btn" class="copy-btn"
onclick="copyResult('result-markdown', 'copy-markdown-btn')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
class="copy-icon">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
</button>
</div>
</div>
</section>
<hr />
<section>
<h2>Usage</h2>
<h4>Request Formats</h4>
<p>Requests for individual build results:</p>
<pre>/status/{project}/{package}/{repo}/{arch}</pre>
<p>Where <code>package</code>, <code>repo</code> and <code>arch</code> are optional parameters.</p>
<p>Requests for project results:</p>
<pre>/status/{project}</pre>
<h4>Output Formats</h4>
<p>By default, SVG output is generated. JSON and XML output is possible by setting the <code>Accept</code>
request header:</p>
<table>
<thead>
<tr>
<th>Accept Header</th>
<th>Output Format</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>(default)</em></td>
<td>SVG image</td>
</tr>
<tr>
<td><code>application/json</code></td>
<td>JSON data</td>
</tr>
<tr>
<td><code>application/obs+xml</code></td>
<td>XML output</td>
</tr>
</tbody>
</table>
</section>
</main>
<footer>
<p><small>Powered by <a href="https://src.opensuse.org/git-workflow/autogits" target="_blank" rel="noopener">Autogits</a> | openSUSE Project</small></p>
</footer>
</body>
</html>

View File

@@ -0,0 +1 @@
<svg fill="#73BA25" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><title>openSUSE icon</title><path d="M21.51 8.107a.976.976 0 0 0-.708.264.993.993 0 0 0 .64 1.714.991.991 0 0 0 1.024-.954.992.992 0 0 0-.955-1.024zm.162 1.082c-.242 0-.438-.131-.438-.292 0-.163.196-.293.438-.293.243 0 .44.13.44.293 0 .16-.197.292-.44.292zm2.306 1.18c.007-.006.024-.02.022-.034-.055-.343-.565-2.004-.952-2.404-.106-.109-.191-.216-.364-.317-1.398-.814-4.713-1.306-4.869-1.328 0 0-.015-.004-.026.007-.009.008-.01.024-.01.024l-.015.764c-.339-.114-2.8-.91-5.108-.99C10.7 6.024 7.85 5.77 4.072 8.093l-.111.07C2.184 9.27.957 10.637.316 12.224c-.201.5-.472 1.628-.204 2.688.116.464.331.93.621 1.347.656.943 1.757 1.568 2.943 1.674 1.674.15 2.941-.602 3.392-2.01.31-.971 0-2.397-1.188-3.124-.967-.591-2.006-.457-2.609-.058-.523.347-.819.886-.814 1.477.012 1.05.917 1.608 1.567 1.61.189 0 .378-.033.592-.103a.921.921 0 0 0 .227-.1l.025-.015.015-.01-.005.003a.535.535 0 0 0 .217-.587.533.533 0 0 0-.612-.377l-.036.008-.05.015-.072.025c-.15.037-.262.04-.286.041-.076-.005-.45-.117-.45-.527v-.005c0-.151.06-.257.093-.314.117-.183.435-.362.866-.325.565.05.973.34 1.243.886.25.508.185 1.134-.17 1.592-.35.454-.976.647-1.809.557a2.48 2.48 0 0 1-1.946-1.327c-.389-.735-.41-1.607-.055-2.276.85-1.604 2.455-1.587 3.334-1.435 1.302.226 2.784 1.427 3.309 2.814.085.22.128.396.166.556l.057.24 1.47.718c.032.015.043.02.055.011.016-.011.007-.042.007-.042-.01-.033-.03-.063-.065-.475-.027-.365-.084-1.365.42-1.86.195-.195.492-.367.728-.423.964-.235 2.094-.073 3.163 1.164.553.64.823.93.959 1.061 0 0 .03.03.047.043.018.015.03.027.055.041.045.025 1.838.85 1.838.85s.022.011.037-.008c.016-.02.001-.038.001-.038-.012-.014-1.137-1.468-.937-2.665.158-.954.917-.867 1.967-.749.343.04.733.085 1.137.094 1.127.007 2.342-.201 3.09-.529.485-.21.794-.35.988-.526.07-.058.106-.152.143-.253l.027-.066c.031-.082.077-.254.097-.348.009-.042.018-.083-.016-.11-.032-.024-.104.02-.104.02-.329.198-1.15.573-1.919.589-.954.019-2.887-.966-3.087-1.07-.134-.32-.268-.639-.404-.957 1.383.911 2.53 1.415 3.408 1.492.977.088 1.74-.446 2.07-.668.043-.028.086-.06.126-.092zm-3.923-1.311c.014-.379.173-.73.45-.988a1.414 1.414 0 0 1 1.017-.38 1.423 1.423 0 0 1 1.37 1.468c-.015.379-.174.73-.45.987-.277.26-.638.394-1.019.381a1.424 1.424 0 0 1-1.368-1.468z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -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();
});

View File

@@ -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;
}
}