From 8712ddc377ca726c3ebb20b41e987e25e29d3b10 Mon Sep 17 00:00:00 2001
From: Reid D McKenzie <me@arrdem.com>
Date: Fri, 7 Feb 2025 23:59:32 -0700
Subject: [PATCH] Color and continuous printing

---
 .../tentacles/src/tentacles/blueprints/api.py |  1 +
 .../src/tentacles/blueprints/job_ui.py        |  5 +-
 projects/tentacles/src/tentacles/sql/jobs.sql | 79 +++++++++++--------
 .../src/tentacles/templates/jobs_list.html.j2 | 44 +++++------
 .../src/tentacles/templates/macros.html.j2    |  8 +-
 projects/tentacles/src/tentacles/workers.py   |  3 +-
 6 files changed, 81 insertions(+), 59 deletions(-)

diff --git a/projects/tentacles/src/tentacles/blueprints/api.py b/projects/tentacles/src/tentacles/blueprints/api.py
index 1f9b9a5..a16eef6 100644
--- a/projects/tentacles/src/tentacles/blueprints/api.py
+++ b/projects/tentacles/src/tentacles/blueprints/api.py
@@ -119,6 +119,7 @@ def create_file(location: Optional[str] = None):
                 fid=row.id,
                 cid=None,
                 pid=None,
+                cont=False,  # FIXME: How to decide this?
             )
 
         return {"status": "ok"}, 202
diff --git a/projects/tentacles/src/tentacles/blueprints/job_ui.py b/projects/tentacles/src/tentacles/blueprints/job_ui.py
index d9ae2fa..03d6466 100644
--- a/projects/tentacles/src/tentacles/blueprints/job_ui.py
+++ b/projects/tentacles/src/tentacles/blueprints/job_ui.py
@@ -39,17 +39,20 @@ def manipulate_jobs():
                 fid=int(request.form.get("file_id")),
                 cid=maybe(int, request.form.get("color_id")),
                 pid=maybe(int, request.form.get("printer_id")),
