First commit

Signed-off-by: Jonas Rosland <jonas.rosland@gmail.com>
This commit is contained in:
Jonas Rosland 2025-03-14 09:04:43 -04:00
parent c7c3a91f62
commit 06a77bb5e6
65 changed files with 8470 additions and 0 deletions

194
templates/add_repo.html Normal file
View file

@ -0,0 +1,194 @@
{% extends "base.html" %}
{% block title %}Add Repository - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Add Repository</h1>
<div class="mb-3">
<a href="/repos" class="btn btn-secondary">Back to Repositories</a>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Repository Information</h5>
</div>
<div class="card-body">
<form method="post" id="add-repo-form">
<div class="mb-3">
<label for="github_repo" class="form-label">GitHub Repository</label>
<input type="text" class="form-control" id="github_repo" name="github_repo"
placeholder="owner/repo or https://github.com/owner/repo" required
value="{{ github_repo if github_repo else '' }}">
<div class="form-text">Enter the GitHub repository in the format owner/repo or the full URL</div>
</div>
<div class="mb-3">
<label for="gitea_owner" class="form-label">Gitea Owner</label>
<input type="text" class="form-control" id="gitea_owner" name="gitea_owner"
placeholder="owner" required
value="{{ gitea_owner if gitea_owner else '' }}">
<div class="form-text">Enter the Gitea owner username</div>
</div>
<div class="mb-3">
<label for="gitea_repo" class="form-label">Gitea Repository</label>
<input type="text" class="form-control" id="gitea_repo" name="gitea_repo"
placeholder="repo" required
value="{{ gitea_repo if gitea_repo else '' }}">
<div class="form-text">Enter the Gitea repository name</div>
</div>
{% if show_force_recreate %}
<div class="alert alert-warning">
<h5 class="alert-heading">Repository Already Exists</h5>
<p>The repository <strong>{{ gitea_owner }}/{{ gitea_repo }}</strong> already exists in Gitea but is not configured as a mirror.</p>
<p>Since the repository is empty, you can force recreate it as a mirror by checking the option below.</p>
<div class="form-check mb-3">
<div class="custom-switch-container">
<input type="checkbox" id="force_recreate" name="force_recreate" class="custom-switch-input">
<label for="force_recreate" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Force Recreate as Mirror</span>
</div>
<div class="form-text">This will delete the existing repository and recreate it as a mirror.</div>
</div>
</div>
{% endif %}
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0">Mirroring Options</h6>
<div class="form-text">All options are disabled by default for safety. Enable only what you need.</div>
</div>
<div class="card-body">
<div class="form-check mb-3">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_metadata" name="mirror_metadata" class="custom-switch-input"
{% if config and config.mirror_metadata %}checked{% endif %}>
<label for="mirror_metadata" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Metadata</span>
</div>
<div class="form-text">Enable mirroring of metadata (issues, PRs, labels, etc.) from GitHub to Gitea</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0">Metadata Components</h6>
</div>
<div class="card-body">
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_issues" name="mirror_issues" class="custom-switch-input metadata-component"
{% if config and config.mirror_issues %}checked{% endif %} disabled>
<label for="mirror_issues" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Issues</span>
</div>
</div>
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_pull_requests" name="mirror_pull_requests" class="custom-switch-input metadata-component"
{% if config and config.mirror_pull_requests %}checked{% endif %} disabled>
<label for="mirror_pull_requests" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Pull Requests</span>
</div>
</div>
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_labels" name="mirror_labels" class="custom-switch-input metadata-component"
{% if config and config.mirror_labels %}checked{% endif %} disabled>
<label for="mirror_labels" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Labels</span>
</div>
</div>
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_milestones" name="mirror_milestones" class="custom-switch-input metadata-component"
{% if config and config.mirror_milestones %}checked{% endif %} disabled>
<label for="mirror_milestones" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Milestones</span>
</div>
</div>
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_wiki" name="mirror_wiki" class="custom-switch-input metadata-component"
{% if config and config.mirror_wiki %}checked{% endif %} disabled>
<label for="mirror_wiki" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Wiki</span>
</div>
</div>
</div>
</div>
<div class="form-check mb-3">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_releases" name="mirror_releases" class="custom-switch-input"
{% if config and config.mirror_releases %}checked{% endif %}>
<label for="mirror_releases" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Releases</span>
</div>
<div class="form-text">Enable mirroring of releases from GitHub to Gitea</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">Add Repository</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const mirrorMetadataSwitch = document.getElementById('mirror_metadata');
const metadataComponents = document.querySelectorAll('.metadata-component');
function updateComponentsState() {
const isEnabled = mirrorMetadataSwitch.checked;
metadataComponents.forEach(component => {
component.disabled = !isEnabled;
});
}
// Initial state
updateComponentsState();
// Update on change
mirrorMetadataSwitch.addEventListener('change', updateComponentsState);
});
</script>
{% endblock %}

