Redesign the web UI with light and dark themes
This commit is contained in:
+355
-158
@@ -1,118 +1,276 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Shorts Autopilot</title>
|
||||
<script>
|
||||
(function(){try{var t=localStorage.getItem("sa-theme");if(t!=="light"&&t!=="dark"){t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark"}document.documentElement.setAttribute("data-theme",t)}catch(e){}})();
|
||||
</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#0f1115;--panel:#181b22;--border:#2a2f3a;--text:#e6e8ee;--muted:#8a93a6;--accent:#e53935;--ok:#43a047}
|
||||
:root{
|
||||
--font-ui:"Atkinson Hyperlegible",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
--font-display:"Bricolage Grotesque","Avenir Next",-apple-system,BlinkMacSystemFont,sans-serif;
|
||||
--font-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
|
||||
--r-lg:18px;--r-md:12px;--r-sm:8px;
|
||||
}
|
||||
[data-theme="dark"]{
|
||||
color-scheme:dark;
|
||||
--bg:#14162a;--bar:#171a31;--surface:#1d2040;--surface-2:#252953;--sunken:#101226;--line:#2e3263;--line-soft:#252852;
|
||||
--text:#eceaf6;--muted:#9d9fc4;--faint:#6e7199;
|
||||
--gold:#f5b83d;--gold-text:#f5b83d;--on-gold:#221a05;
|
||||
--sage:#6fcf97;--coral:#ff7a70;--sky:#7cb8ff;--violet:#b69cff;--amber:#ffb86b;
|
||||
--shadow:0 1px 0 rgba(255,255,255,.03) inset,0 12px 32px -18px rgba(0,0,0,.6);
|
||||
}
|
||||
[data-theme="light"]{
|
||||
color-scheme:light;
|
||||
--bg:#f5f6fb;--bar:#ffffff;--surface:#ffffff;--surface-2:#eef0f8;--sunken:#f0f1f8;--line:#dfe1ee;--line-soft:#eaebf4;
|
||||
--text:#1c1f3a;--muted:#5f6388;--faint:#8e91b0;
|
||||
--gold:#f2ae2b;--gold-text:#946000;--on-gold:#221a05;
|
||||
--sage:#1d8150;--coral:#cf3f35;--sky:#2c68c9;--violet:#6a4dcf;--amber:#a65f00;
|
||||
--shadow:0 1px 2px rgba(28,31,58,.05),0 10px 28px -20px rgba(28,31,58,.35);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:system-ui,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text)}
|
||||
header{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border)}
|
||||
header h1{font-size:18px;margin:0;flex:1}
|
||||
nav button{background:none;border:none;color:var(--muted);padding:8px 12px;cursor:pointer;font-size:14px}
|
||||
nav button.active{color:var(--text);border-bottom:2px solid var(--accent)}
|
||||
main{padding:20px;max-width:1100px;margin:0 auto}
|
||||
.panel{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:16px;overflow-x:auto}
|
||||
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
|
||||
.btn{border:none;border-radius:8px;padding:10px 18px;font-size:15px;cursor:pointer;color:#fff;background:#3949ab;text-decoration:none;display:inline-block}
|
||||
.btn:disabled{opacity:.4;cursor:default}
|
||||
.btn.go{background:var(--ok);font-size:20px;padding:14px 40px;min-width:150px}
|
||||
.btn.go.running{background:var(--accent)}
|
||||
.btn.go.busy{background:#e65100}
|
||||
.btn.stop{background:var(--accent)}
|
||||
.btn.secondary{background:#394150}
|
||||
.stat{min-width:150px}
|
||||
.stat .k{color:var(--muted);font-size:12px}
|
||||
.stat .v{font-size:17px;margin-top:4px}
|
||||
.stat .v.small{font-size:13px;word-break:break-all}
|
||||
.stat select{min-width:200px;margin-top:4px}
|
||||
#progress-panel{display:none}
|
||||
.bar{height:10px;background:#0f1115;border:1px solid var(--border);border-radius:6px;overflow:hidden;margin-top:6px}
|
||||
.bar span{display:block;height:100%;width:0;background:var(--ok);transition:width .4s ease}
|
||||
.bar.done span{background:#1b5e20}
|
||||
.badge{display:none;background:#6a1b9a;color:#fff;border-radius:10px;padding:3px 10px;font-size:12px}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--border);vertical-align:top}
|
||||
th{color:var(--muted);font-weight:500}
|
||||
html,body{margin:0}
|
||||
body{font-family:var(--font-ui);font-size:15px;line-height:1.5;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
|
||||
a{color:var(--gold-text);text-decoration-thickness:1px;text-underline-offset:3px}
|
||||
:focus-visible{outline:2px solid var(--gold);outline-offset:2px;border-radius:6px}
|
||||
button{font:inherit;color:inherit}
|
||||
|
||||
.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:24px;padding:12px 28px;background:color-mix(in srgb,var(--bar) 88%,transparent);backdrop-filter:blur(10px);border-bottom:1px solid var(--line-soft)}
|
||||
.brand{display:flex;align-items:center;gap:10px;font-family:var(--font-display);font-weight:800;font-size:19px;letter-spacing:-.01em;white-space:nowrap}
|
||||
.brand-mark{width:28px;height:28px;border-radius:9px;background:var(--gold);display:grid;place-items:center;color:var(--on-gold)}
|
||||
.tabs{display:flex;gap:4px;flex:1}
|
||||
.tabs button{background:none;border:0;padding:8px 14px;border-radius:999px;color:var(--muted);cursor:pointer;font-size:15px}
|
||||
.tabs button:hover{color:var(--text);background:var(--surface-2)}
|
||||
.tabs button.active{color:var(--text);background:var(--surface-2);font-weight:700}
|
||||
.icon-btn{width:38px;height:38px;border-radius:999px;border:1px solid var(--line);background:var(--surface);display:grid;place-items:center;cursor:pointer;color:var(--muted)}
|
||||
.icon-btn:hover{color:var(--text);border-color:var(--faint)}
|
||||
.icon-btn .sun{display:none}
|
||||
[data-theme="light"] .icon-btn .sun{display:block}
|
||||
[data-theme="light"] .icon-btn .moon{display:none}
|
||||
|
||||
main{max-width:1180px;margin:0 auto;padding:28px 28px 120px}
|
||||
.tab{display:none}.tab.active{display:block}
|
||||
#log{background:#0a0c10;border:1px solid var(--border);border-radius:8px;height:65vh;overflow:auto;padding:10px;font-family:Consolas,monospace;font-size:12px;white-space:pre-wrap}
|
||||
.ERROR,.CRITICAL{color:#ef5350}.WARNING{color:#ffb74d}.DEBUG{color:#78909c}
|
||||
label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}
|
||||
input,select{width:100%;padding:8px;background:#0f1115;border:1px solid var(--border);border-radius:6px;color:var(--text)}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}
|
||||
.grid h3{grid-column:1/-1;margin:10px 0 0;font-size:14px;color:var(--text);border-bottom:1px solid var(--border);padding-bottom:6px}
|
||||
.grid .wide{grid-column:1/-1}
|
||||
.hint{font-size:11px;color:var(--muted);margin-top:3px;word-break:break-all}
|
||||
.hint.notes{color:#ffb74d;word-break:normal}
|
||||
a{color:#64b5f6}
|
||||
.pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:#394150}
|
||||
.pill.published{background:#1b5e20}.pill.failed{background:#b71c1c}.pill.running{background:#e65100}.pill.review{background:#6a1b9a}.pill.scheduled{background:#1565c0}
|
||||
h2,h3{font-family:var(--font-display);letter-spacing:-.01em;margin:0}
|
||||
.muted{color:var(--muted)}
|
||||
.small{font-size:13px}
|
||||
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);background:var(--surface);color:var(--text);padding:9px 16px;border-radius:999px;cursor:pointer;font-size:14px;text-decoration:none;white-space:nowrap}
|
||||
.btn:hover{border-color:var(--faint)}
|
||||
.btn:disabled{opacity:.5;cursor:default}
|
||||
.btn.primary{background:var(--gold);border-color:var(--gold);color:var(--on-gold);font-weight:700}
|
||||
.btn.primary:hover{filter:brightness(1.05)}
|
||||
.btn.danger{color:var(--coral);border-color:color-mix(in srgb,var(--coral) 45%,var(--line))}
|
||||
.btn.quiet{border-color:transparent;background:transparent;color:var(--muted)}
|
||||
.btn.quiet:hover{color:var(--text);background:var(--surface-2)}
|
||||
.btn.sm{padding:6px 12px;font-size:13px}
|
||||
|
||||
.console{display:grid;grid-template-columns:auto 1fr;gap:32px;align-items:center;padding:28px 32px;background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--r-lg);box-shadow:var(--shadow)}
|
||||
.go{position:relative;width:128px;height:128px;border-radius:50%;border:0;cursor:pointer;font-family:var(--font-display);font-weight:800;font-size:28px;letter-spacing:.02em;background:var(--gold);color:var(--on-gold);box-shadow:0 0 0 8px color-mix(in srgb,var(--gold) 16%,transparent),0 18px 40px -16px color-mix(in srgb,var(--gold) 70%,transparent);transition:background .25s,box-shadow .25s,transform .12s}
|
||||
.go:hover{transform:scale(1.02)}
|
||||
.go:active{transform:scale(.98)}
|
||||
.go.running{background:var(--coral);color:#fff;box-shadow:0 0 0 8px color-mix(in srgb,var(--coral) 18%,transparent),0 18px 40px -16px color-mix(in srgb,var(--coral) 70%,transparent)}
|
||||
.go.running::after{content:"";position:absolute;inset:-8px;border-radius:50%;border:2px solid var(--coral);opacity:0;animation:pulse 2.4s ease-out infinite}
|
||||
.go.busy{background:var(--surface-2);color:var(--muted);box-shadow:0 0 0 8px var(--line-soft);cursor:progress}
|
||||
.go:disabled{cursor:progress}
|
||||
@keyframes pulse{0%{transform:scale(1);opacity:.55}100%{transform:scale(1.28);opacity:0}}
|
||||
@media (prefers-reduced-motion:reduce){.go.running::after{animation:none}.go{transition:none}}
|
||||
|
||||
.state{min-width:0}
|
||||
.state-line{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.stage{font-family:var(--font-display);font-weight:700;font-size:28px;line-height:1.15;letter-spacing:-.015em;margin:0;overflow-wrap:anywhere}
|
||||
.stage::first-letter{text-transform:uppercase}
|
||||
.chip{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-size:12.5px;font-weight:700;background:color-mix(in srgb,var(--c,var(--muted)) 16%,transparent);color:var(--c,var(--muted));white-space:nowrap}
|
||||
.chip.kids{--c:var(--violet)}
|
||||
.progress{margin-top:16px;display:none}
|
||||
.progress.show{display:block}
|
||||
.segs{display:flex;gap:4px;height:10px}
|
||||
.segs i{flex:1;border-radius:3px;background:var(--line-soft)}
|
||||
.segs i.on{background:var(--gold)}
|
||||
.segs i.now{background:color-mix(in srgb,var(--gold) 45%,var(--line-soft))}
|
||||
.pg-meta{margin-top:8px;font-size:13px;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||
.idle-note{margin-top:8px;color:var(--muted);font-size:14px}
|
||||
|
||||
.facts{display:grid;grid-template-columns:minmax(230px,1.5fr) repeat(5,minmax(0,1fr));gap:0;margin-top:20px;border:1px solid var(--line-soft);border-radius:var(--r-md);overflow:hidden;background:var(--surface)}
|
||||
.fact{padding:14px 18px;border-right:1px solid var(--line-soft);min-width:0}
|
||||
.fact:last-child{border-right:0}
|
||||
.fact dt{font-size:13px;color:var(--muted);margin-bottom:4px}
|
||||
.fact dd{margin:0;font-weight:700;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fact dd.warn{color:var(--amber)}
|
||||
.fact select{font-weight:700}
|
||||
|
||||
.section-head{display:flex;align-items:center;justify-content:space-between;gap:12px 16px;flex-wrap:wrap;margin:36px 0 12px}
|
||||
.section-head h2{font-size:22px}
|
||||
.section-head .actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||
|
||||
.table-wrap{background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--r-md);overflow-x:auto}
|
||||
table{width:100%;min-width:760px;border-collapse:collapse;font-size:14px}
|
||||
th{font-weight:700;color:var(--muted);text-align:left;font-size:13px;padding:12px 16px;border-bottom:1px solid var(--line-soft);white-space:nowrap}
|
||||
td{padding:14px 16px;border-bottom:1px solid var(--line-soft);vertical-align:top}
|
||||
tr:last-child td{border-bottom:0}
|
||||
td.nowrap{white-space:nowrap;font-variant-numeric:tabular-nums}
|
||||
td.title{font-weight:700}
|
||||
td .sub{font-size:12.5px;color:var(--muted);margin-top:6px;overflow-wrap:anywhere}
|
||||
td .sub.notes{color:var(--amber)}
|
||||
td .sub.err{color:var(--coral)}
|
||||
.st-published{--c:var(--sage)}.st-failed{--c:var(--coral)}.st-running{--c:var(--gold-text)}.st-review{--c:var(--violet)}.st-scheduled{--c:var(--sky)}.st-cancelled{--c:var(--muted)}
|
||||
.links{display:flex;gap:6px}
|
||||
.empty{padding:36px 16px;text-align:center;color:var(--muted)}
|
||||
.source a{color:var(--text);text-decoration-color:var(--line)}
|
||||
.source a:hover{text-decoration-color:var(--gold)}
|
||||
|
||||
label{display:block}
|
||||
input[type=text],input[type=password],input[type=number],select{width:100%;padding:10px 12px;background:var(--sunken);border:1px solid var(--line);border-radius:var(--r-sm);color:var(--text);font:inherit;font-size:14.5px}
|
||||
select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 17px) 55%,calc(100% - 12px) 55%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:34px}
|
||||
input:focus,select:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px color-mix(in srgb,var(--gold) 22%,transparent)}
|
||||
input:disabled,select:disabled{opacity:.55}
|
||||
|
||||
.settings{display:grid;grid-template-columns:220px 1fr;gap:36px;align-items:start}
|
||||
.snav{position:sticky;top:84px;display:flex;flex-direction:column;gap:2px}
|
||||
.snav a{display:block;padding:8px 12px;border-radius:var(--r-sm);color:var(--muted);text-decoration:none;font-size:14.5px}
|
||||
.snav a:hover{color:var(--text);background:var(--surface-2)}
|
||||
.snav a.active{color:var(--text);background:var(--surface-2);font-weight:700;box-shadow:inset 3px 0 0 var(--gold)}
|
||||
.profile-bar{display:flex;gap:12px;align-items:end;flex-wrap:wrap;padding:18px 20px;background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--r-md);margin-bottom:28px}
|
||||
.profile-bar .grow{flex:1;min-width:220px}
|
||||
.profile-bar .lbl{font-size:13px;color:var(--muted);margin-bottom:6px}
|
||||
.group{padding:4px 0 28px;margin-bottom:28px;border-bottom:1px solid var(--line-soft);scroll-margin-top:90px}
|
||||
.group:last-child{border-bottom:0}
|
||||
.group h3{font-size:20px}
|
||||
.group .desc{color:var(--muted);font-size:14px;margin:4px 0 18px}
|
||||
.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 24px}
|
||||
.field.wide{grid-column:1/-1}
|
||||
.field .lbl{font-weight:700;font-size:14px;margin-bottom:6px}
|
||||
.field .hint{font-size:13px;color:var(--muted);margin-top:6px}
|
||||
.field .hint.path{font-family:var(--font-mono);font-size:12px;overflow-wrap:anywhere}
|
||||
.field{transition:opacity .2s}
|
||||
.toggle{display:flex;align-items:flex-start;gap:12px;cursor:pointer}
|
||||
.toggle input{position:absolute;opacity:0;pointer-events:none}
|
||||
.track{flex:0 0 auto;width:40px;height:24px;border-radius:999px;background:var(--line);position:relative;transition:background .2s;margin-top:1px}
|
||||
.track::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.3);transition:transform .2s}
|
||||
.toggle input:checked+.track{background:var(--gold)}
|
||||
.toggle input:checked+.track::after{transform:translateX(16px)}
|
||||
.toggle input:focus-visible+.track{outline:2px solid var(--gold);outline-offset:2px}
|
||||
.toggle input:disabled+.track{opacity:.5}
|
||||
.toggle .tl{font-weight:700;font-size:14px}
|
||||
.days{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.day input{position:absolute;opacity:0;pointer-events:none}
|
||||
.day span{display:inline-block;padding:7px 12px;border-radius:999px;border:1px solid var(--line);background:var(--sunken);font-size:13.5px;cursor:pointer;min-width:52px;text-align:center}
|
||||
.day input:checked+span{background:color-mix(in srgb,var(--gold) 22%,transparent);border-color:var(--gold);color:var(--text);font-weight:700}
|
||||
.day input:focus-visible+span{outline:2px solid var(--gold);outline-offset:2px}
|
||||
.day input:disabled+span{opacity:.5;cursor:default}
|
||||
.savebar{position:fixed;left:0;right:0;bottom:0;z-index:15;display:none;justify-content:center;padding:14px 28px;background:color-mix(in srgb,var(--bar) 92%,transparent);backdrop-filter:blur(10px);border-top:1px solid var(--line-soft)}
|
||||
.savebar.show{display:flex}
|
||||
.savebar .inner{width:100%;max-width:1180px;display:flex;align-items:center;justify-content:flex-end;gap:14px}
|
||||
.saved{font-size:14px;color:var(--muted)}
|
||||
.saved.ok{color:var(--sage)}.saved.bad{color:var(--coral)}
|
||||
|
||||
.logbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px}
|
||||
.logbar .spacer{flex:1}
|
||||
.check{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:14px;cursor:pointer}
|
||||
.check input{accent-color:var(--gold);width:16px;height:16px}
|
||||
#log{background:var(--sunken);border:1px solid var(--line-soft);border-radius:var(--r-md);height:calc(100vh - 230px);min-height:320px;overflow:auto;padding:14px 16px;font-family:var(--font-mono);font-size:12.5px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
#log .ERROR,#log .CRITICAL{color:var(--coral)}
|
||||
#log .WARNING{color:var(--amber)}
|
||||
#log .DEBUG{color:var(--faint)}
|
||||
.trend{margin-top:12px}
|
||||
|
||||
@media (max-width:860px){
|
||||
.topbar{padding:10px 16px;gap:12px}
|
||||
.brand span{display:none}
|
||||
main{padding:20px 16px 120px}
|
||||
.console{grid-template-columns:1fr;justify-items:start;padding:22px}
|
||||
.settings{grid-template-columns:1fr}
|
||||
.snav{position:static;flex-direction:row;overflow-x:auto;gap:6px}
|
||||
.snav a{white-space:nowrap}
|
||||
.fields{grid-template-columns:1fr}
|
||||
.facts{grid-template-columns:1fr 1fr}
|
||||
.fact{border-right:0;border-bottom:1px solid var(--line-soft)}
|
||||
.fact:first-child{grid-column:1/-1}
|
||||
.go{width:104px;height:104px;font-size:24px}
|
||||
.stage{font-size:24px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Shorts Autopilot</h1>
|
||||
<nav>
|
||||
<header class="topbar">
|
||||
<div class="brand"><div class="brand-mark" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 1.8v10.4c0 .6.7 1 1.2.7l8.3-5.2c.5-.3.5-1.1 0-1.4L4.2 1.1C3.7.8 3 1.2 3 1.8z" fill="currentColor"/></svg></div><span>Shorts Autopilot</span></div>
|
||||
<nav class="tabs" aria-label="Sections">
|
||||
<button data-tab="dash" class="active">Dashboard</button>
|
||||
<button data-tab="settings">Settings</button>
|
||||
<button data-tab="logs">Log</button>
|
||||
</nav>
|
||||
<button class="icon-btn" id="theme" aria-label="Switch between light and dark mode" title="Switch theme">
|
||||
<svg class="moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
|
||||
<svg class="sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<main>
|
||||
<section id="dash" class="tab active">
|
||||
<div class="panel row">
|
||||
<button id="go" class="btn go">GO</button>
|
||||
<div class="stat"><div class="k">Active profile</div><select id="prof-dash" class="prof-select"></select></div>
|
||||
<div class="stat"><div class="k">Status</div><div class="v" id="st-stage">idle</div></div>
|
||||
<div class="stat"><div class="k">Engine</div><div class="v" id="st-engine"></div></div>
|
||||
<div class="stat"><div class="k">Published today</div><div class="v" id="st-today">0</div></div>
|
||||
<div class="stat"><div class="k">Channel</div><div class="v" id="st-channel">Not linked</div></div>
|
||||
<span class="badge" id="kids-badge">Made for kids: compliance checks on</span>
|
||||
<div class="console">
|
||||
<button id="go" class="go" aria-live="polite">GO</button>
|
||||
<div class="state">
|
||||
<div class="state-line"><p class="stage" id="st-stage">Idle</p><span class="chip kids" id="kids-badge" hidden>Made for kids checks on</span></div>
|
||||
<div class="progress" id="progress"><div class="segs" id="pg-segs"></div><div class="pg-meta" id="pg-meta"></div></div>
|
||||
<div class="idle-note" id="idle-note">Press GO to start making videos for the active profile.</div>
|
||||
</div>
|
||||
<div class="panel" id="progress-panel">
|
||||
<div class="row" style="justify-content:space-between"><div id="pg-stage" style="font-size:15px"></div><div class="hint" id="pg-meta"></div></div>
|
||||
<div class="bar"><span id="pg-fill"></span></div>
|
||||
</div>
|
||||
<div class="panel row">
|
||||
<div class="stat"><div class="k">Storage folder</div><div class="v small" id="st-storage"></div></div>
|
||||
<div class="stat"><div class="k">Free space</div><div class="v" id="st-free">?</div></div>
|
||||
<div class="stat"><div class="k">After publishing</div><div class="v" id="st-after"></div></div>
|
||||
<dl class="facts">
|
||||
<div class="fact"><dt>Active profile</dt><dd><select id="prof-dash" class="prof-select" aria-label="Active profile"></select></dd></div>
|
||||
<div class="fact"><dt>YouTube channel</dt><dd id="st-channel">Not linked</dd></div>
|
||||
<div class="fact"><dt>Published today</dt><dd id="st-today">0</dd></div>
|
||||
<div class="fact"><dt>Video engine</dt><dd id="st-engine"></dd></div>
|
||||
<div class="fact"><dt>Free space</dt><dd id="st-free">?</dd></div>
|
||||
<div class="fact"><dt>After publishing</dt><dd id="st-after"></dd></div>
|
||||
</dl>
|
||||
<div class="section-head">
|
||||
<h2>Jobs</h2>
|
||||
<div class="actions">
|
||||
<button class="btn sm" id="link">Link YouTube channel</button>
|
||||
<button class="btn sm" id="preview">Preview trending videos</button>
|
||||
<button class="btn sm quiet" id="clear-jobs">Clear list</button>
|
||||
</div>
|
||||
<div class="panel row">
|
||||
<button class="btn secondary" id="link">Link YouTube Channel</button>
|
||||
<button class="btn secondary" id="preview">Preview Trending Videos</button>
|
||||
<span class="hint" id="preview-msg"></span>
|
||||
</div>
|
||||
<div class="panel" id="trending-panel" style="display:none"><h3>Trending</h3><table id="trending"></table></div>
|
||||
<div class="panel"><div class="row" style="justify-content:space-between"><h3 style="margin:0">Jobs</h3><button class="btn secondary" id="clear-jobs" style="padding:6px 12px;font-size:13px">Clear View</button></div><table id="jobs" style="margin-top:10px"></table></div>
|
||||
<div class="table-wrap"><table id="jobs"></table></div>
|
||||
<div id="trending-panel" hidden>
|
||||
<div class="section-head"><h2>Trending right now</h2><span class="muted small" id="preview-msg"></span></div>
|
||||
<div class="table-wrap"><table id="trending"></table></div>
|
||||
</div>
|
||||
<p class="muted small" id="st-storage" style="margin-top:18px"></p>
|
||||
</section>
|
||||
<section id="settings" class="tab">
|
||||
<div class="panel row">
|
||||
<div class="stat"><div class="k">Editing profile</div><select id="prof-set" class="prof-select"></select></div>
|
||||
<button class="btn secondary" id="new-profile">New profile</button>
|
||||
<button class="btn stop" id="del-profile">Delete profile</button>
|
||||
<div class="settings">
|
||||
<nav class="snav" id="snav" aria-label="Settings sections"></nav>
|
||||
<div>
|
||||
<div class="profile-bar">
|
||||
<div class="grow"><div class="lbl">Editing profile</div><select id="prof-set" class="prof-select" aria-label="Editing profile"></select></div>
|
||||
<button class="btn" id="new-profile">New profile</button>
|
||||
<button class="btn danger" id="del-profile">Delete profile</button>
|
||||
</div>
|
||||
<div id="settings-grid"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="grid" id="settings-grid"></div>
|
||||
<div class="row" style="margin-top:16px"><button class="btn" id="save">Save settings</button><span id="saved" class="hint"></span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="logs" class="tab">
|
||||
<div class="panel row">
|
||||
<label style="margin:0;display:flex;gap:6px;align-items:center"><input type="checkbox" id="autoscroll" checked style="width:auto">Auto scroll</label>
|
||||
<button class="btn secondary" id="copy-log">Copy Log</button>
|
||||
<button class="btn secondary" id="clear">Clear View</button>
|
||||
<a href="/api/logs/download">Download full log</a>
|
||||
<span class="hint" id="copy-msg"></span>
|
||||
<div class="logbar">
|
||||
<button class="btn sm" id="copy-log">Copy log</button>
|
||||
<button class="btn sm" id="clear">Clear view</button>
|
||||
<a class="btn sm quiet" href="/api/logs/download">Download full log</a>
|
||||
<span class="muted small" id="copy-msg"></span>
|
||||
<span class="spacer"></span>
|
||||
<label class="check"><input type="checkbox" id="autoscroll" checked>Follow new lines</label>
|
||||
</div>
|
||||
<div id="log"></div>
|
||||
<div id="log" role="log" aria-live="off"></div>
|
||||
</section>
|
||||
</main>
|
||||
<div class="savebar" id="savebar"><div class="inner"><span class="saved" id="saved"></span><button class="btn primary" id="save">Save settings</button></div></div>
|
||||
<script>
|
||||
const CATS="1=Film and Animation|2=Autos and Vehicles|10=Music|15=Pets and Animals|17=Sports|19=Travel and Events|20=Gaming|22=People and Blogs|23=Comedy|24=Entertainment|25=News and Politics|26=Howto and Style|27=Education|28=Science and Technology";
|
||||
const FIELDS=[
|
||||
["","Profile: audience and channel","header","",""],
|
||||
["","Channel and audience","header","Applies to the profile selected above",""],
|
||||
["name","Profile name","text","","p"],
|
||||
["channel_niche","Channel niche","wide","Steers every script, e.g. 60 second animated movies for kids ages 3 to 7","p"],
|
||||
["made_for_kids","Made for kids","checkbox","Flags uploads as made for kids and enforces child-safe rules, word filters, LLM compliance review, and frame vision checks","p"],
|
||||
@@ -130,26 +288,26 @@ const FIELDS=[
|
||||
["trending_count","Videos to scan","select:10|25|50","","p"],
|
||||
["videos_per_day","Videos per day","select:0=Unlimited|1|2|3|4|5|6|8|10|12|15|20","The default YouTube API quota allows about 6 uploads a day. Higher values need a quota increase from Google","p"],
|
||||
["minutes_between_videos","Minutes between videos","select:0=No gap|30|60|120|180|240|360","","p"],
|
||||
["","Publishing schedule","header","",""],
|
||||
["","Publishing schedule","header","When finished videos go public on this profile's channel",""],
|
||||
["publish_mode","When to publish","select:immediate=As soon as it is uploaded|scheduled=At set days and times","Scheduled videos upload as private and YouTube makes them public at the next open time slot","p"],
|
||||
["publish_timezone","Publishing time zone","select:America/New_York=Eastern (US and Canada)|America/Chicago=Central (US and Canada)|America/Denver=Mountain (US and Canada)|America/Phoenix=Arizona|America/Los_Angeles=Pacific (US and Canada)|America/Anchorage=Alaska|Pacific/Honolulu=Hawaii|America/Halifax=Atlantic (Canada)|America/Mexico_City=Mexico City|Europe/London=London|Europe/Berlin=Central Europe|Asia/Kolkata=India|Australia/Sydney=Sydney|UTC=UTC","Times below are in this zone, pick your audience's","p"],
|
||||
["publish_days","Publishing days","days","","p"],
|
||||
["publish_time_1","Publish time 1","time","","p"],
|
||||
["publish_time_2","Publish time 2","time:opt","Optional","p"],
|
||||
["publish_time_3","Publish time 3","time:opt","Optional","p"],
|
||||
["","Working hours (shared by all profiles)","header","",""],
|
||||
["","Working hours","header","Shared by all profiles",""],
|
||||
["work_hours_enabled","Only work during set hours","checkbox","Outside these hours no new video is started. A video already in progress finishes first. Uses this computer's clock","g"],
|
||||
["work_days","Working days","days","","g"],
|
||||
["work_start","Start work at","time","","g"],
|
||||
["work_end","Stop starting new videos at","time","If this is earlier than the start time, the window runs overnight","g"],
|
||||
["","Folders (shared by all profiles)","header","",""],
|
||||
["","Folders and storage","header","Shared by all profiles",""],
|
||||
["storage_dir","Storage folder","wide","Full path on a large disk, e.g. /Volumes/iDrive/shorts-autopilot. Jobs are built in output/ and moved to published/","g"],
|
||||
["models_dir","Models folder","wide","Where AI models download to (several GB). Blank puts them in the storage folder","g"],
|
||||
["temp_dir","Temp folder","wide","Scratch space used while rendering. Blank puts it in the storage folder","g"],
|
||||
["delete_after_publish","Delete videos after publishing","checkbox","Removes the job folder once the upload succeeds instead of moving it to published","g"],
|
||||
["keep_clips","Keep individual clips","checkbox","Keeps the scene clips, images, and narration alongside final.mp4","g"],
|
||||
["auto_install","Install missing pieces automatically","checkbox","Downloads models and installs packages when a run needs them","g"],
|
||||
["","Video engine (shared by all profiles)","header","",""],
|
||||
["","Video engine","header","Shared by all profiles",""],
|
||||
["video_engine","Video engine","select:local=Local (free: AI images, narration, motion)|veo=Veo (paid Google API)","Local runs entirely on this machine at no cost. Veo makes true AI video but needs billing","g"],
|
||||
["sd_model","Image model","select:stabilityai/sdxl-turbo=Fast (SDXL Turbo)|stabilityai/stable-diffusion-xl-base-1.0=Best quality (SDXL Base, slower)","Picking a model fills in its recommended steps, guidance, and size. First use downloads about 7 GB","g"],
|
||||
["sd_steps","Image steps","select:4|6|8|20|25|30","4 to 8 for Fast, 20 to 30 for Best quality","g"],
|
||||
@@ -162,7 +320,7 @@ const FIELDS=[
|
||||
["piper_model","Piper voice file","wide","Blank uses the first voice found in the voices folder under Models","g"],
|
||||
["gemini_api_key","Gemini API key (Veo)","password","Only needed for the Veo engine. Billing must be enabled","g"],
|
||||
["veo_model","Veo model","text","e.g. veo-3.1-fast-generate-preview","g"],
|
||||
["","Text and shared options","header","",""],
|
||||
["","Writing and timing","header","Shared by all profiles",""],
|
||||
["ollama_url","Ollama URL","text","","g"],
|
||||
["ollama_model","Ollama model","model:text","Writes scripts and tags. Models not installed yet are downloaded automatically","g"],
|
||||
["vision_model","Ollama vision model","model:vision","Checks every scene on made-for-kids profiles. qwen2.5vl:7b is recommended, moondream is too weak to judge anatomy","g"],
|
||||
@@ -174,51 +332,76 @@ const FIELDS=[
|
||||
];
|
||||
const DAYS=[["mon","Mon"],["tue","Tue"],["wed","Wed"],["thu","Thu"],["fri","Fri"],["sat","Sat"],["sun","Sun"]];
|
||||
const TIMES=[];for(let h=0;h<24;h++)for(const m of [0,30]){const v=String(h).padStart(2,"0")+":"+String(m).padStart(2,"0");const hr=h%12||12;TIMES.push([v,hr+":"+String(m).padStart(2,"0")+(h<12?" AM":" PM")])}
|
||||
const NUMERIC=new Set(["sd_steps","sd_guidance","sd_width","sd_height","trending_days","trending_count","videos_per_day","minutes_between_videos","target_seconds","clip_seconds","ollama_timeout","error_cooldown_minutes","tts_rate"]);
|
||||
const $=s=>document.querySelector(s);
|
||||
const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
||||
async function api(path,opts={}){const r=await fetch(path,{headers:{"Content-Type":"application/json"},...opts});const j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.detail||r.statusText);return j}
|
||||
document.querySelectorAll("nav button").forEach(b=>b.onclick=()=>{document.querySelectorAll("nav button").forEach(x=>x.classList.remove("active"));document.querySelectorAll(".tab").forEach(x=>x.classList.remove("active"));b.classList.add("active");$("#"+b.dataset.tab).classList.add("active")});
|
||||
function opt(o){const i=o.indexOf("=");return i<0?[o,o]:[o.slice(0,i),o.slice(i+1)]}
|
||||
|
||||
$("#theme").onclick=()=>{const next=document.documentElement.getAttribute("data-theme")==="light"?"dark":"light";document.documentElement.setAttribute("data-theme",next);try{localStorage.setItem("sa-theme",next)}catch(e){}};
|
||||
|
||||
function showTab(name){
|
||||
document.querySelectorAll(".tabs button").forEach(x=>x.classList.toggle("active",x.dataset.tab===name));
|
||||
document.querySelectorAll(".tab").forEach(x=>x.classList.toggle("active",x.id===name));
|
||||
$("#savebar").classList.toggle("show",name==="settings");
|
||||
try{localStorage.setItem("sa-tab",name)}catch(e){}}
|
||||
document.querySelectorAll(".tabs button").forEach(b=>b.onclick=()=>showTab(b.dataset.tab));
|
||||
|
||||
function renderProfiles(list,active){
|
||||
document.querySelectorAll(".prof-select").forEach(sel=>{sel.innerHTML=list.map(p=>`<option value="${esc(p.id)}" ${p.id===active?"selected":""}>${esc(p.name)}${p.channel?" ("+esc(p.channel)+")":""}</option>`).join("")})}
|
||||
document.querySelectorAll(".prof-select").forEach(sel=>{sel.innerHTML=list.map(p=>`<option value="${esc(p.id)}" ${p.id===active?"selected":""}>${esc(p.name)}${p.channel&&p.channel!==p.name?" ("+esc(p.channel)+")":""}</option>`).join("")})}
|
||||
|
||||
function fieldInput(k,type,s,data){
|
||||
if(type.startsWith("select:")){const list=type.slice(7).split("|").map(opt);const cur=String(s[k]??"");if(cur!==""&&!list.some(([v])=>v===cur))list.unshift([cur,cur+" (current)"]);return `<select id="f-${k}">${list.map(([v,l])=>`<option value="${esc(v)}" ${cur===v?"selected":""}>${esc(l)}</option>`).join("")}</select>`}
|
||||
if(type==="days"){const on=new Set(String(s[k]||"").split(","));return `<div class="days" id="f-${k}">${DAYS.map(([v,l])=>`<label class="day"><input type="checkbox" value="${v}" ${on.has(v)?"checked":""}><span>${l}</span></label>`).join("")}</div>`}
|
||||
if(type.startsWith("time")){const cur=String(s[k]||"");const opts=(type==="time:opt"?[["","None"]]:[]).concat(TIMES);if(cur&&!opts.some(([v])=>v===cur))opts.push([cur,cur]);return `<select id="f-${k}">${opts.map(([v,l])=>`<option value="${esc(v)}" ${cur===v?"selected":""}>${esc(l)}</option>`).join("")}</select>`}
|
||||
if(type.startsWith("model:")){
|
||||
const kind=type.slice(6);
|
||||
const all=data.installed_models||[];const vis=new Set(data.installed_vision_models||[]);
|
||||
const installed=kind==="vision"?all.filter(m=>vis.has(m)):all.filter(m=>!vis.has(m));
|
||||
const suggested=(kind==="vision"?data.suggested_vision_models:data.suggested_text_models)||[];
|
||||
const cur=String(s[k]||"");const seen=new Set();const opts=[];
|
||||
for(const m of installed){if(seen.has(m))continue;seen.add(m);opts.push(`<option value="${esc(m)}" ${cur===m?"selected":""}>${esc(m)}</option>`)}
|
||||
for(const m of suggested){if(seen.has(m)||seen.has(m+":latest"))continue;seen.add(m);opts.push(`<option value="${esc(m)}" ${cur===m?"selected":""}>${esc(m)} (downloads on next run)</option>`)}
|
||||
if(cur&&!seen.has(cur))opts.unshift(`<option value="${esc(cur)}" selected>${esc(cur)} (current)</option>`);
|
||||
return `<select id="f-${k}">${opts.join("")}</select>`}
|
||||
return `<input type="${type==="wide"?"text":type}" id="f-${k}" value="${esc(s[k])}">`}
|
||||
|
||||
let spy=null;
|
||||
async function loadSettings(){
|
||||
const data=await api("/api/settings");
|
||||
renderProfiles(data.profiles,data.active);
|
||||
const g=$("#settings-grid");g.innerHTML="";
|
||||
const root=$("#settings-grid");root.innerHTML="";
|
||||
const nav=$("#snav");nav.innerHTML="";
|
||||
let grid=null,n=0;
|
||||
for(const [k,label,type,hint,scope] of FIELDS){
|
||||
if(type==="header"){const h=document.createElement("h3");h.textContent=label;g.appendChild(h);continue}
|
||||
if(type==="header"){
|
||||
n++;const sec=document.createElement("section");sec.className="group";sec.id="sec-"+n;
|
||||
sec.innerHTML=`<h3>${esc(label)}</h3>${hint?`<p class="desc">${esc(hint)}</p>`:""}<div class="fields"></div>`;
|
||||
root.appendChild(sec);grid=sec.querySelector(".fields");
|
||||
const a=document.createElement("a");a.href="#sec-"+n;a.textContent=label;a.dataset.sec="sec-"+n;
|
||||
a.onclick=e=>{e.preventDefault();sec.scrollIntoView({behavior:matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth",block:"start"})};
|
||||
nav.appendChild(a);continue}
|
||||
const s=scope==="g"?data.global:data.profile;
|
||||
let input;
|
||||
if(type.startsWith("select:")){const list=type.slice(7).split("|").map(opt);const cur=String(s[k]??"");if(!list.some(([v])=>v===cur))list.unshift([cur,cur+" (current)"]);input=`<select id="f-${k}">${list.map(([v,l])=>`<option value="${esc(v)}" ${cur===v?"selected":""}>${esc(l)}</option>`).join("")}</select>`}
|
||||
else if(type==="checkbox"){input=`<input type="checkbox" id="f-${k}" ${s[k]?"checked":""} style="width:auto">`}
|
||||
else if(type==="days"){const on=new Set(String(s[k]||"").split(","));input=`<div class="row" id="f-${k}" data-days="1" style="gap:10px">${DAYS.map(([v,l])=>`<label style="margin:0;display:flex;gap:4px;align-items:center;color:var(--text)"><input type="checkbox" value="${v}" ${on.has(v)?"checked":""} style="width:auto">${l}</label>`).join("")}</div>`}
|
||||
else if(type.startsWith("time")){const cur=String(s[k]||"");const opts=(type==="time:opt"?[["","None"]]:[]).concat(TIMES);if(cur&&!opts.some(([v])=>v===cur))opts.push([cur,cur]);input=`<select id="f-${k}">${opts.map(([v,l])=>`<option value="${esc(v)}" ${cur===v?"selected":""}>${esc(l)}</option>`).join("")}</select>`}
|
||||
else if(type.startsWith("model:")){
|
||||
const kind=type.slice(6);
|
||||
const allInstalled=data.installed_models||[];const vis=new Set(data.installed_vision_models||[]);
|
||||
const installed=kind==="vision"?allInstalled.filter(m=>vis.has(m)):allInstalled.filter(m=>!vis.has(m));
|
||||
const suggested=(kind==="vision"?data.suggested_vision_models:data.suggested_text_models)||[];
|
||||
const cur=String(s[k]||"");
|
||||
const seen=new Set();
|
||||
const opts=[];
|
||||
for(const m of installed){if(seen.has(m))continue;seen.add(m);opts.push(`<option value="${esc(m)}" ${cur===m?"selected":""}>${esc(m)}</option>`)}
|
||||
for(const m of suggested){if(seen.has(m)||seen.has(m+":latest"))continue;seen.add(m);opts.push(`<option value="${esc(m)}" ${cur===m?"selected":""}>${esc(m)} (not installed)</option>`)}
|
||||
if(cur&&!seen.has(cur))opts.unshift(`<option value="${esc(cur)}" selected>${esc(cur)} (not installed)</option>`);
|
||||
input=`<select id="f-${k}">${opts.join("")}</select>`}
|
||||
else{input=`<input type="${type==="wide"?"text":type}" id="f-${k}" value="${esc(s[k])}">`}
|
||||
const d=document.createElement("div");d.className="field"+(type==="wide"||type==="days"?" wide":"");
|
||||
let extra="";
|
||||
if(k==="storage_dir")extra=`<div class="hint">Resolves to: ${esc(data.storage_resolved)}</div>`;
|
||||
if(k==="models_dir")extra=`<div class="hint">Resolves to: ${esc(data.models_resolved)}</div>`;
|
||||
if(k==="temp_dir")extra=`<div class="hint">Resolves to: ${esc(data.temp_resolved)}</div>`;
|
||||
const d=document.createElement("div");if(type==="wide")d.className="wide";
|
||||
d.innerHTML=`<label>${label}</label>${input}${hint?`<div class="hint">${esc(hint)}</div>`:""}${extra}`;g.appendChild(d)}
|
||||
if(k==="storage_dir")extra=`<div class="hint path">${esc(data.storage_resolved)}</div>`;
|
||||
if(k==="models_dir")extra=`<div class="hint path">${esc(data.models_resolved)}</div>`;
|
||||
if(k==="temp_dir")extra=`<div class="hint path">${esc(data.temp_resolved)}</div>`;
|
||||
if(type==="checkbox"){
|
||||
d.innerHTML=`<label class="toggle"><input type="checkbox" id="f-${k}" ${s[k]?"checked":""}><span class="track"></span><span><span class="tl">${esc(label)}</span>${hint?`<span class="hint" style="display:block">${esc(hint)}</span>`:""}</span></label>`}
|
||||
else{
|
||||
d.innerHTML=`<label class="lbl" for="f-${k}">${esc(label)}</label>${fieldInput(k,type,s,data)}${hint?`<div class="hint">${esc(hint)}</div>`:""}${extra}`}
|
||||
grid.appendChild(d)}
|
||||
if(spy)spy.disconnect();
|
||||
spy=new IntersectionObserver(es=>{for(const e of es){if(e.isIntersecting){nav.querySelectorAll("a").forEach(a=>a.classList.toggle("active",a.dataset.sec===e.target.id))}}},{rootMargin:"-90px 0px -65% 0px"});
|
||||
root.querySelectorAll(".group").forEach(g=>spy.observe(g));
|
||||
const first=nav.querySelector("a");if(first)first.classList.add("active");
|
||||
syncFields()}
|
||||
|
||||
function dim(ids,off){for(const id of ids){const el=$(id);if(!el)continue;el.querySelectorAll("input").forEach(x=>x.disabled=off);if(el.tagName!=="DIV")el.disabled=off;const f=el.closest(".field");if(f)f.style.opacity=off?.5:1}}
|
||||
function syncFields(){
|
||||
const kids=$("#f-made_for_kids");
|
||||
if(kids){
|
||||
for(const id of ["#f-kids_manual_review","#f-kids_llm_review"]){const el=$(id);if(!el)continue;el.disabled=!kids.checked;el.closest("div").style.opacity=kids.checked?1:.5}
|
||||
kids.onchange=syncFields}
|
||||
const dim=(ids,off)=>{for(const id of ids){const el=$(id);if(!el)continue;el.querySelectorAll?el.querySelectorAll("input").forEach(x=>x.disabled=off):0;if(el.tagName!=="DIV")el.disabled=off;el.closest(".grid > div").style.opacity=off?.5:1}};
|
||||
if(kids){dim(["#f-kids_manual_review","#f-kids_llm_review"],!kids.checked);kids.onchange=syncFields}
|
||||
const wh=$("#f-work_hours_enabled");
|
||||
if(wh){dim(["#f-work_days","#f-work_start","#f-work_end"],!wh.checked);wh.onchange=syncFields}
|
||||
const pm=$("#f-publish_mode");
|
||||
@@ -229,45 +412,56 @@ const best=sdm.value.includes("stable-diffusion-xl-base");
|
||||
const set=(id,v)=>{const el=$(id);if(!el)return;if(![...el.options].some(o=>o.value===v)){const o=document.createElement("option");o.value=v;o.textContent=v;el.appendChild(o)}el.value=v};
|
||||
set("#f-sd_steps",best?"25":"4");set("#f-sd_guidance",best?"7":"2");set("#f-sd_width",best?"768":"704");set("#f-sd_height",best?"1344":"1216")})}
|
||||
const eng=$("#f-video_engine");
|
||||
if(eng){
|
||||
const local=eng.value==="local";
|
||||
for(const id of ["#f-sd_model","#f-sd_steps","#f-sd_guidance","#f-sd_width","#f-sd_height","#f-tts_engine","#f-tts_voice","#f-tts_rate","#f-piper_model"]){const el=$(id);if(!el)continue;el.disabled=!local;el.closest("div").style.opacity=local?1:.5}
|
||||
for(const id of ["#f-gemini_api_key","#f-veo_model"]){const el=$(id);if(!el)continue;el.disabled=local;el.closest("div").style.opacity=local?.5:1}
|
||||
if(eng){const local=eng.value==="local";
|
||||
dim(["#f-sd_model","#f-sd_steps","#f-sd_guidance","#f-sd_width","#f-sd_height","#f-tts_engine","#f-tts_voice","#f-tts_rate","#f-piper_model"],!local);
|
||||
dim(["#f-gemini_api_key","#f-veo_model"],local);
|
||||
eng.onchange=syncFields}}
|
||||
|
||||
async function saveSettings(){
|
||||
const body={global:{},profile:{}};
|
||||
const NUMERIC=new Set(["sd_steps","sd_guidance","sd_width","sd_height","trending_days","trending_count","videos_per_day","minutes_between_videos","target_seconds","clip_seconds","ollama_timeout","error_cooldown_minutes","tts_rate"]);
|
||||
for(const [k,,type,,scope] of FIELDS){if(type==="header")continue;const el=$("#f-"+k);const dest=body[scope==="g"?"global":"profile"];
|
||||
for(const [k,,type,,scope] of FIELDS){if(type==="header")continue;const el=$("#f-"+k);if(!el)continue;const dest=body[scope==="g"?"global":"profile"];
|
||||
if(type==="days"){dest[k]=[...el.querySelectorAll("input:checked")].map(x=>x.value).join(",");continue}
|
||||
dest[k]=type==="checkbox"?el.checked:(type==="number"||NUMERIC.has(k))?Number(el.value):el.value}
|
||||
try{await api("/api/settings",{method:"POST",body:JSON.stringify(body)});await loadSettings();$("#saved").textContent="Saved "+new Date().toLocaleTimeString();refresh()}catch(e){$("#saved").textContent="Error: "+e.message}}
|
||||
const msg=$("#saved");const btn=$("#save");btn.disabled=true;
|
||||
try{await api("/api/settings",{method:"POST",body:JSON.stringify(body)});await loadSettings();msg.className="saved ok";msg.textContent="Saved at "+new Date().toLocaleTimeString([], {hour:"numeric",minute:"2-digit"});refresh()}
|
||||
catch(e){msg.className="saved bad";msg.textContent=e.message}
|
||||
btn.disabled=false}
|
||||
|
||||
async function activate(id){try{await api("/api/profiles/activate",{method:"POST",body:JSON.stringify({id})})}catch(e){alert(e.message)}await loadSettings();refresh()}
|
||||
|
||||
const STATUS_TEXT={review:"Private, needs review",published:"Published",failed:"Failed",running:"Running",cancelled:"Cancelled",scheduled:"Scheduled"};
|
||||
function fmtDur(d){d=Math.max(0,d|0);const h=Math.floor(d/3600),m=Math.floor(d%3600/60),s=d%60;return h?`${h}h ${m}m`:m?`${m}m ${s}s`:`${s}s`}
|
||||
function fmtStart(t){if(!t)return"";const d=new Date(t.replace(" ","T"));if(isNaN(d))return t;return d.toLocaleString([], {month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}
|
||||
|
||||
async function refresh(){
|
||||
try{
|
||||
const s=await api("/api/status");
|
||||
$("#st-stage").textContent=s.stage;
|
||||
const stage=s.stage||"idle";
|
||||
$("#st-stage").textContent=stage;
|
||||
$("#st-engine").textContent=s.engine==="local"?"Local (free)":"Veo (paid)";
|
||||
$("#st-today").textContent=s.published_today+(s.videos_per_day>0?" / "+s.videos_per_day:"");
|
||||
$("#st-today").textContent=s.published_today+(s.videos_per_day>0?" of "+s.videos_per_day:"");
|
||||
$("#st-channel").textContent=s.channel||"Not linked";
|
||||
$("#st-storage").textContent=s.storage;
|
||||
$("#st-free").textContent=s.storage_free_gb===null?"not created yet":s.storage_free_gb+" GB";
|
||||
$("#st-free").style.color=s.storage_free_gb!==null&&s.storage_free_gb<10?"#ffb74d":"";
|
||||
const free=$("#st-free");free.textContent=s.storage_free_gb===null?"Folder not created":s.storage_free_gb.toLocaleString()+" GB";free.classList.toggle("warn",s.storage_free_gb!==null&&s.storage_free_gb<10);
|
||||
$("#st-after").textContent=s.delete_after_publish?"Delete files":"Move to published";
|
||||
$("#kids-badge").style.display=s.made_for_kids?"inline-block":"none";
|
||||
const panel=$("#progress-panel");
|
||||
$("#st-storage").textContent=s.storage?"Videos are stored in "+s.storage:"";
|
||||
$("#kids-badge").hidden=!s.made_for_kids;
|
||||
const pg=$("#progress");const note=$("#idle-note");
|
||||
if(s.steps>0&&(s.running||s.step<s.steps)){
|
||||
panel.style.display="block";
|
||||
pg.classList.add("show");note.hidden=true;
|
||||
const segs=$("#pg-segs");if(segs.childElementCount!==s.steps){segs.innerHTML="<i></i>".repeat(s.steps)}
|
||||
[...segs.children].forEach((el,i)=>{el.className=i<s.step-1?"on":i===s.step-1?"now":""});
|
||||
const pct=Math.min(100,Math.round(s.step*100/s.steps));
|
||||
$("#pg-fill").style.width=pct+"%";
|
||||
$("#pg-stage").textContent=s.stage;
|
||||
const mins=Math.floor(s.elapsed/60),secs=s.elapsed%60;
|
||||
$("#pg-meta").textContent=pct+"% | step "+s.step+" of "+s.steps+(s.elapsed?" | "+(mins?mins+"m ":"")+secs+"s":"");
|
||||
}else{panel.style.display="none"}
|
||||
const lb=$("#link");lb.textContent=s.channel?"Unlink YouTube Channel":"Link YouTube Channel";lb.dataset.linked=s.channel?"1":"";
|
||||
$("#pg-meta").textContent=`Step ${s.step} of ${s.steps}, ${pct}% done`+(s.elapsed?`, running for ${fmtDur(s.elapsed)}`:"");
|
||||
}else{pg.classList.remove("show");note.hidden=false;note.textContent=s.running?"Waiting for the next video to start.":"Press GO to start making videos for the active profile."}
|
||||
const lb=$("#link");lb.textContent=s.channel?"Unlink YouTube channel":"Link YouTube channel";lb.dataset.linked=s.channel?"1":"";
|
||||
setButton(s.running);
|
||||
const dur=h=>{if(!h.started_ts)return"";const end=h.finished_ts||(h.status==="running"?s.now:0);if(!end)return"";const d=Math.max(0,end-h.started_ts);const m=Math.floor(d/60),sec=d%60;return(m?m+"m ":"")+sec+"s"};
|
||||
$("#jobs").innerHTML="<tr><th>Started</th><th>Time</th><th>Profile</th><th>Source</th><th>Title</th><th>Status</th><th>Video</th></tr>"+s.history.map(h=>`<tr><td>${esc(h.started)}</td><td style="white-space:nowrap">${esc(dur(h))}</td><td>${esc(h.profile)}</td><td><a target="_blank" href="https://youtube.com/watch?v=${esc(h.source_id)}">${esc(h.source_title)}</a></td><td>${esc(h.title)}</td><td><span class="pill ${esc(h.status)}">${h.status==="review"?"private, needs review":h.status==="scheduled"&&h.publish_at?"scheduled "+new Date(h.publish_at).toLocaleString([], {weekday:"short",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):esc(h.status)}</span>${h.notes?`<div class="hint notes">Reviewer notes: ${esc(h.notes)}</div>`:""}${h.error?`<div class="hint">${esc(h.error)}</div>`:""}${h.location?`<div class="hint">${esc(h.location)}</div>`:""}</td><td>${h.youtube_id?`<a target="_blank" href="https://studio.youtube.com/video/${esc(h.youtube_id)}/edit">studio</a> <a target="_blank" href="https://youtube.com/shorts/${esc(h.youtube_id)}">open</a>`:""}</td></tr>`).join("")
|
||||
const dur=h=>{if(!h.started_ts)return"";const end=h.finished_ts||(h.status==="running"?s.now:0);return end?fmtDur(end-h.started_ts):""};
|
||||
const rows=s.history.map(h=>{
|
||||
const label=h.status==="scheduled"&&h.publish_at?"Scheduled for "+new Date(h.publish_at).toLocaleString([], {weekday:"short",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):(STATUS_TEXT[h.status]||h.status);
|
||||
return `<tr><td class="nowrap">${esc(fmtStart(h.started))}</td><td class="nowrap">${esc(dur(h))}</td><td class="title">${esc(h.title||"Writing script")}<div class="sub">${esc(h.profile)}</div></td><td class="source"><a target="_blank" rel="noopener" href="https://youtube.com/watch?v=${esc(h.source_id)}">${esc(h.source_title)}</a></td><td><span class="chip st-${esc(h.status)}">${esc(label)}</span>${h.notes?`<div class="sub notes">Reviewer notes: ${esc(h.notes)}</div>`:""}${h.error?`<div class="sub err">${esc(h.error)}</div>`:""}</td><td>${h.youtube_id?`<div class="links"><a class="btn sm" target="_blank" rel="noopener" href="https://studio.youtube.com/video/${esc(h.youtube_id)}/edit">Studio</a><a class="btn sm" target="_blank" rel="noopener" href="https://youtube.com/shorts/${esc(h.youtube_id)}">Watch</a></div>`:""}</td></tr>`}).join("");
|
||||
$("#jobs").innerHTML=`<thead><tr><th>Started</th><th>Time</th><th>Video</th><th>Inspired by</th><th>Status</th><th></th></tr></thead><tbody>${rows||`<tr><td colspan="6" class="empty">No jobs yet. Press GO and finished videos will show up here.</td></tr>`}</tbody>`;
|
||||
}catch(e){}}
|
||||
|
||||
let since=0;
|
||||
async function pollLogs(){
|
||||
try{
|
||||
@@ -276,25 +470,28 @@ for(const [seq,level,line] of r.lines){const d=document.createElement("div");d.c
|
||||
while(box.childElementCount>3000)box.firstChild.remove();
|
||||
if(r.lines.length&&$("#autoscroll").checked)box.scrollTop=box.scrollHeight
|
||||
}catch(e){}}
|
||||
|
||||
document.querySelectorAll(".prof-select").forEach(sel=>sel.onchange=e=>activate(e.target.value));
|
||||
$("#new-profile").onclick=async()=>{
|
||||
const name=prompt("New profile name (starts as a copy of the current profile's settings)");
|
||||
const name=prompt("Name for the new profile. It starts as a copy of the current profile's settings.");
|
||||
if(!name)return;
|
||||
try{await api("/api/profiles",{method:"POST",body:JSON.stringify({name})});await loadSettings();refresh()}catch(e){alert(e.message)}};
|
||||
$("#del-profile").onclick=async()=>{
|
||||
const sel=$("#prof-set");const label=sel.options[sel.selectedIndex]?.text||"";
|
||||
if(!confirm(`Delete profile "${label}" and its linked channel token? Videos on disk are kept.`))return;
|
||||
if(!confirm(`Delete the profile "${label}" and its linked channel? Videos already on disk are kept.`))return;
|
||||
try{await api("/api/profiles/delete",{method:"POST",body:JSON.stringify({id:sel.value})});await loadSettings();refresh()}catch(e){alert(e.message)}};
|
||||
|
||||
function setButton(running){
|
||||
const b=$("#go");
|
||||
if(b.dataset.busy==="1")return;
|
||||
b.classList.toggle("running",running);
|
||||
b.textContent=running?"STOP":"GO";
|
||||
b.setAttribute("aria-label",running?"Stop making videos":"Start making videos");
|
||||
b.disabled=false}
|
||||
$("#go").onclick=async()=>{
|
||||
const b=$("#go");const running=b.classList.contains("running");
|
||||
b.dataset.busy="1";b.disabled=true;b.classList.remove("running");b.classList.add("busy");
|
||||
b.textContent=running?"Stopping...":"Starting...";
|
||||
b.textContent="...";
|
||||
try{await api(running?"/api/stop":"/api/start",{method:"POST"})}catch(e){alert(e.message)}
|
||||
b.dataset.busy="";b.classList.remove("busy");
|
||||
setButton(!running);
|
||||
@@ -307,27 +504,27 @@ else{window.location.href="/auth/start"}};
|
||||
$("#save").onclick=saveSettings;
|
||||
$("#clear").onclick=()=>{$("#log").innerHTML=""};
|
||||
$("#copy-log").onclick=async()=>{
|
||||
const text=$("#log").innerText.trim();
|
||||
if(!text){$("#copy-msg").textContent="Nothing to copy";return}
|
||||
try{await navigator.clipboard.writeText(text);$("#copy-msg").textContent="Copied "+text.split("\n").length+" lines"}
|
||||
catch(e){
|
||||
const ta=document.createElement("textarea");ta.value=text;document.body.appendChild(ta);ta.select();
|
||||
try{document.execCommand("copy");$("#copy-msg").textContent="Copied"}catch(err){$("#copy-msg").textContent="Could not copy"}
|
||||
ta.remove()}
|
||||
setTimeout(()=>{$("#copy-msg").textContent=""},3000)};
|
||||
const text=$("#log").innerText.trim();const m=$("#copy-msg");
|
||||
if(!text){m.textContent="Nothing to copy";return}
|
||||
try{await navigator.clipboard.writeText(text);m.textContent="Copied "+text.split("\n").length+" lines"}
|
||||
catch(e){const ta=document.createElement("textarea");ta.value=text;document.body.appendChild(ta);ta.select();try{document.execCommand("copy");m.textContent="Copied"}catch(err){m.textContent="Could not copy"}ta.remove()}
|
||||
setTimeout(()=>{m.textContent=""},3000)};
|
||||
$("#clear-jobs").onclick=async()=>{
|
||||
const b=$("#clear-jobs");
|
||||
if(b.disabled)return;
|
||||
b.disabled=true;
|
||||
const b=$("#clear-jobs");if(b.disabled)return;b.disabled=true;
|
||||
try{await api("/api/history/clear",{method:"POST"});await refresh()}catch(e){alert(e.message)}
|
||||
b.disabled=false};
|
||||
$("#preview").onclick=async()=>{
|
||||
$("#preview-msg").textContent="Loading...";
|
||||
const panel=$("#trending-panel");const m=$("#preview-msg");const b=$("#preview");
|
||||
panel.hidden=false;m.textContent="Loading";b.disabled=true;
|
||||
try{
|
||||
const r=await api("/api/trending");
|
||||
$("#trending").innerHTML="<tr><th>Views</th><th>Title</th><th>Channel</th></tr>"+r.items.map(v=>`<tr><td>${v.views.toLocaleString()}</td><td><a target="_blank" href="https://youtube.com/watch?v=${esc(v.id)}">${esc(v.title)}</a></td><td>${esc(v.channel)}</td></tr>`).join("");
|
||||
$("#trending-panel").style.display="block";$("#preview-msg").textContent=""
|
||||
}catch(e){$("#preview-msg").textContent="Error: "+e.message}};
|
||||
$("#trending").innerHTML=`<thead><tr><th>Views</th><th>Title</th><th>Channel</th></tr></thead><tbody>${r.items.map(v=>`<tr><td class="nowrap">${v.views.toLocaleString()}</td><td class="source"><a target="_blank" rel="noopener" href="https://youtube.com/watch?v=${esc(v.id)}">${esc(v.title)}</a></td><td>${esc(v.channel)}</td></tr>`).join("")||`<tr><td colspan="3" class="empty">No videos matched the current search settings.</td></tr>`}</tbody>`;
|
||||
m.textContent=r.items.length+" videos";
|
||||
}catch(e){m.textContent=e.message}
|
||||
b.disabled=false};
|
||||
|
||||
let startTab="dash";try{const t=localStorage.getItem("sa-tab");if(["dash","settings","logs"].includes(t))startTab=t}catch(e){}
|
||||
showTab(startTab);
|
||||
loadSettings();refresh();pollLogs();
|
||||
setInterval(refresh,3000);setInterval(pollLogs,2000);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user