Browse Source

Add DB-backed device file browser page

master
Don Kingdon 3 weeks ago
parent
commit
5e4e703097
  1. 16
      PROJECT_STATE.md
  2. 6
      README.md
  3. 2
      VERSION
  4. 54
      app/main/routes.py
  5. 7
      app/templates/cloud/dashboard.html
  6. 101
      app/templates/cloud/device_files.html

16
PROJECT_STATE.md

@ -4,7 +4,7 @@
OTB Cloud
## Current version
v0.2.0
v0.2.1
## Build date
2026-04-12
@ -50,14 +50,15 @@ Portal-authenticated secure backup and storage platform for customer files, incl
- Add Device flow
- Remove Device flow for empty devices
- Browser upload flow to device originals
- Device file browser page
## Immediate next tasks
1. Build first file library page
2. Add uploaded file listing per device
3. Add upload audit log UI or admin reference
1. Add single-file download
2. Add searchable file listing
3. Add rename basename-only flow
4. Add zip export flow
5. Add searchable file listing
6. Add media processing jobs
5. Add media processing jobs
6. Add derived/original filtering
## Notes
Original uploaded files should remain preserved and effectively read-only.
@ -65,4 +66,5 @@ Any user-facing edits or processing outputs should create derivative files.
Admin access should require owner-issued one-time support authorization.
New tenants no longer receive default devices automatically; devices are now user-created.
Devices can only be removed when no files are associated with them.
Browser uploads now write original files into device-specific originals directories and create DB records.
Browser uploads write original files into device-specific originals directories and create DB records.
The device browser is DB-backed and tenant-scoped.

6
README.md

@ -1,5 +1,11 @@
# OTB Cloud
## v0.2.1 - 2026-04-12
- Added device file browser page
- Added Browse Files action per device
- File browser lists DB-backed files by device and tenant
- Added file count and device summary on browser page
## v0.2.0 - 2026-04-12
- Added first browser upload flow for user-created devices
- Added Upload Files action per device

2
VERSION

@ -1 +1 @@
v0.2.0
v0.2.1

54
app/main/routes.py

@ -329,3 +329,57 @@ def upload_files(device_id: int):
flash(f"Uploaded {uploaded_count} file(s) to {device['device_name']}.", "success")
return redirect(url_for("main.dashboard"))
@bp.route("/devices/<int:device_id>/files", methods=["GET"])
@portal_session_required
def browse_device_files(device_id: int):
db = get_db()
with db.cursor() as cur:
cur.execute(
"""
SELECT id, device_name, device_type, relative_path
FROM devices
WHERE id = %s AND tenant_id = %s
""",
(device_id, session["otb_tenant_id"]),
)
device = cur.fetchone()
if not device:
flash("Device not found.", "warning")
return redirect(url_for("main.dashboard"))
cur.execute(
"""
SELECT
id,
file_kind,
relative_path,
directory_path,
original_filename,
basename,
extension,
mime_type,
size_bytes,
sha256,
uploaded_at,
is_immutable
FROM files
WHERE tenant_id = %s
AND device_id = %s
AND is_deleted = 0
ORDER BY uploaded_at DESC, id DESC
""",
(session["otb_tenant_id"], device_id),
)
files = cur.fetchall()
return render_template(
"cloud/device_files.html",
user_email=session.get("otb_email"),
tenant_slug=session.get("otb_tenant_slug"),
device=device,
files=files,
file_count=len(files),
)

7
app/templates/cloud/dashboard.html

@ -62,6 +62,7 @@
<span style="opacity:0.75;">{{ device.relative_path }}</span><br>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:10px;">
<a class="portal-btn primary" href="{{ url_for('main.upload_files', device_id=device.id) }}">Upload Files</a>
<a class="portal-btn" href="{{ url_for('main.browse_device_files', device_id=device.id) }}">Browse Files</a>
<form method="post" action="{{ url_for('main.delete_device', device_id=device.id) }}">
<button class="portal-btn" type="submit" onclick="return confirm('Remove device {{ device.device_name|e }}? This only works if no files are linked to it.');">Remove Device</button>
</form>
@ -76,7 +77,7 @@
<div class="service-card-header">
<div>
<h2>Current scope</h2>
<p>OTB Cloud now supports browser uploads to device originals.</p>
<p>OTB Cloud now supports browser uploads and device file browsing.</p>
</div>
<div>
<span class="service-badge service-badge-beta">Live</span>
@ -85,7 +86,7 @@
<div class="service-card-actions">
<p style="margin:0;">
Next steps are uploaded file listing, searchable library pages, zip export, and media processing jobs.
Next steps are single-file download, searchable library pages, zip export, and media processing jobs.
</p>
</div>
</article>
@ -121,7 +122,7 @@
<div class="service-card-actions">
<p style="margin:0;">
After adding a device, you can upload one or more files into that device’s originals storage.
After adding a device, you can upload one or more files into that device’s originals storage and browse them here.
</p>
</div>
</article>