187
templates/base.html Normal file
View file

@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}GitHub to Gitea Mirror{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
padding-top: 20px;
padding-bottom: 20px;
}
.navbar {
margin-bottom: 20px;
}
.log-content {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
white-space: pre-wrap;
font-family: monospace;
max-height: 600px;
overflow-y: auto;
}
.repo-card {
margin-bottom: 20px;
}
/* Dark mode styles */
[data-bs-theme="dark"] {
--bs-body-bg: #121212;
--bs-body-color: #e0e0e0;
}
[data-bs-theme="dark"] .card {
--bs-card-bg: #1e1e1e;
--bs-card-border-color: #333;
}
[data-bs-theme="dark"] .table {
--bs-table-striped-bg: #2a2a2a;
--bs-table-bg: #1e1e1e;
--bs-table-color: #e0e0e0;
--bs-table-border-color: #333;
}
[data-bs-theme="dark"] .log-content {
background-color: #212529;
color: #f8f9fa;
border: 1px solid #333;
}
[data-bs-theme="dark"] .navbar {
background-color: #1e1e1e !important;
border-color: #333;
}
[data-bs-theme="dark"] .navbar-light .navbar-brand,
[data-bs-theme="dark"] .navbar-light .nav-link {
color: #e0e0e0;
}
[data-bs-theme="dark"] .navbar-light .nav-link:hover {
color: #ffffff;
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
background-color: #2a2a2a;
border-color: #444;
color: #e0e0e0;
}
[data-bs-theme="dark"] .form-check-input {
background-color: #2a2a2a;
border-color: #444;
}
/* Dark mode toggle button */
.theme-toggle {
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
}
.theme-toggle i {
font-size: 1.2rem;
}
[data-bs-theme="dark"] .theme-toggle {
color: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded">
<div class="container-fluid">
<a class="navbar-brand" href="/">GitHub to Gitea Mirror</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/repos">Repositories</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logs">Logs</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config">
<i class="bi bi-gear"></i> Configuration
</a>
</li>
</ul>
<div class="theme-toggle" id="theme-toggle" title="Toggle dark mode">
<i class="bi bi-sun-fill"></i>
</div>
</div>
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="row">
<div class="col-md-12">
{% block content %}{% endblock %}
</div>
</div>
<footer class="mt-5 pt-3 border-top text-muted">
<p>&copy; {{ current_year }} GitHub to Gitea Mirror</p>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = themeToggle.querySelector('i');
const htmlElement = document.documentElement;
// Check for saved theme preference or use preferred color scheme
const savedTheme = localStorage.getItem('theme');
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Set initial theme
if (savedTheme === 'dark' || (!savedTheme && prefersDarkMode)) {
htmlElement.setAttribute('data-bs-theme', 'dark');
themeIcon.classList.replace('bi-sun-fill', 'bi-moon-fill');
}
// Toggle theme on click
themeToggle.addEventListener('click', function() {
const currentTheme = htmlElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
htmlElement.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Update icon
if (newTheme === 'dark') {
themeIcon.classList.replace('bi-sun-fill', 'bi-moon-fill');
} else {
themeIcon.classList.replace('bi-moon-fill', 'bi-sun-fill');
}
});
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>

151
templates/config.html Normal file
View file

@ -0,0 +1,151 @@
{% extends "base.html" %}
{% block title %}Configuration - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Configuration</h1>
<a href="/" class="btn btn-secondary mb-3">
<i class="bi bi-arrow-left"></i> Back to Home
</a>
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="bi bi-gear"></i> Mirror Scheduler Settings
</h5>
</div>
<div class="card-body">
<form action="/config" method="post">
<div class="mb-3">
<label for="mirror_interval" class="form-label">Mirror Interval (hours)</label>
<input type="number" class="form-control" id="mirror_interval" name="mirror_interval"
value="{{ config.mirror_interval }}" min="1" required>
<div class="form-text">How often the mirror script should run automatically (in hours)</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="scheduler_enabled" name="scheduler_enabled"
{% if config.scheduler_enabled %}checked{% endif %}>
<label class="form-check-label" for="scheduler_enabled">Enable Scheduler</label>
<div class="form-text">When enabled, the mirror script will run automatically at the specified interval</div>
</div>
<div class="mb-3">
<label for="log_level" class="form-label">Logging Level</label>
<select class="form-select" id="log_level" name="log_level">
{% for level in valid_log_levels %}
<option value="{{ level }}" {% if config.log_level == level %}selected{% endif %}>{{ level }}</option>
{% endfor %}
</select>
<div class="form-text">
<ul class="mb-0 ps-3">
<li><strong>DEBUG</strong>: Detailed information, typically of interest only when diagnosing problems</li>
<li><strong>INFO</strong>: Confirmation that things are working as expected</li>
<li><strong>WARNING</strong>: Indication that something unexpected happened, but the application is still working</li>
<li><strong>ERROR</strong>: Due to a more serious problem, the application has not been able to perform some function</li>
<li><strong>CRITICAL</strong>: A serious error, indicating that the application itself may be unable to continue running</li>
</ul>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Save Configuration
</button>
<button type="button" id="runNowBtn" class="btn btn-success ms-2">
<i class="bi bi-play"></i> Run Mirror Now
</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="bi bi-info-circle"></i> Scheduler Status
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Scheduler Status:</strong>
{% if config.scheduler_enabled %}
<span class="badge bg-success">Enabled</span>
{% else %}
<span class="badge bg-danger">Disabled</span>
{% endif %}
</p>
<p><strong>Mirror Interval:</strong> {{ config.mirror_interval }} hours</p>
<p><strong>Logging Level:</strong> <span class="badge bg-secondary">{{ config.log_level }}</span></p>
</div>
<div class="col-md-6">
<p><strong>Last Run:</strong>
{% if config.last_mirror_run %}
{{ config.last_mirror_run|timestamp_to_datetime }}
{% else %}
Never
{% endif %}
</p>
<p><strong>Next Run:</strong>
{% if next_run %}
{{ next_run.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
Not scheduled
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const runNowBtn = document.getElementById('runNowBtn');
runNowBtn.addEventListener('click', function() {
// Disable button and show loading state
runNowBtn.disabled = true;
runNowBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Running...';
// Call the API to run the mirror script
fetch('/api/run-now', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
// Show result
if (data.success) {
alert('Mirror script started successfully');
} else {
alert('Error: ' + data.message);
}
// Reset button
runNowBtn.disabled = false;
runNowBtn.innerHTML = '<i class="bi bi-play"></i> Run Mirror Now';
// Reload page to update status
setTimeout(() => {
window.location.reload();
}, 1000);
})
.catch(error => {
alert('Error: ' + error.message);
// Reset button
runNowBtn.disabled = false;
runNowBtn.innerHTML = '<i class="bi bi-play"></i> Run Mirror Now';
});
});
});
</script>
{% endblock %}

