Compare commits

...

7 Commits

Author SHA1 Message Date
Jessica765
f794ce4242
Merge 3a9b8c9b22 into 78d6e5931c 2026-02-04 09:18:12 +11:00
Jake Kenneally
78d6e5931c
fix: Correct debugging_monitor.py script instructions (#676)
Some checks are pending
CI / build (push) Waiting to run
## Summary

**What is the goal of this PR?**
- Minor correction to the `debugging_monitor.py` script instructions

**What changes are included?**
- `pyserial` should be installed, NOT `serial`, which is a [different
lib](https://pypi.org/project/serial/)
- Added macOS serial port

## Additional Context

- Just a minor docs update. I can confirm the debugging script is
working great on macOS

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-02-04 00:33:20 +03:00
Luke Stein
dac11c3fdd
fix: Correct instruction text to match actual button text (#672)
## Summary

* Instruction text says "Press OK to scan again" but button label is
actually "Connect" (not OK)
* Corrects instruction text

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-04 00:32:52 +03:00
Jessica Harrison
3a9b8c9b22 fix: remove test script from .gitignore 2026-02-03 15:39:09 +02:00
Jessica Harrison
4f18ec280c fix: correct formatting in file table for folder rows 2026-02-03 15:39:09 +02:00
Jessica Harrison
254f2b4f79 fix: update .gitignore 2026-02-03 15:38:58 +02:00
Jessica Harrison
5f1c6144a1 feat: enhance file deletion functionality with multi-select 2026-02-03 15:14:11 +02:00
5 changed files with 195 additions and 110 deletions

5
.gitignore vendored
View File

@ -9,3 +9,8 @@ build
**/__pycache__/ **/__pycache__/
/compile_commands.json /compile_commands.json
/.cache /.cache
.history/
node_modules/
package.json
package-lock.json
mise.toml

View File

@ -102,13 +102,18 @@ After flashing the new features, its recommended to capture detailed logs fro
First, make sure all required Python packages are installed: First, make sure all required Python packages are installed:
```python ```python
python3 -m pip install serial colorama matplotlib python3 -m pip install pyserial colorama matplotlib
``` ```
after that run the script: after that run the script:
```sh ```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
``` ```
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS. Minor adjustments may be required for Windows.
## Internals ## Internals

View File

@ -520,7 +520,7 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found"); renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again"); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again");
} else { } else {
// Calculate how many networks we can display // Calculate how many networks we can display
constexpr int startY = 60; constexpr int startY = 60;

View File

@ -706,84 +706,108 @@ void CrossPointWebServer::handleCreateFolder() const {
} }
void CrossPointWebServer::handleDelete() const { void CrossPointWebServer::handleDelete() const {
// Get path from form data // Check if 'paths' argument is provided
if (!server->hasArg("path")) { if (!server->hasArg("paths")) {
server->send(400, "text/plain", "Missing path"); server->send(400, "text/plain", "Missing paths");
return; return;
} }
String itemPath = server->arg("path"); // Parse paths
const String itemType = server->hasArg("type") ? server->arg("type") : "file"; String pathsArg = server->arg("paths");
DynamicJsonDocument doc(2048);
// Validate path DeserializationError error = deserializeJson(doc, pathsArg);
if (itemPath.isEmpty() || itemPath == "/") { if (error) {
server->send(400, "text/plain", "Cannot delete root directory"); server->send(400, "text/plain", "Invalid paths format");
return; return;
} }
// Ensure path starts with / JsonArray paths = doc.as<JsonArray>();
if (!itemPath.startsWith("/")) { if (paths.isNull() || paths.size() == 0) {
itemPath = "/" + itemPath; server->send(400, "text/plain", "No paths provided");
}
// Security check: prevent deletion of protected items
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
// Check if item starts with a dot (hidden/system file)
if (itemName.startsWith(".")) {
Serial.printf("[%lu] [WEB] Delete rejected - hidden/system item: %s\n", millis(), itemPath.c_str());
server->send(403, "text/plain", "Cannot delete system files");
return; return;
} }
// Check against explicitly protected items // Iterate over paths and delete each item
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) { bool allSuccess = true;
if (itemName.equals(HIDDEN_ITEMS[i])) { String failedItems;
Serial.printf("[%lu] [WEB] Delete rejected - protected item: %s\n", millis(), itemPath.c_str());
server->send(403, "text/plain", "Cannot delete protected items"); for (const auto& p : paths) {
return; String itemPath = p.as<String>();
// Validate path
if (itemPath.isEmpty() || itemPath == "/") {
failedItems += itemPath + " (cannot delete root); ";
allSuccess = false;
continue;
} }
}
// Check if item exists // Ensure path starts with /
if (!SdMan.exists(itemPath.c_str())) { if (!itemPath.startsWith("/")) {
Serial.printf("[%lu] [WEB] Delete failed - item not found: %s\n", millis(), itemPath.c_str()); itemPath = "/" + itemPath;
server->send(404, "text/plain", "Item not found"); }
return;
}
Serial.printf("[%lu] [WEB] Attempting to delete %s: %s\n", millis(), itemType.c_str(), itemPath.c_str()); // Security check: prevent deletion of protected items
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
bool success = false; // Hidden/system files are protected
if (itemName.startsWith(".")) {
failedItems += itemPath + " (hidden/system file); ";
allSuccess = false;
continue;
}
if (itemType == "folder") { // Check against explicitly protected items
// For folders, try to remove (will fail if not empty) bool isProtected = false;
FsFile dir = SdMan.open(itemPath.c_str()); for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
if (dir && dir.isDirectory()) { if (itemName.equals(HIDDEN_ITEMS[i])) {
// Check if folder is empty isProtected = true;
FsFile entry = dir.openNextFile(); break;
if (entry) {
// Folder is not empty
entry.close();
dir.close();
Serial.printf("[%lu] [WEB] Delete failed - folder not empty: %s\n", millis(), itemPath.c_str());
server->send(400, "text/plain", "Folder is not empty. Delete contents first.");
return;
} }
dir.close();
} }
success = SdMan.rmdir(itemPath.c_str()); if (isProtected) {
} else { failedItems += itemPath + " (protected file); ";
// For files, use remove allSuccess = false;
success = SdMan.remove(itemPath.c_str()); continue;
}
// Check if item exists
if (!SdMan.exists(itemPath.c_str())) {
failedItems += itemPath + " (not found); ";
allSuccess = false;
continue;
}
// Decide whether it's a directory or file by opening it
bool success = false;
FsFile f = SdMan.open(itemPath.c_str());
if (f && f.isDirectory()) {
// For folders, ensure empty before removing
FsFile entry = f.openNextFile();
if (entry) {
entry.close();
f.close();
failedItems += itemPath + " (folder not empty); ";
allSuccess = false;
continue;
}
f.close();
success = SdMan.rmdir(itemPath.c_str());
} else {
// It's a file (or couldn't open as dir) — remove file
if (f) f.close();
success = SdMan.remove(itemPath.c_str());
}
if (!success) {
failedItems += itemPath + " (deletion failed); ";
allSuccess = false;
}
} }
if (success) { if (allSuccess) {
Serial.printf("[%lu] [WEB] Successfully deleted: %s\n", millis(), itemPath.c_str()); server->send(200, "text/plain", "All items deleted successfully");
server->send(200, "text/plain", "Deleted successfully");
} else { } else {
Serial.printf("[%lu] [WEB] Failed to delete: %s\n", millis(), itemPath.c_str()); server->send(500, "text/plain", "Failed to delete some items: " + failedItems);
server->send(500, "text/plain", "Failed to delete item");
} }
} }

