diff --git a/projects/tentacles/src/python/tentacles/__main__.py b/projects/tentacles/src/python/tentacles/__main__.py
index 4001c7c..80d66d7 100644
--- a/projects/tentacles/src/python/tentacles/__main__.py
+++ b/projects/tentacles/src/python/tentacles/__main__.py
@@ -2,15 +2,21 @@
"""The core app entrypoint."""
+from datetime import datetime
from pathlib import Path
+import tomllib
+
import click
from flask import Flask, request
-import tomllib
-from datetime import datetime
-
-from tentacles.blueprints import user_ui, printer_ui, job_ui, file_ui, api
-from tentacles.store import Store
+from tentacles.blueprints import (
+ api,
+ file_ui,
+ job_ui,
+ printer_ui,
+ user_ui,
+)
from tentacles.globals import _ctx, Ctx, ctx
+from tentacles.store import Store
from tentacles.workers import create_workers
diff --git a/projects/tentacles/src/python/tentacles/blueprints/api.py b/projects/tentacles/src/python/tentacles/blueprints/api.py
index 680ede1..53f3007 100644
--- a/projects/tentacles/src/python/tentacles/blueprints/api.py
+++ b/projects/tentacles/src/python/tentacles/blueprints/api.py
@@ -2,14 +2,17 @@
"""API endpoints supporting the 'ui'."""
+from hashlib import sha3_256
import os
from typing import Optional
+from flask import Blueprint, current_app, request
+from tentacles.blueprints.util import (
+ requires_admin,
+ requires_auth,
+)
from tentacles.globals import ctx
-from tentacles.blueprints.util import requires_admin, requires_auth
-from flask import Blueprint, request, current_app
-from hashlib import sha3_256
BLUEPRINT = Blueprint("api", __name__, url_prefix="/api")
@@ -51,11 +54,6 @@ def delete_printer():
def create_file(location: Optional[str] = None):
# This is the important one, because it's the one that PrusaSlicer et all use to upload jobs.
- print(request)
- print(request.headers)
- print(request.files)
- print(request.form)
-
if "file" not in request.files:
return {"status": "error", "error": "No file to upload"}, 400
diff --git a/projects/tentacles/src/python/tentacles/blueprints/file_ui.py b/projects/tentacles/src/python/tentacles/blueprints/file_ui.py
index c189715..37352dd 100644
--- a/projects/tentacles/src/python/tentacles/blueprints/file_ui.py
+++ b/projects/tentacles/src/python/tentacles/blueprints/file_ui.py
@@ -1,31 +1,58 @@
#!/usr/bin/env python3
import logging
-from datetime import timedelta
-import re
from tentacles.globals import ctx
from .util import requires_auth
+from .api import create_file
from flask import (
Blueprint,
- current_app,
+ flash,
+ render_template,
request,
redirect,
- render_template,
- flash,
)
-
-from .util import salt, is_logged_in
+import os
log = logging.getLogger(__name__)
BLUEPRINT = Blueprint("files", __name__)
@requires_auth
-@BLUEPRINT.route("/files", methods=["GET", "POST"])
-def files():
+@BLUEPRINT.route("/files", methods=["GET"])
+def list_files():
if request.method == "POST":
flash("Not supported yet", category="warning")
return render_template("files.html.j2")
+
+
+@requires_auth
+@BLUEPRINT.route("/files", methods=["POST"])
+def manipulate_files():
+ match request.form.get("action"):
+ case "upload":
+ resp, code = create_file()
+ if 200 <= code <= 300:
+ flash("File created", category="info")
+ else:
+ flash(resp.get("error"), category="error")
+ return render_template("files.html.j2"), code
+
+ case "delete":
+ file = ctx.db.fetch_file(ctx.uid, int(request.form.get("file_id")))
+ if any(job.finished_at is None for job in ctx.db.list_jobs_by_file(file.id)):
+ flash("File is in use", category="error")
+ return render_template("files.html.j2"), 400
+
+ os.unlink(file.path)
+ ctx.db.delete_file(ctx.uid, file.id)
+ flash("File deleted", category="info")
+
+ case _:
+ print(request.form)
+ flash("Not supported yet", category="warning")
+ return render_template("files.html.j2"), 400
+
+ return redirect("/files")
diff --git a/projects/tentacles/src/python/tentacles/blueprints/job_ui.py b/projects/tentacles/src/python/tentacles/blueprints/job_ui.py
index 0c875fe..512515c 100644
--- a/projects/tentacles/src/python/tentacles/blueprints/job_ui.py
+++ b/projects/tentacles/src/python/tentacles/blueprints/job_ui.py
@@ -1,32 +1,55 @@
#!/usr/bin/env python3
import logging
-from datetime import timedelta
-import re
-from tentacles.globals import ctx
from .util import requires_auth
from flask import (
Blueprint,
- current_app,
- request,
- redirect,
- render_template,
flash,
+ redirect,
+ request,
+ render_template,
)
+from tentacles.globals import ctx
-from .util import salt, is_logged_in
log = logging.getLogger(__name__)
BLUEPRINT = Blueprint("jobs", __name__)
@requires_auth
-@BLUEPRINT.route("/jobs", methods=["GET", "POST"])
-def jobs():
- if request.method == "POST":
- ctx.db.create_job(ctx.uid, int(request.form.get("file_id")))
- flash("Job created!", category="info")
+@BLUEPRINT.route("/jobs", methods=["GET"])
+def list_jobs():
+ return render_template("jobs.html.j2")
- return redirect("/")
+
+@requires_auth
+@BLUEPRINT.route("/jobs", methods=["POST"])
+def manipulate_jobs():
+ match request.form.get("action"):
+ case "enqueue":
+ ctx.db.create_job(ctx.uid, int(request.form.get("file_id")))
+ flash("Job created!", category="info")
+
+ case "duplicate":
+ if job := ctx.db.fetch_job(ctx.uid, int(request.form.get("job_id"))):
+ ctx.db.create_job(ctx.uid, job.file_id)
+ flash("Job created!", category="info")
+ else:
+ flash("Could not duplicate", category="error")
+
+ case "cancel":
+ ctx.db.cancel_job(ctx.uid, int(request.form.get("job_id")))
+ flash("Cancellation reqiested", category="info")
+
+ case "delete":
+ ctx.db.delete_job(ctx.uid, int(request.form.get("job_id")))
+ flash("Job deleted", category="info")
+
+ case _:
+ print(request.form)
+ flash("Unsupported operation", category="error")
+ return render_template("jobs.html.j2"), 400
+
+ return redirect("/jobs")
diff --git a/projects/tentacles/src/python/tentacles/blueprints/printer_ui.py b/projects/tentacles/src/python/tentacles/blueprints/printer_ui.py
index 1ac0b54..c9f9d29 100644
--- a/projects/tentacles/src/python/tentacles/blueprints/printer_ui.py
+++ b/projects/tentacles/src/python/tentacles/blueprints/printer_ui.py
@@ -2,16 +2,17 @@
import logging
+from .util import is_logged_in, requires_admin
+
from flask import (
Blueprint,
- request,
+ flash,
redirect,
render_template,
- flash,
+ request,
)
-
from tentacles.globals import ctx
-from .util import is_logged_in, requires_admin
+
log = logging.getLogger(__name__)
BLUEPRINT = Blueprint("printer", __name__)
diff --git a/projects/tentacles/src/python/tentacles/blueprints/user_ui.py b/projects/tentacles/src/python/tentacles/blueprints/user_ui.py
index 77d1230..2d06204 100644
--- a/projects/tentacles/src/python/tentacles/blueprints/user_ui.py
+++ b/projects/tentacles/src/python/tentacles/blueprints/user_ui.py
@@ -1,21 +1,21 @@
#!/usr/bin/env python3
-import logging
from datetime import timedelta
+import logging
import re
-from tentacles.globals import ctx
+from .util import is_logged_in, salt
from flask import (
Blueprint,
current_app,
- request,
+ flash,
redirect,
render_template,
- flash,
+ request,
)
+from tentacles.globals import ctx
-from .util import salt, is_logged_in
log = logging.getLogger(__name__)
BLUEPRINT = Blueprint("user", __name__)
diff --git a/projects/tentacles/src/python/tentacles/blueprints/util.py b/projects/tentacles/src/python/tentacles/blueprints/util.py
index a50d740..be30237 100644
--- a/projects/tentacles/src/python/tentacles/blueprints/util.py
+++ b/projects/tentacles/src/python/tentacles/blueprints/util.py
@@ -2,15 +2,10 @@
import logging
-
-from flask import (
- current_app,
- redirect,
- flash,
-)
-
+from flask import current_app, flash, redirect
from tentacles.globals import ctx
+
log = logging.getLogger(__name__)
diff --git a/projects/tentacles/src/python/tentacles/schema.sql b/projects/tentacles/src/python/tentacles/schema.sql
index aebda76..29f6da1 100644
--- a/projects/tentacles/src/python/tentacles/schema.sql
+++ b/projects/tentacles/src/python/tentacles/schema.sql
@@ -85,8 +85,10 @@ CREATE TABLE IF NOT EXISTS jobs (
, file_id INTEGER NOT NULL
, priority INTEGER CHECK(priority IS NOT NULL AND 0 <= priority)
, started_at TEXT
+ , cancelled_at TEXT
, finished_at TEXT
, state TEXT
+ , message TEXT
, printer_id INTEGER
, FOREIGN KEY(user_id) REFERENCES users(id)
, FOREIGN KEY(file_id) REFERENCES files(id)
diff --git a/projects/tentacles/src/python/tentacles/static/css/_normalize.scss b/projects/tentacles/src/python/tentacles/static/css/_normalize.scss
new file mode 100644
index 0000000..81c6f31
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/_normalize.scss
@@ -0,0 +1,427 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
\ No newline at end of file
diff --git a/projects/tentacles/src/python/tentacles/static/css/_skeleton.scss b/projects/tentacles/src/python/tentacles/static/css/_skeleton.scss
new file mode 100644
index 0000000..f28bf6c
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/_skeleton.scss
@@ -0,0 +1,418 @@
+/*
+* Skeleton V2.0.4
+* Copyright 2014, Dave Gamache
+* www.getskeleton.com
+* Free to use under the MIT license.
+* http://www.opensource.org/licenses/mit-license.php
+* 12/29/2014
+*/
+
+
+/* Table of contents
+––––––––––––––––––––––––––––––––––––––––––––––––––
+- Grid
+- Base Styles
+- Typography
+- Links
+- Buttons
+- Forms
+- Lists
+- Code
+- Tables
+- Spacing
+- Utilities
+- Clearing
+- Media Queries
+*/
+
+
+/* Grid
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.container {
+ position: relative;
+ width: 100%;
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 0 20px;
+ box-sizing: border-box; }
+.column,
+.columns {
+ width: 100%;
+ float: left;
+ box-sizing: border-box; }
+
+/* For devices larger than 400px */
+@media (min-width: 400px) {
+ .container {
+ width: 85%;
+ padding: 0; }
+}
+
+/* For devices larger than 550px */
+@media (min-width: 550px) {
+ .container {
+ width: 80%; }
+ .column,
+ .columns {
+ margin-left: 4%; }
+ .column:first-child,
+ .columns:first-child {
+ margin-left: 0; }
+
+ .one.column,
+ .one.columns { width: 4.66666666667%; }
+ .two.columns { width: 13.3333333333%; }
+ .three.columns { width: 22%; }
+ .four.columns { width: 30.6666666667%; }
+ .five.columns { width: 39.3333333333%; }
+ .six.columns { width: 48%; }
+ .seven.columns { width: 56.6666666667%; }
+ .eight.columns { width: 65.3333333333%; }
+ .nine.columns { width: 74.0%; }
+ .ten.columns { width: 82.6666666667%; }
+ .eleven.columns { width: 91.3333333333%; }
+ .twelve.columns { width: 100%; margin-left: 0; }
+
+ .one-third.column { width: 30.6666666667%; }
+ .two-thirds.column { width: 65.3333333333%; }
+
+ .one-half.column { width: 48%; }
+
+ /* Offsets */
+ .offset-by-one.column,
+ .offset-by-one.columns { margin-left: 8.66666666667%; }
+ .offset-by-two.column,
+ .offset-by-two.columns { margin-left: 17.3333333333%; }
+ .offset-by-three.column,
+ .offset-by-three.columns { margin-left: 26%; }
+ .offset-by-four.column,
+ .offset-by-four.columns { margin-left: 34.6666666667%; }
+ .offset-by-five.column,
+ .offset-by-five.columns { margin-left: 43.3333333333%; }
+ .offset-by-six.column,
+ .offset-by-six.columns { margin-left: 52%; }
+ .offset-by-seven.column,
+ .offset-by-seven.columns { margin-left: 60.6666666667%; }
+ .offset-by-eight.column,
+ .offset-by-eight.columns { margin-left: 69.3333333333%; }
+ .offset-by-nine.column,
+ .offset-by-nine.columns { margin-left: 78.0%; }
+ .offset-by-ten.column,
+ .offset-by-ten.columns { margin-left: 86.6666666667%; }
+ .offset-by-eleven.column,
+ .offset-by-eleven.columns { margin-left: 95.3333333333%; }
+
+ .offset-by-one-third.column,
+ .offset-by-one-third.columns { margin-left: 34.6666666667%; }
+ .offset-by-two-thirds.column,
+ .offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
+
+ .offset-by-one-half.column,
+ .offset-by-one-half.columns { margin-left: 52%; }
+
+}
+
+
+/* Base Styles
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/* NOTE
+html is set to 62.5% so that all the REM measurements throughout Skeleton
+are based on 10px sizing. So basically 1.5rem = 15px :) */
+html {
+ font-size: 62.5%; }
+body {
+ font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
+ line-height: 1.6;
+ font-weight: 400;
+ font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color: #222; }
+
+
+/* Typography
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+h1, h2, h3, h4, h5, h6 {
+ margin-top: 0;
+ margin-bottom: 2rem;
+ font-weight: 300; }
+h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
+h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
+h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
+h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
+h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
+h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
+
+/* Larger than phablet */
+@media (min-width: 550px) {
+ h1 { font-size: 5.0rem; }
+ h2 { font-size: 4.2rem; }
+ h3 { font-size: 3.6rem; }
+ h4 { font-size: 3.0rem; }
+ h5 { font-size: 2.4rem; }
+ h6 { font-size: 1.5rem; }
+}
+
+p {
+ margin-top: 0; }
+
+
+/* Links
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+a {
+ color: #1EAEDB; }
+a:hover {
+ color: #0FA0CE; }
+
+
+/* Buttons
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.button,
+button,
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+ display: inline-block;
+ height: 38px;
+ padding: 0 30px;
+ color: #555;
+ text-align: center;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 38px;
+ letter-spacing: .1rem;
+ text-transform: uppercase;
+ text-decoration: none;
+ white-space: nowrap;
+ background-color: transparent;
+ border-radius: 4px;
+ border: 1px solid #bbb;
+ cursor: pointer;
+ box-sizing: border-box; }
+.button:hover,
+button:hover,
+input[type="submit"]:hover,
+input[type="reset"]:hover,
+input[type="button"]:hover,
+.button:focus,
+button:focus,
+input[type="submit"]:focus,
+input[type="reset"]:focus,
+input[type="button"]:focus {
+ color: #333;
+ border-color: #888;
+ outline: 0; }
+.button.button-primary,
+button.button-primary,
+input[type="submit"].button-primary,
+input[type="reset"].button-primary,
+input[type="button"].button-primary {
+ color: #FFF;
+ background-color: #33C3F0;
+ border-color: #33C3F0; }
+.button.button-primary:hover,
+button.button-primary:hover,
+input[type="submit"].button-primary:hover,
+input[type="reset"].button-primary:hover,
+input[type="button"].button-primary:hover,
+.button.button-primary:focus,
+button.button-primary:focus,
+input[type="submit"].button-primary:focus,
+input[type="reset"].button-primary:focus,
+input[type="button"].button-primary:focus {
+ color: #FFF;
+ background-color: #1EAEDB;
+ border-color: #1EAEDB; }
+
+
+/* Forms
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+textarea,
+select {
+ height: 38px;
+ padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
+ background-color: #fff;
+ border: 1px solid #D1D1D1;
+ border-radius: 4px;
+ box-shadow: none;
+ box-sizing: border-box; }
+/* Removes awkward default styles on some inputs for iOS */
+input[type="email"],
+input[type="number"],
+input[type="search"],
+input[type="text"],
+input[type="tel"],
+input[type="url"],
+input[type="password"],
+textarea {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none; }
+textarea {
+ min-height: 65px;
+ padding-top: 6px;
+ padding-bottom: 6px; }
+input[type="email"]:focus,
+input[type="number"]:focus,
+input[type="search"]:focus,
+input[type="text"]:focus,
+input[type="tel"]:focus,
+input[type="url"]:focus,
+input[type="password"]:focus,
+textarea:focus,
+select:focus {
+ border: 1px solid #33C3F0;
+ outline: 0; }
+label,
+legend {
+ display: block;
+ margin-bottom: .5rem;
+ font-weight: 600; }
+fieldset {
+ padding: 0;
+ border-width: 0; }
+input[type="checkbox"],
+input[type="radio"] {
+ display: inline; }
+label > .label-body {
+ display: inline-block;
+ margin-left: .5rem;
+ font-weight: normal; }
+
+
+/* Lists
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+ul {
+ list-style: circle inside; }
+ol {
+ list-style: decimal inside; }
+ol, ul {
+ padding-left: 0;
+ margin-top: 0; }
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+ margin: 1.5rem 0 1.5rem 3rem;
+ font-size: 90%; }
+li {
+ margin-bottom: 1rem; }
+
+
+/* Code
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+code {
+ padding: .2rem .5rem;
+ margin: 0 .2rem;
+ font-size: 90%;
+ white-space: nowrap;
+ background: #F1F1F1;
+ border: 1px solid #E1E1E1;
+ border-radius: 4px; }
+pre > code {
+ display: block;
+ padding: 1rem 1.5rem;
+ white-space: pre; }
+
+
+/* Tables
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+th,
+td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid #E1E1E1; }
+th:first-child,
+td:first-child {
+ padding-left: 0; }
+th:last-child,
+td:last-child {
+ padding-right: 0; }
+
+
+/* Spacing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+button,
+.button {
+ margin-bottom: 1rem; }
+input,
+textarea,
+select,
+fieldset {
+ margin-bottom: 1.5rem; }
+pre,
+blockquote,
+dl,
+figure,
+table,
+p,
+ul,
+ol,
+form {
+ margin-bottom: 2.5rem; }
+
+
+/* Utilities
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.u-full-width {
+ width: 100%;
+ box-sizing: border-box; }
+.u-max-full-width {
+ max-width: 100%;
+ box-sizing: border-box; }
+.u-pull-right {
+ float: right; }
+.u-pull-left {
+ float: left; }
+
+
+/* Misc
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+hr {
+ margin-top: 3rem;
+ margin-bottom: 3.5rem;
+ border-width: 0;
+ border-top: 1px solid #E1E1E1; }
+
+
+/* Clearing
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+
+/* Self Clearing Goodness */
+.container:after,
+.row:after,
+.u-cf {
+ content: "";
+ display: table;
+ clear: both; }
+
+
+/* Media Queries
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+/*
+Note: The best way to structure the use of media queries is to create the queries
+near the relevant code. For example, if you wanted to change the styles for buttons
+on small devices, paste the mobile query code up in the buttons section and style it
+there.
+*/
+
+
+/* Larger than mobile */
+@media (min-width: 400px) {}
+
+/* Larger than phablet (also point when grid becomes active) */
+@media (min-width: 550px) {}
+
+/* Larger than tablet */
+@media (min-width: 750px) {}
+
+/* Larger than desktop */
+@media (min-width: 1000px) {}
+
+/* Larger than Desktop HD */
+@media (min-width: 1200px) {}
diff --git a/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss b/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss
index b596b4e..4522d77 100644
--- a/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss
+++ b/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss
@@ -1,188 +1,5 @@
-$black: #171426;
-$beige: #F4F8EE;
-$red: #BB2D2E;
-$orange: #CA4F1F;
-$yellow: #EDB822;
-$clear: rgba(255, 255, 255, 255);
+@import "tirefire/colors";
+@import "tirefire/fonts";
-$secondary_red: red;
-$secondary_blue: #288BC2;
-$secondary_green: #A5C426;
-$secondary_light_grey: #CACBCA;
-$secondary_dark_grey: #9A9A9A;
-
-@font-face {
- font-family: 'Aaux Next';
- font-style: normal;
- font-weight: 400;
- src: local('Aaux Next'), url('/static/font/AauxNextBlk.otf') format('otf');
-}
-
-@font-face {
- font-family: 'Aaux Next';
- font-style: normal;
- font-weight: 400;
- src: local('Aaux Next'), url('/static/font/aauxnextbdwebfont.otf') format('otf');
-}
-
-@font-face {
- font-family: 'Aaux Next';
- font-style: normal;
- font-weight: 400;
- src: local('Aaux Next'), url('/static/font/aauxnextltwebfont.otf') format('otf');
-}
-
-@font-face {
- font-family: 'Aaux Next';
- font-style: normal;
- font-weight: 400;
- src: local('Aaux Next'), url('/static/font/aauxnextmdwebfont.otf') format('otf');
-}
-
-@import url(https://fonts.googleapis.com/css?family=Raleway);
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Hidable alerts
-.alert .inner {
- display: block;
- padding: 6px;
- margin: 6px;
- border-radius: 3px;
- border: 1px solid rgb(180,180,180);
- background-color: rgb(212,212,212);
-}
-
-.alert .close {
- float: right;
- margin: 3px 12px 0px 0px;
- cursor: pointer;
-}
-
-.alert .inner,.alert .close {
- color: rgb(88,88,88);
-}
-
-.alert input {
- display: none;
-}
-
-.alert input:checked ~ * {
- animation-name: dismiss,hide;
- animation-duration: 300ms;
- animation-iteration-count: 1;
- animation-timing-function: ease;
- animation-fill-mode: forwards;
- animation-delay: 0s,100ms;
-}
-
-.alert.error .inner {
- border: 1px solid rgb(238,211,215);
- background-color: rgb(242,222,222);
-}
-
-.alert.error .inner,.alert.error .close {
- color: rgb(185,74,72);
-}
-
-.alert.success .inner {
- border: 1px solid rgb(214,233,198);
- background-color: rgb(223,240,216);
-}
-
-.alert.success .inner,.alert.success .close {
- color: rgb(70,136,71);
-}
-
-.alert.info .inner {
- border: 1px solid rgb(188,232,241);
- background-color: rgb(217,237,247);
-}
-
-.alert.info .inner,.alert.info .close {
- color: rgb(58,135,173);
-}
-
-.alert.warning .inner {
- border: 1px solid rgb(251,238,213);
- background-color: rgb(252,248,227);
-}
-
-.alert.warning .inner,.alert.warning .close {
- color: rgb(192,152,83);
-}
-
-@keyframes dismiss {
- 0% {
- opacity: 1;
- }
- 90%, 100% {
- opacity: 0;
- font-size: 0.1px;
- transform: scale(0);
- }
-}
-
-@keyframes hide {
- 100% {
- height: 0px;
- width: 0px;
- overflow: hidden;
- margin: 0px;
- padding: 0px;
- border: 0px;
- }
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// A timer animation
-
-.timer {
- background: -webkit-linear-gradient(left, skyBlue 50%, #eee 50%);
- border-radius: 100%;
- height: calc(var(--size) * 1px);
- width: calc(var(--size) * 1px);
- position: relative;
- -webkit-animation: time calc(var(--duration) * 1s) steps(1000, start);
- -webkit-mask: radial-gradient(transparent 50%,#000 50%);
- mask: radial-gradient(transparent 50%,#000 50%);
-}
-.mask {
- border-radius: 100% 0 0 100% / 50% 0 0 50%;
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- width: 50%;
- -webkit-animation: mask calc(var(--duration) * 1s) steps(500, start);
- -webkit-transform-origin: 100% 50%;
-}
-@-webkit-keyframes time {
- 100% {
- -webkit-transform: rotate(360deg);
- }
-}
-@-webkit-keyframes mask {
- 0% {
- background: #eee;
- -webkit-transform: rotate(0deg);
- }
- 50% {
- background: #eee;
- -webkit-transform: rotate(-180deg);
- }
- 50.01% {
- background: skyBlue;
- -webkit-transform: rotate(0deg);
- }
- 100% {
- background: skyBlue;
- -webkit-transform: rotate(-180deg);
- }
-}
-
-.alert .timer {
- --size: 10;
- --duration: 5;
- padding: 6px;
- margin: 6px;
-}
+@import "tirefire/alerts";
+@import "tirefire/timers";
diff --git a/projects/tentacles/src/python/tentacles/static/css/style.scss b/projects/tentacles/src/python/tentacles/static/css/style.scss
index 7d128da..cfec423 100644
--- a/projects/tentacles/src/python/tentacles/static/css/style.scss
+++ b/projects/tentacles/src/python/tentacles/static/css/style.scss
@@ -1,328 +1,26 @@
-@import "tirefire";
+@import "normalize";
+@import "skeleton";
-.color-yellow {
- color: $yellow;
+// And the TireFire® bits
+@import "tirefire/fonts";
+@import "tirefire/basics";
+@import "tirefire/alerts";
+@import "tirefire/timers";
+@import "tirefire/nav";
+@import "tirefire/dots";
+
+.controls a,
+.controls form {
+ margin-right: 2px;
}
-html {
- font-family: 'Aaux Next', sans-serif;
- background-color: $beige;
- color: $black;
+.file,
+.printer,
+.key,
+.job {
+ margin-top: 4px;
}
-html, body {
- margin: 0;
- height: 100%;
- width: 100%;
- min-width: 400px;
-
- display: flex;
- flex-grow: 1;
- flex-direction: column;
- justify-content: center;
-}
-
-.content, .footer {
- padding-left: 10%;
- padding-right: 10%;
-}
-
-.content {
- .flash, .panel {
- margin-bottom: 40px;
- }
-}
-
-a {
- color: $secondary_blue;
- text-decoration: none;
-}
-
-*, *::before, *::after {
- margin: 0;
- padding: 0;
- align-content: center;
-}
-
-h1, h2, h3, h4, h5, h6 {
- width: 100%;
-}
-
-span {
- display: flex;
- align-self: center;
-}
-
-ul {
- list-style: none;
-
- .decorated {
- list-style: auto;
- padding: 1em;
- }
-
- li {
- padding-top: 0.1em;
- }
-}
-
-nav {
- background-color: $beige;
- box-shadow: 0px 10px 0px $red,
- 0px 12px 0px $clear,
- 0px 22px 0px $orange,
- 0px 24px 0px $clear,
- 0px 34px 0px $yellow;
- margin-bottom: 34px;
-
- .logo {
- text-decoration: none;
- font-weight: bold;
- font-size: 60px;
-
- img {
- height: 56px;
- }
- }
-}
-
-.nav-links {
- list-style: none;
-}
-
-.nav-item a {
- display: inline-block;
- padding: 10px 15px;
-}
-
-.nav-item:hover {
- background-color: white;
-}
-
-.nav-item:hover a {
- color: $secondary_blue;
-}
-
-$navbar_height: 50px;
-$navbar_padding: 10px;
-.navbar {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- color: #FFF;
- padding-left: $navbar_padding;
- padding-right: $navbar_padding;
- margin-top: $navbar_padding;
-}
-
-.menu {
- display: flex;
- flex-direction: row;
- list-style-type: none;
- margin: 0;
- padding: 0;
-}
-
-.menu > li {
- margin: 0 1rem;
- // overflow: hidden;
-}
-
-.menu-button-container {
- display: none;
- height: 100%;
- width: 30px;
- cursor: pointer;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-}
-
-#menu-toggle {
- display: none;
-}
-
-.menu-button,
-.menu-button::before,
-.menu-button::after {
- display: block;
- background-color: $red;
- position: absolute;
- height: 4px;
- width: 30px;
- transition: transform 400ms cubic-bezier(0.23, 1, 0.32, 1);
- border-radius: 2px;
-}
-
-.menu-button::before {
- content: '';
- margin-top: -8px;
-}
-
-.menu-button::after {
- content: '';
- margin-top: 8px;
-}
-
-#menu-toggle:checked + .menu-button-container .menu-button::before {
- margin-top: 0px;
- transform: rotate(405deg);
-}
-
-#menu-toggle:checked + .menu-button-container .menu-button {
- background: rgba(255, 255, 255, 0);
-}
-
-#menu-toggle:checked + .menu-button-container .menu-button::after {
- margin-top: 0px;
- transform: rotate(-405deg);
-}
-
-@media (max-width: 700px) {
- .menu-button-container {
- display: flex;
- }
- .menu {
- position: absolute;
- top: 0;
- margin-top: $navbar_height + ($navbar_padding * 3);
- left: 0;
- flex-direction: column;
- width: 100%;
- justify-content: left;
- align-items: center;
- }
- #menu-toggle ~ .menu li {
- height: 0;
- margin: 0;
- padding: 0;
- border: 0;
- transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
- }
- #menu-toggle:checked ~ .menu li {
- height: 2.5em;
- padding: 0.5em;
- transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
- }
- .menu > li {
- display: flex;
- justify-content: center;
- margin: 0;
- padding: 0.5em 0;
- width: 100%;
- color: $secondary_blue;
- background-color: $beige;
- }
- #menu-toggle:checked ~ .menu > li {
- border-top: 1px solid #444;
- }
- #menu-toggle:checked ~ .menu > li:last-child {
- border-bottom: 1px solid #444;
- }
-}
-
-.content {
- padding-top: 1em;
- padding-left: 10%;
- padding-right: 10%;
-}
-
-.footer {
- margin-top: auto;
- width: 100%;
-}
-
-.row {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- width: 100%;
-}
-
-.container {
- display: flex;
- flex-direction: column;
- flex-wrap: wrap;
- width: 100%;
-}
-
-.mr-auto {
- margin-right: auto;
-}
-
-.ml-auto {
- margin-left: auto;
-}
-
-.flashes {
- .alert {
- p {
- font-size: 20px;
- margin-top: 10px;
- margin-left: 10px;
- }
- }
-}
-
-form {
- display: flex;
- flex-direction: column;
-
- .form-input {
- display: flex;
- flex-direction: row;
-
- .form-label {
- width: 200px;
- margin-right: 10px;
- }
-
- input[type=text] {
- margin-right: auto;
- width: 400px;
- }
- }
-}
-
-.button,
-input[type=submit],
-input[type=button],
-button[type=submit] {
- border: 1px solid $secondary_blue;
- border-radius: 0.25em;
- padding: 0.5em;
- background-color: $secondary_blue;
- color: $beige;
- cursor: pointer;
- text-transform: uppercase;
- font-weight: bold;
-}
-
-.keys ul li {
- display: flex;
- flex-direction: row;
-
- span {
- margin-right: 1em;
- }
-
- .key-key {
- max-width: 20em;
- overflow: clip;
- }
-}
-
-.printer-name,
-.printer-status,
-.printer-url,
-.printer-date,
-.printer-controls {
- margin-top: 0.1em;
- margin-right: 1em;
- min-width: 10em;
-}
-
-.printer-controls * {
- margin-right: 0.1em;
+.details {
+ overflow: hidden;
}
diff --git a/projects/tentacles/src/python/tentacles/static/css/tirefire/_alerts.scss b/projects/tentacles/src/python/tentacles/static/css/tirefire/_alerts.scss
new file mode 100644
index 0000000..b8f07a7
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/tirefire/_alerts.scss
@@ -0,0 +1,89 @@
+.alert .inner {
+ display: block;
+ padding: 6px;
+ margin: 6px;
+ border-radius: 3px;
+ border: 1px solid rgb(180,180,180);
+ background-color: rgb(212,212,212);
+}
+
+.alert .close {
+ float: right;
+ margin: 3px 12px 0px 0px;
+ cursor: pointer;
+}
+
+.alert .inner,.alert .close {
+ color: rgb(88,88,88);
+}
+
+.alert input {
+ display: none;
+}
+
+.alert input:checked ~ * {
+ animation-name: dismiss,hide;
+ animation-duration: 300ms;
+ animation-iteration-count: 1;
+ animation-timing-function: ease;
+ animation-fill-mode: forwards;
+ animation-delay: 0s,100ms;
+}
+
+.alert.error .inner {
+ border: 1px solid rgb(238,211,215);
+ background-color: rgb(242,222,222);
+}
+
+.alert.error .inner,.alert.error .close {
+ color: rgb(185,74,72);
+}
+
+.alert.success .inner {
+ border: 1px solid rgb(214,233,198);
+ background-color: rgb(223,240,216);
+}
+
+.alert.success .inner,.alert.success .close {
+ color: rgb(70,136,71);
+}
+
+.alert.info .inner {
+ border: 1px solid rgb(188,232,241);
+ background-color: rgb(217,237,247);
+}
+
+.alert.info .inner,.alert.info .close {
+ color: rgb(58,135,173);
+}
+
+.alert.warning .inner {
+ border: 1px solid rgb(251,238,213);
+ background-color: rgb(252,248,227);
+}
+
+.alert.warning .inner,.alert.warning .close {
+ color: rgb(192,152,83);
+}
+
+@keyframes dismiss {
+ 0% {
+ opacity: 1;
+ }
+ 90%, 100% {
+ opacity: 0;
+ font-size: 0.1px;
+ transform: scale(0);
+ }
+}
+
+@keyframes hide {
+ 100% {
+ height: 0px;
+ width: 0px;
+ overflow: hidden;
+ margin: 0px;
+ padding: 0px;
+ border: 0px;
+ }
+}
diff --git a/projects/tentacles/src/python/tentacles/static/css/tirefire/_basics.scss b/projects/tentacles/src/python/tentacles/static/css/tirefire/_basics.scss
new file mode 100644
index 0000000..fa8672a
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/tirefire/_basics.scss
@@ -0,0 +1,36 @@
+@import "tirefire/colors";
+@import "tirefire/fonts";
+
+html,
+body {
+ font-family: 'Aaux Next', sans-serif;
+ background-color: $beige;
+ color: $black;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 99vh;
+}
+
+.footer {
+ margin-top: auto
+}
+
+.u-flex {
+ display: flex;
+}
+
+.inline,
+.inline > * {
+ margin: 0;
+}
+
+.u-mr-auto {
+ margin-right: auto;
+}
+
+.u-ml-auto {
+ margin-left: auto;
+}
diff --git a/projects/tentacles/src/python/tentacles/static/css/tirefire/_colors.scss b/projects/tentacles/src/python/tentacles/static/css/tirefire/_colors.scss
new file mode 100644
index 0000000..aff57f0
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/tirefire/_colors.scss
@@ -0,0 +1,12 @@
+$black: #171426;
+$beige: #F4F8EE;
+$red: #BB2D2E;
+$orange: #CA4F1F;
+$yellow: #EDB822;
+$clear: rgba(255, 255, 255, 255);
+
+$secondary_red: red;
+$secondary_blue: #288BC2;
+$secondary_green: #A5C426;
+$secondary_light_grey: #CACBCA;
+$secondary_dark_grey: #9A9A9A;
diff --git a/projects/tentacles/src/python/tentacles/static/css/tirefire/_dots.scss b/projects/tentacles/src/python/tentacles/static/css/tirefire/_dots.scss
new file mode 100644
index 0000000..b855021
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/tirefire/_dots.scss
@@ -0,0 +1,49 @@
+@import "tirefire/colors";
+
+@keyframes blink {
+ 0% { opacity: 0; }
+ 50% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+.dot {
+ width: var(--dot-size);
+ height: var(--dot-size);
+ background-color: var(--dot-color);
+ border-radius: 50%;
+}
+
+.dot.success {
+ background-color: $secondary_green;
+}
+
+.dot.queued {
+ background-color: $secondary_blue;
+}
+
+.dot.running {
+ background-color: $secondary_green;
+}
+
+.dot.error,
+.dot.cancelled {
+ background-color: $red;
+}
+
+.dot--basic {
+ animation: blink 2s infinite;
+}
+
+.dot--once {
+ animation: blink 2s 1;
+}
+
+.dot--delayed {
+ animation: blink 2s infinite 4s;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .dot {
+ animation: none;
+ }
+}
diff --git a/projects/tentacles/src/python/tentacles/static/css/tirefire/_fonts.scss b/projects/tentacles/src/python/tentacles/static/css/tirefire/_fonts.scss
new file mode 100644
index 0000000..91e42ff
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/tirefire/_fonts.scss
@@ -0,0 +1,29 @@
+@font-face {
+ font-family: 'Aaux Next';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Aaux Next'), url('/static/font/AauxNextBlk.otf') format('otf');
+}
+
+@font-face {
+ font-family: 'Aaux Next';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Aaux Next'), url('/static/font/aauxnextbdwebfont.otf') format('otf');
+}
+
+@font-face {
+ font-family: 'Aaux Next';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Aaux Next'), url('/static/font/aauxnextltwebfont.otf') format('otf');
+}
+
+@font-face {
+ font-family: 'Aaux Next';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Aaux Next'), url('/static/font/aauxnextmdwebfont.otf') format('otf');
+}
+
+@import url(https://fonts.googleapis.com/css?family=Raleway);
diff --git a/projects/tentacles/src/python/tentacles/static/css/tirefire/_nav.scss b/projects/tentacles/src/python/tentacles/static/css/tirefire/_nav.scss
new file mode 100644
index 0000000..8a573bc
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/tirefire/_nav.scss
@@ -0,0 +1,161 @@
+@import "tirefire/colors";
+
+.nav-links {
+ list-style: none;
+}
+
+.nav-item a {
+ display: inline-block;
+ padding: 10px 15px;
+}
+
+.nav-item:hover {
+}
+
+.nav-item:hover a {
+ color: $secondary_blue;
+}
+
+$navbar_height: 50px;
+$navbar_padding: 10px;
+$stripe_thickness: 8px;
+$stripe_padding: ($stripe_thickness / 4);
+
+.navbar {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding-left: $navbar_padding;
+ padding-right: $navbar_padding;
+ margin-top: $navbar_padding;
+ background-color: $beige;
+ box-shadow: 0px (1 * $stripe_thickness + 0 * $stripe_padding) 0px $red,
+ 0px (1 * $stripe_thickness + 1 * $stripe_padding) 0px $clear,
+ 0px (2 * $stripe_thickness + 1 * $stripe_padding) 0px $orange,
+ 0px (2 * $stripe_thickness + 2 * $stripe_padding) 0px $clear,
+ 0px (3 * $stripe_thickness + 2 * $stripe_padding) 0px $yellow;
+ margin-bottom: (3 * ($stripe_thickness + $stripe_padding));
+
+ .logo {
+ text-decoration: none;
+ font-size: 60px;
+
+ a { text-decoration: none; color: $black; }
+
+ img {
+ height: 56px;
+ }
+ }
+}
+
+.menu {
+ display: flex;
+ flex-direction: row;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ z-index: 10000;
+}
+
+.menu > li {
+ margin: 0 1rem;
+ overflow: hidden;
+}
+
+.menu-button-container {
+ display: none;
+ height: 100%;
+ width: 30px;
+ cursor: pointer;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ margin-left: auto; // Float hard right
+}
+
+#menu-toggle {
+ display: none;
+}
+
+.menu-button,
+.menu-button::before,
+.menu-button::after {
+ display: block;
+ background-color: $red;
+ position: absolute;
+ height: 4px;
+ width: 30px;
+ transition: transform 400ms cubic-bezier(0.23, 1, 0.32, 1);
+ border-radius: 2px;
+}
+
+.menu-button::before {
+ content: '';
+ margin-top: -8px;
+}
+
+.menu-button::after {
+ content: '';
+ margin-top: 8px;
+}
+
+#menu-toggle:checked + .menu-button-container .menu-button::before {
+ margin-top: 0px;
+ transform: rotate(405deg);
+}
+
+#menu-toggle:checked + .menu-button-container .menu-button {
+ background: rgba(255, 255, 255, 0);
+}
+
+#menu-toggle:checked + .menu-button-container .menu-button::after {
+ margin-top: 0px;
+ transform: rotate(-405deg);
+}
+
+//@media (max-width: 700px) {
+ .menu-button-container {
+ display: flex;
+ }
+ .menu {
+ position: absolute;
+ top: 0;
+ margin-top: $navbar_height + ($navbar_padding * 3);
+ left: 0;
+ flex-direction: column;
+ width: 100%;
+ justify-content: left;
+ align-items: center;
+ }
+ #menu-toggle ~ .menu li {
+ height: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
+ }
+ #menu-toggle:checked ~ .menu li {
+ height: 2.5em;
+ padding: 0.5em;
+ transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
+ }
+ .menu > li {
+ display: flex;
+ justify-content: center;
+ margin: 0;
+ padding: 0.5em 0;
+ width: 100%;
+ color: $secondary_blue;
+ background-color: $beige;
+ }
+ /*
+ #menu-toggle:checked ~ .menu > li {
+ border-top: 1px solid #444;
+ }
+ */
+ #menu-toggle:checked ~ .menu > li:last-child {
+ border-bottom: 1px solid #444;
+ }
+//}
diff --git a/projects/tentacles/src/python/tentacles/static/css/tirefire/_timers.scss b/projects/tentacles/src/python/tentacles/static/css/tirefire/_timers.scss
new file mode 100644
index 0000000..4cf1a64
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/static/css/tirefire/_timers.scss
@@ -0,0 +1,52 @@
+@import "tirefire/colors";
+
+.timer {
+ background: -webkit-linear-gradient(left, $secondary_light_grey 50%, $clear 50%);
+ border-radius: 100%;
+ height: calc(var(--size) * 1px);
+ width: calc(var(--size) * 1px);
+ position: relative;
+ -webkit-animation: time calc(var(--duration) * 1s) steps(1000, start);
+ -webkit-mask: radial-gradient(transparent 50%, #000 50%);
+ mask: radial-gradient(transparent 50%, #000 50%);
+}
+.mask {
+ border-radius: 100% 0 0 100% / 50% 0 0 50%;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 50%;
+ -webkit-animation: mask calc(var(--duration) * 1s) steps(500, start);
+ -webkit-transform-origin: 100% 50%;
+}
+@-webkit-keyframes time {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+@-webkit-keyframes mask {
+ 0% {
+ background: $clear;
+ -webkit-transform: rotate(0deg);
+ }
+ 50% {
+ background: $clear;
+ -webkit-transform: rotate(-180deg);
+ }
+ 50.01% {
+ background: $secondary_light_grey;
+ -webkit-transform: rotate(0deg);
+ }
+ 100% {
+ background: $secondary_light_grey;
+ -webkit-transform: rotate(-180deg);
+ }
+}
+
+.alert .timer {
+ --size: 10;
+ --duration: 5;
+ padding: 6px;
+ margin: 6px;
+}
diff --git a/projects/tentacles/src/python/tentacles/store.py b/projects/tentacles/src/python/tentacles/store.py
index 7baac77..54dd9f4 100644
--- a/projects/tentacles/src/python/tentacles/store.py
+++ b/projects/tentacles/src/python/tentacles/store.py
@@ -4,7 +4,6 @@ from collections import namedtuple
from datetime import datetime, timedelta
from hashlib import sha3_256
from importlib.resources import files
-from pathlib import Path
import sqlite3
from textwrap import indent
from typing import Optional
@@ -187,10 +186,10 @@ class Store(object):
@requires_conn
def list_keys(self, uid: int):
- for id, name, exp in self._conn.execute(
- "SELECT id, name, expiration FROM user_keys WHERE user_id = ?1", [uid]
- ).fetchall():
- yield id, name, datetime.fromisoformat(exp) if exp else None
+ return [(id, name, datetime.fromisoformat(exp) if exp else None)
+ for id, name, exp in self._conn.execute(
+ "SELECT id, name, expiration FROM user_keys WHERE user_id = ?1 AND name != 'web session'", [uid]
+ ).fetchall()]
@requires_conn
def fetch_key(self, kid) -> tuple:
@@ -296,7 +295,7 @@ class Store(object):
# A record of local files on disk, and the users who own then.
@fmap(one)
@requires_conn
- def create_file(self, uid: int, name: str, path: Path) -> int:
+ def create_file(self, uid: int, name: str, path: str) -> int:
return self._conn.execute(
"INSERT INTO files (user_id, filename, path, upload_date) VALUES (?, ?, ?, datetime('now')) RETURNING (id)",
[uid, name, path],
@@ -313,8 +312,8 @@ class Store(object):
self._conn.execute("DELETE FROM files WHERE user_id = ? AND id = ?", [uid, fid])
@requires_conn
- def fetch_file(self, fid: int):
- return self._conn.execute("SELECT * FROM files WHERE id = ?", [fid]).fetchone()
+ def fetch_file(self, uid: int, fid: int):
+ return self._conn.execute("SELECT * FROM files WHERE user_id = ?1 AND id = ?2", [uid, fid]).fetchone()
################################################################################
# Job
@@ -346,6 +345,28 @@ class Store(object):
f"""
SELECT * FROM jobs
WHERE {cond}
+ """,
+ [],
+ ).fetchall()
+
+ @requires_conn
+ def list_jobs_by_file(self, fid: int):
+ return self._conn.execute(
+ f"""
+ SELECT * FROM jobs
+ WHERE file_id = ?1
+ """,
+ [fid],
+ ).fetchall()
+
+ @requires_conn
+ def list_job_queue(self, uid: Optional[int] = None):
+ """Enumerate jobs in priority order. Note: ignores completed jobs."""
+ cond = f"user_id = {uid}" if uid else "TRUE"
+ return self._conn.execute(
+ f"""
+ SELECT * FROM jobs
+ WHERE finished_at IS NULL AND {cond}
ORDER BY priority DESC
""",
[],
@@ -365,7 +386,15 @@ class Store(object):
@requires_conn
def list_running_jobs(self):
- """Scheduler detail. List running jobs."""
+ """Scheduler detail. List running jobs.
+
+ Note that jobs for which cancellation has been requested but which HAVE
+ NOT YET BEEN ACKNOWLEDGED AS CANCELLED BY OctoPrint must still be
+ "running". This prevents the cancelling printer from being rescheduled
+ prematurely and from allows the job to be moved into the cancelled state
+ by normal printer status inspection.
+
+ """
return self._conn.execute(
"""
@@ -377,6 +406,21 @@ class Store(object):
[],
).fetchall()
+ @requires_conn
+ def list_cancelled_jobs(self):
+ """Scheduler detail. List jobs which have been cancelled but are still 'live'."""
+
+ return self._conn.execute(
+ """
+ SELECT * FROM jobs
+ WHERE started_at IS NOT NULL
+ AND printer_id IS NOT NULL
+ AND finished_at IS NULL
+ AND cancelled_at IS NOT NULL
+ """,
+ [],
+ ).fetchall()
+
@fmap(one)
@requires_conn
def poll_job_queue(self):
@@ -398,12 +442,26 @@ class Store(object):
"SELECT * FROM jobs WHERE user_id = ? AND id = ?", [uid, jid]
).fetchone()
+ @requires_conn
+ def fetch_job_by_printer(self, pid: int) -> Optional[tuple]:
+ """Find 'the' mapped incomplete job for a given printer."""
+
+ return self._conn.execute(
+ "SELECT * FROM jobs WHERE printer_id = ? AND finished_at IS NULL", [pid]
+ ).fetchone()
+
@requires_conn
def assign_job(self, job_id: int, printer_id: int):
return self._conn.execute(
"UPDATE jobs SET printer_id = ?2 WHERE id = ?1", [job_id, printer_id]
)
+ @requires_conn
+ def cancel_job(self, uid: int, job_id: int):
+ return self._conn.execute(
+ "UPDATE jobs SET cancelled_at = datetime('now') WHERE user_id = ?1 AND id = ?2", [uid, job_id]
+ )
+
@requires_conn
def start_job(self, job_id: int):
return self._conn.execute(
@@ -411,10 +469,10 @@ class Store(object):
)
@requires_conn
- def finish_job(self, job_id: int, state: str):
+ def finish_job(self, job_id: int, state: str, message: str = None):
return self._conn.execute(
- "UPDATE jobs SET finished_at = datetime('now'), state = ?2 WHERE id = ?1",
- [job_id, state],
+ "UPDATE jobs SET finished_at = datetime('now'), state = ?2, message = ?3 WHERE id = ?1",
+ [job_id, state, message],
)
@requires_conn
diff --git a/projects/tentacles/src/python/tentacles/templates/add_printer.html.j2 b/projects/tentacles/src/python/tentacles/templates/add_printer.html.j2
index 49545b4..5074a97 100644
--- a/projects/tentacles/src/python/tentacles/templates/add_printer.html.j2
+++ b/projects/tentacles/src/python/tentacles/templates/add_printer.html.j2
@@ -1,16 +1,30 @@
{% extends "base.html.j2" %}
{% block content %}
-
-