Compare commits
59 Commits
1f16e5d130
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
528b49f78e | ||
![]() |
46d9635d1e | ||
![]() |
a6d252e627 | ||
![]() |
a754dcea53 | ||
![]() |
3d5189d9e3 | ||
![]() |
1ed40e96dd | ||
![]() |
a8954020f6 | ||
![]() |
c4b5e703f3 | ||
![]() |
52694519d7 | ||
![]() |
7706a521b4 | ||
![]() |
125599e86a | ||
![]() |
88583dba31 | ||
![]() |
1a1e84ad11 | ||
![]() |
36af672735 | ||
![]() |
847e46bf02 | ||
![]() |
e0f739e9a9 | ||
![]() |
5f4973bfa4 | ||
![]() |
6fd28d5765 | ||
![]() |
6f4c1b56ad | ||
![]() |
9cffcb0ca6 | ||
![]() |
7fb796357f | ||
![]() |
146e0f5ab2 | ||
![]() |
ef31984144 | ||
![]() |
ec364aac0c | ||
![]() |
b516ee9e52 | ||
![]() |
5189c60b1f | ||
![]() |
38e62df17d | ||
![]() |
75a9e2399b | ||
![]() |
d003683cd2 | ||
![]() |
362fe6338f | ||
![]() |
3d35f2bdee | ||
![]() |
7dc099a2b9 | ||
![]() |
36915ace4b | ||
![]() |
9c3cfd644a | ||
![]() |
389e98a4fc | ||
![]() |
48cf5841f2 | ||
![]() |
0a80e38564 | ||
![]() |
9714954828 | ||
![]() |
67c6e8a92e | ||
![]() |
7d4a856a03 | ||
![]() |
942ce73975 | ||
![]() |
985d3465c8 | ||
![]() |
3d2b05676b | ||
![]() |
1dc4300881 | ||
![]() |
f8a9dd451b | ||
![]() |
02088c1782 | ||
![]() |
a5c59f8a64 | ||
![]() |
2904209332 | ||
![]() |
55f9cbfb9a | ||
![]() |
4ac60ee541 | ||
![]() |
4827f611b6 | ||
![]() |
f151a8a050 | ||
![]() |
0b3bf4a654 | ||
![]() |
aa0a0a0025 | ||
![]() |
cae5ed787d | ||
![]() |
c4b753d4bd | ||
![]() |
4979365617 | ||
![]() |
8023794451 | ||
![]() |
982a5049d7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ __pycache__
|
||||
data/
|
||||
instance/
|
||||
venv/
|
||||
todo.txt
|
34
alters.txt
Normal file
34
alters.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
ALTER TABLE user_settings ADD COLUMN deploy_cron VARCHAR(100) DEFAULT '12 12 * * *';
|
||||
ALTER TABLE user_settings ADD COLUMN backup_cron VARCHAR(100) DEFAULT '12 12 * * *';
|
||||
|
||||
ALTER TABLE host ADD COLUMN auto_deploy_enabled BOOLEAN DEFAULT 1;
|
||||
ALTER TABLE host ADD COLUMN auto_backup_enabled BOOLEAN DEFAULT 1;
|
||||
|
||||
ALTER TABLE user_settings DROP COLUMN deploy_interval;
|
||||
ALTER TABLE user_settings DROP COLUMN backup_interval;
|
||||
|
||||
|
||||
ALTER TABLE host
|
||||
ADD COLUMN use_daemon BOOLEAN NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE host
|
||||
ALTER COLUMN use_daemon DROP DEFAULT;
|
||||
|
||||
ALTER TABLE host
|
||||
ADD COLUMN daemon_url VARCHAR(255);
|
||||
|
||||
ALTER TABLE host
|
||||
ADD COLUMN daemon_token VARCHAR(255);
|
||||
|
||||
ALTER TABLE Host ADD COLUMN preferred_hostfile_id INTEGER;
|
||||
ALTER TABLE Host
|
||||
ADD COLUMN preferred_hostfile_id INTEGER
|
||||
REFERENCES host_file(id);
|
||||
|
||||
ALTER TABLE user_settings ADD COLUMN global_ssh_key TEXT;
|
||||
ALTER TABLE user_settings ADD COLUMN global_key_passphrase VARCHAR(200);
|
||||
|
||||
ALTER TABLE host ADD COLUMN disable_regex_deploy BOOLEAN NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE host ADD COLUMN disable_local_default BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE user_dynamic_variables DROP TABLE;
|
@@ -4,4 +4,6 @@ Flask-SQLAlchemy
|
||||
paramiko
|
||||
apscheduler
|
||||
gunicorn
|
||||
waitress
|
||||
waitress
|
||||
croniter
|
||||
requests
|
@@ -5,8 +5,8 @@ from datetime import datetime
|
||||
if __name__ == "__main__":
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
for job in scheduler.get_jobs():
|
||||
job.modify(next_run_time=datetime.now())
|
||||
|
||||
if not scheduler.running:
|
||||
scheduler.start()
|
||||
serve(app, listen="*:5580", threads=4, ident="")
|
||||
|
||||
serve(app, listen="*:5580", threads=4, ident="")
|
82
static/css/custom.css
Normal file
82
static/css/custom.css
Normal file
@@ -0,0 +1,82 @@
|
||||
/* Style trybu ciemnego – stosujemy je tylko, gdy body ma klasę dark-mode */
|
||||
body.dark-mode {
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
body.dark-mode footer {
|
||||
background-color: #1e1e1e !important;
|
||||
}
|
||||
/* Tabele – style ciemnego motywu */
|
||||
body.dark-mode .table {
|
||||
color: #e0e0e0;
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
body.dark-mode .table th,
|
||||
body.dark-mode .table td {
|
||||
border: 1px solid #444;
|
||||
}
|
||||
body.dark-mode .table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
body.dark-mode .table-striped tbody tr:nth-of-type(even) {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
body.dark-mode .table thead {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
/* Karty */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
border-color: #333;
|
||||
}
|
||||
/* Formularze */
|
||||
body.dark-mode .form-control,
|
||||
body.dark-mode .form-select {
|
||||
background-color: #2e2e2e;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
body.dark-mode .form-control:focus,
|
||||
body.dark-mode .form-select:focus {
|
||||
background-color: #2e2e2e;
|
||||
color: #e0e0e0;
|
||||
border-color: #777;
|
||||
box-shadow: none;
|
||||
}
|
||||
/* Przycisk Wyloguj – solidny przycisk, by był czytelny */
|
||||
.btn-logout {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Zmniejszenie rozmiaru czcionki w navbarze */
|
||||
.navbar {
|
||||
font-size: 0.85rem; /* zmniejszony rozmiar czcionki */
|
||||
}
|
||||
|
||||
/* Sprytne odwracanie kolorow dla svg */
|
||||
html[data-bs-theme="dark"] .mikrotik-logo {
|
||||
filter: invert(1);
|
||||
}
|
||||
html[data-bs-theme="dark"] .linux-logo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Zmniejszenie tekstu w rozwijanym menu navbar */
|
||||
.navbar .dropdown-menu {
|
||||
font-size: 0.80rem; /* Dostosuj rozmiar czcionki */
|
||||
}
|
||||
|
||||
/* Zmniejszenie paddingu dla elementów dropdown */
|
||||
.navbar .dropdown-item {
|
||||
font-size: 0.85rem; /* Dostosuj rozmiar czcionki */
|
||||
padding: 0.2rem 0.8rem; /* Mniejszy padding */
|
||||
}
|
1
static/img/linux.svg
Normal file
1
static/img/linux.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 24 24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m19.7 17.6c-.1-.2-.2-.4-.2-.6 0-.4-.2-.7-.5-1-.1-.1-.3-.2-.4-.2.6-1.8-.3-3.6-1.3-4.9-.8-1.2-2-2.1-1.9-3.7 0-1.9.2-5.4-3.3-5.1-3.6.2-2.6 3.9-2.7 5.2 0 1.1-.5 2.2-1.3 3.1-.2.2-.4.5-.5.7-1 1.2-1.5 2.8-1.5 4.3-.2.2-.4.4-.5.6-.1.1-.2.2-.2.3-.1.1-.3.2-.5.3-.4.1-.7.3-.9.7-.1.3-.2.7-.1 1.1.1.2.1.4 0 .7-.2.4-.2.9 0 1.4.3.4.8.5 1.5.6.5 0 1.1.2 1.6.4.5.3 1.1.5 1.7.5.3 0 .7-.1 1-.2.3-.2.5-.4.6-.7.4 0 1-.2 1.7-.2.6 0 1.2.2 2 .1 0 .1 0 .2.1.3.2.5.7.9 1.3 1h.2c.8-.1 1.6-.5 2.1-1.1.4-.4.9-.7 1.4-.9.6-.3 1-.5 1.1-1 .1-.7-.1-1.1-.5-1.7zm-6.9-12.8c.6.1 1.1.6 1 1.2 0 .3-.1.6-.3.9 0 0 0 0-.1 0-.2-.1-.3-.1-.4-.2.1-.1.1-.3.2-.5 0-.4-.2-.7-.4-.7-.3 0-.5.3-.5.7v.1c-.1-.1-.3-.1-.4-.2v-.1c-.1-.5.3-1.1.9-1.2zm-.3 2c.1.1.3.2.4.2s.3.1.4.2c.2.1.4.2.4.5s-.3.6-.9.8c-.2.1-.3.1-.4.2-.3.2-.6.3-1 .3-.3 0-.6-.2-.8-.4-.1-.1-.2-.2-.4-.3-.1-.1-.3-.3-.4-.6 0-.1.1-.2.2-.3.3-.2.4-.3.5-.4l.1-.1c.2-.3.6-.5 1-.5.3.1.6.2.9.4zm-2.1-1.8c.4 0 .7.4.8 1.1v.2c-.1 0-.3.1-.4.2 0 0 0-.1 0-.2 0-.3-.2-.6-.4-.5-.2 0-.3.3-.3.6 0 .2.1.3.2.4s-.1.1-.2.1c-.2-.2-.4-.5-.4-.8 0-.6.3-1.1.7-1.1zm-1 16.1c-.7.3-1.6.2-2.2-.2-.6-.3-1.1-.4-1.8-.4-.5-.1-1-.1-1.1-.3s-.1-.5.1-1c.1-.3.1-.6 0-.9s-.1-.5 0-.8.3-.4.6-.5.5-.2.7-.4c.1-.1.2-.2.3-.4.3-.4.5-.6.8-.6.6.1 1.1 1 1.5 1.9.2.3.4.7.7 1 .4.5.9 1.2.9 1.6 0 .5-.2.8-.5 1zm4.9-2.2c0 .1 0 .1-.1.2-1.2.9-2.8 1-4.1.3-.2-.3-.4-.6-.6-.9.9-.1.7-1.3-1.2-2.5-2-1.3-.6-3.7.1-4.8.1-.1.1 0-.3.8-.3.6-.9 2.1-.1 3.2 0-.8.2-1.6.5-2.4.7-1.3 1.2-2.8 1.5-4.3.1.1.1.1.2.1.1.1.2.2.3.2.2.3.6.4.9.4h.1c.4 0 .8-.1 1.1-.4.1-.1.2-.2.4-.2.3-.1.6-.3.9-.6.4 1.3.8 2.5 1.4 3.6.4.8.7 1.6.9 2.5.3 0 .7.1 1 .3.8.4 1.1.7 1 1.2-.1 0-.1 0-.2 0 0-.3-.2-.6-.9-.9s-1.3-.3-1.5.4c-.1 0-.2.1-.3.1-.8.4-.8 1.5-.9 2.6.1.4 0 .7-.1 1.1zm4.6.6c-.6.2-1.1.6-1.5 1.1-.4.6-1.1 1-1.9.9-.4 0-.8-.3-.9-.7-.1-.6-.1-1.2.2-1.8.1-.4.2-.7.3-1.1.1-1.2.1-1.9.6-2.2 0 .5.3.8.7 1 .5 0 1-.1 1.4-.5h.2c.3 0 .5 0 .7.2s.3.5.3.7c0 .3.2.6.3.9.5.5.5.8.5.9-.1.2-.5.4-.9.6zm-9-12c-.1 0-.1 0-.1.1 0 0 0 .1.1.1.1 0 .1.1.1.1.3.4.8.6 1.4.7.5-.1 1-.2 1.5-.6.2-.1.4-.2.6-.3.1 0 .1-.1.1-.1 0-.1 0-.1-.1-.1-.2.1-.5.2-.7.3-.4.3-.9.5-1.4.5s-.9-.3-1.2-.6c-.1 0-.2-.1-.3-.1z"/></svg>
|
After Width: | Height: | Size: 2.1 KiB |
1
static/img/mikrotik.svg
Normal file
1
static/img/mikrotik.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="610" viewBox="0 0 610 610" width="610" xmlns="http://www.w3.org/2000/svg"><path d="m586.8 193.4v222.5c0 13.8-1.7 25.6-5.5 34.3-.7 1.6-1.5 3.2-2.3 4.7-5.5 8.9-16.6 17.7-31.6 25.9l-203 111.2c-12.6 6.9-24.2 11.4-34 12.7q-2.8.4-5.4.4-2.7 0-5.5-.4c-9.8-1.3-21.4-5.8-34-12.7l-101.5-55.6-101.4-55.6c-15.1-8.2-26.2-17-31.6-25.9-5.5-9-7.9-22.5-7.9-39v-222.5c0-13.8 1.7-25.5 5.5-34.2.7-1.7 1.5-3.3 2.4-4.7q1.3-2.2 3-4.3c6.1-7.5 16-14.7 28.6-21.7l101.4-55.6 101.5-55.6c15-8.2 28.6-13 39.5-13q2.6 0 5.4.4c9.8 1.2 21.4 5.7 34 12.6l101.5 55.6 101.5 55.6c12.6 7 22.4 14.2 28.5 21.7q1.8 2.1 3.1 4.3c.8 1.4 1.6 3 2.3 4.7 3.8 8.7 5.5 20.4 5.5 34.2zm-102.5 33.2c0-9.8-5.3-18.8-13.8-23.4l-152.7-83.7c-8-4.4-17.7-4.4-25.7 0l-38.9 21.3c-4.6 2.6-4.6 9.2 0 11.7l116.4 63.8c4.6 2.6 4.6 9.2 0 11.7l-51.8 28.4c-8 4.4-17.7 4.4-25.7 0l-112-61.4c-8-4.4-17.7-4.4-25.7 0l-14.9 8.2c-8.6 4.7-13.9 13.6-13.9 23.4v7l135.5 74.3c8.6 4.6 13.9 13.6 13.9 23.3v141.4c0 4.8 2.6 9.3 6.9 11.7l10.2 5.6c8 4.4 17.7 4.4 25.7 0l10.3-5.6c4.2-2.4 6.9-6.9 6.9-11.7v-141.4c0-9.7 5.3-18.7 13.9-23.3l65.5-36c4.5-2.4 9.9.8 9.9 5.9v142.4c0 5.1 5.4 8.3 9.9 5.9l36.3-19.9c8.5-4.7 13.8-13.7 13.8-23.4zm-298.7 78.2c0-4.8-2.6-9.3-6.9-11.7l-43.2-23.7c-4.5-2.4-9.9.8-9.9 5.9v107.5c0 9.7 5.3 18.7 13.9 23.4l36.3 19.9c4.4 2.4 9.8-.8 9.8-5.9z" fill="#263037" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
8
templates/404.html
Normal file
8
templates/404.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}404 - Strona nie znaleziona{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container text-center mt-5">
|
||||
<h1 class="display-4">404</h1>
|
||||
<p class="lead">Przepraszamy, ale strona, której szukasz, nie została odnaleziona.</p>
|
||||
</div>
|
||||
{% endblock %}
|
11
templates/500.html
Normal file
11
templates/500.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}500 - Błąd serwera{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container text-center mt-5">
|
||||
<h1 class="display-4">500</h1>
|
||||
<p class="lead">Wewnętrzny błąd serwera. Przepraszamy za niedogodności.</p>
|
||||
{% if error %}
|
||||
<pre>{{ error }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@@ -1,14 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dodaj serwer - /etc/hosts Manager{% endblock %}
|
||||
{% block extra_css %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.tooltip-inner {
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
{{ super() }}
|
||||
<style>
|
||||
.tooltip-inner {
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Kontenery domyślnie ukryte; pokażemy je przez JavaScript. */
|
||||
#hostPortFields,
|
||||
#userPassFields,
|
||||
#sshKeyFields,
|
||||
#daemonFields {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
@@ -16,44 +25,93 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('add_server') }}">
|
||||
|
||||
<!-- 1) Wybór platformy (Linux / Mikrotik) -->
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">Nazwa hosta (IP lub domena)</label>
|
||||
<input type="text" name="hostname" id="hostname" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Użytkownik SSH</label>
|
||||
<input type="text" name="username" id="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Hasło SSH</label>
|
||||
<input type="password" name="password" id="password" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port SSH</label>
|
||||
<input type="text" name="port" id="port" class="form-control" value="22">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="host_type" class="form-label">Typ</label>
|
||||
<select name="host_type" id="host_type" class="form-select" required>
|
||||
<label for="host_type" class="form-label">Platforma (system)</label>
|
||||
<select name="host_type" id="host_type" class="form-select" required onchange="toggleSystemOptions()">
|
||||
<option value="linux">Linux</option>
|
||||
<option value="mikrotik">Mikrotik</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 2) Wybór metody uwierzytelniania -->
|
||||
<div class="mb-3">
|
||||
<label for="auth_method" class="form-label">Metoda uwierzytelniania</label>
|
||||
<select name="auth_method" id="auth_method" class="form-select">
|
||||
<select name="auth_method" id="auth_method" class="form-select" onchange="toggleAuthFields()">
|
||||
<option value="password">Hasło</option>
|
||||
<option value="ssh_key">Klucz SSH</option>
|
||||
<option value="global_key">Globalny klucz SSH</option>
|
||||
<option value="daemon">Demon (Linux)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="private_key" class="form-label">Klucz prywatny (jeśli używasz klucza SSH)</label>
|
||||
<textarea name="private_key" id="private_key" rows="4" class="form-control"></textarea>
|
||||
|
||||
<!-- Pola nazwy hosta i portu -->
|
||||
<div id="hostPortFields">
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">Nazwa hosta (IP lub domena)</label>
|
||||
<input type="text" name="hostname" id="hostname" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input type="text" name="port" id="port" class="form-control" value="22">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="key_passphrase" class="form-label">Hasło do klucza (jeśli klucz jest zaszyfrowany)</label>
|
||||
<input type="password" name="key_passphrase" id="key_passphrase" class="form-control">
|
||||
|
||||
<!-- Pola user + hasło (dla password / ssh_key / global_key) -->
|
||||
<div id="userPassFields">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Użytkownik</label>
|
||||
<input type="text" name="username" id="username" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3" id="passwordField">
|
||||
<label for="password" class="form-label">Hasło</label>
|
||||
<input type="password" name="password" id="password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pola klucza prywatnego (dla ssh_key) -->
|
||||
<div id="sshKeyFields">
|
||||
<div class="mb-3">
|
||||
<label for="private_key" class="form-label">Klucz prywatny</label>
|
||||
<textarea name="private_key" id="private_key" rows="4" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="key_passphrase" class="form-label">Hasło do klucza</label>
|
||||
<input type="password" name="key_passphrase" id="key_passphrase" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pola dla demona (tylko jeśli auth_method=daemon i platforma=linux) -->
|
||||
<div id="daemonFields">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="use_daemon" id="use_daemon" value="1">
|
||||
<label class="form-check-label" for="use_daemon">
|
||||
Korzystaj z demona (zamiast SSH) [Tylko Linux]
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="daemon_url" class="form-label">URL demona</label>
|
||||
<input type="text" name="daemon_url" id="daemon_url" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="daemon_token" class="form-label">Token autoryzacyjny demona</label>
|
||||
<input type="text" name="daemon_token" id="daemon_token" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nowe: wybór preferowanego pliku /etc/hosts -->
|
||||
<div class="mb-3">
|
||||
<label for="preferred_hostfile_id" class="form-label">Domyślny plik /etc/hosts</label>
|
||||
<select name="preferred_hostfile_id" id="preferred_hostfile_id" class="form-select">
|
||||
<!-- Opcja pusta => None => "Default" -->
|
||||
<option value="">(Default - brak)</option>
|
||||
{% for hf in user_hostfiles %}
|
||||
<option value="{{ hf.id }}">{{ hf.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Dodaj serwer</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -61,6 +119,89 @@
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{{ url_for('server_list') }}" class="btn btn-secondary">Lista serwerów</a>
|
||||
<a href="{{ url_for('import_servers') }}" class="btn btn-secondary">Importuj serwery z CSV</a>
|
||||
<a href="{{ url_for('import_servers') }}" class="btn btn-secondary">Importuj z CSV</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
function toggleSystemOptions() {
|
||||
const hostType = document.getElementById('host_type').value;
|
||||
const authMethodSelect = document.getElementById('auth_method');
|
||||
// Znajdź opcję 'daemon':
|
||||
const daemonOption = Array.from(authMethodSelect.options).find(opt => opt.value === 'daemon');
|
||||
|
||||
// Jeśli to Mikrotik, ukrywamy opcję demona
|
||||
if (hostType === 'mikrotik') {
|
||||
if (daemonOption) {
|
||||
daemonOption.style.display = 'none';
|
||||
if (authMethodSelect.value === 'daemon') {
|
||||
authMethodSelect.value = 'password';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Linux -> z powrotem pokazujemy 'daemon'
|
||||
if (daemonOption) {
|
||||
daemonOption.style.display = 'block';
|
||||
}
|
||||
}
|
||||
toggleAuthFields();
|
||||
}
|
||||
|
||||
function toggleAuthFields() {
|
||||
const hostType = document.getElementById('host_type').value;
|
||||
const authMethod = document.getElementById('auth_method').value;
|
||||
|
||||
// Kontenery:
|
||||
const hostPortFields = document.getElementById('hostPortFields');
|
||||
const userPassFields = document.getElementById('userPassFields');
|
||||
const passwordField = document.getElementById('passwordField');
|
||||
const sshKeyFields = document.getElementById('sshKeyFields');
|
||||
const daemonFields = document.getElementById('daemonFields');
|
||||
const useDaemon = document.getElementById('use_daemon');
|
||||
|
||||
// Ukrywamy wszystkie na start
|
||||
hostPortFields.style.display = 'none';
|
||||
userPassFields.style.display = 'none';
|
||||
passwordField.style.display = 'none';
|
||||
sshKeyFields.style.display = 'none';
|
||||
daemonFields.style.display = 'none';
|
||||
|
||||
if (useDaemon) {
|
||||
useDaemon.checked = false;
|
||||
}
|
||||
|
||||
// Jeżeli authMethod != 'daemon', to normalnie pokazujemy host/port
|
||||
if (authMethod !== 'daemon') {
|
||||
hostPortFields.style.display = 'block';
|
||||
}
|
||||
|
||||
// W zależności od authMethod
|
||||
if (authMethod === 'password') {
|
||||
userPassFields.style.display = 'block';
|
||||
passwordField.style.display = 'block';
|
||||
|
||||
} else if (authMethod === 'ssh_key') {
|
||||
userPassFields.style.display = 'block';
|
||||
sshKeyFields.style.display = 'block';
|
||||
|
||||
} else if (authMethod === 'global_key') {
|
||||
userPassFields.style.display = 'block';
|
||||
|
||||
} else if (authMethod === 'daemon') {
|
||||
// Tylko Linux
|
||||
if (hostType === 'linux') {
|
||||
daemonFields.style.display = 'block';
|
||||
if (useDaemon) {
|
||||
useDaemon.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
toggleSystemOptions();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -19,6 +19,7 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all"></th>
|
||||
<th>Data utworzenia</th>
|
||||
<th>Opis</th>
|
||||
<th>Akcje</th>
|
||||
@@ -27,11 +28,16 @@
|
||||
<tbody>
|
||||
{% for backup in backups %}
|
||||
<tr>
|
||||
<td>
|
||||
<!-- Każdy checkbox jest przypisany do formularza bulkDeleteForm -->
|
||||
<input type="checkbox" name="selected_backups" value="{{ backup.id }}" form="bulkDeleteForm">
|
||||
</td>
|
||||
<td>{{ backup.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||
<td>{{ backup.description }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('view_backup', backup_id=backup.id) }}" class="btn btn-sm btn-info">Podgląd</a>
|
||||
<a href="{{ url_for('restore_backup', backup_id=backup.id) }}" class="btn btn-sm btn-success">Przywróć</a>
|
||||
<!-- Usuwanie pojedynczego backupu -->
|
||||
<form action="{{ url_for('delete_backup', backup_id=backup.id) }}" method="post" style="display:inline;">
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Czy na pewno usunąć backup?');">Usuń</button>
|
||||
</form>
|
||||
@@ -40,6 +46,22 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Formularz do bulk usuwania – checkboxy znajdują się poza tym formularzem, ale dzięki atrybutowi form są z nim powiązane -->
|
||||
<form id="bulkDeleteForm" action="{{ url_for('delete_selected_backups') }}" method="post">
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Czy na pewno usunąć zaznaczone backupy?');">Usuń zaznaczone</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
// Skrypt do zaznaczania/odznaczania wszystkich checkboxów
|
||||
document.getElementById('select-all').addEventListener('change', function(){
|
||||
var checkboxes = document.querySelectorAll('input[name="selected_backups"]');
|
||||
checkboxes.forEach(function(checkbox) {
|
||||
checkbox.checked = document.getElementById('select-all').checked;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -6,60 +6,10 @@
|
||||
<title>{% block title %}/etc/hosts Manager{% endblock %}</title>
|
||||
<!-- Bootstrap CSS (Bootstrap 5.3.0) -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
||||
<!-- Dodatkowe style -->
|
||||
<style>
|
||||
/* Style trybu ciemnego – stosujemy je tylko, gdy body ma klasę dark-mode */
|
||||
body.dark-mode {
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
body.dark-mode footer {
|
||||
background-color: #1e1e1e !important;
|
||||
}
|
||||
/* Tabele – style ciemnego motywu */
|
||||
body.dark-mode .table {
|
||||
color: #e0e0e0;
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
body.dark-mode .table th,
|
||||
body.dark-mode .table td {
|
||||
border: 1px solid #444;
|
||||
}
|
||||
body.dark-mode .table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
body.dark-mode .table-striped tbody tr:nth-of-type(even) {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
body.dark-mode .table thead {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
/* Karty */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
border-color: #333;
|
||||
}
|
||||
/* Formularze */
|
||||
body.dark-mode .form-control,
|
||||
body.dark-mode .form-select {
|
||||
background-color: #2e2e2e;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
body.dark-mode .form-control:focus,
|
||||
body.dark-mode .form-select:focus {
|
||||
background-color: #2e2e2e;
|
||||
color: #e0e0e0;
|
||||
border-color: #777;
|
||||
box-shadow: none;
|
||||
}
|
||||
/* Przycisk Wyloguj – solidny przycisk, by był czytelny */
|
||||
.btn-logout {
|
||||
color: #fff;
|
||||
}
|
||||
/* */
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
@@ -90,13 +40,16 @@
|
||||
<li><a class="dropdown-item" href="{{ url_for('import_servers') }}">Importuj serwery z CSV</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- WYczysc /etc/hosts -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('clear_server') }}">Wyczyść /etc/hosts</a>
|
||||
</li>
|
||||
<!-- Edytuj /etc/hosts -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('edit_local_hosts') }}">Edytuj /etc/hosts</a>
|
||||
<!-- Edytuj /etc/hosts z podsekcjami -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="editHostsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Edytuj domyśny /etc/hosts
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="editHostsDropdown">
|
||||
<li><a class="dropdown-item" href="{{ url_for('edit_local_hosts') }}">Edytuj /etc/hosts</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('default_hostfile_versions') }}">Historia wersji</a></li>
|
||||
<!-- Opcjonalnie: dodaj inne akcje, np. diff wersji -->
|
||||
</ul>
|
||||
</li>
|
||||
<!-- Sieci CIDR / Regex -->
|
||||
<li class="nav-item dropdown">
|
||||
@@ -111,13 +64,24 @@
|
||||
<!-- Pliki /etc/hosts -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="filesDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Pliki /etc/hosts (beta)
|
||||
Pliki /etc/hosts
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="filesDropdown">
|
||||
<li><a class="dropdown-item" href="{{ url_for('list_hosts_files') }}">Lista plików</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('new_hosts_file') }}">Utwórz nowy /etc/hosts</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Dpmyślne wpisy -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('local_defaults') }}">Domyślne /etc/hosts</a>
|
||||
</li>
|
||||
|
||||
<!-- Wyczysc /etc/hosts -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('clear_server') }}">Wyczyść /etc/hosts</a>
|
||||
</li>
|
||||
|
||||
<!-- Kopie zapasowe -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="backupsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@@ -127,6 +91,7 @@
|
||||
<li><a class="dropdown-item" href="{{ url_for('backups') }}">Lista kopii</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Ustawienia -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('settings') }}">Ustawienia</a>
|
||||
|
@@ -48,7 +48,7 @@
|
||||
<label for="host_id" class="form-label">Wybierz serwer:</label>
|
||||
<select class="form-select" id="host_id" name="host_id">
|
||||
{% for host in hosts %}
|
||||
<option value="{{ host.id }}">{{ host.hostname }}</option>
|
||||
<option value="{{ host.id }}">{{ format_host(host) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -61,17 +61,16 @@
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
// Ustaw dynamicznie action formularza dla czyszczenia pojedynczego serwera
|
||||
document.getElementById('clear-single-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var hostId = document.getElementById('host_id').value;
|
||||
if(!hostId) {
|
||||
if (!hostId) {
|
||||
alert("Proszę wybrać serwer!");
|
||||
return;
|
||||
}
|
||||
// Skonstruuj URL bez użycia url_for
|
||||
this.action = "/clear-single-server/" + hostId;
|
||||
this.submit();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -21,7 +21,14 @@
|
||||
{% for host in hosts %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="hosts" value="{{ host.id }}" id="host{{ host.id }}">
|
||||
<label class="form-check-label" for="host{{ host.id }}">{{ host.hostname }} ({{ host.type }})</label>
|
||||
<label class="form-check-label" for="host{{ host.id }}">
|
||||
{% if host.use_daemon and host.type == 'linux' and host.daemon_url %}
|
||||
{{ host.daemon_ip }} - {{ host.resolved_daemon }}
|
||||
{% else %}
|
||||
{{ host.hostname }}
|
||||
{% endif %}
|
||||
({{ host.type }})
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
27
templates/diff_versions.html
Normal file
27
templates/diff_versions.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Porównanie wersji{% endblock %}
|
||||
{% block extra_css %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
/* Przykładowe style dla ciemnego motywu w diff */
|
||||
table.diff {font-family: Courier; border: medium; }
|
||||
.diff_header {background-color: #444; color: #fff; }
|
||||
td.diff_header {text-align: center;}
|
||||
.diff_next {background-color: #333; }
|
||||
.diff_add {background-color: #008800; color: #fff; }
|
||||
.diff_chg {background-color: #4444aa; color: #fff; }
|
||||
.diff_sub {background-color: #aa0000; color: #fff; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Porównanie wersji</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ diff_html|safe }}
|
||||
<hr>
|
||||
<a href="{{ url_for('hostfile_versions', hostfile_id=hostfile_id) }}" class="btn btn-secondary">Powrót do historii</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -1,14 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edytuj lokalny Hosts - /etc/hosts Manager{% endblock %}
|
||||
{% block extra_css %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.tooltip-inner {
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
@@ -24,7 +15,26 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if hostfile %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3>Wersje /etc/hosts File</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Przeglądaj historię zmian, porównuj aktualną zawartość z najnowszą wersją oraz usuwaj stare wersje.</p>
|
||||
<a href="{{ url_for('hostfile_versions', hostfile_id=hostfile.id) }}" class="btn btn-info">Historia wersji</a>
|
||||
<a href="{{ url_for('delete_old_versions', hostfile_id=hostfile.id, days=30) }}" class="btn btn-secondary" onclick="return confirm('Usuń wersje starsze niż 30 dni?');">Usuń wersje starsze niż 30 dni</a>
|
||||
{% if hostfile.versions|length > 0 %}
|
||||
<a href="{{ url_for('diff_current_hostfile', hostfile_id=hostfile.id) }}" class="btn btn-warning" onclick="return confirm('Porównaj aktualną zawartość z najnowszą wersją zapisanej historii?');">Diff aktualny vs. najnowsza</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Brak zapisanych wersji do diff.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Przejdź do pulpitu</a>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Przejdź do dashboardu</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edytuj server - /etc/hosts Manager{% endblock %}
|
||||
{% block title %}Edytuj serwer - /etc/hosts Manager{% endblock %}
|
||||
{% block extra_css %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
@@ -7,8 +7,17 @@
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Domyślnie ukrywamy te kontenery. JS je włącza zależnie od opcji. */
|
||||
#hostPortFields,
|
||||
#userPassFields,
|
||||
#sshKeyFields,
|
||||
#daemonFields {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
@@ -16,44 +25,100 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('edit_server', id=host.id) }}">
|
||||
|
||||
<!-- 1) Platforma (Linux / Mikrotik) -->
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">Nazwa hosta (IP lub domena)</label>
|
||||
<input type="text" name="hostname" id="hostname" class="form-control" value="{{ host.hostname }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Użytkownik SSH</label>
|
||||
<input type="text" name="username" id="username" class="form-control" value="{{ host.username }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Hasło (pozostaw puste, aby nie zmieniać)</label>
|
||||
<input type="password" name="password" id="password" class="form-control" placeholder="Wprowadź nowe hasło">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port SSH</label>
|
||||
<input type="text" name="port" id="port" class="form-control" value="{{ host.port }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="host_type" class="form-label">Typ</label>
|
||||
<select name="host_type" id="host_type" class="form-select" required>
|
||||
<label for="host_type" class="form-label">Platforma (system)</label>
|
||||
<select name="host_type" id="host_type" class="form-select" required onchange="toggleSystemOptions()">
|
||||
<option value="linux" {% if host.type == 'linux' %}selected{% endif %}>Linux</option>
|
||||
<option value="mikrotik" {% if host.type == 'mikrotik' %}selected{% endif %}>Mikrotik</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 2) Metoda uwierzytelniania -->
|
||||
<div class="mb-3">
|
||||
<label for="auth_method" class="form-label">Metoda uwierzytelniania</label>
|
||||
<select name="auth_method" id="auth_method" class="form-select">
|
||||
<option value="password" {% if host.auth_method == 'password' %}selected{% endif %}>Hasło</option>
|
||||
<option value="ssh_key" {% if host.auth_method == 'ssh_key' %}selected{% endif %}>Klucz SSH</option>
|
||||
<select name="auth_method" id="auth_method" class="form-select" onchange="toggleAuthFields()">
|
||||
<option value="password" {% if host.auth_method == 'password' %}selected{% endif %}>Hasło</option>
|
||||
<option value="ssh_key" {% if host.auth_method == 'ssh_key' %}selected{% endif %}>Klucz SSH</option>
|
||||
<option value="global_key" {% if host.auth_method == 'global_key' %}selected{% endif %}>Globalny klucz SSH</option>
|
||||
<option value="daemon" {% if host.use_daemon and host.type == 'linux' %}selected{% endif %}>Demon (Linux)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="private_key" class="form-label">Klucz prywatny (jeśli używasz klucza SSH)</label>
|
||||
<textarea name="private_key" id="private_key" rows="4" class="form-control">{{ host.private_key }}</textarea>
|
||||
|
||||
<!-- Kontener host + port -->
|
||||
<div id="hostPortFields">
|
||||
<div class="mb-3">
|
||||
<label for="hostname" class="form-label">Nazwa hosta (IP lub domena)</label>
|
||||
<input type="text" name="hostname" id="hostname" class="form-control" value="{{ host.hostname }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input type="text" name="port" id="port" class="form-control" value="{{ host.port }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="key_passphrase" class="form-label">Hasło do klucza (jeśli klucz jest zaszyfrowany)</label>
|
||||
<input type="password" name="key_passphrase" id="key_passphrase" class="form-control" value="{{ host.key_passphrase }}">
|
||||
|
||||
<!-- Kontener user + hasło -->
|
||||
<div id="userPassFields">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Użytkownik</label>
|
||||
<input type="text" name="username" id="username" class="form-control" value="{{ host.username }}">
|
||||
</div>
|
||||
<div class="mb-3" id="passwordField">
|
||||
<label for="password" class="form-label">Hasło (pozostaw puste, aby nie zmieniać)</label>
|
||||
<input type="password" name="password" id="password" class="form-control" placeholder="Wprowadź nowe hasło">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kontener klucza SSH (ssh_key) -->
|
||||
<div id="sshKeyFields">
|
||||
<div class="mb-3">
|
||||
<label for="private_key" class="form-label">Klucz prywatny</label>
|
||||
<textarea name="private_key" id="private_key" rows="4" class="form-control">{{ host.private_key }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="key_passphrase" class="form-label">Hasło do klucza</label>
|
||||
<input type="password" name="key_passphrase" id="key_passphrase" class="form-control" value="{{ host.key_passphrase }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kontener demona -->
|
||||
<div id="daemonFields">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="use_daemon" id="use_daemon" value="1"
|
||||
{% if host.use_daemon %}checked{% endif %}>
|
||||
<label class="form-check-label" for="use_daemon">
|
||||
Korzystaj z demona (zamiast SSH) [Tylko Linux]
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="daemon_url" class="form-label">URL demona</label>
|
||||
<input type="text" name="daemon_url" id="daemon_url" class="form-control" value="{{ host.daemon_url }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="daemon_token" class="form-label">Token autoryzacyjny demona</label>
|
||||
<input type="text" name="daemon_token" id="daemon_token" class="form-control" value="{{ host.daemon_token }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nowe: wybór pliku /etc/hosts (preferred_hostfile_id) -->
|
||||
<div class="mb-3">
|
||||
<label for="preferred_hostfile_id" class="form-label">Domyślny plik /etc/hosts dla tego serwera</label>
|
||||
<select name="preferred_hostfile_id" id="preferred_hostfile_id" class="form-select">
|
||||
<!-- pusta wartość => None => "Default" -->
|
||||
<option value=""
|
||||
{% if not host.preferred_hostfile_id %}selected{% endif %}>
|
||||
(Default - brak)
|
||||
</option>
|
||||
{% for hf in user_hostfiles %}
|
||||
<option value="{{ hf.id }}"
|
||||
{% if host.preferred_hostfile_id == hf.id %}selected{% endif %}>
|
||||
{{ hf.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Zapisz zmiany</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -61,7 +126,84 @@
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{{ url_for('server_list') }}" class="btn btn-secondary">Lista serwerów</a>
|
||||
<a href="{{ url_for('import_servers') }}" class="btn btn-secondary">Importuj serwery z CSV</a>
|
||||
<a href="{{ url_for('export_servers_to_csv') }}" class="btn btn-secondary">Eksportuj serwery do CSV</a>
|
||||
<a href="{{ url_for('import_servers') }}" class="btn btn-secondary">Importuj z CSV</a>
|
||||
<a href="{{ url_for('export_servers_to_csv') }}" class="btn btn-secondary">Eksportuj do CSV</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
function toggleSystemOptions() {
|
||||
const hostType = document.getElementById('host_type').value;
|
||||
const authMethodSelect = document.getElementById('auth_method');
|
||||
const daemonOption = Array.from(authMethodSelect.options).find(opt => opt.value === 'daemon');
|
||||
|
||||
// Mikrotik -> chowamy daemon
|
||||
if (hostType === 'mikrotik') {
|
||||
if (daemonOption) {
|
||||
daemonOption.style.display = 'none';
|
||||
if (authMethodSelect.value === 'daemon') {
|
||||
authMethodSelect.value = 'password';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Linux -> pokazujemy
|
||||
if (daemonOption) {
|
||||
daemonOption.style.display = 'block';
|
||||
}
|
||||
}
|
||||
toggleAuthFields();
|
||||
}
|
||||
|
||||
function toggleAuthFields() {
|
||||
const hostType = document.getElementById('host_type').value;
|
||||
const authMethod = document.getElementById('auth_method').value;
|
||||
|
||||
const hostPortFields = document.getElementById('hostPortFields');
|
||||
const userPassFields = document.getElementById('userPassFields');
|
||||
const passwordField = document.getElementById('passwordField');
|
||||
const sshKeyFields = document.getElementById('sshKeyFields');
|
||||
const daemonFields = document.getElementById('daemonFields');
|
||||
const useDaemon = document.getElementById('use_daemon');
|
||||
|
||||
// Najpierw wszystko chowamy
|
||||
hostPortFields.style.display = 'none';
|
||||
userPassFields.style.display = 'none';
|
||||
passwordField.style.display = 'none';
|
||||
sshKeyFields.style.display = 'none';
|
||||
daemonFields.style.display = 'none';
|
||||
if (useDaemon) {
|
||||
useDaemon.checked = false;
|
||||
}
|
||||
|
||||
// Gdy authMethod != 'daemon', włącz hostPort
|
||||
if (authMethod !== 'daemon') {
|
||||
hostPortFields.style.display = 'block';
|
||||
}
|
||||
|
||||
// Szczegółowe reguły
|
||||
if (authMethod === 'password') {
|
||||
userPassFields.style.display = 'block';
|
||||
passwordField.style.display = 'block';
|
||||
} else if (authMethod === 'ssh_key') {
|
||||
userPassFields.style.display = 'block';
|
||||
sshKeyFields.style.display = 'block';
|
||||
} else if (authMethod === 'global_key') {
|
||||
userPassFields.style.display = 'block';
|
||||
} else if (authMethod === 'daemon') {
|
||||
if (hostType === 'linux') {
|
||||
daemonFields.style.display = 'block';
|
||||
if (useDaemon) {
|
||||
useDaemon.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uruchamiamy przy ładowaniu
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
toggleSystemOptions();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
52
templates/hostfile_versions.html
Normal file
52
templates/hostfile_versions.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Historia wersji hosts{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Historia wersji dla: {{ hostfile.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" id="bulkDeleteForm">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all"></th>
|
||||
<th>Data</th>
|
||||
<th>Fragment treści</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set latest_id = versions[0].id if versions|length > 0 else None %}
|
||||
{% for version in versions %}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="selected_versions" value="{{ version.id }}"></td>
|
||||
<td>{{ version.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||
<td>{{ version.content[:50] }}{% if version.content|length > 50 %}...{% endif %}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('view_hostfile_version', version_id=version.id) }}" class="btn btn-sm btn-info">Podgląd</a>
|
||||
<a href="{{ url_for('restore_hostfile_version', version_id=version.id) }}" class="btn btn-sm btn-success" onclick="return confirm('Przywrócić tę wersję?');">Przywróć</a>
|
||||
{% if latest_id and version.id != latest_id %}
|
||||
<a href="{{ url_for('diff_hostfile_versions', version1_id=version.id, version2_id=latest_id) }}" class="btn btn-sm btn-warning">Diff z najnowszą</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Brak diff</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Czy na pewno usunąć zaznaczone wersje?');">Usuń zaznaczone</button>
|
||||
<a href="{{ url_for('delete_old_versions', hostfile_id=hostfile.id, days=30) }}" class="btn btn-secondary" onclick="return confirm('Usuń wersje starsze niż 30 dni?');">Usuń wersje starsze niż 30 dni</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('select-all').addEventListener('change', function(){
|
||||
var checkboxes = document.querySelectorAll('input[name="selected_versions"]');
|
||||
checkboxes.forEach(function(checkbox) {
|
||||
checkbox.checked = document.getElementById('select-all').checked;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@@ -31,6 +31,7 @@
|
||||
<a href="{{ url_for('edit_hosts_file', file_id=file.id) }}" class="btn btn-sm btn-warning">Edytuj</a>
|
||||
<a href="{{ url_for('delete_hosts_file', file_id=file.id) }}" class="btn btn-sm btn-danger" onclick="return confirm('Czy na pewno usunąć plik?');">Usuń</a>
|
||||
<a href="{{ url_for('deploy_hosts_file', file_id=file.id) }}" class="btn btn-sm btn-success">Deploy</a>
|
||||
<a href="{{ url_for('hostfile_versions', hostfile_id=file.id) }}" class="btn btn-sm btn-info">Historia wersji</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
195
templates/local_defaults.html
Normal file
195
templates/local_defaults.html
Normal file
@@ -0,0 +1,195 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Definiowanie wpisów z dynamicznymi zmiennymi{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Ustaw domyślne wpisy /etc/hosts</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Adres IP</label>
|
||||
<input type="text" class="form-control" name="ip_address" placeholder="np. 127.0.0.1" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Hostname</label>
|
||||
<input type="text" class="form-control" name="hostname" placeholder="np. localhost" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn btn-primary w-100">Dodaj wpis</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
<form id="bulkDeleteForm">
|
||||
<div class="mb-2">
|
||||
<button type="button" class="btn btn-danger btn-sm" id="deleteSelectedBtn" disabled>Usuń zaznaczone</button>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped mt-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll"></th>
|
||||
<th>ID</th>
|
||||
<th>Adres IP</th>
|
||||
<th>Hostname</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in entries %}
|
||||
<tr>
|
||||
<td><input type="checkbox" class="entry-checkbox" value="{{ e.id }}"></td>
|
||||
<td>{{ e.id }}</td>
|
||||
<td class="ip-address">{{ e.ip_address }}</td>
|
||||
<td class="hostname">{{ e.hostname }}</td>
|
||||
<td>
|
||||
<button class="btn btn-warning btn-sm edit-btn" data-id="{{ e.id }}">Edytuj</button>
|
||||
<button class="btn btn-success btn-sm save-btn d-none" data-id="{{ e.id }}">Zapisz</button>
|
||||
<form method="POST" action="{{ url_for('delete_local_default', entry_id=e.id) }}"
|
||||
onsubmit="return confirm('Usunąć wpis?');" style="display:inline;">
|
||||
<button class="btn btn-danger btn-sm">Usuń</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5">Brak zdefiniowanych wpisów.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Logika edycji w tabeli
|
||||
document.querySelectorAll(".edit-btn").forEach(button => {
|
||||
button.addEventListener("click", function(event) {
|
||||
event.preventDefault(); // ✅ Zapobiega przeładowaniu strony
|
||||
|
||||
let row = this.closest("tr");
|
||||
let entryId = this.getAttribute("data-id");
|
||||
let ipCell = row.querySelector(".ip-address");
|
||||
let hostnameCell = row.querySelector(".hostname");
|
||||
let saveButton = row.querySelector(".save-btn");
|
||||
|
||||
// Jeśli pola edycji już istnieją, nie duplikuj ich
|
||||
if (ipCell.querySelector("input") || hostnameCell.querySelector("input")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ipInput = document.createElement("input");
|
||||
ipInput.type = "text";
|
||||
ipInput.className = "form-control form-control-sm";
|
||||
ipInput.value = ipCell.textContent.trim();
|
||||
|
||||
let hostnameInput = document.createElement("input");
|
||||
hostnameInput.type = "text";
|
||||
hostnameInput.className = "form-control form-control-sm";
|
||||
hostnameInput.value = hostnameCell.textContent.trim();
|
||||
|
||||
ipCell.textContent = "";
|
||||
hostnameCell.textContent = "";
|
||||
ipCell.appendChild(ipInput);
|
||||
hostnameCell.appendChild(hostnameInput);
|
||||
|
||||
this.classList.add("d-none");
|
||||
saveButton.classList.remove("d-none");
|
||||
|
||||
saveButton.addEventListener("click", function(event) {
|
||||
event.preventDefault(); // ✅ Zapobiega przeładowaniu strony
|
||||
|
||||
let newIp = ipInput.value.trim();
|
||||
let newHostname = hostnameInput.value.trim();
|
||||
|
||||
fetch(`/local-defaults/update/${entryId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ip_address: newIp, hostname: newHostname })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === "success") {
|
||||
ipCell.textContent = newIp;
|
||||
hostnameCell.textContent = newHostname;
|
||||
|
||||
button.classList.remove("d-none");
|
||||
saveButton.classList.add("d-none");
|
||||
} else {
|
||||
alert("Błąd: " + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert("Wystąpił błąd podczas zapisywania: " + error);
|
||||
});
|
||||
}, { once: true }); // ✅ Zapobiega dodaniu wielu eventów do przycisku "Zapisz"
|
||||
});
|
||||
});
|
||||
|
||||
// Logika zaznaczania i usuwania wpisów
|
||||
let deleteButton = document.getElementById("deleteSelectedBtn");
|
||||
let checkboxes = document.querySelectorAll(".entry-checkbox");
|
||||
let selectAllCheckbox = document.getElementById("selectAll");
|
||||
|
||||
function updateDeleteButtonState() {
|
||||
let anyChecked = Array.from(checkboxes).some(checkbox => checkbox.checked);
|
||||
deleteButton.disabled = !anyChecked;
|
||||
}
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener("change", updateDeleteButtonState);
|
||||
});
|
||||
|
||||
selectAllCheckbox.addEventListener("change", function() {
|
||||
let isChecked = this.checked;
|
||||
checkboxes.forEach(checkbox => checkbox.checked = isChecked);
|
||||
updateDeleteButtonState();
|
||||
});
|
||||
|
||||
deleteButton.addEventListener("click", function() {
|
||||
let selectedIds = Array.from(checkboxes)
|
||||
.filter(checkbox => checkbox.checked)
|
||||
.map(checkbox => checkbox.value);
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
alert("Nie zaznaczono żadnych wpisów do usunięcia.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm("Czy na pewno chcesz usunąć zaznaczone wpisy?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/local-defaults/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ entry_ids: selectedIds })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === "success") {
|
||||
selectedIds.forEach(id => {
|
||||
let row = document.querySelector(`input[value='${id}']`).closest("tr");
|
||||
row.remove();
|
||||
});
|
||||
updateDeleteButtonState();
|
||||
} else {
|
||||
alert("Błąd: " + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert("Wystąpił błąd podczas usuwania: " + error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@@ -28,8 +28,25 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if file %}
|
||||
<!-- Sekcja zarządzania wersjami -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3>Wersje pliku /etc/hosts</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Przeglądaj historię zmian, porównaj aktualną zawartość z najnowszą wersją zapisanej historii lub usuń stare wersje.</p>
|
||||
<a href="{{ url_for('hostfile_versions', hostfile_id=file.id) }}" class="btn btn-info">Historia wersji</a>
|
||||
<a href="{{ url_for('delete_old_versions', hostfile_id=file.id, days=30) }}" class="btn btn-secondary" onclick="return confirm('Usuń wersje starsze niż 30 dni?');">Usuń wersje starsze niż 30 dni</a>
|
||||
{% if file.versions|length > 0 %}
|
||||
<a href="{{ url_for('diff_current_hostfile', hostfile_id=file.id) }}" class="btn btn-warning" onclick="return confirm('Porównaj aktualną zawartość z najnowszą wersją zapisanej historii?');">Diff aktualny vs. ostatnia kopia</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{{ url_for('list_hosts_files') }}" class="btn btn-secondary">Lista Hosts Files</a>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Przejdź do pulpitu</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -7,6 +7,25 @@
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Tryb jasny */
|
||||
.regex-container, .example-container {
|
||||
background-color: var(--bg-light);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
/* Tryb ciemny */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.regex-container, .example-container {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-light);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-green {
|
||||
color: #28a745;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
@@ -45,12 +64,91 @@
|
||||
<div class="mb-3">
|
||||
<label for="comment" class="form-label">Komentarz</label>
|
||||
<input type="text" class="form-control" id="comment" name="comment" value="{% if entry %}{{ entry.comment }}{% endif %}">
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-code"></i> Podgląd generowanego Regex</label>
|
||||
<div class="regex-container p-3 border rounded">
|
||||
<code id="regexPreview"></code>
|
||||
</div>
|
||||
|
||||
<label class="form-label mt-2"><i class="fas fa-terminal"></i> Przykładowe wygenerowane wpisy</label>
|
||||
<div class="example-container p-3 border rounded">
|
||||
<ul id="exampleEntries" class="list-unstyled mb-0"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{% if entry %}Zapisz zmiany{% else %}Utwórz{% endif %}</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{{ url_for('list_regex_hosts') }}" class="btn btn-secondary">Lista Regex Hosts</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
function updateRegexPreview() {
|
||||
let cidr = document.getElementById("cidr_range").value.trim();
|
||||
let gatewayIp = document.getElementById("gateway_ip").value.trim();
|
||||
let gatewayHostname = document.getElementById("gateway_hostname").value.trim();
|
||||
let domain = document.getElementById("domain_suffix").value.trim();
|
||||
let prefix = document.getElementById("host_prefix").value.trim();
|
||||
let useGateway = document.getElementById("use_gateway_ip").checked;
|
||||
|
||||
let regexPreview = `<strong>Regex:</strong> <span class="icon-green">^${prefix}[0-9]+\\.${domain}$</span>`;
|
||||
document.getElementById("regexPreview").innerHTML = regexPreview;
|
||||
|
||||
// Generowanie przykładowych wpisów
|
||||
let exampleList = document.getElementById("exampleEntries");
|
||||
exampleList.innerHTML = ""; // Wyczyść stare wpisy
|
||||
|
||||
let exampleIps = generateExampleIps(cidr, 3); // Pobierz 3 pierwsze IP
|
||||
exampleIps.forEach((ip, index) => {
|
||||
let listItem = document.createElement("li");
|
||||
listItem.innerHTML = `<i class="fas fa-check-circle icon-green"></i> ${ip} ${prefix}${index + 1}.${domain}`;
|
||||
exampleList.appendChild(listItem);
|
||||
});
|
||||
|
||||
// Dodaj wpis dla bramy (jeśli zaznaczona)
|
||||
if (useGateway && gatewayIp && gatewayHostname) {
|
||||
let listItem = document.createElement("li");
|
||||
listItem.innerHTML = `<i class="fas fa-network-wired icon-green"></i> ${gatewayIp} ${gatewayHostname}.${domain}`;
|
||||
exampleList.prepend(listItem);
|
||||
}
|
||||
}
|
||||
|
||||
function generateExampleIps(cidr, count) {
|
||||
let ipList = [];
|
||||
if (!cidr.includes('/')) return ipList;
|
||||
|
||||
let [baseIp, subnet] = cidr.split('/');
|
||||
let baseParts = baseIp.split('.').map(Number);
|
||||
|
||||
// Generujemy kolejne adresy IP w zakresie
|
||||
for (let i = 1; i <= count; i++) {
|
||||
let newIp = [...baseParts];
|
||||
newIp[3] += i; // Zwiększamy ostatni oktet
|
||||
ipList.push(newIp.join('.'));
|
||||
}
|
||||
return ipList;
|
||||
}
|
||||
|
||||
// Nasłuchiwanie zmian w polach
|
||||
document.getElementById("cidr_range").addEventListener("input", updateRegexPreview);
|
||||
document.getElementById("gateway_ip").addEventListener("input", updateRegexPreview);
|
||||
document.getElementById("gateway_hostname").addEventListener("input", updateRegexPreview);
|
||||
document.getElementById("domain_suffix").addEventListener("input", updateRegexPreview);
|
||||
document.getElementById("host_prefix").addEventListener("input", updateRegexPreview);
|
||||
document.getElementById("use_gateway_ip").addEventListener("change", updateRegexPreview);
|
||||
|
||||
// Inicjalne wygenerowanie regexu
|
||||
updateRegexPreview();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@@ -1,29 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Lista serwerów - /etc/hosts Manager{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.tooltip-inner {
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
{{ super() }}
|
||||
<style>
|
||||
.tooltip-inner {
|
||||
max-width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Zmniejsz tekst w nagłówku tabeli i wyśrodkuj go */
|
||||
thead th {
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
/* Klasa zmniejszająca przyciski akcji */
|
||||
.btn-action {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
form.inline-button button.btn {
|
||||
background-color: #dc3545; /* btn-danger */
|
||||
border-color: #dc3545;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
line-height: 1;
|
||||
/* ewentualnie dodatkowe właściwości, aby upodobnić go do linków */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Lista serwerów</h2>
|
||||
</div>
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-striped">
|
||||
<table class="table table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nazwa hosta</th>
|
||||
<th>Użytkownik SSH</th>
|
||||
<th>Serwer</th>
|
||||
<th>Użytkownik</th>
|
||||
<th>Port</th>
|
||||
<th>Typ</th>
|
||||
<th>Metoda uwierzytelniania</th>
|
||||
<th>Uwierzytelnianie</th>
|
||||
<th>Wybrany /etc/hosts</th>
|
||||
<th>Auto Deploy</th>
|
||||
<th>Auto Backup</th>
|
||||
<th>Wyłącz <a href="{{ url_for('list_regex_hosts') }}">CIDR / regex</a></th>
|
||||
<th>Wyłącz <a href="{{ url_for('local_defaults') }}">local-defaults</a></th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -31,31 +59,222 @@
|
||||
{% for h in hosts %}
|
||||
<tr>
|
||||
<td>{{ h.id }}</td>
|
||||
<td data-bs-toggle="tooltip" data-bs-placement="top" title="{{ h.resolved_hostname }}">
|
||||
{{ h.hostname }}
|
||||
<td data-bs-toggle="tooltip" data-bs-placement="top" title="{{ h.raw_ip }}">
|
||||
{% if h.use_daemon and h.type == 'linux' and h.daemon_url %}
|
||||
{{ h.resolved_daemon }}
|
||||
{% else %}
|
||||
{{ h.resolved_hostname }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ h.username }}</td>
|
||||
<td>{{ h.port }}</td>
|
||||
<td>{{ h.type }}</td>
|
||||
<td>{{ h.auth_method }}</td>
|
||||
|
||||
<td>
|
||||
<a href="{{ url_for('edit_server', id=h.id) }}" class="btn btn-primary btn-sm">Edytuj</a>
|
||||
<a href="{{ url_for('test_server_connection', id=h.id) }}" class="btn btn-info btn-sm">Testuj</a>
|
||||
<a href="{{ url_for('server_backup', host_id=h.id) }}" class="btn btn-success btn-sm">Backup</a>
|
||||
<form method="GET" action="{{ url_for('delete_server', id=h.id) }}" style="display:inline;">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Usuń</button>
|
||||
</form>
|
||||
{% if h.use_daemon and h.type == 'linux' %}
|
||||
<em>—</em>
|
||||
{% else %}
|
||||
{{ h.username }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if h.use_daemon and h.type == 'linux' %}
|
||||
<em>—</em>
|
||||
{% else %}
|
||||
{{ h.port }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
{% if h.type == 'linux' %}
|
||||
<img src="{{ url_for('static', filename='img/linux.svg') }}"
|
||||
alt="Linux"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="{% if h.use_daemon %}Linux - Używa Linux Demon{% else %}Linux{% endif %}"
|
||||
class="linux-logo"
|
||||
style="width: 40px; height: 40px;">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='img/mikrotik.svg') }}"
|
||||
alt="Mikrotik"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="Mikrotik"
|
||||
class="mikrotik-logo"
|
||||
style="width: 40px; height: 40px;">
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if h.use_daemon and h.type == 'linux' %}
|
||||
<span class="badge bg-secondary" style="font-size: 0.75rem;">- linux daemon -</span>
|
||||
{% else %}
|
||||
{% if h.auth_method == 'password' %}
|
||||
<span class="badge bg-primary" style="font-size: 0.75rem;">Hasło</span>
|
||||
{% elif h.auth_method == 'ssh_key' %}
|
||||
<span class="badge bg-success" style="font-size: 0.75rem;">Klucz SSH</span>
|
||||
{% elif h.auth_method == 'global_key' %}
|
||||
<span class="badge bg-info" style="font-size: 0.75rem;">Globalny klucz</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-dark" style="font-size: 0.75rem;">{{ h.auth_method }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if h.preferred_hostfile %}
|
||||
{{ h.preferred_hostfile.title }}
|
||||
{% else %}
|
||||
(Default)
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" class="d-flex justify-content-center">
|
||||
<input type="hidden" name="setting" value="auto_deploy">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="autoDeployCheckbox{{ h.id }}" name="enabled" value="1" onchange="this.form.submit()" {% if h.auto_deploy_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="autoDeployCheckbox{{ h.id }}"></label>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" class="d-flex justify-content-center">
|
||||
<input type="hidden" name="setting" value="auto_backup">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="autoBackupCheckbox{{ h.id }}" name="enabled" value="1" onchange="this.form.submit()" {% if h.auto_backup_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="autoBackupCheckbox{{ h.id }}"></label>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" class="d-flex justify-content-center">
|
||||
<input type="hidden" name="setting" value="disable_regex">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="disableRegexCheckbox{{ h.id }}" name="enabled" value="1" onchange="this.form.submit()" {% if h.disable_regex_deploy %}checked{% endif %}>
|
||||
<label class="form-check-label" for="disableRegexCheckbox{{ h.id }}"></label>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" class="d-flex justify-content-center">
|
||||
<input type="hidden" name="setting" value="disable_local_default">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="disableLocalDefaultCheckbox{{ h.id }}" name="enabled" value="1" onchange="this.form.submit()" {% if h.disable_local_default %}checked{% endif %}>
|
||||
<label class="form-check-label" for="disableLocalDefaultCheckbox{{ h.id }}"></label>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<a href="{{ url_for('edit_server', id=h.id) }}" class="btn btn-primary btn-sm btn-action">Edytuj</a>
|
||||
{% if h.use_daemon and h.type == 'linux' %}
|
||||
<button class="btn btn-info btn-sm btn-action test-daemon-btn" data-host-id="{{ h.id }}">
|
||||
Test
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{{ url_for('test_server_connection', id=h.id) }}" class="btn btn-info btn-sm btn-action">Test</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('server_backup', host_id=h.id) }}" class="btn btn-success btn-sm btn-action">Backup</a>
|
||||
<form method="GET" action="{{ url_for('delete_server', id=h.id) }}" class="inline-button d-flex justify-content-center">
|
||||
<button type="submit" class="btn btn-danger btn-sm btn-action" onclick="return confirm('Czy na pewno chcesz usunąć serwer?')">Usuń</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal z informacjami -->
|
||||
<div class="modal fade" id="serverInfoModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Informacje o serwerze</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>Host:</strong> <span id="modal-hostname"></span> (<span id="modal-ip"></span>)</p>
|
||||
|
||||
<label><strong>CPU:</strong></label>
|
||||
<div class="progress mb-3">
|
||||
<div id="modal-cpu" class="progress-bar" role="progressbar"></div>
|
||||
</div>
|
||||
|
||||
<label><strong>Pamięć:</strong></label>
|
||||
<div class="progress mb-3">
|
||||
<div id="modal-mem" class="progress-bar bg-warning" role="progressbar"></div>
|
||||
</div>
|
||||
|
||||
<label><strong>Dysk:</strong></label>
|
||||
<div class="progress mb-3">
|
||||
<div id="modal-disk" class="progress-bar bg-success" role="progressbar"></div>
|
||||
</div>
|
||||
|
||||
<p><strong>Czas działania:</strong> <span id="modal-uptime"></span></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Zamknij</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{{ url_for('add_server') }}" class="btn btn-secondary">Dodaj nowy serwer</a>
|
||||
<a href="{{ url_for('import_servers') }}" class="btn btn-secondary">Importuj serwery z CSV</a>
|
||||
<a href="{{ url_for('export_servers_to_csv') }}" class="btn btn-secondary">Eksportuj serwery do CSV</a>
|
||||
<a href="{{ url_for('import_servers') }}" class="btn btn-secondary">Importuj z CSV</a>
|
||||
<a href="{{ url_for('export_servers_to_csv') }}" class="btn btn-secondary">Eksportuj do CSV</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
function secondsToDhms(seconds) {
|
||||
seconds = Number(seconds);
|
||||
const d = Math.floor(seconds / (3600 * 24));
|
||||
const h = Math.floor(seconds % (3600 * 24) / 3600);
|
||||
const m = Math.floor(seconds % 3600 / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${d} dni, ${h} godz, ${m} min, ${s} sek`;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.test-daemon-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const hostId = this.dataset.hostId;
|
||||
fetch(`/server-info/${hostId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
alert(`Błąd: ${data.error}`);
|
||||
return;
|
||||
}
|
||||
document.querySelector('#modal-hostname').textContent = data.hostname;
|
||||
document.querySelector('#modal-ip').textContent = data.ip;
|
||||
|
||||
const cpu = document.querySelector('#modal-cpu');
|
||||
cpu.style.width = `${data.cpu}%`;
|
||||
cpu.textContent = `${data.cpu}%`;
|
||||
|
||||
const mem = document.querySelector('#modal-mem');
|
||||
mem.style.width = `${data.mem}%`;
|
||||
mem.textContent = `${data.mem}%`;
|
||||
|
||||
const disk = document.querySelector('#modal-disk');
|
||||
disk.style.width = `${data.disk}%`;
|
||||
disk.textContent = `${data.disk}%`;
|
||||
|
||||
document.querySelector('#modal-uptime').textContent = secondsToDhms(data.uptime_seconds);
|
||||
|
||||
new bootstrap.Modal(document.getElementById('serverInfoModal')).show();
|
||||
})
|
||||
.catch(() => alert('Błąd podczas pobierania danych.'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -21,12 +21,24 @@
|
||||
<label class="form-check-label" for="auto_deploy">Automatyczny deploy</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="deploy_interval" class="form-label">Interwał deploy (minuty)</label>
|
||||
<input type="number" class="form-control" id="deploy_interval" name="deploy_interval" value="{{ settings.deploy_interval }}">
|
||||
<label for="deploy_cron" class="form-label">Harmonogram deploy (cron)</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="deploy_cron" name="deploy_cron" value="{{ settings.deploy_cron }}">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('deploy_cron')">Generuj cron</button>
|
||||
</div>
|
||||
<small class="text-muted">Np. <code>0 0 * * *</code> – codziennie o północy</small>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="auto_backup" name="auto_backup" {% if settings.auto_backup_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="auto_backup">Automatyczne kopie zapasowe</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="backup_interval" class="form-label">Interwał backupów (minuty)</label>
|
||||
<input type="number" class="form-control" id="backup_interval" name="backup_interval" value="{{ settings.backup_interval }}">
|
||||
<label for="backup_cron" class="form-label">Harmonogram backup (cron)</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="backup_cron" name="backup_cron" value="{{ settings.backup_cron }}">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('backup_cron')">Generuj cron</button>
|
||||
</div>
|
||||
<small class="text-muted">Np. <code>0 */6 * * *</code> – co 6 godzin</small>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="enable_regex_entries" name="enable_regex_entries" {% if settings.regex_deploy_enabled %}checked{% endif %}>
|
||||
@@ -36,8 +48,95 @@
|
||||
<label for="backup_retention_days" class="form-label">Ilość dni przechowywania backupów</label>
|
||||
<input type="number" class="form-control" id="backup_retention_days" name="backup_retention_days" value="{{ settings.backup_retention_days }}">
|
||||
</div>
|
||||
<!-- Nowe pola dla globalnego klucza SSH -->
|
||||
<div class="mb-3">
|
||||
<label for="global_ssh_key" class="form-label">Globalny klucz SSH</label>
|
||||
<textarea class="form-control" id="global_ssh_key" name="global_ssh_key" rows="4">{{ settings.global_ssh_key or '' }}</textarea>
|
||||
<small class="text-muted">Wklej tutaj swój globalny klucz SSH, który będzie używany przez hosty z metodą "global_key".</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="global_key_passphrase" class="form-label">Hasło globalnego klucza SSH</label>
|
||||
<input type="password" class="form-control" id="global_key_passphrase" name="global_key_passphrase" value="{{ settings.global_key_passphrase or '' }}">
|
||||
<small class="text-muted">Opcjonalnie: podaj hasło do globalnego klucza SSH, jeśli jest ustawione.</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal do generowania wyrażenia CRON -->
|
||||
<div class="modal fade" id="cronModal" tabindex="-1" aria-labelledby="cronModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="cronModalLabel">Generuj wyrażenie CRON</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="cron_minute" class="form-label">Minuta</label>
|
||||
<input type="text" class="form-control" id="cron_minute" placeholder="0-59">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cron_hour_modal" class="form-label">Godzina</label>
|
||||
<input type="text" class="form-control" id="cron_hour_modal" placeholder="0-23">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cron_day" class="form-label">Dzień miesiąca</label>
|
||||
<input type="text" class="form-control" id="cron_day" placeholder="1-31 lub *">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cron_month" class="form-label">Miesiąc</label>
|
||||
<input type="text" class="form-control" id="cron_month" placeholder="1-12 lub *">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cron_dow" class="form-label">Dzień tygodnia</label>
|
||||
<input type="text" class="form-control" id="cron_dow" placeholder="0-6 (0 = niedziela) lub *">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button type="button" class="btn btn-primary" onclick="generateCronExpression()">Generuj</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
// Globalna zmienna, która będzie przechowywać ID pola formularza do aktualizacji
|
||||
var currentCronField = null;
|
||||
|
||||
function openCronModal(fieldId) {
|
||||
currentCronField = fieldId;
|
||||
// Resetuj wartości w modal
|
||||
document.getElementById('cron_minute').value = "";
|
||||
document.getElementById('cron_hour_modal').value = "";
|
||||
document.getElementById('cron_day').value = "";
|
||||
document.getElementById('cron_month').value = "";
|
||||
document.getElementById('cron_dow').value = "";
|
||||
// Wyświetl modal przy użyciu Bootstrap
|
||||
var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
|
||||
cronModal.show();
|
||||
}
|
||||
|
||||
function generateCronExpression() {
|
||||
var minute = document.getElementById('cron_minute').value || "*";
|
||||
var hour = document.getElementById('cron_hour_modal').value || "*";
|
||||
var day = document.getElementById('cron_day').value || "*";
|
||||
var month = document.getElementById('cron_month').value || "*";
|
||||
var dow = document.getElementById('cron_dow').value || "*";
|
||||
|
||||
var cronExpression = minute + " " + hour + " " + day + " " + month + " " + dow;
|
||||
if (currentCronField) {
|
||||
document.getElementById(currentCronField).value = cronExpression;
|
||||
}
|
||||
// Zamknij modal
|
||||
var modalEl = document.getElementById('cronModal');
|
||||
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||
modal.hide();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
14
templates/view_hostfile_version.html
Normal file
14
templates/view_hostfile_version.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Podgląd wersji{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Podgląd wersji z: {{ version.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre>{{ version.content }}</pre>
|
||||
<a href="{{ url_for('hostfile_versions', hostfile_id=version.hostfile_id) }}" class="btn btn-secondary">Powrót do historii</a>
|
||||
<a href="{{ url_for('restore_hostfile_version', version_id=version.id) }}" class="btn btn-success" onclick="return confirm('Przywrócić tę wersję?');">Przywróć tę wersję</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user