development into master #1

Merged
lelo merged 9 commits from development into master 2025-05-05 17:22:39 +00:00
3 changed files with 274 additions and 159 deletions
Showing only changes of commit 9cbb58417e - Show all commits

View File

@ -43,20 +43,18 @@
<h1>{{ title_long }}</h1> <h1>{{ title_long }}</h1>
</header> </header>
<div class="wrapper"> <div class="wrapper">
{% if admin_enabled %}
<div style="color: #ccc; border: 1px solid #ccc; padding: 10px; background-color: #979797;">
<a href="{{ url_for('mylinks') }}" class="btn" style="color: #ccc; text-decoration: none;">Meine Links</a>
<span> | </span>
<a href="{{ url_for('connections') }}" class="btn" style="color: #ccc; text-decoration: none;">Verbindungen</a>
<span> | </span>
<a href="{{ url_for('dashboard') }}" class="btn" style="color: #ccc; text-decoration: none;">Auswertung</a>
<span> | </span>
<a href="{{ url_for('folder_secret_config_editor') }}" class="btn" style="color: #ccc; text-decoration: none;" id="edit-folder-config" >Ordnerkonfiguration</a>
</div>
{% endif %}
<div class="container"> <div class="container">
{% if admin_enabled %}
<div style="color: #ccc; margin: 20px; border: 1px solid #ccc; padding: 10px; background-color: #979797;">
<a href="{{ url_for('index') }}" style="color: #ccc; text-decoration: none;">App</a>
<span> | </span>
<a href="{{ url_for('mylinks') }}" style="color: #ccc; text-decoration: none;">Meine Links</a>
<span> | </span>
<a href="{{ url_for('connections') }}" style="color: #ccc; text-decoration: none;">Verbindungen</a>
<span> | </span>
<a href="{{ url_for('dashboard') }}" style="color: #ccc; text-decoration: none;">Auswertung</a>
<span> | </span>
<a href="{{ url_for('folder_secret_config_editor') }}" style="color: #ccc; text-decoration: none;" id="edit-folder-config" >Ordnerkonfiguration</a>
</div>
{% endif %}
<div id="breadcrumbs" class="breadcrumb"></div> <div id="breadcrumbs" class="breadcrumb"></div>
<div id="content"></div> <div id="content"></div>
<div id="directory-controls"> <div id="directory-controls">

View File

@ -37,23 +37,17 @@
<body> <body>
<!-- Navigation Bar --> <!-- Navigation Bar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-secondary mb-3"> <div style="background-color: #979797; padding: 5px; display: flex; gap: 5px; flex-wrap: wrap;">
<div class="container-fluid"> <a href="{{ url_for('index') }}" class="btn btn-secondary btn-sm">App</a>
<div class="navbar-collapse" id="navbarNav"> <a href="{{ url_for('mylinks') }}" class="btn btn-secondary btn-sm">Meine Links</a>
<ul class="navbar-nav ms-auto"> <a href="{{ url_for('connections') }}" class="btn btn-secondary btn-sm">Verbindungen</a>
<li class="nav-item"><a href="{{ url_for('index') }}" class="nav-link">App</a></li> <a href="{{ url_for('dashboard') }}" class="btn btn-secondary btn-sm">Auswertung-Downloads</a>
<li class="nav-item"><a href="{{ url_for('mylinks') }}" class="nav-link">Meine Links</a></li> <a href="{{ url_for('songs_dashboard') }}" class="btn btn-secondary btn-sm">Auswertung-Wiederholungen</a>
<li class="nav-item"><a href="{{ url_for('connections') }}" class="nav-link">Verbindungen</a></li> {% if admin_enabled %}
<li class="nav-item"><a href="{{ url_for('dashboard') }}" class="nav-link">Auswertung-Downloads</a></li> <a href="{{ url_for('folder_secret_config_editor') }}" class="btn btn-secondary btn-sm" id="edit-folder-config">Ordnerkonfiguration</a>
<li class="nav-item"><a href="{{ url_for('songs_dashboard') }}" class="nav-link">Auswertung-Wiederholungen</a></li> {% endif %}
{% if admin_enabled %} {% block nav_extra %}{% endblock %}
<li class="nav-item"><a href="{{ url_for('folder_secret_config_editor') }}" class="nav-link">Ordnerkonfiguration</a></li> </div>
{% endif %}
</ul>
</div>
{% block nav_extra %}{% endblock %}
</div>
</nav>
<div class="container"> <div class="container">
<h2>{% block nav_brand %}Übersicht deiner gültigen Links{% endblock %}</h2> <h2>{% block nav_brand %}Übersicht deiner gültigen Links{% endblock %}</h2>
</div> </div>

