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

Add printer

-
- Printer name - Printer API URL - API key - - - - - +

Add printer

+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ + + +
diff --git a/projects/tentacles/src/python/tentacles/templates/base.html.j2 b/projects/tentacles/src/python/tentacles/templates/base.html.j2 index 197d8ee..e30d294 100644 --- a/projects/tentacles/src/python/tentacles/templates/base.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/base.html.j2 @@ -10,9 +10,9 @@ {% endblock %} -
-