47
templates/index.html Normal file
View file

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Home - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>GitHub to Gitea Mirror</h1>
<p class="lead">Mirror GitHub releases to your Gitea instance</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-start mb-4">
<a href="/run" class="btn btn-primary">Run Mirror</a>
<a href="/repos" class="btn btn-secondary">View Repositories</a>
</div>
<h2>Recent Logs</h2>
{% if log_files %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Filename</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for log in log_files %}
<tr>
<td>{{ log.date.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ log.filename }}</td>
<td>{{ "%.2f"|format(log.size) }} KB</td>
<td>
<a href="/logs/{{ log.filename }}" class="btn btn-sm btn-info">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">No log files found.</div>
{% endif %}
</div>
</div>
{% endblock %}

119
templates/log.html Normal file
View file

@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}Log: {{ filename }} - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Log: {{ filename }}</h1>
<div class="mb-3">
<a href="/logs" class="btn btn-primary">
<i class="bi bi-arrow-left"></i> Back to Logs
</a>
<a href="/repos" class="btn btn-secondary">View Repositories</a>
<button id="toggle-refresh" class="btn btn-outline-primary">Auto-refresh: ON</button>
<span id="refresh-status" class="ms-2">Refreshing every 5 seconds</span>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Log Content</h5>
<div>
<button id="scroll-to-bottom" class="btn btn-sm btn-outline-secondary">Scroll to Bottom</button>
<button id="scroll-to-top" class="btn btn-sm btn-outline-secondary">Scroll to Top</button>
</div>
</div>
<div class="card-body">
<pre id="log-content" class="log-content" style="max-height: 800px; overflow-y: auto;">{{ log_content }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const logContent = document.getElementById('log-content');
const toggleRefreshBtn = document.getElementById('toggle-refresh');
const refreshStatus = document.getElementById('refresh-status');
const scrollToBottomBtn = document.getElementById('scroll-to-bottom');
const scrollToTopBtn = document.getElementById('scroll-to-top');
let autoRefresh = true;
let refreshInterval = 5000; // 5 seconds
let refreshTimer;
// Function to refresh log content
function refreshLog() {
fetch('/api/logs/{{ filename }}')
.then(response => response.json())
.then(data => {
if (data.content) {
logContent.textContent = data.content;
// Scroll to bottom if we were already at the bottom
if (isScrolledToBottom()) {
scrollToBottom();
}
}
})
.catch(error => console.error('Error refreshing log:', error));
}
// Check if scrolled to bottom
function isScrolledToBottom() {
return Math.abs(logContent.scrollHeight - logContent.clientHeight - logContent.scrollTop) < 10;
}
// Scroll to bottom function
function scrollToBottom() {
logContent.scrollTop = logContent.scrollHeight;
}
// Scroll to top function
function scrollToTop() {
logContent.scrollTop = 0;
}
// Start auto-refresh
function startAutoRefresh() {
refreshTimer = setInterval(refreshLog, refreshInterval);
toggleRefreshBtn.textContent = 'Auto-refresh: ON';
toggleRefreshBtn.classList.remove('btn-outline-secondary');
toggleRefreshBtn.classList.add('btn-outline-primary');
refreshStatus.textContent = `Refreshing every ${refreshInterval/1000} seconds`;
}
// Stop auto-refresh
function stopAutoRefresh() {
clearInterval(refreshTimer);
toggleRefreshBtn.textContent = 'Auto-refresh: OFF';
toggleRefreshBtn.classList.remove('btn-outline-primary');
toggleRefreshBtn.classList.add('btn-outline-secondary');
refreshStatus.textContent = 'Auto-refresh is disabled';
}
// Toggle auto-refresh
toggleRefreshBtn.addEventListener('click', function() {
autoRefresh = !autoRefresh;
if (autoRefresh) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
// Scroll to bottom button
scrollToBottomBtn.addEventListener('click', scrollToBottom);
// Scroll to top button
scrollToTopBtn.addEventListener('click', scrollToTop);
// Initial scroll to bottom
scrollToBottom();
// Start auto-refresh by default
startAutoRefresh();
});
</script>
{% endblock %}

56
templates/logs.html Normal file
View file

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block title %}Logs - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Logs</h1>
<div class="mb-3">
<a href="/" class="btn btn-secondary">Back to Home</a>
<a href="/repos" class="btn btn-primary">View Repositories</a>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Available Log Files</h5>
</div>
<div class="card-body">
{% if log_files %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Filename</th>
<th>Size</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for log in log_files %}
<tr>
<td>{{ log.name }}</td>
<td>{{ (log.size / 1024) | round(2) }} KB</td>
<td>{{ log.mtime }}</td>
<td>
<a href="/logs/{{ log.name }}" class="btn btn-sm btn-primary">View Log</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p>No log files found in the logs directory.</p>
<p>Log files are created when mirror operations are performed. Try running a mirror operation first.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

