From 01bcd4fa95b8baafd5a7247ec1d9d3e70926c13a Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie <me@arrdem.com> Date: Sat, 3 Jun 2023 22:19:26 -0600 Subject: [PATCH] feat: Printer edit flow; dashboard webcams --- .../src/python/tentacles/__main__.py | 8 +++- .../python/tentacles/blueprints/admin_ui.py | 20 ++++++++ projects/tentacles/src/python/tentacles/db.py | 17 ++++++- .../tentacles/src/python/tentacles/schema.sql | 42 ++++++++++++++++- .../tentacles/static/css/_skeleton.scss | 2 + .../python/tentacles/static/css/style.scss | 21 +++++++++ .../tentacles/templates/edit_printer.html.j2 | 36 +++++++++++++++ .../python/tentacles/templates/index.html.j2 | 4 ++ .../tentacles/templates/printers_list.html.j2 | 46 +++++++++---------- .../tentacles/templates/streams.html.j2 | 12 +++++ 10 files changed, 182 insertions(+), 26 deletions(-) create mode 100644 projects/tentacles/src/python/tentacles/templates/edit_printer.html.j2 create mode 100644 projects/tentacles/src/python/tentacles/templates/streams.html.j2 diff --git a/projects/tentacles/src/python/tentacles/__main__.py b/projects/tentacles/src/python/tentacles/__main__.py index 46ec85b..c7fb82c 100644 --- a/projects/tentacles/src/python/tentacles/__main__.py +++ b/projects/tentacles/src/python/tentacles/__main__.py @@ -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() diff --git a/projects/tentacles/src/python/tentacles/blueprints/admin_ui.py b/projects/tentacles/src/python/tentacles/blueprints/admin_ui.py index ad55e5b..e0068d1 100644 --- a/projects/tentacles/src/python/tentacles/blueprints/admin_ui.py +++ b/projects/tentacles/src/python/tentacles/blueprints/admin_ui.py @@ -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(): diff --git a/projects/tentacles/src/python/tentacles/db.py b/projects/tentacles/src/python/tentacles/db.py index d19ad1e..6126c51 100644 --- a/projects/tentacles/src/python/tentacles/db.py +++ b/projects/tentacles/src/python/tentacles/db.py @@ -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") diff --git a/projects/tentacles/src/python/tentacles/schema.sql b/projects/tentacles/src/python/tentacles/schema.sql index 6093a96..1e36258 100644 --- a/projects/tentacles/src/python/tentacles/schema.sql +++ b/projects/tentacles/src/python/tentacles/schema.sql @@ -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 ---------------------------------------------------------------------------------------------------- diff --git a/projects/tentacles/src/python/tentacles/static/css/_skeleton.scss b/projects/tentacles/src/python/tentacles/static/css/_skeleton.scss index f28bf6c..2b7ca2e 100644 --- a/projects/tentacles/src/python/tentacles/static/css/_skeleton.scss +++ b/projects/tentacles/src/python/tentacles/static/css/_skeleton.scss @@ -59,6 +59,8 @@ .columns:first-child { margin-left: 0; } + .equal.columns {} + .one.column, .one.columns { width: 4.66666666667%; } .two.columns { width: 13.3333333333%; } diff --git a/projects/tentacles/src/python/tentacles/static/css/style.scss b/projects/tentacles/src/python/tentacles/static/css/style.scss index 526b184..0ea459b 100644 --- a/projects/tentacles/src/python/tentacles/static/css/style.scss +++ b/projects/tentacles/src/python/tentacles/static/css/style.scss @@ -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; + } +} diff --git a/projects/tentacles/src/python/tentacles/templates/edit_printer.html.j2 b/projects/tentacles/src/python/tentacles/templates/edit_printer.html.j2 new file mode 100644 index 0000000..1f3b8f1 --- /dev/null +++ b/projects/tentacles/src/python/tentacles/templates/edit_printer.html.j2 @@ -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 %} diff --git a/projects/tentacles/src/python/tentacles/templates/index.html.j2 b/projects/tentacles/src/python/tentacles/templates/index.html.j2 index 392412e..fefaa1c 100644 --- a/projects/tentacles/src/python/tentacles/templates/index.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/index.html.j2 @@ -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> diff --git a/projects/tentacles/src/python/tentacles/templates/printers_list.html.j2 b/projects/tentacles/src/python/tentacles/templates/printers_list.html.j2 index 5eeaed0..ebf7206 100644 --- a/projects/tentacles/src/python/tentacles/templates/printers_list.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/printers_list.html.j2 @@ -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> diff --git a/projects/tentacles/src/python/tentacles/templates/streams.html.j2 b/projects/tentacles/src/python/tentacles/templates/streams.html.j2 new file mode 100644 index 0000000..16d8713 --- /dev/null +++ b/projects/tentacles/src/python/tentacles/templates/streams.html.j2 @@ -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 %}