mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-28 03:39:32 +00:00
258 lines
No EOL
12 KiB
HTML
258 lines
No EOL
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en"> <!-- data-theme will be added by JS -->
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="refresh" content="30"> <!-- Auto-refresh every 30 seconds -->
|
|
<!-- Pico.css CDN Link -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css">
|
|
<title>Cloudflare Tunnel Manager</title>
|
|
<style>
|
|
/* Basic layout adjustments */
|
|
body { padding-top: 1rem; padding-bottom: 1rem; }
|
|
h1, h2, h3 { margin-top: 1.5em; margin-bottom: 0.5em; }
|
|
h1 { margin-top: 0; text-align: center; }
|
|
article { margin-bottom: 1.5rem; }
|
|
td pre { margin: 0; white-space: pre-wrap; word-break: break-all; font-family: var(--pico-font-family-monospace); font-size: 0.9em; }
|
|
|
|
/* Status-specific colors using Pico variables and left border */
|
|
.status-box {
|
|
padding: 1rem 1.25rem;
|
|
border: 1px solid var(--pico-form-element-border-color);
|
|
border-radius: var(--pico-border-radius);
|
|
margin-top: 1rem;
|
|
word-wrap: break-word;
|
|
background-color: var(--pico-form-element-background-color);
|
|
color: var(--pico-form-element-color);
|
|
border-left-width: 4px; /* Add a thicker left border for emphasis */
|
|
}
|
|
.status-box.error {
|
|
border-left-color: var(--pico-color-red-500);
|
|
}
|
|
.status-box.success {
|
|
border-left-color: var(--pico-color-green-500);
|
|
}
|
|
.status-box.info {
|
|
border-left-color: var(--pico-color-blue-500);
|
|
}
|
|
.status-box.warning {
|
|
border-left-color: var(--pico-color-amber-500);
|
|
}
|
|
/* Ensure text inside is readable */
|
|
.status-box p {
|
|
margin-bottom: 0;
|
|
}
|
|
.status-box strong {
|
|
color: var(--pico-h_color); /* Use heading color for better contrast */
|
|
}
|
|
|
|
/* Rule Status colors */
|
|
.status-active { color: var(--pico-color-green-600); font-weight: bold; }
|
|
.status-pending { color: var(--pico-color-orange-500); font-weight: bold; }
|
|
|
|
/* Form styling adjustments */
|
|
form { display: inline-block; margin: 0; }
|
|
form button { margin: 0; }
|
|
.grid > form > button { width: 100%; } /* Make buttons in grid take full width */
|
|
|
|
/* Status indicator dots */
|
|
.status-indicator { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; vertical-align: middle;}
|
|
.status-indicator.running { background-color: var(--pico-color-green-500); }
|
|
.status-indicator.exited, .status-indicator.dead, .status-indicator.not_found { background-color: var(--pico-color-red-500); }
|
|
.status-indicator.docker_unavailable { background-color: var(--pico-color-gray-500); }
|
|
.status-indicator.unknown, .status-indicator.created, .status-indicator.paused { background-color: var(--pico-color-amber-500); }
|
|
|
|
/* Button customizations */
|
|
button.delete-button {
|
|
/* Use Pico classes 'contrast' and 'outline' */
|
|
font-size: 0.9em; /* Make delete button slightly smaller */
|
|
padding: 0.5rem 0.75rem;
|
|
}
|
|
|
|
/* Theme Toggle Button */
|
|
#theme-toggle {
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
/* Use Pico classes for button styling */
|
|
padding: 0.5rem; /* Smaller padding */
|
|
width: auto; /* Allow button to size to content */
|
|
line-height: 1; /* Align icon better */
|
|
font-size: 1.2rem; /* Adjust icon size */
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--pico-secondary);
|
|
}
|
|
#theme-toggle:hover {
|
|
color: var(--pico-secondary-hover);
|
|
background: transparent; /* Ensure no background on hover */
|
|
border: none;
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<!-- Body uses Pico styles. Main container for centering/max-width. data-theme set by JS -->
|
|
<body>
|
|
<main class="container">
|
|
|
|
<!-- Theme Toggle Button -->
|
|
<button id="theme-toggle" aria-label="Toggle theme">🌓</button> <!-- Initial Icon -->
|
|
|
|
<h1>Cloudflare Tunnel Manager</h1>
|
|
|
|
<!-- Initialization Status Section -->
|
|
<article>
|
|
<hgroup>
|
|
<h2>Initialization Status</h2>
|
|
<h3>Cloudflare Tunnel API Interaction</h3>
|
|
</hgroup>
|
|
<!-- Apply status class directly to the box -->
|
|
<div class="status-box {{ 'error' if tunnel_state.get('error') else ('success' if tunnel_state.get('token') else 'info') }}">
|
|
<p><strong>Message:</strong> {{ tunnel_state.status_message }}</p>
|
|
{% if tunnel_state.get('error') %}
|
|
<p><strong>Error Details:</strong> <pre>{{ tunnel_state.error }}</pre></p>
|
|
{% endif %}
|
|
</div>
|
|
<h4>Tunnel Details</h4>
|
|
<p><strong>Desired Tunnel Name:</strong> <pre>{{ tunnel_state.name }}</pre></p>
|
|
<p><strong>Tunnel ID:</strong> <pre>{{ tunnel_state.id if tunnel_state.id else 'Not available' }}</pre></p>
|
|
<p><strong>Tunnel Token:</strong> <pre>{{ display_token }}</pre></p>
|
|
</article>
|
|
|
|
<!-- Tunnel Agent Control Section -->
|
|
<article>
|
|
<hgroup>
|
|
<h2>Tunnel Agent Control</h2>
|
|
<h3>Docker Container: <pre style="display: inline;">{{ cloudflared_container_name }}</pre></h3>
|
|
</hgroup>
|
|
<p>
|
|
<strong>Agent Container Status:</strong>
|
|
<span class="status-indicator {{ agent_state.container_status }}"></span>
|
|
<strong style="text-transform: capitalize;" class="{{'success' if agent_state.container_status=='running' else ('error' if 'error' in agent_state.container_status or agent_state.container_status=='docker_unavailable' or agent_state.container_status=='dead' or agent_state.container_status=='not_found' else ('warning' if agent_state.container_status=='exited' else 'info')) }}">
|
|
{{ agent_state.container_status.replace('_',' ') }}
|
|
</strong>
|
|
</p>
|
|
{% if agent_state.last_action_status %}
|
|
<!-- Apply status class directly to the box -->
|
|
<div class="status-box {{ 'error' if 'Error:' in agent_state.last_action_status else ('warning' if 'Warning:' in agent_state.last_action_status else 'info') }}">
|
|
<p><strong>Last Action Result:</strong> {{ agent_state.last_action_status }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Agent Control Buttons - Grouped with grid -->
|
|
<div class="grid">
|
|
<form action="{{ url_for('start_tunnel') }}" method="post">
|
|
<button type="submit"
|
|
{{ 'disabled' if not tunnel_state.get('token') or agent_state.container_status=='running' or not docker_available }}>
|
|
Start Agent
|
|
</button>
|
|
</form>
|
|
<form action="{{ url_for('stop_tunnel') }}" method="post">
|
|
<button type="submit" class="secondary"
|
|
{{ 'disabled' if agent_state.container_status!='running' or not docker_available }}>
|
|
Stop Agent
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Managed Ingress Rules Section -->
|
|
<article>
|
|
<h2>Managed Ingress Rules</h2>
|
|
{% if rules %}
|
|
<figure> <!-- Pico styles tables inside figures nicely -->
|
|
<table role="grid"> <!-- role="grid" for accessibility -->
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Hostname</th>
|
|
<th scope="col">Service Target</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Container</th>
|
|
<th scope="col">Delete Scheduled (UTC)</th>
|
|
<th scope="col">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for hostname, details in rules.items()|sort %} {# Sort by hostname #}
|
|
<tr>
|
|
<td><pre>{{ hostname }}</pre></td>
|
|
<td><pre>{{ details.service }}</pre></td>
|
|
<td>
|
|
<strong class="{{ 'status-active' if details.status=='active' else 'status-pending' }}">
|
|
{{ details.status }}
|
|
</strong>
|
|
</td>
|
|
<td><pre title="{{ details.container_id if details.container_id else 'N/A' }}">{{ details.container_id[:12] if details.container_id else 'N/A' }}</pre></td>
|
|
<td>
|
|
{% if details.status=='pending_deletion' and details.delete_at %}
|
|
<span title="{{ details.delete_at.isoformat() }}">{{ details.delete_at.strftime('%H:%M %d.%m.%Y') }}</span>
|
|
{% else %}
|
|
N/A
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<form action="{{ url_for('force_delete_rule', hostname=hostname) }}" method="post"
|
|
onsubmit="return confirm('Are you sure you want to force delete the rule and DNS record for {{ hostname }} immediately? This bypasses the grace period.');">
|
|
<!-- Use Pico classes for styling -->
|
|
<button type="submit" class="delete-button contrast outline" {{ 'disabled' if not docker_available }}>
|
|
Delete
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</figure>
|
|
{% else %}
|
|
<p>No ingress rules are currently being managed.</p>
|
|
{% endif %}
|
|
</article>
|
|
|
|
</main> <!-- end .container -->
|
|
|
|
<!-- JavaScript for Theme Toggle -->
|
|
<script>
|
|
const themeToggleButton = document.getElementById('theme-toggle');
|
|
const htmlElement = document.documentElement; // Target the <html> element
|
|
|
|
// Function to set the theme and update icon
|
|
const setTheme = (theme) => {
|
|
htmlElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme); // Save preference
|
|
// Update button icon based on the new theme
|
|
themeToggleButton.textContent = theme === 'dark' ? '☀️' : '🌓';
|
|
};
|
|
|
|
// Function to toggle the theme
|
|
const toggleTheme = () => {
|
|
// Check the current theme *explicitly* set on <html> or default to light
|
|
const currentTheme = htmlElement.getAttribute('data-theme') || 'light';
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
setTheme(newTheme);
|
|
};
|
|
|
|
// Add event listener to the button
|
|
themeToggleButton.addEventListener('click', toggleTheme);
|
|
|
|
// Apply theme on initial load
|
|
const initTheme = () => {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
// Check for OS preference if no theme is saved
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
|
|
if (savedTheme) {
|
|
setTheme(savedTheme);
|
|
} else {
|
|
// If no saved theme, use OS preference as default
|
|
setTheme(prefersDark ? 'dark' : 'light');
|
|
}
|
|
};
|
|
|
|
// Run theme initialization
|
|
initTheme();
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html> |