View File

@ -586,6 +586,7 @@
<div class="action-buttons"> <div class="action-buttons">
<button class="action-btn upload-action-btn" onclick="openUploadModal()">📤 Upload</button> <button class="action-btn upload-action-btn" onclick="openUploadModal()">📤 Upload</button>
<button class="action-btn folder-action-btn" onclick="openFolderModal()">📁 New Folder</button> <button class="action-btn folder-action-btn" onclick="openFolderModal()">📁 New Folder</button>
<button class="action-btn" style="background-color:#e74c3c" onclick="openDeleteSelectedModal()">🗑️ Delete Selected</button>
</div> </div>
</div> </div>
@ -652,13 +653,11 @@
<div class="modal-overlay" id="deleteModal"> <div class="modal-overlay" id="deleteModal">
<div class="modal"> <div class="modal">
<button class="modal-close" onclick="closeDeleteModal()">&times;</button> <button class="modal-close" onclick="closeDeleteModal()">&times;</button>
<h3>🗑️ Delete Item</h3> <h3>🗑️ Delete Item(s)</h3>
<div class="folder-form"> <div class="folder-form">
<p class="delete-warning">⚠️ This action cannot be undone!</p> <p class="delete-warning">⚠️ This action cannot be undone!</p>
<p class="file-info">Are you sure you want to delete:</p> <p class="file-info">Are you sure you want to delete the following item(s)?</p>
<p class="delete-item-name" id="deleteItemName"></p> <div id="deleteItemList" style="max-height:240px; overflow:auto; margin-bottom:10px;"></div>
<input type="hidden" id="deleteItemPath">
<input type="hidden" id="deleteItemType">
<button class="delete-btn-confirm" onclick="confirmDelete()">Delete</button> <button class="delete-btn-confirm" onclick="confirmDelete()">Delete</button>
<button class="delete-btn-cancel" onclick="closeDeleteModal()">Cancel</button> <button class="delete-btn-cancel" onclick="closeDeleteModal()">Cancel</button>
</div> </div>
@ -739,7 +738,10 @@
fileTable.innerHTML = '<div class="no-files">This folder is empty</div>'; fileTable.innerHTML = '<div class="no-files">This folder is empty</div>';
} else { } else {
let fileTableContent = '<table class="file-table">'; let fileTableContent = '<table class="file-table">';
fileTableContent += '<tr><th>Name</th><th>Type</th><th>Size</th><th class="actions-col">Actions</th></tr>';
// Add select-all checkbox column
fileTableContent += '<tr><th style="width:40px"><input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll(this)"></th><th>Name</th><th>Type</th><th>Size</th><th class="actions-col">Actions</th></tr>';
const sortedFiles = files.sort((a, b) => { const sortedFiles = files.sort((a, b) => {
// Directories first, then epub files, then other files, alphabetically within each group // Directories first, then epub files, then other files, alphabetically within each group
@ -756,7 +758,9 @@
if (!folderPath.endsWith("/")) folderPath += "/"; if (!folderPath.endsWith("/")) folderPath += "/";
folderPath += file.name; folderPath += file.name;
fileTableContent += '<tr class="folder-row">'; // Checkbox cell + folder row
fileTableContent += `<tr class="folder-row">`;
fileTableContent += `<td><input type="checkbox" class="select-item" data-path="${encodeURIComponent(folderPath)}" data-name="${escapeHtml(file.name)}" data-type="folder"></td>`;
fileTableContent += `<td><span class="file-icon">📁</span><a href="/files?path=${encodeURIComponent(folderPath)}" class="folder-link">${escapeHtml(file.name)}</a><span class="folder-badge">FOLDER</span></td>`; fileTableContent += `<td><span class="file-icon">📁</span><a href="/files?path=${encodeURIComponent(folderPath)}" class="folder-link">${escapeHtml(file.name)}</a><span class="folder-badge">FOLDER</span></td>`;
fileTableContent += '<td>Folder</td>'; fileTableContent += '<td>Folder</td>';
fileTableContent += '<td>-</td>'; fileTableContent += '<td>-</td>';
@ -767,7 +771,9 @@
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";
filePath += file.name; filePath += file.name;
// Checkbox cell + file row
fileTableContent += `<tr class="${file.isEpub ? 'epub-file' : ''}">`; fileTableContent += `<tr class="${file.isEpub ? 'epub-file' : ''}">`;
fileTableContent += `<td><input type="checkbox" class="select-item" data-path="${encodeURIComponent(filePath)}" data-name="${escapeHtml(file.name)}" data-type="file"></td>`;
fileTableContent += `<td><span class="file-icon">${file.isEpub ? '📗' : '📄'}</span>${escapeHtml(file.name)}`; fileTableContent += `<td><span class="file-icon">${file.isEpub ? '📗' : '📄'}</span>${escapeHtml(file.name)}`;
if (file.isEpub) fileTableContent += '<span class="epub-badge">EPUB</span>'; if (file.isEpub) fileTableContent += '<span class="epub-badge">EPUB</span>';
fileTableContent += '</td>'; fileTableContent += '</td>';
@ -808,6 +814,92 @@
document.getElementById('folderModal').classList.remove('open'); document.getElementById('folderModal').classList.remove('open');
} }
// Toggle select-all checkbox
function toggleSelectAll(master) {
const checked = master.checked;
document.querySelectorAll('.select-item').forEach(cb => {
cb.checked = checked;
});
}
function getSelectedItems() {
const items = [];
document.querySelectorAll('.select-item:checked').forEach(cb => {
items.push({
name: cb.dataset.name || decodeURIComponent(cb.dataset.path).split('/').pop(),
path: decodeURIComponent(cb.dataset.path),
isFolder: cb.dataset.type === 'folder'
});
});
return items;
}
// Open delete modal for currently selected checkboxes
function openDeleteSelectedModal() {
const items = getSelectedItems();
if (items.length === 0) {
alert('Please select at least one item to delete.');
return;
}
openDeleteModalForItems(items);
}
// Open delete modal for a single item (keeps backwards compatibility with per-row delete button)
function openDeleteModal(name, path, isFolder) {
openDeleteModalForItems([{ name: name, path: path, isFolder: !!isFolder }]);
}
let deleteItemsGlobal = [];
function openDeleteModalForItems(items) {
deleteItemsGlobal = items;
const listEl = document.getElementById('deleteItemList');
listEl.innerHTML = '';
items.forEach(it => {
const div = document.createElement('div');
div.style.marginBottom = '6px';
div.textContent = (it.isFolder ? '📁 ' : '📄 ') + it.path;
listEl.appendChild(div);
});
document.getElementById('deleteModal').classList.add('open');
}
function closeDeleteModal() {
document.getElementById('deleteModal').classList.remove('open');
}
function confirmDelete() {
if (!deleteItemsGlobal || deleteItemsGlobal.length === 0) {
closeDeleteModal();
return;
}
const paths = deleteItemsGlobal.map(it => {
// Ensure path starts with /
let p = it.path;
if (!p.startsWith('/')) p = '/' + p;
return p;
});
const body = 'paths=' + encodeURIComponent(JSON.stringify(paths));
fetch('/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
}).then(async res => {
if (res.ok) {
window.location.reload();
} else {
const text = await res.text();
alert('Failed to delete: ' + text);
closeDeleteModal();
}
}).catch(() => {
alert('Failed to delete - network error');
closeDeleteModal();
});
}
function validateFile() { function validateFile() {
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn'); const uploadBtn = document.getElementById('uploadBtn');
@ -1174,47 +1266,6 @@ function retryAllFailedUploads() {
xhr.send(formData); xhr.send(formData);
} }
// Delete functions
function openDeleteModal(name, path, isFolder) {
document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name;
document.getElementById('deleteItemPath').value = path;
document.getElementById('deleteItemType').value = isFolder ? 'folder' : 'file';
document.getElementById('deleteModal').classList.add('open');
}
function closeDeleteModal() {
document.getElementById('deleteModal').classList.remove('open');
}
function confirmDelete() {
const path = document.getElementById('deleteItemPath').value;
const itemType = document.getElementById('deleteItemType').value;
const formData = new FormData();
formData.append('path', path);
formData.append('type', itemType);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/delete', true);
xhr.onload = function() {
if (xhr.status === 200) {
window.location.reload();
} else {
alert('Failed to delete: ' + xhr.responseText);
closeDeleteModal();
}
};
xhr.onerror = function() {
alert('Failed to delete - network error');
closeDeleteModal();
};
xhr.send(formData);
}
hydrate(); hydrate();
</script> </script>
</body> </body>