Files
changedetection.io/docs/python-apidoc/template.html
T
dgtlmoon 8379fdb1f8
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
Refactor API Documentation (#3383)
2025-08-23 19:28:34 +02:00

506 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Documentation</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; }
.sidebar { position: sticky; top: 0; height: 100vh; overflow-y: auto; background: white; box-shadow: 2px 0 5px rgba(0,0,0,0.1); }
.content { padding: 20px; }
.endpoint { margin-bottom: 40px; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.method { font-weight: bold; text-transform: uppercase; padding: 4px 8px; border-radius: 4px; }
.method.get { background: #d4edda; color: #155724; }
.method.post { background: #cce5ff; color: #004085; }
.method.put { background: #fff3cd; color: #856404; }
.method.delete { background: #f8d7da; color: #721c24; }
.param-table { font-size: 0.9em; }
.optional { color: #6c757d; font-style: italic; }
.example { background: #f8f9fa; border-left: 4px solid #007bff; }
pre { font-size: 0.85em; }
.copy-btn { opacity: 0.7; transition: opacity 0.2s ease; }
.copy-btn:hover { opacity: 1; }
.example:hover .copy-btn { opacity: 1; }
.nav-link.active { background-color: #007bff; color: white; font-weight: bold; }
.nav-link { transition: all 0.2s ease; }
.nav-link:hover:not(.active) { background-color: #e3f2fd; color: #0056b3; }
.group-header.active { font-weight: bold; color: #007bff; }
/* Custom scrollbar styling */
.sidebar::-webkit-scrollbar { width: 8px; }
.sidebar::-webkit-scrollbar-track { background: #f8f9fa; border-radius: 4px; }
.sidebar::-webkit-scrollbar-thumb { background: #dee2e6; border-radius: 4px; }
.sidebar::-webkit-scrollbar-thumb:hover { background: #adb5bd; }
/* Firefox scrollbar */
.sidebar { scrollbar-width: thin; scrollbar-color: #dee2e6 #f8f9fa; }
/* Mobile styles - disable sticky sidebar */
@media (max-width: 800px) {
.sidebar {
position: static !important;
height: auto !important;
overflow-y: visible !important;
}
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<div class="col-md-3 sidebar">
<div class="p-3">
<a href="#introduction" class="text-decoration-none">
{{ sidebar_header_content|safe }}
</a>
<hr>
{% if introduction_content %}
<div class="mb-3">
<a href="#introduction" class="text-decoration-none">
<h6 class="text-muted">Introduction</h6>
</a>
</div>
{% endif %}
{% for group, endpoints in grouped_endpoints.items() %}
<div class="mb-3">
<h6 class="text-muted group-header" data-group="{{ group }}">{{ group }}</h6>
{% for endpoint in endpoints %}
<div class="ms-2 mb-1">
<a href="#{{ group|replace(' ', '_')|replace('/', '')|replace('-', '')|lower }}_{{ endpoint.method|upper }}" class="nav-link py-1 px-2 rounded" data-endpoint="{{ group|replace(' ', '_')|replace('/', '')|replace('-', '')|lower }}_{{ endpoint.method|upper }}">
<span class="method {{ endpoint.method }}">{{ endpoint.method }}</span>
{{ endpoint.title or endpoint.name or endpoint.description }}
</a>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<!-- Main content -->
<div class="col-md-9 content">
{% if introduction_content %}
<div id="introduction" class="mb-5">
{{ introduction_content|safe }}
</div>
{% endif %}
{% for group, endpoints in grouped_endpoints.items() %}
<h2 class="text-primary mb-4" id="group-{{ group|replace(' ', '_')|lower }}">{{ group }}</h2>
{% for endpoint in endpoints %}
<div class="endpoint" id="{{ group|replace(' ', '_')|replace('/', '')|replace('-', '')|lower }}_{{ endpoint.method|upper }}" data-group="{{ group }}">
<div class="row">
<div class="col-md-8">
<h4>
<span class="method {{ endpoint.method }}">{{ endpoint.method }}</span>
<code>{{ endpoint.url|e }}</code>
</h4>
<h5 class="text-muted">{{ endpoint.name or endpoint.title }}</h5>
{% if endpoint.description %}
<p class="mt-3">{{ endpoint.description|safe }}</p>
{% endif %}
</div>
</div>
{% if endpoint.params %}
<h6 class="mt-4">Parameters</h6>
<div class="table-responsive">
<table class="table table-sm param-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for param in endpoint.params %}
<tr>
<td><code>{{ param.name }}</code></td>
<td><span class="badge bg-secondary">{{ param.type }}</span></td>
<td>{% if param.optional %}<span class="optional">Optional</span>{% else %}<span class="text-danger">Required</span>{% endif %}</td>
<td>{{ param.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if endpoint.query %}
<h6 class="mt-4">Query Parameters</h6>
<div class="table-responsive">
<table class="table table-sm param-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for param in endpoint.query %}
<tr>
<td><code>{{ param.name }}</code></td>
<td><span class="badge bg-info">{{ param.type }}</span></td>
<td>{% if param.optional %}<span class="optional">Optional</span>{% else %}<span class="text-danger">Required</span>{% endif %}</td>
<td>{{ param.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if endpoint.success %}
<h6 class="mt-4">Success Responses</h6>
{% for success in endpoint.success %}
<div class="mb-2">
<span class="badge bg-success">{{ success.status }}</span>
<span class="badge bg-secondary ms-1">{{ success.type }}</span>
<strong class="ms-2">{{ success.name }}</strong>
<span class="ms-2 text-muted">{{ success.description }}</span>
</div>
{% endfor %}
{% endif %}
{% if endpoint.error %}
<h6 class="mt-4">Error Responses</h6>
{% for error in endpoint.error %}
<div class="mb-2">
<span class="badge bg-danger">{{ error.status }}</span>
<span class="badge bg-secondary ms-1">{{ error.type }}</span>
<span class="ms-2 text-muted">{{ error.description }}</span>
</div>
{% endfor %}
{% endif %}
{% if endpoint.example or endpoint.example_request or endpoint.example_response %}
<h6 class="mt-4">Example</h6>
{% if endpoint.example_request %}
<h7 class="mt-3 mb-2 text-muted">Request</h7>
<div class="example p-3 rounded position-relative mb-3">
<button class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 copy-btn"
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Copy to clipboard"
onclick="copyToClipboard(this)">
<i class="bi bi-clipboard" aria-hidden="true"></i>
</button>
<pre><code class="language-bash">{{ endpoint.example_request }}</code></pre>
</div>
{% endif %}
{% if endpoint.example_response %}
<h7 class="mt-3 mb-2 text-muted">Response</h7>
<div class="example p-3 rounded position-relative mb-3">
<button class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 copy-btn"
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Copy to clipboard"
onclick="copyToClipboard(this)">
<i class="bi bi-clipboard" aria-hidden="true"></i>
</button>
<pre><code class="language-json">{{ endpoint.example_response }}</code></pre>
</div>
{% endif %}
{% if endpoint.example and not endpoint.example_request and not endpoint.example_response %}
<div class="example p-3 rounded position-relative">
<button class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 copy-btn"
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Copy to clipboard"
onclick="copyToClipboard(this)">
<i class="bi bi-clipboard" aria-hidden="true"></i>
</button>
<pre><code class="language-bash">{{ endpoint.example }}</code></pre>
</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
let isScrolling = false;
let isNavigating = false;
// Check if we should disable scroll handling on mobile
function isMobileWidth() {
return window.innerWidth < 800;
}
// Debounced scroll handler
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Function to scroll sidebar link into view if needed
function scrollIntoViewIfNeeded(element) {
if (!element) return;
const sidebar = $('.sidebar')[0];
const rect = element.getBoundingClientRect();
const sidebarRect = sidebar.getBoundingClientRect();
// Check if element is outside the sidebar viewport
const isAboveView = rect.top < sidebarRect.top;
const isBelowView = rect.bottom > sidebarRect.bottom;
if (isAboveView || isBelowView) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
// Intersection Observer for more efficient viewport detection
const observerOptions = {
root: null,
rootMargin: '-20% 0px -70% 0px', // Trigger when element is in top 30% of viewport
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
// Don't update if user is actively navigating or on mobile
if (isNavigating || isMobileWidth()) return;
entries.forEach(entry => {
if (entry.isIntersecting) {
const targetId = entry.target.id;
const targetGroup = entry.target.dataset.group;
// Update window location hash
if (window.location.hash !== '#' + targetId) {
history.replaceState(null, null, '#' + targetId);
}
// Remove all active states
$('.nav-link').removeClass('active');
$('.group-header').removeClass('active');
// Add active state to current item
const $activeLink = $(`.nav-link[data-endpoint="${targetId}"]`);
$activeLink.addClass('active');
$(`.group-header[data-group="${targetGroup}"]`).addClass('active');
// Handle introduction section
if (targetId === 'introduction') {
const $introLink = $('a[href="#introduction"]');
$introLink.addClass('active');
// Scroll intro link into view in sidebar
scrollIntoViewIfNeeded($introLink[0]);
} else {
// Scroll active link into view in sidebar
if ($activeLink.length) {
scrollIntoViewIfNeeded($activeLink[0]);
}
}
}
});
}, observerOptions);
// Observe all endpoints and introduction (only on desktop)
if (!isMobileWidth()) {
$('.endpoint').each(function() {
observer.observe(this);
});
if ($('#introduction').length) {
observer.observe($('#introduction')[0]);
}
}
// Smooth scrolling for navigation links
$('a[href^="#"]').on('click', function(e) {
e.preventDefault();
const targetHref = this.getAttribute('href');
const target = $(targetHref);
if (target.length) {
// Set navigation flag to prevent observer interference
isNavigating = true;
// Update window location hash immediately
history.pushState(null, null, targetHref);
$('html, body').animate({
scrollTop: target.offset().top - 20
}, 300, function() {
// Clear navigation flag after animation completes
setTimeout(() => {
isNavigating = false;
}, 100);
});
}
});
// Fallback scroll handler with debouncing
const handleScroll = debounce(() => {
if (isScrolling || isNavigating || isMobileWidth()) return;
let current = '';
let currentGroup = '';
// Check which section is currently in view
$('.endpoint, #introduction').each(function() {
const element = $(this);
const elementTop = element.offset().top;
const elementBottom = elementTop + element.outerHeight();
const scrollTop = $(window).scrollTop() + 100; // Offset for better UX
if (scrollTop >= elementTop && scrollTop < elementBottom) {
current = this.id;
currentGroup = element.data('group');
return false; // Break loop
}
});
if (current) {
// Update window location hash
if (window.location.hash !== '#' + current) {
history.replaceState(null, null, '#' + current);
}
$('.nav-link').removeClass('active');
$('.group-header').removeClass('active');
const $activeLink = $(`.nav-link[data-endpoint="${current}"]`);
$activeLink.addClass('active');
if (currentGroup) {
$(`.group-header[data-group="${currentGroup}"]`).addClass('active');
}
if (current === 'introduction') {
const $introLink = $('a[href="#introduction"]');
$introLink.addClass('active');
scrollIntoViewIfNeeded($introLink[0]);
} else if ($activeLink.length) {
scrollIntoViewIfNeeded($activeLink[0]);
}
}
}, 50);
// Only bind scroll handler on desktop
if (!isMobileWidth()) {
$(window).on('scroll', handleScroll);
// Initial call
handleScroll();
}
// Initialize tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
});
// Copy to clipboard function
function copyToClipboard(button) {
const codeBlock = button.parentElement.querySelector('code');
const text = codeBlock.textContent;
// Use modern clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
showCopyFeedback(button, true);
}).catch(() => {
fallbackCopyToClipboard(text, button);
});
} else {
fallbackCopyToClipboard(text, button);
}
}
// Fallback for older browsers
function fallbackCopyToClipboard(text, button) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
showCopyFeedback(button, successful);
} catch (err) {
showCopyFeedback(button, false);
}
document.body.removeChild(textArea);
}
// Show copy feedback
function showCopyFeedback(button, success) {
const icon = button.querySelector('i');
const originalClass = icon.className;
const originalTitle = button.getAttribute('data-bs-original-title') || button.getAttribute('title');
if (success) {
icon.className = 'bi bi-check';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-success');
button.setAttribute('title', 'Copied!');
} else {
icon.className = 'bi bi-x';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-danger');
button.setAttribute('title', 'Failed to copy');
}
// Update tooltip
const tooltip = bootstrap.Tooltip.getInstance(button);
if (tooltip) {
tooltip.dispose();
new bootstrap.Tooltip(button);
}
// Reset after 2 seconds
setTimeout(() => {
icon.className = originalClass;
button.classList.remove('btn-success', 'btn-danger');
button.classList.add('btn-outline-secondary');
button.setAttribute('title', originalTitle);
// Update tooltip again
const tooltip = bootstrap.Tooltip.getInstance(button);
if (tooltip) {
tooltip.dispose();
new bootstrap.Tooltip(button);
}
}, 2000);
}
</script>
</body>
</html>