Add default landing page with link builder (#114) #119
@@ -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)
|
||||
|
||||
1
obs-status-service/static/favicon.svg
Normal 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 |
131
obs-status-service/static/index.html
Normal file
@@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
|
||||
|
||||
<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="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.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="input-group">
|
||||
<input id="result-markdown" readonly />
|
||||
<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"
|
||||
|
dgarcia
commented
I think that there's no need to add all the information, sections from here to the end can be removed. I think that there's no need to add all the information, sections from here to the end can be removed.
mmarhin
commented
Done @dgarcia! Cleanup complete. Done @dgarcia! Cleanup complete.
|
||||
rel="noopener">Autogits</a> | openSUSE Project</small></p>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
obs-status-service/static/logo.svg
Normal 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 |
91
obs-status-service/static/script.js
Normal file
@@ -0,0 +1,91 @@
|
||||
let timeoutID = 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.value = '';
|
||||
linkSection.classList.remove('visible');
|
||||
markdownSection.classList.remove('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
|
dgarcia
commented
I don't think that the I don't think that the `encodeURIComponent` is needed, we can show the path without encoding so the link looks clean and easier to understand.
|
||||
// Build the path correctly with all components
|
||||
let path = '/status/' + project;
|
||||
|
mmarhin marked this conversation as resolved
Outdated
dgarcia
commented
Same as above. Same as above.
|
||||
if (pkg) {
|
||||
path += '/' + pkg;
|
||||
}
|
||||
if (repo) {
|
||||
path += '/' + repo; // repo already contains / for arch
|
||||
}
|
||||
|
||||
const fullUrl = window.location.origin + path;
|
||||
|
||||
hint.style.display = 'none';
|
||||
preview.style.display = 'block';
|
||||
|
||||
// Debounce preview updates to avoid excessive requests
|
||||
if (timeoutID) {
|
||||
|
mmarhin marked this conversation as resolved
Outdated
dgarcia
commented
Updating the preview here directly is creating a request to the server for each character, and producing broken images while writing. To prevent that we can add a timeout to avoid the reload before stop typing, something like: The Or we can even debounce the whole Updating the preview here directly is creating a request to the server for each character, and producing broken images while writing. To prevent that we can add a timeout to avoid the reload before stop typing, something like:
```
if (timeoutID) {
clearTimeout(timeoutID);
timeoutID = 0;
}
timeoutID = setTimeout(() => {
const newPreview = preview.cloneNode(true);
newPreview.data = path;
preview.parentNode.replaceChild(newPreview, preview);
timeoutID = 0;
// Update reference if we were using it elsewhere, though here we just exit
}, 800);
```
The `timeoutID` variable should be a global variable defined as 0 by default.
Or we can even [debounce](https://dev.to/teaganga/understanding-debounce-in-javascript-a-guide-with-examples-3cm4) the whole `updatePreview` function.
|
||||
clearTimeout(timeoutID);
|
||||
timeoutID = 0;
|
||||
}
|
||||
timeoutID = setTimeout(() => {
|
||||
const newPreview = preview.cloneNode(true);
|
||||
newPreview.data = path;
|
||||
preview.parentNode.replaceChild(newPreview, preview);
|
||||
timeoutID = 0;
|
||||
}, 800);
|
||||
|
||||
link.value = fullUrl;
|
||||
markdown.value = '';
|
||||
|
||||
// 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();
|
||||
});
|
||||
144
obs-status-service/static/style.css
Normal file
@@ -0,0 +1,144 @@
|
||||
/* OBS Status Service - Custom Styles */
|
||||
|
||||
/* Override PicoCSS variables for openSUSE branding */
|
||||
/* Using PicoCSS lime color family for openSUSE green branding */
|
||||
:root {
|
||||
--pico-primary: var(--pico-lime-600);
|
||||
--pico-primary-hover: var(--pico-lime-700);
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
height: 2.5rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Preview section */
|
||||
#preview-container {
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#preview {
|
||||
max-width: 100%;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
#preview-hint {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
border: 2px dashed var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Input Groups for Result Sections */
|
||||
.input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--pico-card-background-color);
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1rem;
|
||||
padding-right: 3.5rem;
|
||||
min-height: 3.5rem;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
font-family: monospace !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
color: inherit !important;
|
||||
font-size: inherit !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.75rem;
|
||||
transform: translateY(-50%);
|
||||
padding: 0.5rem 0.75rem;
|
||||
min-height: 2.5rem;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
}
|
||||
|
||||
.copy-btn:active {
|
||||
transform: translateY(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.copy-btn.copied {
|
||||
background: var(--pico-green-600);
|
||||
border-color: var(--pico-green-600);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
Maybe we should remove the
darktheme and let the browser show the user preferred theme.That will require to adapt all the css and replace all custom colors there with some existing color defined in the picocss