feat: Printer edit flow; dashboard webcams

This commit is contained in:
Reid 'arrdem' McKenzie 2023-06-03 22:19:26 -06:00
parent de7f91947f
commit d79a2a578f
10 changed files with 182 additions and 26 deletions

View file

@ -116,14 +116,21 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
if config:
with open(config, "rb") as fp:
app.config.update(tomllib.load(fp))
print(app.config)
# Run migrations once at startup rather than when connecting
with closing(db_factory(app)) as db:
db.migrate()
# Configuring cherrypy is kinda awful
cherrypy.server.unsubscribe()
server = cherrypy._cpserver.Server()
cherrypy.config.update(
{
"environment": "production",
"engine.autoreload.on": False,
"log.screen.on": True,
}
)
cherrypy.tree.graft(app, "/")
@ -135,7 +142,6 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
server.subscribe()
# Spawn the worker thread(s)
Worker(cherrypy.engine, app, db_factory, poll_printers, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, assign_jobs, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, push_jobs, frequency=5).start()

View file

@ -103,6 +103,26 @@ def handle_add_printer():
return render_template("printers.html.j2")
@BLUEPRINT.route("/admin/printers/edit", methods=["GET"])
@requires_admin
def get_edit_printers():
pid = int(request.args.get("id", "-1"))
if row := ctx.db.fetch_printer(pid=pid):
return render_template("edit_printer.html.j2", printer=row)
else:
flash("No such printer", category="error")
return redirect("/admin"), 404
@BLUEPRINT.route("/admin/printers/edit", methods=["POST"])
@requires_admin
def handle_edit_printers():
args = request.form.copy()
args["id"] = int(args["id"])
ctx.db.edit_printer(**request.form)
return redirect("/admin")
@BLUEPRINT.route("/admin/files", methods=["POST"])
@requires_admin
def manipulate_files():

View file

@ -82,7 +82,22 @@ class Db(Queries):
self._conn.row_factory = self._factory
self._conn.isolation_level = None # Disable automagical transactions
self._cursor = self._conn.cursor()
self.create_tables()
def migrate(self):
self.migration_0000_create_migrations()
existing_migrations = {it.name for it in self.list_migrations()}
for query in sorted(
(
q
for q in _queries.available_queries
if q.startswith("migration_") and q not in existing_migrations
)
):
log.warn("Applying migration %s", query)
getattr(self, query)()
digest = sha3_256()
digest.update(getattr(_queries, query).sql.encode())
self.record_migration(name=query, fingerprint=digest.hexdigest())
def begin(self):
self._conn.execute("BEGIN")

View file

@ -1,4 +1,31 @@
-- name: create_tables#
-- name: migration-0000-create_migrations
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT
, name TEXT
, fingerprint TEXT
, executed_at TEXT DEFAULT (datetime('now'))
, UNIQUE(name)
);
-- name: list-migrations
SELECT
*
FROM migrations
ORDER BY
datetime(executed_at) ASC
;
-- name: record-migration!
INSERT INTO migrations (
name
, fingerprint
)
VALUES (
:name
, :fingerprint
);
-- name: migration-0001-create_tables#
-- Initialize the core db tables. Arguably migration 0.
----------------------------------------------------------------------------------------------------
-- User structures
@ -347,6 +374,7 @@ SELECT
p.id
, p.name
, p.url
, p.stream_url
, p.api_key
, p.last_poll_date
, s.name as status
@ -360,6 +388,7 @@ SELECT
p.id
, p.name
, p.url
, p.stream_url
, p.api_key
, p.last_poll_date
, s.name as status
@ -388,6 +417,17 @@ WHERE
id = :pid
;
-- name: edit-printer
UPDATE printers
SET
name = :name
, url = :url
, stream_url = :stream_url
, api_key = :api_key
WHERE
id = :id
;
----------------------------------------------------------------------------------------------------
-- Files
----------------------------------------------------------------------------------------------------

View file

@ -59,6 +59,8 @@
.columns:first-child {
margin-left: 0; }
.equal.columns {}
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }

View file

