fixy i usprwnienia

This commit is contained in:
Mateusz Gruszczyński 2025-03-06 10:38:12 +01:00
parent 4979365617
commit c4b753d4bd
6 changed files with 241 additions and 55 deletions

8
alters.txt Normal file
View File

@ -0,0 +1,8 @@
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;

144
app.py
View File

@ -7,7 +7,9 @@ from datetime import datetime, timezone, timedelta
from io import StringIO from io import StringIO
import socket import socket
import ipaddress import ipaddress
import pytz from croniter import croniter
from tzlocal import get_localzone
from werkzeug.serving import WSGIRequestHandler from werkzeug.serving import WSGIRequestHandler
WSGIRequestHandler.server_version = "" WSGIRequestHandler.server_version = ""
@ -38,7 +40,8 @@ class Host(db.Model):
key_passphrase = db.Column(db.String(200), nullable=True) key_passphrase = db.Column(db.String(200), nullable=True)
port = db.Column(db.Integer, default=22) port = db.Column(db.Integer, default=22)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
auto_deploy_enabled = db.Column(db.Boolean, default=True)
auto_backup_enabled = db.Column(db.Boolean, default=True)
@property @property
def resolved_hostname(self): def resolved_hostname(self):
try: try:
@ -63,8 +66,10 @@ class UserSettings(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True, nullable=False)
auto_deploy_enabled = db.Column(db.Boolean, default=False) auto_deploy_enabled = db.Column(db.Boolean, default=False)
deploy_interval = db.Column(db.Integer, default=60) # interwał wdrożeń (minuty) #deploy_interval = db.Column(db.Integer, default=60) # interwał wdrożeń (minuty)
backup_interval = db.Column(db.Integer, default=60) # interwał backupów (minuty) #backup_interval = db.Column(db.Integer, default=60) # interwał backupów (minuty)
deploy_cron = db.Column(db.String(100), default="12 12 * * *")
backup_cron = db.Column(db.String(100), default="12 12 * * *")
auto_backup_enabled = db.Column(db.Boolean, default=False) auto_backup_enabled = db.Column(db.Boolean, default=False)
last_deploy_time = db.Column(db.DateTime, nullable=True) last_deploy_time = db.Column(db.DateTime, nullable=True)
regex_deploy_enabled = db.Column(db.Boolean, default=True) regex_deploy_enabled = db.Column(db.Boolean, default=True)
@ -170,41 +175,43 @@ def automated_backup_for_host(host):
) )
db.session.add(backup) db.session.add(backup)
db.session.commit() db.session.commit()
log_entry = DeployLog(
details=f'[BACKUP] Automatic backup created for host {host.hostname}',
user_id=host.user_id
)
db.session.add(log_entry)
db.session.commit()
print(f'Automated backup for host {host.hostname} created successfully.') print(f'Automated backup for host {host.hostname} created successfully.')
except Exception as e: except Exception as e:
print(f'Error creating automated backup for host {host.hostname}: {str(e)}') print(f'Error creating automated backup for host {host.hostname}: {str(e)}')
def automated_backups(): def automated_backups():
with app.app_context(): with app.app_context():
logger.debug("Rozpoczynam funkcję automated_backups")
hosts = Host.query.all()
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
hosts = Host.query.all()
for host in hosts: for host in hosts:
settings = UserSettings.query.filter_by(user_id=host.user_id).first() # Dodaj warunek: backup dla danego hosta ma być wykonywany tylko, jeśli jest włączony
if not settings or not settings.auto_backup_enabled: if not host.auto_backup_enabled:
logger.debug(f"Pomijam host {host.hostname} - auto_backup nie włączone")
continue continue
backup_interval = settings.backup_interval if settings.backup_interval else 60
logger.debug(f"Backup interval dla hosta {host.hostname}: {backup_interval} minut") settings = UserSettings.query.filter_by(user_id=host.user_id).first()
if not settings or not settings.auto_backup_enabled or not settings.backup_cron:
continue
# Pobieramy ostatni backup dla hosta
last_backup = Backup.query.filter_by(user_id=host.user_id, host_id=host.id)\ last_backup = Backup.query.filter_by(user_id=host.user_id, host_id=host.id)\
.order_by(Backup.created_at.desc()).first() .order_by(Backup.created_at.desc()).first()
if last_backup: if last_backup:
last_backup_time = last_backup.created_at base_time = last_backup.created_at
if last_backup_time.tzinfo is None: if base_time.tzinfo is None:
# Zakładamy, że zapisany czas jest już w UTC base_time = base_time.replace(tzinfo=timezone.utc)
last_backup_time = last_backup_time.replace(tzinfo=timezone.utc)
diff = (now - last_backup_time).total_seconds()
logger.debug(f"Różnica czasu dla hosta {host.hostname}: {diff} sekund")
else: else:
last_backup_time = None base_time = datetime.now(timezone.utc) - timedelta(minutes=1)
logger.debug(f"Brak poprzedniego backupu dla hosta {host.hostname}") cron = croniter(settings.backup_cron, base_time)
if (last_backup_time is None) or ((now - last_backup_time).total_seconds() >= backup_interval * 60): next_backup_time = cron.get_next(datetime)
logger.debug(f"Wykonuję backup dla hosta {host.hostname}") if now >= next_backup_time:
automated_backup_for_host(host) automated_backup_for_host(host)
db.session.commit() db.session.commit()
db.session.expire_all()
else:
logger.debug(f"Backup dla hosta {host.hostname} nie jest jeszcze potrzebny")
def wrap_content_with_comments(content): def wrap_content_with_comments(content):
@ -838,22 +845,28 @@ def settings():
if request.method == 'POST': if request.method == 'POST':
auto_deploy = request.form.get('auto_deploy') auto_deploy = request.form.get('auto_deploy')
deploy_interval = request.form.get('deploy_interval') deploy_cron = request.form.get('deploy_cron')
backup_interval = request.form.get('backup_interval')
auto_backup = request.form.get('auto_backup') auto_backup = request.form.get('auto_backup')
backup_cron = request.form.get('backup_cron')
enable_regex_entries = request.form.get('enable_regex_entries') enable_regex_entries = request.form.get('enable_regex_entries')
retention_val = request.form.get('backup_retention_days', '0') retention_val = request.form.get('backup_retention_days', '0')
# Walidacja wyrażeń cron przy pomocy croniter
try:
croniter(deploy_cron)
except Exception as e:
flash("Błędne wyrażenie cron dla deploy: " + str(e), "danger")
return redirect(url_for('settings'))
try:
croniter(backup_cron)
except Exception as e:
flash("Błędne wyrażenie cron dla backup: " + str(e), "danger")
return redirect(url_for('settings'))
user_settings.auto_deploy_enabled = bool(auto_deploy) user_settings.auto_deploy_enabled = bool(auto_deploy)
user_settings.auto_backup_enabled = bool(auto_backup) user_settings.auto_backup_enabled = bool(auto_backup)
try: user_settings.deploy_cron = deploy_cron if deploy_cron else "12 12 * * *"
user_settings.deploy_interval = int(deploy_interval) user_settings.backup_cron = backup_cron if backup_cron else "12 12 * * *"
except ValueError:
user_settings.deploy_interval = 60
try:
user_settings.backup_interval = int(backup_interval)
except ValueError:
user_settings.backup_interval = 60
user_settings.regex_deploy_enabled = bool(enable_regex_entries) user_settings.regex_deploy_enabled = bool(enable_regex_entries)
try: try:
user_settings.backup_retention_days = int(retention_val) user_settings.backup_retention_days = int(retention_val)
@ -921,6 +934,9 @@ def deploy_user(user_id):
hosts = Host.query.filter_by(user_id=user_id).all() hosts = Host.query.filter_by(user_id=user_id).all()
for h in hosts: for h in hosts:
# Tylko dla serwerów z włączonym auto_deploy
if not h.auto_deploy_enabled:
continue
try: try:
if h.type == 'linux': if h.type == 'linux':
ssh = open_ssh_connection(h) ssh = open_ssh_connection(h)
@ -934,12 +950,11 @@ def deploy_user(user_id):
sftp.close() sftp.close()
ssh.close() ssh.close()
os.remove(tmp_file_path) os.remove(tmp_file_path)
db.session.add(DeployLog(details=f'[LINUX] Updated {h.hostname} for user {user_id}',user_id=user_id)) db.session.add(DeployLog(details=f'[LINUX] Updated {h.hostname} for user {user_id}', user_id=user_id))
elif h.type == 'mikrotik': elif h.type == 'mikrotik':
wrapped_content = wrap_mikrotik_content(final_content) wrapped_content = wrap_mikrotik_content(final_content)
deploy_mikrotik(h, wrapped_content) deploy_mikrotik(h, wrapped_content)
db.session.add(DeployLog(details=f'[MIKROTIK] Updated {h.hostname} for user {user_id}',user_id=user_id)) db.session.add(DeployLog(details=f'[MIKROTIK] Updated {h.hostname} for user {user_id}', user_id=user_id))
db.session.commit() db.session.commit()
except Exception as e: except Exception as e:
db.session.add(DeployLog(details=f'Failed to update {h.hostname}: {str(e)} for user {user_id}')) db.session.add(DeployLog(details=f'Failed to update {h.hostname}: {str(e)} for user {user_id}'))
@ -1131,20 +1146,61 @@ def delete_regex_host(entry_id):
flash('Row deleted', 'info') flash('Row deleted', 'info')
return redirect(url_for('list_regex_hosts')) return redirect(url_for('list_regex_hosts'))
@app.route('/delete-selected-backups', methods=['POST'])
def delete_selected_backups():
if 'user_id' not in session:
return redirect(url_for('login'))
selected_ids = request.form.getlist('selected_backups')
for backup_id in selected_ids:
backup = db.session.get(Backup, backup_id)
if backup and backup.user_id == session['user_id']:
db.session.delete(backup)
db.session.commit()
flash('Zaznaczone backupy zostały usunięte.', 'info')
return redirect(url_for('backups'))
@app.route('/update-host-automation/<int:id>', methods=['POST'])
def update_host_automation(id):
if 'user_id' not in session:
return redirect(url_for('login'))
host = db.session.get(Host, id)
if not host or host.user_id != session['user_id']:
flash('Serwer nie istnieje lub nie masz uprawnień', 'danger')
return redirect(url_for('server_list'))
setting = request.form.get('setting')
enabled = request.form.get('enabled') == '1'
if setting == 'auto_deploy':
host.auto_deploy_enabled = enabled
elif setting == 'auto_backup':
host.auto_backup_enabled = enabled
db.session.commit()
flash('Ustawienia automatyzacji zostały zaktualizowane.', 'success')
return redirect(url_for('server_list'))
def scheduled_deployments(): def scheduled_deployments():
with app.app_context(): with app.app_context():
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
all_settings = UserSettings.query.filter_by(auto_deploy_enabled=True).all() settings_list = UserSettings.query.filter_by(auto_deploy_enabled=True).all()
for setting in all_settings: for setting in settings_list:
last_deploy_time = setting.last_deploy_time if not setting.deploy_cron:
if last_deploy_time: continue
last_deploy_time = last_deploy_time.replace(tzinfo=timezone.utc) if setting.last_deploy_time:
if not last_deploy_time or now - last_deploy_time >= timedelta(minutes=setting.deploy_interval): base_time = setting.last_deploy_time
if base_time.tzinfo is None:
base_time = base_time.replace(tzinfo=timezone.utc)
else:
base_time = datetime(1970,1,1, tzinfo=timezone.utc)
cron = croniter(setting.deploy_cron, base_time)
next_deploy = cron.get_next(datetime)
if now >= next_deploy:
deploy_user(setting.user_id) deploy_user(setting.user_id)
setting.last_deploy_time = now setting.last_deploy_time = now
db.session.commit() db.session.commit()
scheduler = BackgroundScheduler(timezone="UTC")
scheduler = BackgroundScheduler(timezone=get_localzone())
scheduler.add_job(func=scheduled_deployments, trigger="interval", minutes=1, next_run_time=datetime.now()) scheduler.add_job(func=scheduled_deployments, trigger="interval", minutes=1, next_run_time=datetime.now())
scheduler.add_job(func=automated_backups, trigger="interval", minutes=1, next_run_time=datetime.now()) scheduler.add_job(func=automated_backups, trigger="interval", minutes=1, next_run_time=datetime.now())
scheduler.add_job(func=cleanup_old_backups, trigger="interval", hours=24, next_run_time=datetime.now()) scheduler.add_job(func=cleanup_old_backups, trigger="interval", hours=24, next_run_time=datetime.now())