101
app/templates/cloud/device_files.html

@ -0,0 +1,101 @@
{% extends "portal_base.html" %}
{% block title %}Device Files - OTB Cloud{% endblock %}
{% block portal_content %}
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Device Files</h1>
<p class="portal-client-name">{{ user_email }}</p>
<p class="portal-page-subtitle">
Browsing files for <strong>{{ device.device_name }}</strong> ({{ device.device_type }}).
</p>
</div>
<div class="portal-toolbar" style="display:flex;flex-direction:column;align-items:flex-end;gap:10px;">
<div style="display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap;">
<a class="portal-btn primary" href="{{ url_for('main.upload_files', device_id=device.id) }}">Upload Files</a>
<a class="portal-btn" href="{{ url_for('main.dashboard') }}">Back to Dashboard</a>
</div>
<div style="text-align:right;font-size:14px;opacity:0.95;">
<div>File count: <strong>{{ file_count }}</strong></div>
<div>Device path: <strong>{{ device.relative_path }}</strong></div>
</div>
</div>
</div>
{% if files %}
<section class="services-grid" style="grid-template-columns: 1fr;">
<article class="service-card status-beta">
<div class="service-card-header">
<div>
<h2>Files</h2>
<p>Files recorded in the database for this device.</p>
</div>
<div>
<span class="service-badge service-badge-beta">DB-backed</span>
</div>
</div>
<div class="service-card-actions" style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr>
<th style="text-align:left;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.12);">Name</th>
<th style="text-align:left;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.12);">Kind</th>
<th style="text-align:left;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.12);">Size</th>
<th style="text-align:left;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.12);">Uploaded</th>
<th style="text-align:left;padding:10px 8px;border-bottom:1px solid rgba(255,255,255,0.12);">Path</th>
</tr>
</thead>
<tbody>
{% for file in files %}
<tr>
<td style="padding:12px 8px;border-bottom:1px solid rgba(255,255,255,0.08);vertical-align:top;">
<strong>{{ file.original_filename }}</strong><br>
<span style="opacity:0.75;font-size:0.9rem;">
SHA256: {{ file.sha256 }}
</span>
</td>
<td style="padding:12px 8px;border-bottom:1px solid rgba(255,255,255,0.08);vertical-align:top;">
{{ file.file_kind }}
{% if file.is_immutable %}
<br><span style="opacity:0.75;font-size:0.9rem;">immutable</span>
{% endif %}
</td>
<td style="padding:12px 8px;border-bottom:1px solid rgba(255,255,255,0.08);vertical-align:top;">
{{ "{:,}".format(file.size_bytes or 0) }} bytes
</td>
<td style="padding:12px 8px;border-bottom:1px solid rgba(255,255,255,0.08);vertical-align:top;">
{{ file.uploaded_at }}
</td>
<td style="padding:12px 8px;border-bottom:1px solid rgba(255,255,255,0.08);vertical-align:top;word-break:break-word;">
{{ file.relative_path }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
</section>
{% else %}
<section class="services-grid">
<article class="service-card status-beta">
<div class="service-card-header">
<div>
<h2>No files yet</h2>
<p>This device does not have any uploaded files recorded yet.</p>
</div>
<div>
<span class="service-badge service-badge-beta">Empty</span>
</div>
</div>
<div class="service-card-actions">
<a class="portal-btn primary" href="{{ url_for('main.upload_files', device_id=device.id) }}">Upload Files</a>
</div>
</article>
</section>
{% endif %}
{% endblock %}
Loading…
Cancel
Save