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: if config:
with open(config, "rb") as fp: with open(config, "rb") as fp:
app.config.update(tomllib.load(fp)) app.config.update(tomllib.load(fp))
print(app.config) 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() cherrypy.server.unsubscribe()
server = cherrypy._cpserver.Server() server = cherrypy._cpserver.Server()
cherrypy.config.update( cherrypy.config.update(
{ {
"environment": "production", "environment": "production",
"engine.autoreload.on": False, "engine.autoreload.on": False,
"log.screen.on": True,
} }
) )
cherrypy.tree.graft(app, "/") cherrypy.tree.graft(app, "/")
@ -135,7 +142,6 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
server.subscribe() server.subscribe()
# Spawn the worker thread(s) # Spawn the worker thread(s)
Worker(cherrypy.engine, app, db_factory, poll_printers, frequency=5).start() 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, assign_jobs, frequency=5).start()
Worker(cherrypy.engine, app, db_factory, push_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") 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"]) @BLUEPRINT.route("/admin/files", methods=["POST"])
@requires_admin @requires_admin
def manipulate_files(): def manipulate_files():

View file

@ -82,7 +82,22 @@ class Db(Queries):
self._conn.row_factory = self._factory self._conn.row_factory = self._factory
self._conn.isolation_level = None # Disable automagical transactions self._conn.isolation_level = None # Disable automagical transactions
self._cursor = self._conn.cursor() 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): def begin(self):
self._conn.execute("BEGIN") 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. -- Initialize the core db tables. Arguably migration 0.
---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------
-- User structures -- User structures
@ -347,6 +374,7 @@ SELECT
p.id p.id
, p.name , p.name
, p.url , p.url
, p.stream_url
, p.api_key , p.api_key
, p.last_poll_date , p.last_poll_date
, s.name as status , s.name as status
@ -360,6 +388,7 @@ SELECT
p.id p.id
, p.name , p.name
, p.url , p.url
, p.stream_url
, p.api_key , p.api_key
, p.last_poll_date , p.last_poll_date
, s.name as status , s.name as status
@ -388,6 +417,17 @@ WHERE
id = :pid id = :pid
; ;
-- name: edit-printer
UPDATE printers
SET
name = :name
, url = :url
, stream_url = :stream_url
, api_key = :api_key
WHERE
id = :id
;
---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------
-- Files -- Files
---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------

View file

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

View file

@ -54,6 +54,10 @@ label {
margin-bottom: 20px; margin-bottom: 20px;
} }
.u-flex1 {
flex: 1;
}
@media (max-width: 760px) { @media (max-width: 760px) {
.file, .printer, .key, .job { .file, .printer, .key, .job {
flex-direction: column; 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" %} {% extends "base.html.j2" %}
{% block content %} {% block content %}
<div class="row twelve columns mb-2">
{% include "streams.html.j2" %}
</div>
<div class="row twelve columns mb-2"> <div class="row twelve columns mb-2">
{% include "jobs_list.html.j2" %} {% include "jobs_list.html.j2" %}
</div> </div>

View file

@ -3,22 +3,22 @@
{% with printers = ctx.db.list_printers() %} {% with printers = ctx.db.list_printers() %}
{% if printers %} {% if printers %}
{% for printer in printers %} {% for printer in printers %}
{% with id, name, url, _api_key, last_poll, status = printer %}
<div class="printer row u-flex"> <div class="printer row u-flex">
<div class="details six columns"> <div class="details six columns">
<span class="printer-name">{{name}}</span> <span class="printer-name">{{printer.name}}</span>
<span class="printer-url"><code>{{url}}</code></span> <span class="printer-url"><code>{{printer.url}}</code></span>
<span class="printer-status">{{status}}</span> <span class="printer-status">{{printer.status}}</span>
<span class="printer-date">{{last_poll}}</span> <span class="printer-date">{{printer.last_poll_date}}</span>
</div> </div>
{# FIXME: How should these action buttons work? #} {# FIXME: How should these action buttons work? #}
<div class="controls u-flex u-ml-auto"> <div class="controls u-flex u-ml-auto">
<a class="button" href="/printers/test?id={{id}}">Test</a> {% if ctx.is_admin %}
<a class="button" href="/printers/edit?id={{id}}">Edit</a> <a class="button" href="/admin/printers/test?id={{printer.id}}">Test</a>
<a class="button" href="/printers/delete?id={{id}}">Remove</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 %}
</div> </div>
</div> </div>
{% endwith %}
{% endfor %} {% endfor %}
{% if ctx.is_admin %} {% if ctx.is_admin %}
<a class="button" href="/admin/printers">Add a printer</a> <a class="button" href="/admin/printers">Add a printer</a>

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 %}