408
templates/repo_config.html Normal file
View file

@ -0,0 +1,408 @@
{% extends "base.html" %}
{% block title %}Repository Configuration - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Repository Configuration</h1>
<h3>{{ gitea_owner }}/{{ gitea_repo }}</h3>
<div class="mb-3">
<a href="/repos" class="btn btn-secondary">Back to Repositories</a>
<a href="{{ gitea_url }}/{{ gitea_owner }}/{{ gitea_repo }}" class="btn btn-primary" target="_blank">
View in Gitea
</a>
<a href="https://github.com/{{ github_repo }}" class="btn btn-dark" target="_blank">
View on GitHub
</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Repository Status</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>GitHub Repository:</strong> <a href="https://github.com/{{ github_repo }}" target="_blank">{{ github_repo }}</a></p>
<p><strong>Gitea Repository:</strong> <a href="{{ gitea_url }}/{{ gitea_owner }}/{{ gitea_repo }}" target="_blank">{{ gitea_owner }}/{{ gitea_repo }}</a></p>
</div>
<div class="col-md-6">
<p>
<strong>Last Mirror:</strong> {{ config.last_mirror_date|default('Never') }}
{% if config.last_mirror_status == 'success' %}
<span class="badge bg-success">Success</span>
{% elif config.last_mirror_status == 'warning' %}
<span class="badge bg-warning">Warning</span>
{% elif config.last_mirror_status == 'error' %}
<span class="badge bg-danger">Error</span>
{% endif %}
</p>
<p><strong>Mirror Type:</strong> Pull Mirror</p>
{% if config.last_mirror_messages and config.last_mirror_messages|length > 0 %}
<div class="alert {% if config.last_mirror_status == 'error' %}alert-danger{% elif config.last_mirror_status == 'warning' %}alert-warning{% else %}alert-secondary{% endif %} mt-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>Last Mirror Messages:</strong>
</div>
<ul class="mb-0">
{% for message in config.last_mirror_messages %}
<li>
{% if config.last_mirror_log %}
<a href="/logs/{{ config.last_mirror_log }}" class="{% if config.last_mirror_status == 'error' %}text-danger{% elif config.last_mirror_status == 'warning' %}text-warning{% endif %}" data-bs-toggle="tooltip" title="Click to view log">
{{ message }}
</a>
{% else %}
{{ message }}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Mirroring Configuration</h5>
</div>
<div class="card-body">
<form method="post">
<!-- Custom Switch for Mirror Metadata -->
<div class="form-check mb-3">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_metadata" name="mirror_metadata" class="custom-switch-input" {% if config.mirror_metadata %}checked{% endif %}>
<label for="mirror_metadata" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Metadata</span>
</div>
<div class="form-text">Enable mirroring of metadata (issues, PRs, labels, etc.) from GitHub to Gitea</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0">Metadata Components</h6>
</div>
<div class="card-body">
<!-- Custom Switch for Mirror Issues -->
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_issues" name="mirror_issues" class="custom-switch-input metadata-component" {% if config.mirror_issues %}checked{% endif %}>
<label for="mirror_issues" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Issues</span>
</div>
</div>
<!-- Custom Switch for Mirror Pull Requests -->
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_pull_requests" name="mirror_pull_requests" class="custom-switch-input metadata-component" {% if config.mirror_pull_requests %}checked{% endif %}>
<label for="mirror_pull_requests" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Pull Requests</span>
</div>
</div>
<!-- Custom Switch for Mirror Labels -->
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_labels" name="mirror_labels" class="custom-switch-input metadata-component" {% if config.mirror_labels %}checked{% endif %}>
<label for="mirror_labels" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Labels</span>
</div>
</div>
<!-- Custom Switch for Mirror Milestones -->
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_milestones" name="mirror_milestones" class="custom-switch-input metadata-component" {% if config.mirror_milestones %}checked{% endif %}>
<label for="mirror_milestones" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Milestones</span>
</div>
</div>
<!-- Custom Switch for Mirror Wiki -->
<div class="form-check mb-2">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_wiki" name="mirror_wiki" class="custom-switch-input metadata-component" {% if config.mirror_wiki %}checked{% endif %}>
<label for="mirror_wiki" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Wiki</span>
</div>
</div>
</div>
</div>
<!-- Custom Switch for Mirror Releases -->
<div class="form-check mb-3">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_releases" name="mirror_releases" class="custom-switch-input" {% if config.mirror_releases %}checked{% endif %}>
<label for="mirror_releases" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Mirror Releases</span>
</div>
<div class="form-text">Enable mirroring of releases from GitHub to Gitea</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">Save Configuration</button>
</div>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="card-title mb-0">Manual Actions</h5>
</div>
<div class="card-body">
<form action="/run" method="post" class="mb-3">
<input type="hidden" name="mirror_type" value="specific">
<input type="hidden" name="github_repo" value="{{ github_repo }}">
<input type="hidden" name="gitea_owner" value="{{ gitea_owner }}">
<input type="hidden" name="gitea_repo" value="{{ gitea_repo }}">
<input type="hidden" name="redirect_to_log" value="true">
<!-- Custom Switch for Include Metadata -->
<div class="form-check mb-3">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_metadata_run" name="mirror_metadata" class="custom-switch-input" {% if config.mirror_metadata %}checked{% endif %}>
<label for="mirror_metadata_run" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Include Metadata</span>
</div>
</div>
<!-- Custom Switch for Include Releases -->
<div class="form-check mb-3">
<div class="custom-switch-container">
<input type="checkbox" id="mirror_releases_run" name="mirror_releases" class="custom-switch-input" {% if config.mirror_releases %}checked{% endif %}>
<label for="mirror_releases_run" class="custom-switch-label">
<span class="custom-switch-inner"></span>
<span class="custom-switch-switch"></span>
</label>
<span class="ms-2">Include Releases</span>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Run Mirror Now</button>
</div>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Destructive Actions</h5>
</div>
<div class="card-body">
<div class="alert alert-danger">
<strong>Warning!</strong> The actions below are destructive and cannot be undone. Proceed with caution.
</div>
<h6>Delete All Issues and Pull Requests</h6>
<p>This will delete or close all issues and pull requests in the Gitea repository. This is useful for cleaning up duplicate issues.</p>
<form action="/repos/{{ gitea_owner }}/{{ gitea_repo }}/delete-issues" method="post" class="mb-3" id="delete-issues-form">
<div class="mb-3">
<label for="confirmation" class="form-label">Type <strong>{{ gitea_owner }}/{{ gitea_repo }}</strong> to confirm deletion:</label>
<input type="text" class="form-control" id="confirmation" name="confirmation" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-danger" id="delete-issues-btn">Delete All Issues and PRs</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block styles %}
<style>
/* Custom switch styles */
.custom-switch-container {
display: flex;
align-items: center;
}
.custom-switch-input {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.custom-switch-label {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
width: 60px;
height: 30px;
background: #6c757d;
border-radius: 100px;
position: relative;
transition: background-color 0.2s;
margin: 0;
}
.custom-switch-label .custom-switch-inner {
display: block;
width: 100%;
height: 100%;
border-radius: 100px;
position: relative;
}
.custom-switch-label .custom-switch-inner:before,
.custom-switch-label .custom-switch-inner:after {
content: "";
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50%;
text-align: center;
line-height: 30px;
font-size: 12px;
font-weight: bold;
color: white;
}
.custom-switch-label .custom-switch-inner:before {
content: "OFF";
left: 10px;
}
.custom-switch-label .custom-switch-inner:after {
content: "ON";
right: 10px;
opacity: 0;
}
.custom-switch-label .custom-switch-switch {
position: absolute;
top: 2px;
left: 2px;
width: 26px;
height: 26px;
border-radius: 45px;
transition: 0.2s;
background: #fff;
box-shadow: 0 0 2px 0 rgba(10, 10, 10, 0.29);
}
.custom-switch-input:checked + .custom-switch-label {
background: #198754;
}
.custom-switch-input:checked + .custom-switch-label .custom-switch-inner:before {
opacity: 0;
}
.custom-switch-input:checked + .custom-switch-label .custom-switch-inner:after {
opacity: 1;
}
.custom-switch-input:checked + .custom-switch-label .custom-switch-switch {
left: calc(100% - 2px);
transform: translateX(-100%);
}
.custom-switch-input:disabled + .custom-switch-label {
opacity: 0.5;
cursor: not-allowed;
}
/* Dark mode specific styles */
[data-bs-theme="dark"] .custom-switch-label {
background: #343a40;
}
[data-bs-theme="dark"] .custom-switch-input:checked + .custom-switch-label {
background: #00bc8c;
}
[data-bs-theme="dark"] .custom-switch-label .custom-switch-switch {
background: #e9ecef;
}
</style>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const mirrorMetadataSwitch = document.getElementById('mirror_metadata');
const metadataComponents = document.querySelectorAll('.metadata-component');
// Function to update the state of metadata components
function updateComponentsState() {
const isEnabled = mirrorMetadataSwitch.checked;
metadataComponents.forEach(component => {
component.disabled = !isEnabled;
// Update the visual state of the label
const label = component.nextElementSibling;
if (!isEnabled) {
label.style.opacity = "0.5";
label.style.cursor = "not-allowed";
} else {
label.style.opacity = "1";
label.style.cursor = "pointer";
}
});
}
// Initial state for metadata components
updateComponentsState();
// Update on change
mirrorMetadataSwitch.addEventListener('change', updateComponentsState);
// Confirmation for delete issues form
const deleteIssuesForm = document.getElementById('delete-issues-form');
if (deleteIssuesForm) {
deleteIssuesForm.addEventListener('submit', function(e) {
if (!confirm('Are you sure you want to delete all issues and pull requests? This action cannot be undone.')) {
e.preventDefault();
return false;
}
return true;
});
}
});
</script>
{% endblock %}