@ -54,6 +54,10 @@ label {
margin-bottom: 20px;
}
.u-flex1 {
flex: 1;
}
@media (max-width: 760px) {
.file, .printer, .key, .job {
flex-direction: column;
@ -65,3 +69,20 @@ label {
}
}
}
.row.webcams {
display: flex;
justify-content: space-between;
img {
object-fit: contain;
}
.webcam {
padding-right: 10px;
}
.webcam:last-child {
padding-right: 0px;
}
}

View file

@ -0,0 +1,36 @@
{% extends "base.html.j2" %}
{% block content %}
<h1>Edit printer</h1>
<div class="row">
<form method="post">
<div class="row">
<div class="twelve columns">
<label for="name">Printer name</label>
<input type="text" name="name" value="{{ printer.name }}" />
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="url">Printer base URL</label>
<input type="text" name="url" value="{{ printer.url }}" />
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="url">Printer stream URL</label>
<input type="text" name="stream_url" value="{{ printer.stream_url or ''}}" />
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="api_key">API key</label>
<input type="text" name="api_key" value="{{ printer.api_key }}" />
</div>
</div>
<div class="row">
<input type="hidden" name="id" value="{{ printer.id }}" />
<input id="submit" type="submit" value="Submit" />
</div>
</form>
</div>
{% endblock %}

View file

@ -1,5 +1,9 @@
{% extends "base.html.j2" %}
{% block content %}
<div class="row twelve columns mb-2">
{% include "streams.html.j2" %}
</div>
<div class="row twelve columns mb-2">
{% include "jobs_list.html.j2" %}
</div>

View file

@ -1,30 +1,30 @@
<div class="printers">
<h2>Printers</h2>
{% with printers = ctx.db.list_printers() %}
{% if printers %}
{% for printer in printers %}
{% with id, name, url, _api_key, last_poll, status = printer %}
<div class="printer row u-flex">
<div class="details six columns">
<span class="printer-name">{{name}}</span>
<span class="printer-url"><code>{{url}}</code></span>
<span class="printer-status">{{status}}</span>
<span class="printer-date">{{last_poll}}</span>
</div>
{# FIXME: How should these action buttons work? #}
<div class="controls u-flex u-ml-auto">
<a class="button" href="/printers/test?id={{id}}">Test</a>
<a class="button" href="/printers/edit?id={{id}}">Edit</a>
<a class="button" href="/printers/delete?id={{id}}">Remove</a>
</div>
</div>
{% endwith %}
{% endfor %}
{% if printers %}
{% for printer in printers %}
<div class="printer row u-flex">
<div class="details six columns">
<span class="printer-name">{{printer.name}}</span>
<span class="printer-url"><code>{{printer.url}}</code></span>
<span class="printer-status">{{printer.status}}</span>
<span class="printer-date">{{printer.last_poll_date}}</span>
</div>
{# FIXME: How should these action buttons work? #}
<div class="controls u-flex u-ml-auto">
{% if ctx.is_admin %}
<a class="button" href="/admin/printers">Add a printer</a>
<a class="button" href="/admin/printers/test?id={{printer.id}}">Test</a>
<a class="button" href="/admin/printers/edit?id={{printer.id}}">Edit</a>
<a class="button" href="/admin/printers/delete?id={{printer.id}}">Remove</a>
{% endif %}
{% else %}
No printers available. {% if ctx.is_admin %}<a href="/admin/printers">Configure one!</a>{% else %}Ask the admin to configure one!{% endif %}
{% endif %}
</div>
</div>
{% endfor %}
{% if ctx.is_admin %}
<a class="button" href="/admin/printers">Add a printer</a>
{% endif %}
{% else %}
No printers available. {% if ctx.is_admin %}<a href="/admin/printers">Configure one!</a>{% else %}Ask the admin to configure one!{% endif %}
{% endif %}
{% endwith %}
</div>

View file

@ -0,0 +1,12 @@
{% import "macros.html.j2" as macros %}
<h2>Webcams</h2>
{% with printers = ctx.db.list_printers() %}
<div class="webcams row">
{% for printer in printers if printer.stream_url %}
<div class="u-flex1 webcam" style="max-width: calc(100% / {{printers|length}})">
<label>{{ printer.name }}</label>
<img id="printer_{{printer.id}}_stream" src="{{ printer.stream_url }}" style="max-width: 100%;" />
</div>
{% endfor %}
</div>
{% endwith %}