- Fix symlinks % bug

- A cleaner settings page
- More bug fixes
This commit is contained in:
Mukhtar Akere
2025-04-25 12:36:12 +01:00
parent 267430e6fb
commit 07f1d0f28d
22 changed files with 1351 additions and 1140 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -112,7 +112,7 @@
formData.append('notSymlink', document.getElementById('isSymlink').checked);
formData.append('downloadUncached', document.getElementById('downloadUncached').checked);
const response = await fetch('/internal/add', {
const response = await fetcher('/api/add', {
method: 'POST',
body: formData
});

View File

@@ -190,7 +190,7 @@
async function loadTorrents() {
try {
const response = await fetch('/internal/torrents');
const response = await fetcher('/api/torrents');
const torrents = await response.json();
state.torrents = torrents;
@@ -256,7 +256,7 @@
if (!confirm('Are you sure you want to delete this torrent?')) return;
try {
await fetch(`/internal/torrents/${category}/${hash}?removeFromDebrid=${removeFromDebrid}`, {
await fetcher(`/api/torrents/${category}/${hash}?removeFromDebrid=${removeFromDebrid}`, {
method: 'DELETE'
});
await loadTorrents();
@@ -273,7 +273,7 @@
try {
// COmma separated list of hashes
const hashes = Array.from(state.selectedTorrents).join(',');
await fetch(`/internal/torrents/?hashes=${encodeURIComponent(hashes)}`, {
await fetcher(`/api/torrents/?hashes=${encodeURIComponent(hashes)}`, {
method: 'DELETE'
});
await loadTorrents();

View File

@@ -197,8 +197,8 @@
{{ template "config" . }}
{{ else if eq .Page "login" }}
{{ template "login" . }}
{{ else if eq .Page "auth" }}
{{ template "auth" . }}
{{ else if eq .Page "register" }}
{{ template "register" . }}
{{ else }}
{{ end }}
@@ -206,6 +206,30 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
window.urlBase = "{{.URLBase}}";
function joinUrl (base, path) {
if (path.substring(0, 1) === "/") {
// path starts with `/`. Thus it is absolute.
return path;
}
if (base.substring(base.length-1) === "/") {
// base ends with `/`
return base + path;
}
return base + "/" + path;
}
function fetcher(endpoint, options = {}) {
// Use the global urlBase or default to empty string
let baseUrl = window.urlBase || '';
let url = joinUrl(baseUrl, endpoint);
// Return the regular fetcher with the complete URL
return fetch(url, options);
}
/**
* Create a toast message
* @param {string} message - The message to display
@@ -243,6 +267,7 @@
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: toastTimeouts[type]
});
toast.show();
@@ -302,7 +327,7 @@
}
document.addEventListener('DOMContentLoaded', function() {
fetch('/internal/version')
fetcher('/version')
.then(response => response.json())
.then(data => {
const versionBadge = document.getElementById('version-badge');

View File

@@ -36,7 +36,7 @@
};
try {
const response = await fetch('/login', {
const response = await fetcher('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -55,77 +55,4 @@
}
});
</script>
{{ end }}
{{ define "auth" }}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="mb-0 text-center">First Time Setup</h4>
</div>
<div class="card-body">
<form id="authForm">
<div class="mb-3">
<label for="username" class="form-label">Choose Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Choose Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Set Credentials</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('authForm').addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (password !== confirmPassword) {
createToast('Passwords do not match', 'error');
return;
}
const formData = {
username: document.getElementById('username').value,
password: password,
confirmPassword: confirmPassword
};
try {
const response = await fetch('/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (response.ok) {
window.location.href = '/';
} else {
const error = await response.text();
createToast(error, 'error');
}
} catch (error) {
console.error('Setup error:', error);
createToast('Setup failed', 'error');
}
});
</script>
{{ end }}

View File

@@ -0,0 +1,92 @@
{{ define "register" }}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="mb-0 text-center">First Time Auth Setup</h4>
</div>
<div class="card-body">
<form id="authForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary mb-2">Save</button>
<button type="button" id="skipAuthBtn" class="btn btn-secondary">Skip</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const authForm = document.getElementById('authForm');
const skipAuthBtn = document.getElementById('skipAuthBtn');
authForm.addEventListener('submit', async function (e) {
e.preventDefault();
// Validate passwords match
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (password !== confirmPassword) {
alert('Passwords do not match!');
return;
}
// Collect form data
let formData = new FormData();
formData.append('username', document.getElementById('username').value);
formData.append('password', password);
formData.append('confirmPassword', confirmPassword);
await fetcher('/register', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.text().then(errorText => {
// Throw an error with the response text
createToast(errorText || 'Registration failed', 'error');
});
} else {
window.location.href = joinUrl(window.urlBase, '/');
}
})
.catch(error => {
alert('Registration failed: ' + error.message);
});
});
// Handle skip auth button
skipAuthBtn.addEventListener('click', function() {
fetcher('/skip-auth', { method: 'GET' })
.then(response => {
if (response.ok) {
window.location.href = joinUrl(window.urlBase, '/');
} else {
throw new Error('Failed to skip authentication');
}
})
.catch(error => {
alert('Error: ' + error.message);
});
});
});
</script>
{{ end }}

View File

@@ -152,7 +152,7 @@
<script>
document.addEventListener('DOMContentLoaded', () => {
// Load Arr instances
fetch('/internal/arrs')
fetcher('/api/arrs')
.then(response => response.json())
.then(arrs => {
const select = document.getElementById('arrSelect');
@@ -175,7 +175,7 @@
let mediaIds = document.getElementById('mediaIds').value.split(',').map(id => id.trim());
let arr = document.getElementById('arrSelect').value;
try {
const response = await fetch('/internal/repair', {
const response = await fetcher('/api/repair', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -207,8 +207,8 @@
// Load jobs function
async function loadJobs(page) {
try {
const response = await fetch('/internal/repair/jobs');
if (!response.ok) throw new Error('Failed to fetch jobs');
const response = await fetcher('/api/repair/jobs');
if (!response.ok) throw new Error('Failed to fetcher jobs');
allJobs = await response.json();
renderJobsTable(page);
@@ -403,7 +403,7 @@
async function deleteJob(jobId) {
if (confirm('Are you sure you want to delete this job?')) {
try {
const response = await fetch(`/internal/repair/jobs`, {
const response = await fetcher(`/api/repair/jobs`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
@@ -422,7 +422,7 @@
async function deleteMultipleJobs(jobIds) {
try {
const response = await fetch(`/internal/repair/jobs`, {
const response = await fetcher(`/api/repair/jobs`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
@@ -441,7 +441,7 @@
// Process job function
async function processJob(jobId) {
try {
const response = await fetch(`/internal/repair/jobs/${jobId}/process`, {
const response = await fetcher(`/api/repair/jobs/${jobId}/process`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -1,32 +0,0 @@
{{ define "auth" }}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="mb-0 text-center">First Time Setup</h4>
</div>
<div class="card-body">
<form id="authForm" method="POST" action="/auth">
<div class="mb-3">
<label for="username" class="form-label">Choose Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Choose Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Set Credentials</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{{ end }}