View File

@ -35,160 +35,283 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script> <script>
const ALPHABET = {{ alphabet|tojson }}; const ALPHABET = {{ alphabet|tojson }};
let data = []; let data = [];
let editing = new Set(); let editing = new Set();
let pendingDelete = null; // store secret pending confirmation let pendingDelete = null;
function loadData() { // helper to format DD.MM.YYYY → YYYY-MM-DD
$.getJSON('/admin/folder_secret_config_editor/data', res => { data = res; render(); }); function formatISO(d) {
const [dd, mm, yyyy] = d.split('.');
return (dd && mm && yyyy) ? `${yyyy}-${mm}-${dd}` : '';
} }
// load from server
async function loadData() {
try {
const res = await fetch('/admin/folder_secret_config_editor/data');
data = await res.json();
render();
} catch (err) {
console.error(err);
}
}
// send delete/update actions
async function sendAction(payload) {
await fetch('/admin/folder_secret_config_editor/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
await loadData();
}
// delete record after confirmation
function deleteRec(secret) {
sendAction({ action: 'delete', secret });
}
// build all cards
function render() { function render() {
const $cont = $('#records').empty(); const cont = document.getElementById('records');
cont.innerHTML = '';
data.forEach(rec => { data.forEach(rec => {
const key = rec.secret; const key = rec.secret;
const isEdit = editing.has(key); const isEdit = editing.has(key);
const expired = (formatISO(rec.validity) && new Date(formatISO(rec.validity)) < new Date());
const cls = isEdit ? 'unlocked' : 'locked'; const cls = isEdit ? 'unlocked' : 'locked';
// Determine if entry has expired // outer card
const validityISO = formatISO(rec.validity); const wrapper = document.createElement('div');
const expired = validityISO ? (new Date(validityISO) < new Date()) : false; wrapper.className = 'card mb-3';
wrapper.dataset.secret = key;
// Build card HTML const body = document.createElement('div');
let html = `<div class="card mb-3" data-secret="${key}"><div class="card-body ${cls}">`; body.className = `card-body ${cls}`;
// Show exclamation if expired
html += `<h5>Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}</h5>`; // header
html += `<div class="mb-2">Secret: <input class="form-control" type="text" value="${rec.secret}" ${isEdit?'':'readonly'} data-field="secret"></div>`; const h5 = document.createElement('h5');
html += `<div class="mb-2">Validity: <input class="form-control" type="date" value="${validityISO}" ${isEdit?'':'readonly'} data-field="validity"></div>`; h5.innerHTML = `Link${expired ? ' <span class="text-danger fw-bold"> ! abgelaufen !</span>' : ''}`;
html += `<h6>Ordner</h6>`; body.appendChild(h5);
rec.folders.forEach((f,i) => {
html += `<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; border-radius: 5px;">`; // secret input
html += `Ordnername: <div class="input-group mb-2">`; const secDiv = document.createElement('div');
html += `<input class="form-control" type="text" value="${f.foldername}" ${isEdit?'':'readonly'} data-field="foldername-${i}">`; secDiv.className = 'mb-2';
if(isEdit) html += `<button class="btn btn-outline-danger" onclick="removeFolder('${key}',${i})">Remove</button>`; secDiv.innerHTML = `Secret: `;
html += `</div>`; const secInput = document.createElement('input');
html += `Ordnerpfad: <input class="form-control mb-2" type="text" value="${f.folderpath}" ${isEdit?'':'readonly'} data-field="folderpath-${i}">`; secInput.className = 'form-control';
html += `</div>`; secInput.type = 'text';
secInput.value = rec.secret;
secInput.readOnly = !isEdit;
secInput.dataset.field = 'secret';
secDiv.appendChild(secInput);
body.appendChild(secDiv);
// validity input
const valDiv = document.createElement('div');
valDiv.className = 'mb-2';
valDiv.innerHTML = `Gültig bis: `;
const valInput = document.createElement('input');
valInput.className = 'form-control';
valInput.type = 'date';
valInput.value = formatISO(rec.validity);
valInput.readOnly = !isEdit;
valInput.dataset.field = 'validity';
valDiv.appendChild(valInput);
body.appendChild(valDiv);
// folders
const folderHeader = document.createElement('h6');
folderHeader.textContent = 'Ordner';
body.appendChild(folderHeader);
rec.folders.forEach((f, i) => {
const fwrap = document.createElement('div');
fwrap.style = 'border:1px solid #ccc; padding:10px; margin-bottom:10px; border-radius:5px;';
// name
fwrap.appendChild(document.createTextNode("Ordnername: "));
const nameGroup = document.createElement('div');
nameGroup.className = 'input-group mb-2';
const nameInput = document.createElement('input');
nameInput.className = 'form-control';
nameInput.type = 'text';
nameInput.value = f.foldername;
nameInput.readOnly = !isEdit;
nameInput.dataset.field = `foldername-${i}`;
nameGroup.appendChild(nameInput);
if (isEdit) {
const remBtn = document.createElement('button');
remBtn.className = 'btn btn-outline-danger';
remBtn.type = 'button';
remBtn.textContent = 'Remove';
remBtn.addEventListener('click', () => removeFolder(key, i));
nameGroup.appendChild(remBtn);
}
fwrap.appendChild(nameGroup);
// path
fwrap.appendChild(document.createTextNode("Ordnerpfad: "));
const pathInput = document.createElement('input');
pathInput.className = 'form-control mb-2';
pathInput.type = 'text';
pathInput.value = f.folderpath;
pathInput.readOnly = !isEdit;
pathInput.dataset.field = `folderpath-${i}`;
fwrap.appendChild(pathInput);
body.appendChild(fwrap);
}); });
if(isEdit) html += `<button class="btn btn-sm btn-primary mb-2" onclick="addFolder('${key}')">Add Folder</button>`;
html += `<div>`; if (isEdit) {
if (!isEdit) html += `<a class="btn btn-secondary btn-sm me-2" href="/?secret=${rec.secret}">Link öffnen</a>`; const addFld = document.createElement('button');
html += `<button class="btn btn-danger btn-sm me-2 delete-btn" data-secret="${key}">Delete</button>`; addFld.className = 'btn btn-sm btn-primary mb-2';
html += `<button class="btn btn-secondary btn-sm me-2" onclick="cloneRec('${key}')">Clone</button>`; addFld.type = 'button';
if(isEdit) html += `<button class="btn btn-success btn-sm" onclick="saveRec('${key}')">Save</button>`; addFld.textContent = 'Add Folder';
else html += `<button class="btn btn-warning btn-sm" onclick="editRec('${key}')">Edit</button>`; addFld.addEventListener('click', () => addFolder(key));
html += `</div></div></div>`; body.appendChild(addFld);
$cont.append(html); }
// actions row
const actions = document.createElement('div');
if (!isEdit) {
const openLink = document.createElement('a');
openLink.className = 'btn btn-secondary btn-sm me-2';
openLink.href = `/?secret=${rec.secret}`;
openLink.textContent = 'Link öffnen';
actions.appendChild(openLink);
}
const delBtn = document.createElement('button');
delBtn.className = 'btn btn-danger btn-sm me-2 delete-btn';
delBtn.type = 'button';
delBtn.textContent = 'Delete';
delBtn.dataset.secret = key;
actions.appendChild(delBtn);
const cloneBtn = document.createElement('button');
cloneBtn.className = 'btn btn-secondary btn-sm me-2';
cloneBtn.type = 'button';
cloneBtn.textContent = 'Clone';
cloneBtn.addEventListener('click', () => cloneRec(key));
actions.appendChild(cloneBtn);
if (isEdit) {
const saveBtn = document.createElement('button');
saveBtn.className = 'btn btn-success btn-sm';
saveBtn.type = 'button';
saveBtn.textContent = 'Save';
saveBtn.addEventListener('click', () => saveRec(key));
actions.appendChild(saveBtn);
} else {
const editBtn = document.createElement('button');
editBtn.className = 'btn btn-warning btn-sm';
editBtn.type = 'button';
editBtn.textContent = 'Edit';
editBtn.addEventListener('click', () => editRec(key));
actions.appendChild(editBtn);
}
body.appendChild(actions);
wrapper.appendChild(body);
cont.appendChild(wrapper);
}); });
} }
function formatISO(d) { // generate a unique secret
const p = d.split('.');
return p.length===3 ? `${p[2]}-${p[1]}-${p[0]}` : '';
}
// original delete function, called after confirmation
function deleteRec(secret) {
$.ajax({
url: '/admin/folder_secret_config_editor/action',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ action: 'delete', secret })
}).always(loadData);
}
// hook delete buttons to show modal
$(document).on('click', '.delete-btn', function() {
pendingDelete = $(this).data('secret');
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
modal.show();
});
// when user confirms deletion
$('.confirm-delete').on('click', function() {
if (pendingDelete) {
deleteRec(pendingDelete);
pendingDelete = null;
}
const modalEl = document.getElementById('confirmDeleteModal');
const modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
});
function editRec(secret) { editing.add(secret); render(); }
function cloneRec(secret) {
const rec = data.find(r=>r.secret===secret);
const existing = data.map(r=>r.secret);
const clone = JSON.parse(JSON.stringify(rec));
clone.secret = generateSecret(existing);
data.splice(data.findIndex(r=>r.secret===secret)+1, 0, clone);
editing.add(clone.secret);
render();
}
function addFolder(secret) {
const rec = data.find(r=>r.secret===secret);
rec.folders.push({foldername:'',folderpath:''}); render();
}
function removeFolder(secret,i) {
const rec = data.find(r=>r.secret===secret);
rec.folders.splice(i,1); render();
}
function saveRec(secret) {
const rec = data.find(r=>r.secret===secret);
const $card = $(`[data-secret='${secret}']`);
const newSecret = $card.find('input[data-field=secret]').val();
const validity = $card.find('input[data-field=validity]').val();
const folders = rec.folders.map((_,i) => ({
foldername: $card.find(`[data-field=foldername-${i}]`).val(),
folderpath: $card.find(`[data-field=folderpath-${i}]`).val()
}));
if(!newSecret||data.some(r=>r.secret!==secret&&r.secret===newSecret)){
alert('Secret must be unique and non-empty');
return;
}
if(!validity){
alert('Validity required');
return;
}
if(folders.some(f=>!f.foldername||!f.folderpath)){
alert('Folder entries cannot be empty');
return;
}
$.ajax({
url:'/admin/folder_secret_config_editor/action',
method:'POST',
contentType:'application/json',
data: JSON.stringify({
action:'update',
oldSecret:secret,
newSecret,
validity,
folders
})
}).always(()=>{
editing.delete(secret);
loadData();
});
}
function generateSecret(existing) { function generateSecret(existing) {
let s; let s;
do { do {
s = Array.from({length:32},_=>ALPHABET[Math.floor(Math.random()*ALPHABET.length)]).join(''); s = Array.from({length:32}, () => ALPHABET[Math.floor(Math.random()*ALPHABET.length)]).join('');
} while(existing.includes(s)); } while (existing.includes(s));
return s; return s;
} }
$(document).ready(loadData); // CRUD helpers
$('#add-btn').click(() => { function editRec(secret) {
const existing = data.map(r=>r.secret); editing.add(secret);
const newSecret = generateSecret(existing);
data.push({secret:newSecret, validity:new Date().toISOString().substr(0,10), folders:[]});
editing.add(newSecret);
render(); render();
}
function cloneRec(secret) {
const idx = data.findIndex(r => r.secret === secret);
const rec = JSON.parse(JSON.stringify(data[idx]));
const existing = data.map(r => r.secret);
rec.secret = generateSecret(existing);
data.splice(idx+1, 0, rec);
editing.add(rec.secret);
render();
}
function addFolder(secret) {
data.find(r => r.secret === secret).folders.push({foldername:'', folderpath:''});
render();
}
function removeFolder(secret, i) {
data.find(r => r.secret === secret).folders.splice(i,1);
render();
}
async function saveRec(secret) {
const card = document.querySelector(`[data-secret="${secret}"]`);
const newSecret = card.querySelector('input[data-field="secret"]').value.trim();
const validity = card.querySelector('input[data-field="validity"]').value;
const rec = data.find(r => r.secret === secret);
const folders = rec.folders.map((_, i) => ({
foldername: card.querySelector(`input[data-field="foldername-${i}"]`).value.trim(),
folderpath: card.querySelector(`input[data-field="folderpath-${i}"]`).value.trim()
}));
if (!newSecret || data.some(r => r.secret !== secret && r.secret === newSecret)) {
return alert('Secret must be unique and non-empty');
}
if (!validity) {
return alert('Validity required');
}
if (folders.some(f => !f.foldername || !f.folderpath)) {
return alert('Folder entries cannot be empty');
}
await sendAction({
action: 'update',
oldSecret: secret,
newSecret,
validity,
folders
});
editing.delete(secret);
await loadData();
}
// modal handling for delete
document.addEventListener('click', e => {
if (e.target.matches('.delete-btn')) {
pendingDelete = e.target.dataset.secret;
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
modal.show();
}
});
document.querySelector('.confirm-delete').addEventListener('click', () => {
if (pendingDelete) deleteRec(pendingDelete);
pendingDelete = null;
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal'));
modal.hide();
});
// init
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('add-btn').addEventListener('click', () => {
const existing = data.map(r => r.secret);
const newSecret = generateSecret(existing);
data.push({ secret: newSecret, validity: new Date().toISOString().slice(0,10), folders: [] });
editing.add(newSecret);
render();
});
loadData();
}); });
</script> </script>
{% endblock %} {% endblock %}