feat: displaying and managing active jobs

This commit is contained in:
Per Stark
2025-01-28 11:51:45 +01:00
parent c6bc0c44f3
commit 2460d430b2
13 changed files with 522 additions and 181 deletions

View File

@@ -828,6 +828,69 @@
}
}
}
.tab {
position: relative;
display: inline-flex;
cursor: pointer;
appearance: none;
flex-wrap: wrap;
align-items: center;
justify-content: center;
text-align: center;
webkit-user-select: none;
user-select: none;
&:hover {
@media (hover: hover) {
color: var(--color-base-content);
}
}
--tab-p: 1rem;
--tab-bg: var(--color-base-100);
--tab-border-color: var(--color-base-300);
--tab-radius-ss: 0;
--tab-radius-se: 0;
--tab-radius-es: 0;
--tab-radius-ee: 0;
--tab-order: 0;
--tab-radius-min: calc(0.75rem - var(--border));
border-color: transparent;
order: var(--tab-order);
height: calc(var(--size-field, 0.25rem) * 10);
font-size: 0.875rem;
padding-inline-start: var(--tab-p);
padding-inline-end: var(--tab-p);
&:is(input[type="radio"]) {
min-width: fit-content;
&:after {
content: attr(aria-label);
}
}
&:checked, &:is(.tab-active, [aria-selected="true"]) {
& + .tab-content {
display: block;
height: 100%;
}
}
&:not(:checked, :hover, .tab-active, [aria-selected="true"]) {
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
}
&:not(input):empty {
flex-grow: 1;
cursor: default;
}
&:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
&:focus-visible {
outline: 2px solid currentColor;
outline-offset: -5px;
}
&[disabled] {
pointer-events: none;
opacity: 40%;
}
}
.dropdown {
position: relative;
display: inline-block;
@@ -2343,6 +2406,25 @@
}
}
}
.tab-content {
order {
}
order: var(--tabcontent-order);
display: none;
border-color: transparent;
--tabcontent-radius-ss: 0;
--tabcontent-radius-se: 0;
--tabcontent-radius-es: 0;
--tabcontent-radius-ee: 0;
--tabcontent-order: 1;
width: 100%;
margin: var(--tabcontent-margin);
border-width: var(--border);
border-start-start-radius: var(--tabcontent-radius-ss);
border-start-end-radius: var(--tabcontent-radius-se);
border-end-start-radius: var(--tabcontent-radius-es);
border-end-end-radius: var(--tabcontent-radius-ee);
}
.modal-box {
grid-column-start: 1;
grid-row-start: 1;
@@ -2390,6 +2472,11 @@
font-size: 2rem;
font-weight: 800;
}
.drawer-content {
grid-column-start: 2;
grid-row-start: 1;
min-width: calc(0.25rem * 0);
}
.chat-image {
grid-row: span 2 / span 2;
align-self: flex-end;
@@ -2427,6 +2514,15 @@
max-width: 96rem;
}
}
.m-0 {
margin: calc(var(--spacing) * 0);
}
.m-6 {
margin: calc(var(--spacing) * 6);
}
.m-8 {
margin: calc(var(--spacing) * 8);
}
.filter {
display: flex;
flex-wrap: wrap;
@@ -3026,6 +3122,9 @@
.mt-8 {
margin-top: calc(var(--spacing) * 8);
}
.mt-10 {
margin-top: calc(var(--spacing) * 10);
}
.mr-1 {
margin-right: calc(var(--spacing) * 1);
}
@@ -3048,6 +3147,9 @@
.mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
.mb-10 {
margin-bottom: calc(var(--spacing) * 10);
}
.alert {
display: grid;
width: 100%;
@@ -3319,6 +3421,13 @@
column-gap: calc(0.25rem * 3);
padding-block: calc(0.25rem * 1);
}
.mask {
display: inline-block;
vertical-align: middle;
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
}
.block {
display: block;
}
@@ -3352,6 +3461,27 @@
.table {
display: table;
}
.btn-square {
padding-inline: calc(0.25rem * 0);
width: var(--size);
height: var(--size);
}
.size-6 {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.size-8 {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
}
.size-10 {
width: calc(var(--spacing) * 10);
height: calc(var(--spacing) * 10);
}
.size-\[1\.2em\] {
width: 1.2em;
height: 1.2em;
}
.h-5 {
height: calc(var(--spacing) * 5);
}
@@ -3367,12 +3497,6 @@
.w-5 {
width: calc(var(--spacing) * 5);
}
.w-12 {
width: calc(var(--spacing) * 12);
}
.w-24 {
width: calc(var(--spacing) * 24);
}
.w-32 {
width: calc(var(--spacing) * 32);
}
@@ -3388,6 +3512,9 @@
.flex-shrink {
flex-shrink: 1;
}
.shrink {
flex-shrink: 1;
}
.flex-grow {
flex-grow: 1;
}
@@ -3463,15 +3590,24 @@
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.\!flex-col {
flex-direction: column !important;
}
.flex-col {
flex-direction: column;
}
.flex-row {
flex-direction: row;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
@@ -3481,6 +3617,9 @@
.justify-start {
justify-content: flex-start;
}
.gap-1 {
gap: calc(var(--spacing) * 1);
}
.gap-4 {
gap: calc(var(--spacing) * 4);
}
@@ -3546,12 +3685,21 @@
.border-transparent {
border-color: transparent;
}
.bg-accent {
background-color: var(--color-accent);
}
.bg-base-100 {
background-color: var(--color-base-100);
}
.bg-base-200 {
background-color: var(--color-base-200);
}
.bg-primary {
background-color: var(--color-primary);
}
.bg-secondary {
background-color: var(--color-secondary);
}
.bg-linear-to-r {
--tw-gradient-position: to right in oklab,;
background-image: linear-gradient(var(--tw-gradient-stops));
@@ -3619,6 +3767,9 @@
.pt-10 {
padding-top: calc(var(--spacing) * 10);
}
.pb-2 {
padding-bottom: calc(var(--spacing) * 2);
}
.text-center {
text-align: center;
}
@@ -3672,6 +3823,10 @@
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
}
.tracking-wide {
--tw-tracking: var(--tracking-wide);
letter-spacing: var(--tracking-wide);
}
.text-nowrap {
text-wrap: nowrap;
}
@@ -3693,6 +3848,9 @@
.text-accent {
color: var(--color-accent);
}
.text-accent-content {
color: var(--color-accent-content);
}
.text-base-content {
color: var(--color-base-content);
}
@@ -3717,6 +3875,9 @@
.text-secondary {
color: var(--color-secondary);
}
.text-secondary-content {
color: var(--color-secondary-content);
}
.text-transparent {
color: transparent;
}
@@ -3729,6 +3890,10 @@
.italic {
font-style: italic;
}
.ordinal {
--tw-ordinal: ordinal;
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
}
.btn-link {
text-decoration-line: underline;
outline-color: currentColor;
@@ -3746,6 +3911,9 @@
.underline {
text-decoration-line: underline;
}
.accent-accent-content {
accent-color: var(--color-accent-content);
}
.opacity-60 {
opacity: 60%;
}
@@ -3757,10 +3925,6 @@
opacity: 100%;
}
}
.ring {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -3866,6 +4030,11 @@
margin-top: calc(var(--spacing) * 4);
}
}
.sm\:w-fit {
@media (width >= 40rem) {
width: fit-content;
}
}
.sm\:max-w-md {
@media (width >= 40rem) {
max-width: var(--container-md);
@@ -3876,6 +4045,16 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.sm\:flex-col {
@media (width >= 40rem) {
flex-direction: column;
}
}
.sm\:flex-row {
@media (width >= 40rem) {
flex-direction: row;
}
}
.sm\:px-0 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 0);
@@ -3887,6 +4066,11 @@
line-height: var(--tw-leading, var(--text-6xl--line-height));
}
}
.md\:grid-cols-2 {
@media (width >= 48rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.md\:grid-cols-3 {
@media (width >= 48rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -3907,6 +4091,50 @@
grid-template-columns: auto 1fr;
}
}
.\[\&\:before\]\:normal-case {
&:before {
text-transform: none;
}
}
.\[\&\:before\]\:uppercase {
&:before {
text-transform: uppercase;
}
}
.\[\&\:before\]\:opacity-60 {
&:before {
opacity: 60%;
}
}
.\[\&\:before\]\:opacity-100 {
&:before {
opacity: 100%;
}
}
.\[\&\:before\]\:content-\[\'Content\:_\'\] {
&:before {
--tw-content: 'Content: ';
content: var(--tw-content);
}
}
.\[\&\:before\]\:content-\[\'Instructions\:_\'\] {
&:before {
--tw-content: 'Instructions: ';
content: var(--tw-content);
}
}
.\[\&\:before\]\:content-\[\'Status\:_\'\] {
&:before {
--tw-content: 'Status: ';
content: var(--tw-content);
}
}
.\[\&\:before\]\:content-\[\'Text\:_\'\] {
&:before {
--tw-content: 'Text: ';
content: var(--tw-content);
}
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
@@ -4262,6 +4490,12 @@ video {
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
--tw-font-weight: initial;
--tw-tracking: initial;
--tw-ordinal: initial;
--tw-slashed-zero: initial;
--tw-numeric-figure: initial;
--tw-numeric-spacing: initial;
--tw-numeric-fraction: initial;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-inset-shadow: 0 0 #0000;
@@ -4294,6 +4528,7 @@ video {
--tw-backdrop-saturate: initial;
--tw-backdrop-sepia: initial;
--tw-ease: initial;
--tw-content: "";
}
}
}
@@ -4404,6 +4639,30 @@ video {
syntax: "*";
inherits: false;
}
@property --tw-tracking {
syntax: "*";
inherits: false;
}
@property --tw-ordinal {
syntax: "*";
inherits: false;
}
@property --tw-slashed-zero {
syntax: "*";
inherits: false;
}
@property --tw-numeric-figure {
syntax: "*";
inherits: false;
}
@property --tw-numeric-spacing {
syntax: "*";
inherits: false;
}
@property --tw-numeric-fraction {
syntax: "*";
inherits: false;
}
@property --tw-shadow {
syntax: "*";
inherits: false;
@@ -4540,3 +4799,8 @@ video {
syntax: "*";
inherits: false;
}
@property --tw-content {
syntax: "*";
inherits: false;
initial-value: "";
}

View File

@@ -31,9 +31,8 @@ use zettle_db::{
admin_panel::{show_admin_panel, toggle_registration_status},
documentation::index::show_documentation_index,
gdpr::{accept_gdpr, deny_gdpr},
index::index_handler,
ingress_form::{process_ingress_form, show_ingress_form},
ingress_tasks::{delete_task, show_queue_tasks},
index::{delete_job, delete_text_content, index_handler},
ingress_form::{hide_ingress_form, process_ingress_form, show_ingress_form},
privacy_policy::show_privacy_policy,
search_result::search_result_handler,
signin::{authenticate_user, show_signin_form},
@@ -168,8 +167,9 @@ fn html_routes(
"/ingress-form",
get(show_ingress_form).post(process_ingress_form),
)
.route("/queue", get(show_queue_tasks))
.route("/queue/:delivery_tag", delete(delete_task))
.route("/hide-ingress-form", get(hide_ingress_form))
.route("/text-content/:id", delete(delete_text_content))
.route("/jobs/:job_id", delete(delete_job))
.route("/account", get(show_account_page))
.route("/admin", get(show_admin_panel))
.route("/toggle-registrations", patch(toggle_registration_status))

View File

@@ -57,6 +57,31 @@ impl JobQueue {
Ok(jobs)
}
/// Gets all active jobs for a specific user
pub async fn get_unfinished_user_jobs(&self, user_id: &str) -> Result<Vec<Job>, AppError> {
let jobs: Vec<Job> = self
.db
.query(
"SELECT * FROM type::table($table)
WHERE user_id = $user_id
AND (
status = 'Created'
OR (
status.InProgress != NONE
AND status.InProgress.attempts < $max_attempts
)
)
ORDER BY created_at DESC",
)
.bind(("table", Job::table_name()))
.bind(("user_id", user_id.to_owned()))
.bind(("max_attempts", MAX_ATTEMPTS))
.await?
.take(0)?;
debug!("{:?}", jobs);
Ok(jobs)
}
pub async fn delete_job(&self, id: &str, user_id: &str) -> Result<(), AppError> {
get_item::<Job>(&self.db.client, id)
.await?

View File

@@ -1,4 +1,7 @@
use axum::{extract::State, response::IntoResponse};
use axum::{
extract::{Path, State},
response::{IntoResponse, Redirect},
};
use axum_session::Session;
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
@@ -6,17 +9,23 @@ use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
error::HtmlError,
error::{AppError, HtmlError},
page_data,
server::{routes::html::render_template, AppState},
storage::types::{text_content::TextContent, user::User},
server::{
routes::html::{render_block, render_template},
AppState,
},
storage::{
db::delete_item,
types::{job::Job, text_content::TextContent, user::User},
},
};
page_data!(IndexData, "index/index.html", {
gdpr_accepted: bool,
queue_length: u32,
user: Option<User>,
latest_text_contents: Vec<TextContent>
latest_text_contents: Vec<TextContent>,
active_jobs: Vec<Job>
});
pub async fn index_handler(
@@ -28,17 +37,16 @@ pub async fn index_handler(
let gdpr_accepted = auth.current_user.is_some() | session.get("gdpr_accepted").unwrap_or(false);
let queue_length = match auth.current_user.is_some() {
let active_jobs = match auth.current_user.is_some() {
true => state
.job_queue
.get_user_jobs(&auth.current_user.clone().unwrap().id)
.get_unfinished_user_jobs(&auth.current_user.clone().unwrap().id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?
.len(),
false => 0,
.map_err(|e| HtmlError::new(e, state.templates.clone()))?,
false => vec![],
};
let latest_text_contents = match auth.current_user.is_some() {
let latest_text_contents = match auth.current_user.clone().is_some() {
true => User::get_latest_text_contents(
auth.current_user.clone().unwrap().id.as_str(),
&state.surreal_db_client,
@@ -65,10 +73,90 @@ pub async fn index_handler(
let output = render_template(
IndexData::template_name(),
IndexData {
queue_length: queue_length.try_into().unwrap(),
gdpr_accepted,
user: auth.current_user,
latest_text_contents,
active_jobs,
},
state.templates.clone(),
)?;
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct LatestTextContentData {
latest_text_contents: Vec<TextContent>,
user: User,
}
pub async fn delete_text_content(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match &auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
delete_item::<TextContent>(&state.surreal_db_client, &id)
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
let latest_text_contents = User::get_latest_text_contents(&user.id, &state.surreal_db_client)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
info!("{:?}", latest_text_contents);
let output = render_block(
"index/signed_in/recent_content.html",
"latest_content_section",
LatestTextContentData {
user: user.clone(),
latest_text_contents,
},
state.templates.clone(),
)?;
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct ActiveJobsData {
active_jobs: Vec<Job>,
user: User,
}
pub async fn delete_job(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
state
.job_queue
.delete_job(&id, &user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let active_jobs = state
.job_queue
.get_unfinished_user_jobs(&user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_block(
"index/signed_in/active_jobs.html",
"active_jobs_section",
ActiveJobsData {
user: user.clone(),
active_jobs,
},
state.templates.clone(),
)?;

View File

@@ -33,6 +33,19 @@ pub async fn show_ingress_form(
Ok(output.into_response())
}
pub async fn hide_ingress_form(
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
if !auth.is_authenticated() {
return Ok(Redirect::to("/").into_response());
}
Ok(Html(
"<a class='btn btn-primary' hx-get='/ingress-form' hx-swap='outerHTML'>Add Content</a>",
)
.into_response())
}
#[derive(Debug, TryFromMultipart)]
pub struct IngressParams {
pub content: Option<String>,

View File

@@ -1,60 +0,0 @@
use crate::{
error::HtmlError,
page_data,
server::AppState,
storage::types::{job::Job, user::User},
};
use axum::{
extract::{Path, State},
response::{Html, IntoResponse, Redirect},
};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use super::render_template;
page_data!(ShowQueueTasks, "queue_tasks.html", {user : User,jobs: Vec<Job>});
pub async fn show_queue_tasks(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
let jobs = state
.job_queue
.get_user_jobs(&user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let rendered = render_template(
ShowQueueTasks::template_name(),
ShowQueueTasks { jobs, user },
state.templates.clone(),
)?;
Ok(rendered.into_response())
}
pub async fn delete_task(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
state
.job_queue
.delete_job(&id, &user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
Ok(Html("").into_response())
}

View File

@@ -11,7 +11,6 @@ pub mod documentation;
pub mod gdpr;
pub mod index;
pub mod ingress_form;
pub mod ingress_tasks;
pub mod privacy_policy;
pub mod search_result;
pub mod signin;

View File

@@ -0,0 +1,46 @@
{% block active_jobs_section %}
{% if active_jobs %}
<ul id="active_jobs_section" class="list bg-base-100 rounded-box shadow-md">
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide">Active Jobs</li>
{% for item in active_jobs %}
<li class="list-row">
<div class="bg-secondary rounded-box size-10 flex justify-center items-center text-secondary-content">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-8">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
</div>
<div>
<div>
{{item.created_at|datetimeformat(format="short", tz=user.timezone)}}</div>
<div
class="text-xs font-semibold opacity-60 [&:before]:content-['Status:_'] [&:before]:uppercase [&:before]:opacity-60">
{{item.status}}
</div>
</div>
<p class="list-col-wrap text-xs [&:before]:content-['Content:_'] [&:before]:uppercase [&:before]:opacity-60">
{{item.content}}
</p>
<!-- <button class="btn disabled btn-square btn-ghost"> -->
<!-- <svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> -->
<!-- <g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"> -->
<!-- <path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75z"></path> -->
<!-- <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"></path> -->
<!-- </g> -->
<!-- </svg> -->
<!-- </button> -->
<button hx-delete="/jobs/{{item.id}}" hx-target="#active_jobs_section" hx-swap="outerHTML"
class="btn btn-square btn-ghost">
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</g>
</svg>
</button>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -2,8 +2,13 @@
<div class="container">
{% include 'index/signed_in/searchbar.html' %}
{% include "index/signed_in/recent_content.html" %}
{% include "index/signed_in/quick_actions.html" %}
<div class="grid grid-cols-1 md:grid-cols-2">
{% include "index/signed_in/active_jobs.html" %}
{% include "index/signed_in/recent_content.html" %}
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<div class="flex gap-4">
<a class="btn btn-primary" hx-get="/ingress-form" hx-swap="outerHTML">Add Content</a>
<button class="btn btn-primary" hx-get="/ingress-form" hx-swap="outerHTML">Add Content</button>
</div>
</div>
</div>

View File

@@ -1,24 +1,45 @@
<div class="card bg-base-100 shadow-xl mt-4">
<!-- <div class="mt-4"> -->
<p>{{latest_text_contents}}</p>
<div class="card-body">
<h2 class="card-title">Recently Added Content</h2>
<ul class="list bg-base-100 rounded-box shadow-md">
{% for item in latest_text_contents %}
<li class="list-row">
<div class="text-2xl text-ellipsis text-nowrap overflow-hidden">
{{item.created_at|datetimeformat(format="short", tz="Europe/Stockholm")}}</div>
<div>
<div>{{item.category}}</div>
<div class="text-xs uppercase font-semibold opacity-60">{{item.text}}</div>
</div>
<button class="btn btn-outline">
Edit
</button>
<button class="btn btn-error">
Delete </button>
</li>
{% endfor %}
</ul>
</div>
</div>
{% block latest_content_section %}
<ul id="latest_content_section" class="list bg-base-100 rounded-box shadow-md">
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide">Recently added content</li>
{% for item in latest_text_contents %}
<li class="list-row">
<div class="bg-accent rounded-box size-10 flex justify-center items-center text-accent-content">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-8">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
</div>
<div>
<div>
{{item.created_at|datetimeformat(format="short", tz=user.timezone)}}</div>
<div
class="text-xs font-semibold opacity-60 [&:before]:content-['Instructions:_'] [&:before]:uppercase [&:before]:opacity-60">
{{item.instructions}}
</div>
</div>
<p class="list-col-wrap text-xs [&:before]:content-['Content:_'] [&:before]:uppercase [&:before]:opacity-60">
{{item.text}}
</p>
<!-- <button class="btn disabled btn-square btn-ghost"> -->
<!-- <svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> -->
<!-- <g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"> -->
<!-- <path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75z"></path> -->
<!-- <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"></path> -->
<!-- </g> -->
<!-- </svg> -->
<!-- </button> -->
<button hx-delete="/text-content/{{item.id}}" hx-target="#latest_content_section" hx-swap="outerHTML"
class="btn btn-square btn-ghost">
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</g>
</svg>
</button>
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -1,4 +1,4 @@
<form class="space-y-4 mt-2 w-full" hx-post="/ingress-form" enctype="multipart/form-data">
<form id="ingress-form" class="space-y-4 mt-2 w-full" hx-post="/ingress-form" enctype="multipart/form-data">
<div class="form-control">
<label class="floating-label">
<span>Instructions</span>
@@ -34,7 +34,9 @@
<div id="error-message" class="text-error text-center {% if not error %}hidden{% endif %}">{{ error }}</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full">Submit</button>
<div class="form-control mt-6 flex flex-col sm:flex-row gap-1">
<button hx-get="/hide-ingress-form" hx-target="#ingress-form" hx-swap=outerHTML"
class="btn btn-outline w-full sm:w-fit">Cancel</button>
<button type="submit" class="btn btn-primary w-full sm:w-fit">Submit</button>
</div>
</form>

View File

@@ -1,62 +0,0 @@
{% extends "body_base.html" %}
{% block main %}
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Active Tasks</h1>
{% if not jobs %}
<div class="alert alert-info">
<span>No active tasks</span>
</div>
{% else %}
<div class="grid gap-4">
{% for job in jobs %}
{% if job.status == "Created" or job.status is mapping and job.status.InProgress %}
<div class="card bg-base-200 shadow-xl" id="job-card-{{ job.id }}">
<div class="card-body">
<div class="card-title">
{% if job.content.Url %}
<h2>URL Task</h2>
<p class="text-sm text-gray-500 break-all">{{ job.content.Url.url }}</p>
{% elif job.content.File %}
<h2>File Task</h2>
<p class="text-sm text-gray-500">{{ job.content.File.file_info.path }}</p>
{% elif job.content.Text %}
<h2>Text Task</h2>
<p class="text-sm text-gray-500">{{ job.content.Text.text }}</p>
{% endif %}
</div>
<div class="space-y-2">
<p><span class="font-medium">Status:</span>
{% if job.status == "Created" %}
Created
{% elif job.status.InProgress %}
In Progress
{% endif %}
</p>
{% if job.status.InProgress %}
<p><span class="font-medium">Attempts:</span> {{ job.status.InProgress.attempts }}</p>
<p><span class="font-medium">Last Attempt:</span>
{{ job.status.InProgress.last_attempt }}</p>
{% endif %}
<p><span class="font-medium">Category:</span>
{% if job.content.Url %}
{{ job.content.Url.category }}
{% elif job.content.File %}
{{ job.content.File.category }}
{% elif job.content.Text %}
{{ job.content.Text.category }}
{% endif %}
</p>
</div>
<div class="card-actions justify-end mt-4">
<form hx-delete="/queue/{{ job.id }}" hx-target="#job-card-{{ job.id }}" hx-swap="outerHTML">
<button class="btn btn-error">Cancel Task</button>
</form>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}