View File

@ -5,9 +5,8 @@ from datetime import datetime
if __name__ == "__main__": if __name__ == "__main__":
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
for job in scheduler.get_jobs():
job.modify(next_run_time=datetime.now())
print(job)
if not scheduler.running: if not scheduler.running:
scheduler.start() scheduler.start()
serve(app, listen="*:5580", threads=4, ident="")
serve(app, listen="*:5580", threads=4, ident="")

View File

@ -19,6 +19,7 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th><input type="checkbox" id="select-all"></th>
<th>Data utworzenia</th> <th>Data utworzenia</th>
<th>Opis</th> <th>Opis</th>
<th>Akcje</th> <th>Akcje</th>
@ -27,11 +28,16 @@
<tbody> <tbody>
{% for backup in backups %} {% for backup in backups %}
<tr> <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.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ backup.description }}</td> <td>{{ backup.description }}</td>
<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('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> <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;"> <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> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Czy na pewno usunąć backup?');">Usuń</button>
</form> </form>
@ -40,6 +46,22 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </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>
</div> </div>
{% endblock %} {% 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 %}

View File

@ -24,6 +24,8 @@
<th>Port</th> <th>Port</th>
<th>Typ</th> <th>Typ</th>
<th>Metoda uwierzytelniania</th> <th>Metoda uwierzytelniania</th>
<th>Auto Deploy</th>
<th>Auto Backup</th>
<th>Akcje</th> <th>Akcje</th>
</tr> </tr>
</thead> </thead>
@ -38,10 +40,24 @@
<td>{{ h.port }}</td> <td>{{ h.port }}</td>
<td>{{ h.type }}</td> <td>{{ h.type }}</td>
<td>{{ h.auth_method }}</td> <td>{{ h.auth_method }}</td>
<!-- Formularz aktualizujący automatyczny deploy dla serwera -->
<td>
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" style="display:inline;">
<input type="hidden" name="setting" value="auto_deploy">
<input type="checkbox" name="enabled" value="1" onchange="this.form.submit()" {% if h.auto_deploy_enabled %}checked{% endif %}>
</form>
</td>
<!-- Formularz aktualizujący automatyczny backup dla serwera -->
<td>
<form method="POST" action="{{ url_for('update_host_automation', id=h.id) }}" style="display:inline;">
<input type="hidden" name="setting" value="auto_backup">
<input type="checkbox" name="enabled" value="1" onchange="this.form.submit()" {% if h.auto_backup_enabled %}checked{% endif %}>
</form>
</td>
<td> <td>
<a href="{{ url_for('edit_server', id=h.id) }}" class="btn btn-primary btn-sm">Edytuj</a> <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('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> <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;"> <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> <button type="submit" class="btn btn-danger btn-sm">Usuń</button>
</form> </form>

View File

@ -21,17 +21,25 @@
<label class="form-check-label" for="auto_deploy">Automatyczny deploy</label> <label class="form-check-label" for="auto_deploy">Automatyczny deploy</label>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="deploy_interval" class="form-label">Interwał deploy (minuty)</label> <label for="deploy_cron" class="form-label">Harmonogram deploy (cron)</label>
<input type="number" class="form-control" id="deploy_interval" name="deploy_interval" value="{{ settings.deploy_interval }}"> <div class="input-group">
</div> <input type="text" class="form-control" id="deploy_cron" name="deploy_cron" value="{{ settings.deploy_cron }}">
<div class="mb-3"> <button type="button" class="btn btn-outline-secondary" onclick="openCronModal('deploy_cron')">Generuj cron</button>
<label for="backup_interval" class="form-label">Interwał backupów (minuty)</label> </div>
<input type="number" class="form-control" id="backup_interval" name="backup_interval" value="{{ settings.backup_interval }}"> <small class="text-muted">Np. <code>0 0 * * *</code> codziennie o północy</small>
</div> </div>
<div class="mb-3 form-check"> <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 %}> <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> <label class="form-check-label" for="auto_backup">Automatyczne kopie zapasowe</label>
</div> </div>
<div class="mb-3">
<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"> <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 %}> <input type="checkbox" class="form-check-input" id="enable_regex_entries" name="enable_regex_entries" {% if settings.regex_deploy_enabled %}checked{% endif %}>
<label class="form-check-label" for="enable_regex_entries">Włącz regex/CIDR deploy</label> <label class="form-check-label" for="enable_regex_entries">Włącz regex/CIDR deploy</label>
@ -44,4 +52,81 @@
</form> </form>
</div> </div>
</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 %} {% 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 %}