247
templates/repos.html Normal file
View file

@ -0,0 +1,247 @@
{% extends "base.html" %}
{% block title %}Repositories - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Repositories</h1>
<div class="d-flex justify-content-between mb-3">
<div>
<a href="/" class="btn btn-secondary">Back to Home</a>
<a href="/add" class="btn btn-success ms-2">Add Repository</a>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary active" id="list-view-btn">List View</button>
<button type="button" class="btn btn-outline-primary" id="card-view-btn">Card View</button>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Mirrored Repositories</h5>
{% if repos %}
<!-- List View -->
<div class="table-responsive" id="list-view">
<table class="table table-striped">
<thead>
<tr>
<th>GitHub Repository</th>
<th>Gitea Repository</th>
<th>Mirror Type</th>
<th>Mirror Interval</th>
<th>Last Mirrored</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for repo in repos %}
<tr>
<td>
<a href="https://github.com/{{ repo.github_repo }}" target="_blank">
{{ repo.github_repo }}
</a>
</td>
<td>
<a href="{{ gitea_url }}/{{ repo.gitea_owner }}/{{ repo.gitea_repo }}" target="_blank">
{{ repo.gitea_owner }}/{{ repo.gitea_repo }}
</a>
</td>
<td>
{% if repo.is_mirror %}
<span class="badge bg-success">Pull Mirror</span>
{% else %}
<span class="badge bg-warning">Legacy</span>
{% endif %}
</td>
<td>{{ repo.mirror_interval }}</td>
<td>{{ repo.last_mirror_date }}</td>
<td>
{% if repo.last_mirror_status == 'success' %}
<div class="d-flex align-items-center">
<span class="badge bg-success" data-bs-toggle="tooltip" title="Mirror completed successfully">Success</span>
</div>
{% elif repo.last_mirror_status == 'warning' %}
<div class="d-flex align-items-center">
<span class="badge bg-warning" data-bs-toggle="tooltip" title="Mirror completed with warnings">Warning</span>
</div>
{% if repo.last_mirror_messages and repo.last_mirror_messages|length > 0 %}
<div class="small {% if repo.last_mirror_status == 'error' %}text-danger{% elif repo.last_mirror_status == 'warning' %}text-warning{% endif %} mt-1">
{% if repo.last_mirror_log %}
<a href="/logs/{{ repo.last_mirror_log }}" class="{% if repo.last_mirror_status == 'error' %}text-danger{% elif repo.last_mirror_status == 'warning' %}text-warning{% endif %}" data-bs-toggle="tooltip" title="Click to view log">
{{ repo.last_mirror_messages[0] }}
</a>
{% else %}
{{ repo.last_mirror_messages[0] }}
{% endif %}
{% if repo.last_mirror_messages|length > 1 %}
<span class="text-muted">and {{ repo.last_mirror_messages|length - 1 }} more issues</span>
{% endif %}
</div>
{% endif %}
{% elif repo.last_mirror_status == 'error' %}
<div class="d-flex align-items-center">
<span class="badge bg-danger" data-bs-toggle="tooltip" title="Mirror completed with errors">Error</span>
</div>
{% if repo.last_mirror_messages and repo.last_mirror_messages|length > 0 %}
<div class="small {% if repo.last_mirror_status == 'error' %}text-danger{% elif repo.last_mirror_status == 'warning' %}text-warning{% endif %} mt-1">
{% if repo.last_mirror_log %}
<a href="/logs/{{ repo.last_mirror_log }}" class="{% if repo.last_mirror_status == 'error' %}text-danger{% elif repo.last_mirror_status == 'warning' %}text-warning{% endif %}" data-bs-toggle="tooltip" title="Click to view log">
{{ repo.last_mirror_messages[0] }}
</a>
{% else %}
{{ repo.last_mirror_messages[0] }}
{% endif %}
{% if repo.last_mirror_messages|length > 1 %}
<span class="text-muted">and {{ repo.last_mirror_messages|length - 1 }} more issues</span>
{% endif %}
</div>
{% endif %}
{% else %}
<span class="badge bg-secondary">Unknown</span>
{% endif %}
</td>
<td>
<a href="/repos/{{ repo.gitea_owner }}/{{ repo.gitea_repo }}/config" class="btn btn-sm btn-primary">Configure</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Card View -->
<div class="row" id="card-view" style="display: none;">
{% for repo in repos %}
<div class="col-md-4 mb-3">
<div class="card h-100 {% if repo.last_mirror_status == 'error' %}border-danger{% elif repo.last_mirror_status == 'warning' %}border-warning{% endif %}">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
{{ repo.gitea_owner }}/{{ repo.gitea_repo }}
</h5>
{% if repo.last_mirror_status == 'success' %}
<span class="badge bg-success" data-bs-toggle="tooltip" title="Mirror completed successfully">Success</span>
{% elif repo.last_mirror_status == 'warning' %}
<span class="badge bg-warning" data-bs-toggle="tooltip" title="Mirror completed with warnings">Warning</span>
{% elif repo.last_mirror_status == 'error' %}
<span class="badge bg-danger" data-bs-toggle="tooltip" title="Mirror completed with errors">Error</span>
{% else %}
<span class="badge bg-secondary">Unknown</span>
{% endif %}
</div>
<div class="card-body">
<p class="card-text">
<strong>GitHub:</strong>
<a href="https://github.com/{{ repo.github_repo }}" target="_blank">
{{ repo.github_repo }}
</a>
</p>
<p class="card-text">
<strong>Gitea:</strong>
<a href="{{ gitea_url }}/{{ repo.gitea_owner }}/{{ repo.gitea_repo }}" target="_blank">
{{ repo.gitea_owner }}/{{ repo.gitea_repo }}
</a>
</p>
<p class="card-text">
<strong>Type:</strong>
{% if repo.is_mirror %}
<span class="badge bg-success">Pull Mirror</span>
{% else %}
<span class="badge bg-warning">Legacy</span>
{% endif %}
</p>
<p class="card-text">
<strong>Interval:</strong> {{ repo.mirror_interval }}
</p>
<p class="card-text">
<strong>Last Mirrored:</strong> {{ repo.last_mirror_date }}
</p>
{% if repo.last_mirror_messages and repo.last_mirror_messages|length > 0 %}
<div class="alert {% if repo.last_mirror_status == 'error' %}alert-danger{% elif repo.last_mirror_status == 'warning' %}alert-warning{% else %}alert-secondary{% endif %} mt-2 p-2">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong>Mirror Messages:</strong>
</div>
<ul class="mb-0 ps-3">
{% for message in repo.last_mirror_messages %}
<li>
{% if repo.last_mirror_log %}
<a href="/logs/{{ repo.last_mirror_log }}" class="{% if repo.last_mirror_status == 'error' %}text-danger{% elif repo.last_mirror_status == 'warning' %}text-warning{% endif %}" data-bs-toggle="tooltip" title="Click to view log">
{{ message }}
</a>
{% else %}
{{ message }}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div class="card-footer d-flex justify-content-between">
<a href="/repos/{{ repo.gitea_owner }}/{{ repo.gitea_repo }}/config" class="btn btn-primary">Configure</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
No mirrored repositories found.
<div class="mt-3">
<a href="/add" class="btn btn-success">Add Your First Repository</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const listViewBtn = document.getElementById('list-view-btn');
const cardViewBtn = document.getElementById('card-view-btn');
const listView = document.getElementById('list-view');
const cardView = document.getElementById('card-view');
// Initialize tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Check if there's a saved preference
const viewPreference = localStorage.getItem('repoViewPreference') || 'list';
// Set initial view based on preference
if (viewPreference === 'card') {
listView.style.display = 'none';
cardView.style.display = 'flex';
listViewBtn.classList.remove('active');
cardViewBtn.classList.add('active');
}
// Switch to list view
listViewBtn.addEventListener('click', function() {
listView.style.display = 'block';
cardView.style.display = 'none';
listViewBtn.classList.add('active');
cardViewBtn.classList.remove('active');
localStorage.setItem('repoViewPreference', 'list');
});
// Switch to card view
cardViewBtn.addEventListener('click', function() {
listView.style.display = 'none';
cardView.style.display = 'flex';
listViewBtn.classList.remove('active');
cardViewBtn.classList.add('active');
localStorage.setItem('repoViewPreference', 'card');
});
});
</script>
{% endblock %}

