UI overhaul; job cancellation
This commit is contained in:
parent
5709cac469
commit
af741dfeeb
35 changed files with 1755 additions and 708 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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":
|
||||
@BLUEPRINT.route("/jobs", methods=["GET"])
|
||||
def list_jobs():
|
||||
return render_template("jobs.html.j2")
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
return redirect("/")
|
||||
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")
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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) {}
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
//}
|
|
@ -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;
|
||||
}
|
|
@ -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):
|
||||
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", [uid]
|
||||
).fetchall():
|
||||
yield id, name, datetime.fromisoformat(exp) if exp else None
|
||||
"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
|
||||
|
|
|
@ -1,16 +1,30 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Add printer</h1>
|
||||
<form class="row" method="post">
|
||||
<span class="form-input row"><span class="form-label">Printer name</span><input type="text" name="name" /></span>
|
||||
<span class="form-input row"><span class="form-label">Printer API URL</span><input type="text" name="url" /></span>
|
||||
<span class="form-input row"><span class="form-label">API key</span><input type="text" name="api_key" /></span>
|
||||
<div class="row">
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<label for="name">Printer name</label>
|
||||
<input type="text" name="name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<label for="url">Printer base URL</label>
|
||||
<input type="text" name="url" />
|
||||
</div>
|
||||
<div class="six columns">
|
||||
<label for="api_key">API key</label>
|
||||
<input type="text" name="api_key" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="hidden" name="tested" value="false" />
|
||||
<span class="row">
|
||||
<span><input id="test" type="button" value="Test" enabled="false" /></span>
|
||||
<span><input id="submit" type="submit" value="Add" onclick="maybeSubmit();" /></span>
|
||||
</span>
|
||||
<input id="test" type="button" value="Test" enabled="false" />
|
||||
<input id="submit" type="submit" value="Add" onclick="maybeSubmit();" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<span class="logo">
|
||||
<nav class="container navbar">
|
||||
<span class="logo six columns">
|
||||
<a class="row" href="/">
|
||||
<img src="/static/tentacles.svg" alt="Tentacles">
|
||||
<span class="color-yellow">Tentacles</span>
|
||||
|
@ -24,21 +24,23 @@
|
|||
</div>
|
||||
</label>
|
||||
|
||||
<ul class="menu">
|
||||
<ul class="menu container">
|
||||
{% if not ctx.uid %}
|
||||
<li><a href="/user/login"><span class="button slide">Log in</span></a></li>
|
||||
<li><a href="/user/register"><span class="button slide">Register<span></a></li>
|
||||
<li><a class="twelve columns slide" href="/user/login">Log in</a></li>
|
||||
<li><a class="twelve columns slide" href="/user/register">Register</a></li>
|
||||
{% else %}
|
||||
<li><a class="twelve columns button slide" href="/jobs">Jobs</a></li>
|
||||
<li><a class="twelve columns button slide" href="/files">Files</a></li>
|
||||
{% if ctx.is_admin %}
|
||||
<li><a href="/printers"><span class="button slide">Printers</span></a></li>
|
||||
<li><a class="twelve columns button slide" href="/printers">Printers</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/user"><span class="button slide">Settings</span></a></li>
|
||||
<li><a href="/user/logout"><span class="button slide">Log out</span></a></li>
|
||||
<li><a class="twelve columns button slide" href="/user">Settings</a></li>
|
||||
<li><a class="twelve columns button slide" href="/user/logout">Log out</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container content">
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
|
@ -60,7 +62,7 @@
|
|||
|
||||
{% block content %}Oops, an empty page :/{% endblock %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="container footer">
|
||||
{% block footer %}
|
||||
© Copyright 2023 by <a href="https://arrdem.com/">@arrdem</a>.
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<div class="row twelve columns">
|
||||
{% include "files_list.html.j2" %}
|
||||
</div>
|
||||
|
||||
<div class="row twelve columns">
|
||||
<h2>Upload a file</h2>
|
||||
<form method="post" action="/files" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" value="upload" />
|
||||
<input type="hidden" name="select" value="false" />
|
||||
<input type="hidden" name="start" value="false" />
|
||||
<div class="row">
|
||||
<input type="file" name="file" accept=".gcode,text/plain" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input id="submit" type="submit" value="Upload"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,31 +1,21 @@
|
|||
{% import "macros.html.j2" as macros %}
|
||||
<div class="panel files">
|
||||
<div class="files">
|
||||
<h2>Files</h2>
|
||||
{% with files = ctx.db.list_files(uid=ctx.uid) %}
|
||||
{% if files %}
|
||||
<ul>
|
||||
{% for file in files %}
|
||||
<li class="file">
|
||||
<div class="file row u-flex">
|
||||
<div class="details six columns">
|
||||
<span class="file-name">{{ file.filename }}</span>
|
||||
<span class="file-controls">
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
{{ macros.start_job(file.id) }}
|
||||
{{ macros.delete_file(file.id) }}
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
You don't have any files. Upload something!
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="panel file-upload">
|
||||
<h2>Upload a file</h2>
|
||||
<form method="post" action="/api/files/local" enctype="multipart/form-data">
|
||||
<input type="hidden" name="select" value="false" />
|
||||
<input type="hidden" name="start" value="false" />
|
||||
<input type="file" name="file" accept=".gcode,text/plain" />
|
||||
<span><input id="submit" type="submit" value="Upload"/></span>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<div class="row twelve columns">
|
||||
{% include "jobs_list.html.j2" %}
|
||||
</div>
|
||||
|
||||
{% if ctx.uid %}
|
||||
<div class="row twelve columns">
|
||||
{% include "files_list.html.j2" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
{% include "jobs_list.html.j2" %}
|
||||
{% include "jobs_history.html.j2" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{% import "macros.html.j2" as macros %}
|
||||
<div class="history">
|
||||
<h2>Job history</h2>
|
||||
{% with jobs = ctx.db.list_jobs(uid=ctx.uid) %}
|
||||
{% if jobs %}
|
||||
{% for job in jobs if job.finished_at %}
|
||||
<div class="job row u-flex">
|
||||
<div class="details">
|
||||
<span class="job-status one column">
|
||||
<div class="dot {{ macros.job_state(job) }} {{ 'dot--basic' if not job.state else '' }}" style="--dot-size: 1em;">
|
||||
</div>
|
||||
</span>
|
||||
<span class="job-filename eleven columns">{{ctx.db.fetch_file(ctx.uid, job.file_id).filename}}</span>
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
{{ macros.delete_job(job.id) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No job history to display.</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
|
@ -1,16 +1,25 @@
|
|||
<div class="panel queue">
|
||||
{% import "macros.html.j2" as macros %}
|
||||
<div class="queue">
|
||||
<h2>Job queue</h2>
|
||||
{% with jobs = ctx.db.list_jobs(uid=ctx.uid) %}
|
||||
{% with jobs = ctx.db.list_job_queue(uid=ctx.uid) %}
|
||||
{% if jobs %}
|
||||
<ul>
|
||||
{% for job in jobs %}
|
||||
<li class="job">
|
||||
<span class="job-id">{{job.id}}</span>
|
||||
<span class="job-filename">{{ctx.db.fetch_file(job.file_id).filename}}</span>
|
||||
<span class="job-status">{{ 'pending' if not job.printer_id else 'uploading' if not job.started_at else 'running' if not job.finished_at else job.state }}
|
||||
</li>
|
||||
<div class="job row u-flex">
|
||||
<div class="details seven colums">
|
||||
<div class="job-status one column">
|
||||
<div class="dot {{ macros.job_state(job) }} {{ 'dot--basic' if not job.state else '' }}" style="--dot-size: 1em;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-filename six columns">
|
||||
{{ctx.db.fetch_file(ctx.uid, job.file_id).filename}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
{{ macros.duplicate_job(job.id) }}
|
||||
{{ macros.cancel_job(job.id) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
No pending tasks. {% if ctx.uid %}Start something!{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,15 +1,47 @@
|
|||
{# #################################################################################################### #}
|
||||
{# Job CRUD #}
|
||||
{% macro start_job(id) %}
|
||||
<form method="post" action="/jobs">
|
||||
<input type="hidden" name="action" value="queue" />
|
||||
<form class="inline" method="post" action="/jobs">
|
||||
<input type="hidden" name="action" value="enqueue" />
|
||||
<input type="hidden" name="file_id" value="{{ id }}" />
|
||||
<span><input id="submit" type="submit" value="Queue"/></span>
|
||||
<input id="submit" type="submit" value="Enqueue"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro delete_file(id) %}
|
||||
<form method="post" action="/files">
|
||||
<input type="hidden" name="action" value="delete" />
|
||||
<input type="hidden" name="id" value="{{ id }}" />
|
||||
<span><input id="submit" type="submit" value="Delete"/></span>
|
||||
{% macro duplicate_job(id) %}
|
||||
<form class="inline" method="post" action="/jobs">
|
||||
<input type="hidden" name="action" value="duplicate" />
|
||||
<input type="hidden" name="job_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Duplicate"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro cancel_job(id) %}
|
||||
<form class="inline" method="post" action="/jobs">
|
||||
<input type="hidden" name="action" value="cancel" />
|
||||
<input type="hidden" name="job_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Cancel"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro delete_job(id) %}
|
||||
<form class="inline" method="post" action="/jobs">
|
||||
<input type="hidden" name="action" value="delete" />
|
||||
<input type="hidden" name="job_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Delete"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro job_state(job) %}
|
||||
{{ 'queued' if (not job.finished_at and not job.printer_id) else 'running' if not job.finished_at else job.state }}
|
||||
{% endmacro %}
|
||||
|
||||
{# #################################################################################################### #}
|
||||
{# File CRUD #}
|
||||
{% macro delete_file(id) %}
|
||||
<form class="inline" method="post" action="/files">
|
||||
<input type="hidden" name="action" value="delete" />
|
||||
<input type="hidden" name="file_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Delete"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<div class="twelve columns">
|
||||
{% include "printers_list.html.j2" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,25 +2,27 @@
|
|||
<h2>Printers</h2>
|
||||
{% with printers = ctx.db.list_printers() %}
|
||||
{% if printers %}
|
||||
<ul>
|
||||
{% for printer in printers %}
|
||||
{% with id, name, url, _api_key, last_poll, status = printer %}
|
||||
<li class="printer row">
|
||||
<div class="printer row u-flex">
|
||||
<div class="details six columns">
|
||||
<span class="printer-name">{{name}}</span>
|
||||
<span class="printer-url"><code>{{url}}</code></span>
|
||||
<span class="printer-status">{{status}}</span>
|
||||
<span class="printer-date">{{last_poll}}</span>
|
||||
</div>
|
||||
{# FIXME: How should these action buttons work? #}
|
||||
<span class="printer-controls ml-auto">
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
<a class="button" href="/printers/test?id={{id}}">Test</a>
|
||||
<a class="button" href="/printers/edit?id={{id}}">Edit</a>
|
||||
<a class="button" href="/printers/delete?id={{id}}">Remove</a>
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if ctx.is_admin %}<a class="button" href="/printers/add">Add a printer</a>{% endif %}
|
||||
{% if ctx.is_admin %}
|
||||
<a class="button" href="/printers/add">Add a printer</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No printers available. {% if ctx.is_admin %}<a href="/printers/add">Configure one!</a>{% else %}Ask the admin to configure one!{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,45 +1,53 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<h1>User settings</h1>
|
||||
<div class="keys container">
|
||||
<div class="row twelve columns keys">
|
||||
<h2>API keys</h2>
|
||||
{% with keys = ctx.db.list_keys(ctx.uid) %}
|
||||
<ul>
|
||||
{% for id, name, exp in keys if name != 'web session' %}
|
||||
<li class="row">
|
||||
<div class="row key">
|
||||
<div class="row">
|
||||
{% if keys %}
|
||||
{% for id, name, exp in keys %}
|
||||
<div class="row key u-flex">
|
||||
<div class="details six columns">
|
||||
<span class="key-name">{{ name or 'anonymous' }}</span>
|
||||
<span class="key-expiration ml-auto">{{ 'Expires in ' if exp else ''}}{{ exp - datetime.now() if exp else 'Never expires' }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="key-key">{{ id[:10] }}...</span>
|
||||
<form method="post" class="ml-auto">
|
||||
<span class="key-expiration u-ml-auto">{{ 'Expires in ' if exp else ''}}{{ exp - datetime.now() if exp else 'Never expires' }}</span>
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
<form class="inline" method="post" class="ml-auto">
|
||||
<input type="hidden" name="action" value="revoke">
|
||||
<input type="hidden" name="id" value="{{ id }}">
|
||||
<span><input id="submit" type="submit" value="Revoke"/></span>
|
||||
<input id="submit" type="submit" value="Revoke"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No API keys configured.</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="keys-form container">
|
||||
<div class="row twelve columns keys">
|
||||
<h2>Add a key</h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<span class="form-input mr-auto row"><span class="form-label">API key name</span><input type="text" name="name" /></span>
|
||||
<span class="form-input mr-auto row"><span class="form-label">Key lifetime</span>
|
||||
<div class="row">
|
||||
<div class="row six columns">
|
||||
<label for="name">API key name</label>
|
||||
<input class="u-full-width" type="text" name="name" value="anonymous" />
|
||||
</div>
|
||||
<div class="row six columns">
|
||||
<label for="ttl">Key lifetime</label>
|
||||
<select name="ttl">
|
||||
<option value="30d">30 days</option>
|
||||
<option value="90d">90 days</option>
|
||||
<option value="365d">1y</option>
|
||||
<option value="forever">Forever (not recommended)</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="form-input mr-auto row"><input id="submit" type="submit" value="Add" onclick="maybeSubmit();" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input id="submit" type="submit" value="Add" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,19 +7,23 @@ Supporting the core app with asynchronous maintenance tasks.
|
|||
Mostly related to monitoring and managing Printer state.
|
||||
"""
|
||||
|
||||
from time import sleep
|
||||
from threading import Thread, Event
|
||||
from typing import Callable
|
||||
from datetime import timedelta, datetime
|
||||
import logging
|
||||
from contextlib import closing
|
||||
from urllib import parse as urlparse
|
||||
from tentacles.store import Store
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from threading import Event, Thread
|
||||
from time import sleep
|
||||
from typing import Callable
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from octorest import OctoRest as _OR
|
||||
from requests import Response
|
||||
from requests.exceptions import HTTPError, ConnectionError, Timeout
|
||||
from requests.exceptions import (
|
||||
ConnectionError,
|
||||
HTTPError,
|
||||
Timeout,
|
||||
)
|
||||
from tentacles.store import Store
|
||||
|
||||
|
||||
class OctoRest(_OR):
|
||||
|
@ -67,25 +71,41 @@ def poll_printers(db_factory: Callable[[], Store]) -> None:
|
|||
with closing(db_factory()) as db:
|
||||
for printer in db.list_printers():
|
||||
id, name, url, api_key, last_poll, status = printer
|
||||
mapped_job = db.fetch_job_by_printer(id)
|
||||
try:
|
||||
client = OctoRest(url=url, apikey=api_key)
|
||||
job = client.job_info()
|
||||
state = client.printer().get("state").get("flags", {})
|
||||
if state.get("error"):
|
||||
printer_job = client.job_info()
|
||||
print(printer_job)
|
||||
try:
|
||||
printer_state = client.printer().get("state").get("flags", {})
|
||||
except:
|
||||
printer_state = {"error": printer_job.get("error")}
|
||||
|
||||
if printer_state.get("error"):
|
||||
# If there's a mapped job, we manually fail it so that
|
||||
# there's no possibility of a sync problem between the
|
||||
# polling tasks. This violates separation of concerns a bit,
|
||||
# but appears required for correctness.
|
||||
if mapped_job:
|
||||
db.finish_job(mapped_job.id, "error")
|
||||
|
||||
db.update_printer_status(id, "error")
|
||||
elif state.get("ready"):
|
||||
|
||||
elif printer_state.get("ready"):
|
||||
db.update_printer_status(id, "idle")
|
||||
elif state.get("printing"):
|
||||
|
||||
elif printer_state.get("printing"):
|
||||
db.update_printer_status(id, "running")
|
||||
|
||||
else:
|
||||
raise Exception(f"Indeterminate state {state!r}")
|
||||
raise Exception(f"Indeterminate state {printer_state!r}")
|
||||
|
||||
except (ConnectionError, Timeout):
|
||||
db.update_printer_status(id, "error")
|
||||
|
||||
except HTTPError as e:
|
||||
assert isinstance(e.response, Response)
|
||||
if e.response.status_code in [403, 401] or "error" in job:
|
||||
if e.response.status_code in [403, 401] or "error" in printer_job:
|
||||
db.update_printer_status(id, "error")
|
||||
elif e.response.json().get("error") == "Printer is not operational":
|
||||
db.update_printer_status(id, "disconnected")
|
||||
|
@ -113,7 +133,11 @@ def push_jobs(db_factory: Callable[[], Store]) -> None:
|
|||
with closing(db_factory()) as db:
|
||||
for job in db.list_mapped_jobs():
|
||||
printer = db.fetch_printer(job.printer_id)
|
||||
file = db.fetch_file(job.file_id)
|
||||
file = db.fetch_file(job.user_id, job.file_id)
|
||||
if not file:
|
||||
log.error(f"Job {job.id} no longer maps to a file")
|
||||
db.delete_job(job.user_id, job.id)
|
||||
|
||||
try:
|
||||
client = OctoRest(url=printer.url, apikey=printer.api_key)
|
||||
try:
|
||||
|
@ -134,6 +158,23 @@ def push_jobs(db_factory: Callable[[], Store]) -> None:
|
|||
log.exception("Oop")
|
||||
|
||||
|
||||
@corn_job(timedelta(seconds=5))
|
||||
def revoke_jobs(db_factory: Callable[[], Store]) -> None:
|
||||
"""Ensure that job files are uploaded and started to the assigned printer."""
|
||||
|
||||
with closing(db_factory()) as db:
|
||||
for job in db.list_cancelled_jobs():
|
||||
printer = db.fetch_printer(job.printer_id)
|
||||
try:
|
||||
client = OctoRest(url=printer.url, apikey=printer.api_key)
|
||||
client.cancel()
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
log.exception("Oop")
|
||||
|
||||
|
||||
@corn_job(timedelta(seconds=5))
|
||||
def pull_jobs(db_factory: Callable[[], Store]) -> None:
|
||||
"""Poll the state of mapped printers to control jobs."""
|
||||
|
@ -150,6 +191,7 @@ def create_workers(db_factory: Callable[[], Store]) -> Event:
|
|||
Thread(target=poll_printers, args=[db_factory]).start()
|
||||
Thread(target=assign_jobs, args=[db_factory]).start()
|
||||
Thread(target=push_jobs, args=[db_factory]).start()
|
||||
Thread(target=revoke_jobs, args=[db_factory]).start()
|
||||
Thread(target=pull_jobs, args=[db_factory]).start()
|
||||
|
||||
return SHUTDOWN
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import timedelta
|
||||
import tentacles.store as s
|
||||
|
||||
import pytest
|
||||
import tentacles.store as s
|
||||
|
||||
|
||||
@pytest.yield_fixture
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
from tentacles.store import Store
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_store_initializes(store: Store):
|
||||
assert isinstance(store, Store)
|
||||
|
|
Loading…
Reference in a new issue