+                cont=request.form.get("continuous") == "on",
             )
             flash("Job created!", category="info")
 
         case "duplicate":
             if job := ctx.db.fetch_job(
-                uid=ctx.uid, jid=int(request.form.get("job_id"))
+                uid=ctx.uid,
+                jid=int(request.form.get("job_id")),
             ):
                 ctx.db.create_job(
                     uid=ctx.uid,
                     fid=job.file_id,
                     cid=job.color_id,
+                    cont=job.continuous,
                     # FIXME: Need to dissociate job copies from the original mapped printer. Can't tell the difference
                     # between the requested printer in the original job and the mapped printer from a scheduler run.
                     pid=None,
diff --git a/projects/tentacles/src/tentacles/sql/jobs.sql b/projects/tentacles/src/tentacles/sql/jobs.sql
index 990f485..19973f4 100644
--- a/projects/tentacles/src/tentacles/sql/jobs.sql
+++ b/projects/tentacles/src/tentacles/sql/jobs.sql
@@ -27,10 +27,13 @@ CREATE TABLE IF NOT EXISTS jobs (
 );
 
 -- name: migration-0001-jobs-add-print-time#
-ALTER TABLE jobs ADD COLUMN IF NOT EXISTS time_left INTEGER DEFAULT (0);
+ALTER TABLE jobs ADD COLUMN time_left INTEGER DEFAULT (0);
 
 -- name: migration-0002-jobs-add-print-color#
-ALTER TABLE jobs ADD COLUMN IF NOT EXISTS color_id INTEGER DEFAULT (NULL);
+ALTER TABLE jobs ADD COLUMN color_id INTEGER DEFAULT (NULL);
+
+-- name: migration-0003-jobs-add-continuous#
+ALTER TABLE jobs ADD COLUMN continuous BOOLEAN DEFAULT (FALSE);
 
 -- name: create-job^
 INSERT INTO jobs (
@@ -38,12 +41,14 @@ INSERT INTO jobs (
  , file_id
  , color_id
  , printer_id
+ , continuous
 )
 VALUES (
    :uid
  , :fid
  , :cid
  , :pid
+ , :cont
 )
 RETURNING
    *
@@ -78,36 +83,46 @@ WHERE
 
 -- name: list-job-queue
 SELECT
-   j.id as id
- , j.file_id
- , coalesce(j.color_id, fa.color_id) as color_id
- , fa.id as analysis_id
- , fa.max_x
- , fa.max_y
- , fa.max_z
- , fa.max_bed
- , fa.max_end
- , fa.nozzle_diameter
- , fa.filament_id
- , (SELECT name FROM filament WHERE id = fa.filament_id) AS filament_name
- , (SELECT name AS name FROM filament_color WHERE id = coalesce(j.color_id, fa.color_id)) AS color_name
- , (SELECT name FROM job_statuses WHERE id = j.status_id) AS status
- , j.started_at
- , j.time_left
- , j.cancelled_at
- , j.finished_at
- , j.user_id
- , j.printer_id
-FROM jobs j
-INNER JOIN files f
-  ON j.file_id = f.id
-LEFT JOIN file_analysis fa
-  ON fa.file_id = f.id
-WHERE
-   finished_at IS NULL
-   AND cancelled_at IS NULL
-   AND (:uid IS NULL OR j.user_id = :uid)
-   AND f.id IS NOT NULL
+   *
+FROM (
+  SELECT
+     j.id as id
+   , j.file_id
+   , coalesce(j.color_id, fa.color_id) as color_id
+   , fa.id as analysis_id
+   , fa.max_x
+   , fa.max_y
+   , fa.max_z
+   , fa.max_bed
+   , fa.max_end
+   , fa.nozzle_diameter
+   , fa.filament_id
+   , (SELECT name FROM filament WHERE id = fa.filament_id) AS filament_name
+   , (SELECT name AS name FROM filament_color WHERE id = coalesce(j.color_id, fa.color_id)) AS color_name
+   , j.status_id
+   , (SELECT name FROM job_statuses WHERE id = j.status_id) AS status
+   , j.started_at
+   , j.time_left
+   , j.cancelled_at
+   , j.finished_at
+   , j.user_id
+   , j.printer_id
+   , j.continuous
+  FROM jobs j
+  INNER JOIN files f
+    ON j.file_id = f.id
+  LEFT JOIN file_analysis fa
+    ON fa.file_id = f.id
+  WHERE
+     finished_at IS NULL
+     AND cancelled_at IS NULL
+     AND (:uid IS NULL OR j.user_id = :uid)
+     AND f.id IS NOT NULL
+)
+ORDER BY
+   status_id DESC
+ , continuous DESC
+ , id
 ;
 
 -- name: poll-job-queue^
diff --git a/projects/tentacles/src/tentacles/templates/jobs_list.html.j2 b/projects/tentacles/src/tentacles/templates/jobs_list.html.j2
index 6ccd812..77e2bf5 100644
--- a/projects/tentacles/src/tentacles/templates/jobs_list.html.j2
+++ b/projects/tentacles/src/tentacles/templates/jobs_list.html.j2
@@ -9,32 +9,14 @@
       <label for="filename">File</label>
       <span name="filename">{{ctx.db.fetch_file(ctx.uid, job.file_id).filename or "it's a secret"}}</span>
     </div>
-    {% if job.printer_id %}
-    <div class="job-printer u-flex u-flex-break">
-      <label for="printer">Printer</label>
-      <span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span>
-    </div>
-    {% endif %}
-    {% if job.started_at %}
-      <div class="job-runtime u-flex u-flex-break">
-        <label for="runtime">Runtime</label>
-        <span name="Runtime">{{ (datetime.utcnow() - datetime.fromisoformat(job.started_at)) }}</span>
-      </div>
-    {% endif %}
-    {% if job.time_left > 0 %}
-      <div class="job-remaining-time u-flex u-flex-break">
-        <label for="remaining-time">Remaining time</label>
-        <span name="remaining-time">{{ timedelta(seconds=job.time_left) }}</span>
-      </div>
-    {% endif %}
-    <div class="job-constraint u-flex u-flex-break">
+    <div class="job-constraint u-flex">
       <label>Material</label>
       {{ job.color_name }}
       {{ job.filament_name }}
     </div>
-    <div class="job-constraint u-flex u-flex-break">
-      <label>Limits</label>
-      {{ job.max_x }}mm x{{ job.max_y }}mm x{{ job.max_z }}mm, bed {{ job.max_bed }}c, end {{ job.max_end }}c, nozzle {{ "%.2f"|format(job.nozzle_diameter) }}mm
+    <div class="job-cont u-flex">
+      <label>Continuous</label>
+      {% if job.continuous == 1 %}True{% else %}False{% endif %}
     </div>
     {% if job.user_id != ctx.uid %}
     <div class="job-user u-flex">
@@ -42,6 +24,24 @@
       {{ ctx.db.fetch_user(uid=job.user_id).name }}
     </div>
     {% endif %}
+    {% if job.printer_id %}
+    <div class="job-printer u-flex">
+      <label for="printer">Printer</label>
+      <span name="printer">{{ ctx.db.fetch_printer(job.printer_id).name }}</span>
+    </div>
+    {% endif %}
+    {% if job.started_at %}
+      <div class="job-runtime u-flex">
+        <label for="runtime">Runtime</label>
+        <span name="Runtime">{{ (datetime.utcnow() - datetime.fromisoformat(job.started_at)) }}</span>
+      </div>
+    {% endif %}
+    {% if job.time_left > 0 %}
+      <div class="job-remaining-time u-flex">
+        <label for="remaining-time">Remaining time</label>
+        <span name="remaining-time">{{ timedelta(seconds=job.time_left) }}</span>
+      </div>
+    {% endif %}
   </div>
   <div class="two columns u-mv-auto">
     <div class="job-status u-flex">
diff --git a/projects/tentacles/src/tentacles/templates/macros.html.j2 b/projects/tentacles/src/tentacles/templates/macros.html.j2
index 101ffda..417810d 100644
--- a/projects/tentacles/src/tentacles/templates/macros.html.j2
+++ b/projects/tentacles/src/tentacles/templates/macros.html.j2
@@ -1,7 +1,7 @@
 {# #################################################################################################### #}
 {# Job CRUD #}
 {% macro start_job(file) %}
-<form class="inline" method="post" action="/jobs">
+<form class="start-menu inline u-flex" method="post" action="/jobs">
   <input type="hidden" name="action" value="enqueue" />
   <input type="hidden" name="file_id" value="{{ file.id }}" />
   <select name="color_id">
@@ -9,7 +9,11 @@
     <option value="{{c.id}}" {% if file.color_id == c.id %}selected{%endif%}>{{c.name}}</option>
     {%- endfor %}
   </select>
-  <input id="submit" type="image" src="/static/print.svg" height="24" width="24" />
+  <div class="u-flex u-ml-auto u-mv-auto u-flex u-ml-1">
+    <label class="u-flex u-mv-auto" for="continuous">Cont.</label>
+    <input class="u-flex u-mv-auto" type="checkbox" name="continuous" false>
+  </div>
+    <input id="submit" type="image" src="/static/print.svg" height="24" width="24" />
 </form>
 {% endmacro %}
 
diff --git a/projects/tentacles/src/tentacles/workers.py b/projects/tentacles/src/tentacles/workers.py
index f3659a7..80c69e8 100644
--- a/projects/tentacles/src/tentacles/workers.py
+++ b/projects/tentacles/src/tentacles/workers.py
@@ -270,8 +270,7 @@ def pull_jobs(app: App, db: Db) -> None:
             if runtime >= timedelta(seconds=15):  # 3 polling cycles
                 log.info(f"Job {job.id} has succeeded")
 
-                # Attempt to automatically clear the bed
-                if False:
+                if job.continuous:
                     log.info(f"Attempting to clear bed of {printer.id} ({printer.url})")
                     client.gcode(
                         """\