130
templates/run.html Normal file
View file

@ -0,0 +1,130 @@
{% extends "base.html" %}
{% block title %}Run Mirror - GitHub to Gitea Mirror{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>Run Mirror</h1>
<a href="/" class="btn btn-secondary mb-3">Back to Home</a>
<div class="card">
<div class="card-body">
<h5 class="card-title">Mirror Options</h5>
<form action="/run" method="post">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="mirror_type" id="mirror_all" value="all" checked>
<label class="form-check-label" for="mirror_all">
Mirror all repositories (auto-discovery)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="mirror_type" id="mirror_specific" value="specific">
<label class="form-check-label" for="mirror_specific">
Mirror specific repository
</label>
</div>
</div>
<div id="specific_repo_fields" style="display: none;">
<div class="mb-3">
<label for="github_repo" class="form-label">GitHub Repository (owner/repo)</label>
<input type="text" class="form-control" id="github_repo" name="github_repo" placeholder="e.g., octocat/Hello-World">
</div>
<div class="mb-3">
<label for="gitea_owner" class="form-label">Gitea Owner</label>
<input type="text" class="form-control" id="gitea_owner" name="gitea_owner" placeholder="e.g., admin">
</div>
<div class="mb-3">
<label for="gitea_repo" class="form-label">Gitea Repository</label>
<input type="text" class="form-control" id="gitea_repo" name="gitea_repo" placeholder="e.g., Hello-World">
</div>
</div>
<div class="mb-3">
<h6 class="card-subtitle mb-2 text-muted">Metadata Mirroring Options</h6>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" name="mirror_metadata" id="mirror_metadata" checked>
<label class="form-check-label" for="mirror_metadata">
Mirror metadata
</label>
<div class="form-text">Enable mirroring of metadata (issues, PRs, labels, etc.) from GitHub to Gitea</div>
</div>
<div id="metadata_options" class="ms-4 mt-3">
<div class="form-check form-switch mb-2">
<input class="form-check-input metadata-option" type="checkbox" name="mirror_issues" id="mirror_issues" checked>
<label class="form-check-label" for="mirror_issues">Mirror Issues</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input metadata-option" type="checkbox" name="mirror_pull_requests" id="mirror_pull_requests" checked>
<label class="form-check-label" for="mirror_pull_requests">Mirror Pull Requests</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input metadata-option" type="checkbox" name="mirror_labels" id="mirror_labels" checked>
<label class="form-check-label" for="mirror_labels">Mirror Labels</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input metadata-option" type="checkbox" name="mirror_milestones" id="mirror_milestones" checked>
<label class="form-check-label" for="mirror_milestones">Mirror Milestones</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input metadata-option" type="checkbox" name="mirror_wiki" id="mirror_wiki" checked>
<label class="form-check-label" for="mirror_wiki">Mirror Wiki</label>
</div>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Run Mirror</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const mirrorTypeRadios = document.querySelectorAll('input[name="mirror_type"]');
const specificRepoFields = document.getElementById('specific_repo_fields');
const mirrorMetadataSwitch = document.getElementById('mirror_metadata');
const metadataOptions = document.querySelectorAll('.metadata-option');
// Function to toggle specific repo fields visibility
function toggleSpecificRepoFields() {
const selectedValue = document.querySelector('input[name="mirror_type"]:checked').value;
specificRepoFields.style.display = selectedValue === 'specific' ? 'block' : 'none';
}
// Function to toggle metadata options
function toggleMetadataOptions() {
const isEnabled = mirrorMetadataSwitch.checked;
metadataOptions.forEach(option => {
option.disabled = !isEnabled;
});
document.getElementById('metadata_options').style.opacity = isEnabled ? '1' : '0.5';
}
// Add event listeners to radio buttons
mirrorTypeRadios.forEach(radio => {
radio.addEventListener('change', toggleSpecificRepoFields);
});
// Add event listener to metadata switch
mirrorMetadataSwitch.addEventListener('change', toggleMetadataOptions);
// Initial toggles
toggleSpecificRepoFields();
toggleMetadataOptions();
});
</script>
{% endblock %}