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 OTB Cloud
## Current version ## Current version
v0.2.0 v0.2.1
## Build date ## Build date
2026-04-12 2026-04-12
@ -50,14 +50,15 @@ Portal-authenticated secure backup and storage platform for customer files, incl
- Add Device flow - Add Device flow
- Remove Device flow for empty devices - Remove Device flow for empty devices
- Browser upload flow to device originals - Browser upload flow to device originals
- Device file browser page
## Immediate next tasks ## Immediate next tasks
1. Build first file library page 1. Add single-file download
2. Add uploaded file listing per device 2. Add searchable file listing
3. Add upload audit log UI or admin reference 3. Add rename basename-only flow
4. Add zip export flow 4. Add zip export flow
5. Add searchable file listing 5. Add media processing jobs
6. Add media processing jobs 6. Add derived/original filtering
## Notes ## Notes
Original uploaded files should remain preserved and effectively read-only. 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. Admin access should require owner-issued one-time support authorization.
New tenants no longer receive default devices automatically; devices are now user-created. 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. 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 # 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 ## v0.2.0 - 2026-04-12
- Added first browser upload flow for user-created devices - Added first browser upload flow for user-created devices
- Added Upload Files action per device - 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") flash(f"Uploaded {uploaded_count} file(s) to {device['device_name']}.", "success")
return redirect(url_for("main.dashboard")) 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> <span style="opacity:0.75;">{{ device.relative_path }}</span><br>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:10px;"> <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 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) }}"> <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> <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> </form>
@ -76,7 +77,7 @@
<div class="service-card-header"> <div class="service-card-header">
<div> <div>
<h2>Current scope</h2> <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>
<div> <div>
<span class="service-badge service-badge-beta">Live</span> <span class="service-badge service-badge-beta">Live</span>
@ -85,7 +86,7 @@
<div class="service-card-actions"> <div class="service-card-actions">
<p style="margin:0;"> <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> </p>
</div> </div>
</article> </article>
@ -121,7 +122,7 @@
<div class="service-card-actions"> <div class="service-card-actions">
<p style="margin:0;"> <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> </p>
</div> </div>
</article> </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