@telepat/rilo 0.1.0 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -16
- package/frontend/dist/assets/index-BvNWHzrr.js +13 -0
- package/frontend/dist/assets/index-Wcsyfv3q.css +1 -0
- package/frontend/dist/favicon.svg +19 -0
- package/frontend/dist/index.html +14 -0
- package/frontend/dist/talefire-logo-dark.svg +34 -0
- package/frontend/dist/talefire-logo-light.svg +36 -0
- package/package.json +3 -1
- package/src/api/middleware/auth.js +67 -11
- package/src/api/openapi/spec.js +3 -1
- package/src/api/routes/jobs.js +21 -2
- package/src/api/routes/projects.js +16 -1
- package/src/api/server.js +42 -8
- package/src/cli/commands/preview.js +126 -0
- package/src/cli/index.js +41 -2
- package/src/pipeline/orchestrator.js +17 -1
- package/src/store/projectStore.js +5 -0
- package/src/types/job.js +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{color-scheme:dark;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:15px;--bg: #0d1117;--bg-card: #161b22;--bg-elevated: #1c2330;--border: #30363d;--border-subtle: #21262d;--text: #e6edf3;--text-muted: #7d8590;--text-subtle: #484f58;--accent: #ff4e00;--accent-hover: #ff7a00;--green: #3fb950;--yellow: #d29922;--orange: #db6d28;--red: #f85149;--flame: #ff4e00;--flame-mid: #ff7a00;--flame-yellow: #ffcc00;--sidebar-w: 260px;background:var(--bg);color:var(--text)}*,*:before,*:after{box-sizing:border-box}body{margin:0;height:100dvh;overflow:hidden;background:var(--bg)}button,input,textarea,select{font:inherit;color:inherit}button{cursor:pointer}p,h2,h3{margin:0}textarea,input{border:1px solid var(--border);border-radius:8px;padding:.55rem .75rem;background:var(--bg);color:var(--text);width:100%;resize:vertical;transition:border-color .15s}textarea:focus,input:focus{outline:none;border-color:var(--accent)}.app-shell{display:grid;grid-template-columns:var(--sidebar-w) 1fr;height:100dvh;overflow:hidden}.sidebar{display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg-card);overflow:hidden}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:1.1rem 1rem;border-bottom:1px solid var(--border-subtle);flex-shrink:0}.brand-wordmark{font-size:1.1rem;font-weight:800;letter-spacing:.02em;text-transform:uppercase;color:var(--text);flex-shrink:0}.sidebar-section{flex:1;display:flex;flex-direction:column;gap:.65rem;padding:.9rem;overflow:hidden}.sidebar-row{display:flex;align-items:center;justify-content:space-between}.sidebar-section-title{font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}.project-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;overflow-y:auto;flex:1;min-height:0}.project-item{width:100%;text-align:left;background:transparent;border:1px solid transparent;border-radius:6px;padding:.55rem .75rem;font-size:.88rem;color:var(--text-muted);transition:background .1s,color .1s,border-color .1s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.project-item:hover{background:var(--bg-elevated);color:var(--text)}.project-item-active{background:var(--bg-elevated);border-color:var(--border);border-left-color:var(--accent);color:var(--text);font-weight:500}.sidebar-footer{padding:.6rem .75rem;border-top:1px solid var(--border-subtle);flex-shrink:0}.main-area{display:flex;flex-direction:column;overflow:hidden}.project-header{flex-shrink:0;background:var(--bg-card);border-bottom:1px solid var(--border);padding:1rem 1.5rem 0;display:flex;flex-direction:column;gap:.75rem;z-index:10}.project-header-top{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap}.project-title-row{display:flex;align-items:center;gap:.6rem;min-width:0}.project-name{font-size:1.05rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-actions{display:flex;gap:.4rem;align-items:center;flex-wrap:wrap;flex-shrink:0}.step-dots{display:flex;gap:0;align-items:center;overflow-x:auto;padding-bottom:.15rem}.dot{display:flex;align-items:center;gap:.35rem;font-size:.75rem;padding:.2rem .65rem;border-radius:20px;transition:background .2s;white-space:nowrap;flex-shrink:0}.dot:before{content:"";width:7px;height:7px;border-radius:50%;flex-shrink:0}.dot-done{color:var(--green)}.dot-done:before{background:var(--green)}.dot-running{color:var(--yellow);animation:pulse 1.2s ease-in-out infinite}.dot-running:before{background:var(--yellow)}.dot-idle{color:var(--text-muted)}.dot-idle:before{background:var(--border)}.dot-label{font-weight:500}@keyframes pulse{0%,to{opacity:1}50%{opacity:.45}}.tab-spacer{flex:1}.analytics-pane{gap:1.25rem}.analytics-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem}@media(max-width:800px){.analytics-summary{grid-template-columns:repeat(2,1fr)}}.analytics-stat-card{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:10px;padding:1.2rem 1.4rem;display:flex;flex-direction:column;gap:.4rem}.stat-value{font-size:1.5rem;font-weight:600;color:var(--text);line-height:1}.stat-label{font-size:.78rem;color:var(--text-muted)}.stat-ok{color:var(--green)}.stat-fail{color:var(--red)}.analytics-empty{margin-top:.25rem}.analytics-error{color:var(--red)}.analytics-runs{display:flex;flex-direction:column;gap:.35rem}.analytics-runs-title{font-size:.78rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem}.run-row{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden}.run-row-header{display:flex;align-items:center;gap:.75rem;width:100%;background:transparent;border:none;padding:.8rem 1rem;text-align:left;cursor:pointer;transition:background .12s}.run-row-header:hover{background:var(--bg-elevated)}.run-badge{font-size:.72rem;font-weight:600;padding:.15em .55em;border-radius:6px;flex-shrink:0}.run-badge-ok{background:color-mix(in srgb,var(--green) 15%,transparent);color:var(--green)}.run-badge-fail{background:color-mix(in srgb,var(--red) 15%,transparent);color:var(--red)}.run-badge-running{background:color-mix(in srgb,var(--yellow) 15%,transparent);color:var(--yellow)}.run-row-date{font-size:.82rem;color:var(--text);flex:1}.run-row-meta{font-size:.82rem;color:var(--text-muted);flex-shrink:0}.run-row-cost{min-width:5rem;text-align:right}.run-row-chevron{font-size:.65rem;color:var(--text-subtle);flex-shrink:0}.run-stages{border-top:1px solid var(--border-subtle);padding:.75rem 1rem 1rem}.stages-table{width:100%;border-collapse:collapse;font-size:.82rem}.stages-table th{text-align:left;color:var(--text-muted);font-weight:500;padding:.2rem .6rem .4rem;border-bottom:1px solid var(--border-subtle)}.stages-table td{padding:.3rem .6rem;color:var(--text)}.stage-name-cell{display:flex;align-items:center;gap:.45rem}.stage-status-cell{color:var(--text-muted)}.stage-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;display:inline-block}.stage-dot-ok{background:var(--green)}.stage-dot-reused{background:var(--accent)}.stage-dot-fail{background:var(--red)}.stage-dot-running{background:var(--yellow)}.stage-dot-idle{background:var(--border)}.stage-row-pending td{color:var(--text-subtle)}.tab-bar{display:flex;gap:0;border-bottom:none;margin:0 -1.5rem;padding:0 1.5rem;overflow-x:auto}.tab{background:transparent;border:none;border-bottom:2px solid transparent;border-radius:0;padding:.7rem 1rem;font-size:.88rem;font-weight:500;color:var(--text-muted);transition:color .15s,border-color .15s;white-space:nowrap;flex-shrink:0;display:flex;align-items:center;gap:.4rem}.tab:hover{color:var(--text)}.tab-active{color:var(--text);border-bottom-color:var(--accent)}.tab-count{font-size:.72rem;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:10px;padding:.05em .45em;color:var(--text-muted);font-weight:500;min-width:1.4em;text-align:center}.tab-content{flex:1;overflow-y:auto;padding:1.5rem}.tab-pane{display:flex;flex-direction:column;gap:2rem}.config-inner-tab-bar{display:flex;gap:0;border-bottom:1px solid var(--border-subtle);margin-bottom:1.5rem;overflow-x:auto}.config-inner-tab{background:transparent;border:none;border-bottom:2px solid transparent;border-radius:0;padding:.45rem .85rem;font-size:.82rem;font-weight:500;color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s;white-space:nowrap;margin-bottom:-1px}.config-inner-tab:hover{color:var(--text)}.config-inner-tab-active{color:var(--text);border-bottom-color:var(--accent)}.btn{background:var(--bg-elevated);border:1px solid var(--border);border-radius:6px;padding:.45rem .9rem;font-size:.88rem;font-weight:500;color:var(--text);transition:background .15s,border-color .15s,opacity .15s;display:inline-flex;align-items:center;gap:.3rem;white-space:nowrap}.btn:hover:not(:disabled){background:#2d3748;border-color:#4a5568}.btn:disabled{opacity:.45;cursor:default}.btn-sm{padding:.3rem .65rem;font-size:.8rem}.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}.btn-primary:hover:not(:disabled){background:var(--accent-hover);border-color:var(--accent-hover)}.btn-secondary{background:color-mix(in srgb,var(--accent) 12%,var(--bg-elevated));border-color:color-mix(in srgb,var(--accent) 35%,transparent);color:var(--accent-hover)}.btn-secondary:hover:not(:disabled){background:color-mix(in srgb,var(--accent) 22%,var(--bg-elevated))}.btn-ghost{background:transparent;border-color:transparent}.btn-ghost:hover:not(:disabled){background:var(--bg-elevated);border-color:var(--border)}.btn-danger{color:var(--red)}.btn-danger:hover:not(:disabled){border-color:var(--red)}.full-width{width:100%;justify-content:center}.badge{font-size:.72rem;font-weight:600;padding:.15em .55em;border-radius:10px;border:1px solid currentColor;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0}.badge-ok{color:var(--green)}.badge-running{color:var(--yellow);animation:pulse 1.2s ease-in-out infinite}.badge-pending{color:var(--orange)}.badge-fail{color:var(--red)}.badge-paused{color:var(--yellow)}.pause-banner{font-size:.82rem;color:var(--yellow);background:color-mix(in srgb,var(--yellow) 10%,transparent);border:1px solid color-mix(in srgb,var(--yellow) 30%,transparent);border-radius:6px;padding:.5rem .75rem;margin-bottom:.5rem}.spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite;flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes toast-in{0%{opacity:0;transform:translateY(.75rem)}to{opacity:1;transform:translateY(0)}}.toast-popup{position:fixed;bottom:1.25rem;right:1.25rem;z-index:200;display:flex;align-items:center;justify-content:space-between;gap:.75rem;min-width:240px;max-width:400px;padding:.65rem .9rem;border-radius:10px;font-size:.84rem;background:var(--bg-elevated);border:1px solid var(--border);box-shadow:0 8px 32px #00000059;animation:toast-in .18s ease-out}.toast-ok{border-left:3px solid var(--green)}.toast-error{border-left:3px solid var(--red)}.toast-close{background:transparent;border:none;padding:0 .2rem;color:var(--text-muted);font-size:.8rem;flex-shrink:0;cursor:pointer}.toast-close:hover{color:var(--text)}.modal-overlay{position:fixed;inset:0;background:#000000a6;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:100;padding:1rem}.modal-panel{background:var(--bg-card);border:1px solid var(--border);border-radius:14px;width:100%;max-width:540px;max-height:90dvh;overflow-y:auto;box-shadow:0 24px 64px #00000080;outline:none}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:1.15rem 1.5rem;border-bottom:1px solid var(--border-subtle)}.modal-title{font-size:1rem;font-weight:600}.modal-close{background:transparent;border:none;font-size:1rem;color:var(--text-muted);padding:.15rem .35rem;border-radius:4px}.modal-close:hover{color:var(--text);background:var(--bg-elevated)}.modal-body{padding:1.5rem}.modal-form{display:flex;flex-direction:column;gap:1rem}.modal-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.field{display:flex;flex-direction:column;gap:.5rem}.field-label{font-size:.82rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.field-hint{font-size:.78rem;color:var(--text-muted);font-weight:400;text-transform:none;letter-spacing:0}.editor-grid{display:grid;gap:1rem;grid-template-columns:repeat(2,minmax(0,1fr))}.editor-grid .full-width{grid-column:1 / -1}.save-row{display:flex;align-items:center;gap:.75rem}.output-actions-row{margin-top:.75rem;display:flex;gap:.5rem;flex-wrap:wrap}.config-grid{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}.config-item{border:1px solid var(--border-subtle);border-radius:10px;background:var(--bg-card);padding:.7rem .8rem;display:flex;flex-direction:column;gap:.3rem}.config-key{font-size:.74rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600}.config-value{font-size:.95rem;color:var(--text);font-weight:500;word-break:break-word}.config-json{margin:0;border:1px solid var(--border-subtle);border-radius:10px;background:var(--bg-card);padding:.85rem;overflow:auto;font-size:.8rem;line-height:1.45;color:var(--text)}.config-form{display:flex;flex-direction:column;gap:1.5rem;margin-bottom:2rem}.config-form-fields{display:flex;flex-direction:column;gap:1rem}.config-group{border:1px solid var(--border);border-radius:10px;background:color-mix(in srgb,var(--bg-elevated) 65%,transparent);padding:1rem 1.15rem}.config-group-header{margin-bottom:.9rem}.config-group-title{margin:0;font-size:.86rem;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--text)}.config-group-note{margin:.2rem 0 0}.config-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.config-form-field{display:flex;flex-direction:column;gap:.35rem}.config-form-field:has(input[type=checkbox]){flex-direction:row;align-items:center;gap:.5rem}.config-form-field:has(input[type=checkbox]) input[type=checkbox]{width:1rem;height:1rem;flex-shrink:0;cursor:pointer;order:-1}.config-form-field--readonly{justify-content:flex-end;padding:.4rem 0 .1rem}.config-form-label{font-size:.74rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600}.config-form-optional{font-weight:400;text-transform:none;letter-spacing:0;opacity:.7}.config-select,.config-input{background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:.9rem;padding:.45rem .65rem;width:100%;outline:none;transition:border-color .15s;box-sizing:border-box;font-family:inherit;appearance:auto}.config-select:focus,.config-input:focus{border-color:var(--accent)}.config-form-actions{display:flex;align-items:center;gap:.75rem}.config-color-input-row{display:flex;align-items:center;gap:.5rem}.config-inline-actions{display:flex;gap:.5rem;flex-wrap:wrap}.config-color-picker{width:2.35rem;height:2.1rem;border:1px solid var(--border);border-radius:7px;padding:.1rem;background:var(--bg-elevated);cursor:pointer;flex-shrink:0}@media(max-width:860px){.config-group-grid{grid-template-columns:1fr}}.combo-box{position:relative}.combo-box-list{position:absolute;top:100%;left:0;right:0;z-index:200;margin:2px 0 0;padding:0;list-style:none;background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;box-shadow:0 4px 16px #00000040;max-height:220px;overflow-y:auto}.combo-box-option{padding:.45rem .65rem;font-size:.9rem;color:var(--text);cursor:pointer}.combo-box-option:hover,.combo-box-option--active{background:var(--accent);color:#fff}.model-config-section{border:1px solid var(--border);border-radius:10px;margin-bottom:1.5rem;overflow:hidden}.model-config-toggle{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;gap:.75rem;padding:.9rem 1.15rem;cursor:pointer;background:var(--bg-elevated);box-sizing:border-box;transition:background .15s}.model-config-toggle:hover{background:var(--bg-hover, color-mix(in srgb, var(--bg-elevated) 85%, var(--accent)))}.model-config-summary{display:flex;flex-direction:column;gap:.2rem;min-width:0}.model-config-name{font-size:.92rem;font-weight:600;color:var(--text)}.model-config-url{font-size:.78rem}a.model-config-url{color:var(--accent);text-decoration:none}a.model-config-url:hover{text-decoration:underline}.model-config-pricing{font-size:.78rem}.model-config-chevron{flex-shrink:0;font-size:.72rem;color:var(--text-muted)}.model-config-body{padding:1.15rem 1.25rem;border-top:1px solid var(--border);background:var(--bg)}.model-config-body .config-form{margin-bottom:0}.asset-grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fill,minmax(var(--grid-col-min, 200px),1fr))}.asset-card{border:1px solid var(--border-subtle);border-radius:10px;padding:.65rem;display:flex;flex-direction:column;gap:.6rem;background:var(--bg-card)}.asset-card>.btn:last-child{margin-top:auto}.asset-index{font-size:.72rem;color:var(--text-muted);font-weight:600;padding:0 .15rem}.asset-status-row{display:flex;align-items:center;justify-content:space-between;gap:.45rem}.card-prompt{display:flex;flex-direction:column;gap:.3rem}.card-prompt-header{display:flex;align-items:center;justify-content:space-between}.card-prompt-edit-btn{padding:.1rem .35rem;font-size:.75rem;opacity:.6;flex-shrink:0}.card-prompt-edit-btn:hover:not(:disabled){opacity:1}.card-prompt-row{display:flex;align-items:flex-start;gap:.35rem}.prompt-text{font-size:.78rem;color:var(--text-muted);line-height:1.45;margin:0;flex:1}.prompt-textarea{font-size:.8rem;resize:none;overflow:hidden;min-height:3.5rem}.prompt-edit-actions{display:flex;gap:.4rem}.media-wrap{aspect-ratio:var(--ar, 9/16);width:100%;background:#000;border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center}.media-wrap img,.media-wrap video{width:100%;height:100%;object-fit:cover;display:block}.media-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:var(--bg-elevated)}.media-placeholder-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.45rem;text-align:center;padding:.65rem}.media-placeholder-title{font-size:.76rem;color:var(--text-muted);font-weight:500}.final-video-section{margin-bottom:.5rem}.final-video-wrap{margin:0 auto}.tab-content:has(.tab-pane-output){overflow:hidden;display:flex;flex-direction:column}.tab-pane-output{flex:1;min-height:0;display:flex;flex-direction:column;gap:0}.tab-pane-output .final-video-section{flex:1;min-height:0;margin-bottom:0;display:flex;flex-direction:column}.tab-pane-output .final-video-wrap{flex:1;min-height:0;display:flex;align-items:center;justify-content:center}.tab-pane-output .media-wrap{aspect-ratio:unset;height:100%;width:auto;max-width:100%}.section-label{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin-bottom:.65rem}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.85rem;padding:3.5rem 1.5rem;text-align:center;height:100%;min-height:200px}.empty-state-title{font-size:1.15rem;font-weight:600;color:var(--text)}.empty-state-brand{font-size:clamp(1.8rem,5vw,2.4rem);font-weight:900;letter-spacing:.04em;text-transform:uppercase;color:var(--text);opacity:.9;margin-bottom:.5rem}.muted{color:var(--text-muted)}.size-sm{font-size:.82rem}.size-xs{font-size:.75rem}@media(max-width:900px){:root{--sidebar-w: 200px}.editor-grid{grid-template-columns:1fr}.project-header-top{gap:.5rem}}@media(max-width:640px){.app-shell{grid-template-columns:1fr}.sidebar{display:none}body{overflow:auto}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
|
4
|
+
<stop offset="0%" stop-color="#ff3d00"/>
|
|
5
|
+
<stop offset="60%" stop-color="#ff7a00"/>
|
|
6
|
+
<stop offset="100%" stop-color="#ffcc00"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
|
|
10
|
+
<!-- Rounded square background -->
|
|
11
|
+
<rect width="32" height="32" rx="7" fill="#111111"/>
|
|
12
|
+
|
|
13
|
+
<!-- Flame: main triangle, centered in 32x32, with ~4px padding -->
|
|
14
|
+
<!-- Points: tip at top-center, base spanning bottom -->
|
|
15
|
+
<polygon points="16,5 26,27 6,27" fill="#ff4e00"/>
|
|
16
|
+
<!-- Inner lighter triangle -->
|
|
17
|
+
<polygon points="16,12 22,27 10,27" fill="#ff7a00"/>
|
|
18
|
+
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<title>Rilo</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BvNWHzrr.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Wcsyfv3q.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="18 26 197 48" width="394" height="96">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
|
4
|
+
<stop offset="0%" stop-color="#ff3d00"/>
|
|
5
|
+
<stop offset="60%" stop-color="#ff7a00"/>
|
|
6
|
+
<stop offset="100%" stop-color="#ffcc00"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
|
|
9
|
+
<filter id="flameGlow" x="-40%" y="-40%" width="180%" height="180%">
|
|
10
|
+
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
|
11
|
+
<feMerge>
|
|
12
|
+
<feMergeNode in="blur"/>
|
|
13
|
+
<feMergeNode in="SourceGraphic"/>
|
|
14
|
+
</feMerge>
|
|
15
|
+
</filter>
|
|
16
|
+
</defs>
|
|
17
|
+
|
|
18
|
+
<!-- Transparent background -->
|
|
19
|
+
|
|
20
|
+
<!-- Minimal flame — two triangles -->
|
|
21
|
+
<polygon points="44,31 58,68 30,68" fill="#ff4e00"/>
|
|
22
|
+
<polygon points="44,44 53,68 35,68" fill="#ff7a00"/>
|
|
23
|
+
|
|
24
|
+
<!-- Wordmark — white text, orange "fire" -->
|
|
25
|
+
<text
|
|
26
|
+
x="66" y="68"
|
|
27
|
+
font-family="'Helvetica Neue', Helvetica, Arial, sans-serif"
|
|
28
|
+
font-size="52"
|
|
29
|
+
font-weight="200"
|
|
30
|
+
letter-spacing="-1"
|
|
31
|
+
fill="#ffffff"
|
|
32
|
+
>tale<tspan font-weight="600" fill="#ff4e00">fire</tspan></text>
|
|
33
|
+
|
|
34
|
+
</svg>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="18 26 197 48" width="394" height="96">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
|
4
|
+
<stop offset="0%" stop-color="#ff3d00"/>
|
|
5
|
+
<stop offset="60%" stop-color="#ff7a00"/>
|
|
6
|
+
<stop offset="100%" stop-color="#ffcc00"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
|
|
9
|
+
<filter id="flameGlow" x="-40%" y="-40%" width="180%" height="180%">
|
|
10
|
+
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
|
11
|
+
<feMerge>
|
|
12
|
+
<feMergeNode in="blur"/>
|
|
13
|
+
<feMergeNode in="SourceGraphic"/>
|
|
14
|
+
</feMerge>
|
|
15
|
+
</filter>
|
|
16
|
+
</defs>
|
|
17
|
+
|
|
18
|
+
<!-- Transparent background -->
|
|
19
|
+
|
|
20
|
+
<!-- Minimal flame — two triangles, clean and simple -->
|
|
21
|
+
<!-- Main triangle -->
|
|
22
|
+
<polygon points="44,31 58,68 30,68" fill="#ff4e00"/>
|
|
23
|
+
<!-- Small inner triangle cutout to suggest flame shape -->
|
|
24
|
+
<polygon points="44,44 53,68 35,68" fill="#ff7a00"/>
|
|
25
|
+
|
|
26
|
+
<!-- Wordmark — original letter spacing -->
|
|
27
|
+
<text
|
|
28
|
+
x="66" y="68"
|
|
29
|
+
font-family="'Helvetica Neue', Helvetica, Arial, sans-serif"
|
|
30
|
+
font-size="52"
|
|
31
|
+
font-weight="200"
|
|
32
|
+
letter-spacing="-1"
|
|
33
|
+
fill="#1a1a1a"
|
|
34
|
+
>tale<tspan font-weight="600" fill="#ff4e00">fire</tspan></text>
|
|
35
|
+
|
|
36
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telepat/rilo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rilo": "src/cli/index.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"src/",
|
|
10
|
+
"frontend/dist/",
|
|
10
11
|
"models/",
|
|
11
12
|
"index.js",
|
|
12
13
|
"README.md"
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"frontend:lint": "npm --prefix frontend run lint",
|
|
31
32
|
"frontend:build": "npm --prefix frontend run build",
|
|
32
33
|
"frontend:preview": "npm --prefix frontend run preview",
|
|
34
|
+
"prepack": "npm run frontend:build",
|
|
33
35
|
"docs:start": "npm --prefix docs-site run start",
|
|
34
36
|
"docs:build": "npm --prefix docs-site run build",
|
|
35
37
|
"docs:serve": "npm --prefix docs-site run serve",
|
|
@@ -5,6 +5,32 @@ function unauthorized(res) {
|
|
|
5
5
|
res.status(401).json({ error: 'Unauthorized' });
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function isLoopbackAddress(address) {
|
|
9
|
+
if (!address || typeof address !== 'string') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const normalized = address.toLowerCase();
|
|
14
|
+
return normalized === '127.0.0.1'
|
|
15
|
+
|| normalized === '::1'
|
|
16
|
+
|| normalized === '::ffff:127.0.0.1';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shouldBypassAuth(req, options) {
|
|
20
|
+
if (!options.previewMode) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.allowUnauthenticatedExposedPreview) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const socketAddress = typeof req?.socket?.remoteAddress === 'string'
|
|
29
|
+
? req.socket.remoteAddress
|
|
30
|
+
: '';
|
|
31
|
+
return isLoopbackAddress(socketAddress);
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
function getTokenFromAuthorizationHeader(req) {
|
|
9
35
|
const authHeader = req.get('authorization');
|
|
10
36
|
if (!authHeader) {
|
|
@@ -51,20 +77,50 @@ export function isAuthorizedApiRequest(req, { allowQueryAccessToken = false } =
|
|
|
51
77
|
return isMatchingToken(queryToken);
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
export function createAuthGuards(options = {}) {
|
|
81
|
+
const authOptions = {
|
|
82
|
+
previewMode: Boolean(options.previewMode),
|
|
83
|
+
allowUnauthenticatedExposedPreview: Boolean(options.allowUnauthenticatedExposedPreview)
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function requireBearerTokenWithOptions(req, res, next) {
|
|
87
|
+
if (shouldBypassAuth(req, authOptions)) {
|
|
88
|
+
next();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!isAuthorizedApiRequest(req)) {
|
|
93
|
+
unauthorized(res);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
next();
|
|
58
98
|
}
|
|
59
99
|
|
|
60
|
-
next
|
|
61
|
-
|
|
100
|
+
function requireBearerTokenOrAccessTokenWithOptions(req, res, next) {
|
|
101
|
+
if (shouldBypassAuth(req, authOptions)) {
|
|
102
|
+
next();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
62
105
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
if (!isAuthorizedApiRequest(req, { allowQueryAccessToken: true })) {
|
|
107
|
+
unauthorized(res);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
next();
|
|
67
112
|
}
|
|
68
113
|
|
|
69
|
-
|
|
114
|
+
return {
|
|
115
|
+
requireBearerToken: requireBearerTokenWithOptions,
|
|
116
|
+
requireBearerTokenOrAccessToken: requireBearerTokenOrAccessTokenWithOptions
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function requireBearerToken(req, res, next) {
|
|
121
|
+
return createAuthGuards().requireBearerToken(req, res, next);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function requireBearerTokenOrAccessToken(req, res, next) {
|
|
125
|
+
return createAuthGuards().requireBearerTokenOrAccessToken(req, res, next);
|
|
70
126
|
}
|
package/src/api/openapi/spec.js
CHANGED
|
@@ -57,7 +57,8 @@ const schemas = {
|
|
|
57
57
|
properties: {
|
|
58
58
|
story: { type: 'string' },
|
|
59
59
|
project: { type: 'string' },
|
|
60
|
-
forceRestart: { type: 'boolean' }
|
|
60
|
+
forceRestart: { type: 'boolean' },
|
|
61
|
+
pauseAfterKeyframes: { type: 'boolean' }
|
|
61
62
|
},
|
|
62
63
|
required: ['story'],
|
|
63
64
|
additionalProperties: true
|
|
@@ -180,6 +181,7 @@ const schemas = {
|
|
|
180
181
|
type: 'object',
|
|
181
182
|
properties: {
|
|
182
183
|
forceRestart: { type: 'boolean' },
|
|
184
|
+
pauseAfterKeyframes: { type: 'boolean' },
|
|
183
185
|
targetType: {
|
|
184
186
|
type: 'string',
|
|
185
187
|
enum: ['script', 'voiceover', 'keyframe', 'segment', 'align', 'burnin']
|
package/src/api/routes/jobs.js
CHANGED
|
@@ -7,12 +7,27 @@ export function createJobsRouter() {
|
|
|
7
7
|
const router = express.Router();
|
|
8
8
|
|
|
9
9
|
router.post('/', async (req, res) => {
|
|
10
|
-
const { story, project, forceRestart } = req.body || {};
|
|
10
|
+
const { story, project, forceRestart, pauseAfterKeyframes } = req.body || {};
|
|
11
11
|
if (!story) {
|
|
12
12
|
res.status(400).json({ error: 'story is required' });
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
if (typeof story !== 'string') {
|
|
17
|
+
res.status(400).json({ error: 'story must be a string' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (forceRestart !== undefined && typeof forceRestart !== 'boolean') {
|
|
22
|
+
res.status(400).json({ error: 'forceRestart must be a boolean when provided' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (pauseAfterKeyframes !== undefined && typeof pauseAfterKeyframes !== 'boolean') {
|
|
27
|
+
res.status(400).json({ error: 'pauseAfterKeyframes must be a boolean when provided' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
const resolvedProject = resolveProjectName(project || `api-${Date.now()}`);
|
|
17
32
|
const existingActiveJob = findActiveJobByProject(resolvedProject);
|
|
18
33
|
if (existingActiveJob) {
|
|
@@ -28,7 +43,11 @@ export function createJobsRouter() {
|
|
|
28
43
|
await writeProjectStory(resolvedProject, story);
|
|
29
44
|
|
|
30
45
|
const job = createJob({ story, project: resolvedProject });
|
|
31
|
-
|
|
46
|
+
const pipelineOptions = { forceRestart: forceRestart === true };
|
|
47
|
+
if (pauseAfterKeyframes !== undefined) {
|
|
48
|
+
pipelineOptions.pauseAfterKeyframes = pauseAfterKeyframes;
|
|
49
|
+
}
|
|
50
|
+
setImmediate(() => runPipeline(job.id, pipelineOptions));
|
|
32
51
|
res.status(202).json({ jobId: job.id, status: job.status });
|
|
33
52
|
});
|
|
34
53
|
|
|
@@ -578,6 +578,7 @@ export function createProjectsRouter(deps = {}) {
|
|
|
578
578
|
|
|
579
579
|
const {
|
|
580
580
|
forceRestart,
|
|
581
|
+
pauseAfterKeyframes,
|
|
581
582
|
targetType,
|
|
582
583
|
index
|
|
583
584
|
} = req.body || {};
|
|
@@ -597,6 +598,11 @@ export function createProjectsRouter(deps = {}) {
|
|
|
597
598
|
return;
|
|
598
599
|
}
|
|
599
600
|
|
|
601
|
+
if (pauseAfterKeyframes !== undefined) {
|
|
602
|
+
res.status(400).json({ error: 'pauseAfterKeyframes is not supported for targeted regeneration' });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
600
606
|
const targetRequiresIndex = normalizedTargetType === 'keyframe' || normalizedTargetType === 'segment';
|
|
601
607
|
if (targetRequiresIndex && !hasIndex) {
|
|
602
608
|
res.status(400).json({ error: 'index is required for keyframe/segment targeted regeneration' });
|
|
@@ -628,9 +634,18 @@ export function createProjectsRouter(deps = {}) {
|
|
|
628
634
|
return;
|
|
629
635
|
}
|
|
630
636
|
|
|
637
|
+
if (pauseAfterKeyframes !== undefined && typeof pauseAfterKeyframes !== 'boolean') {
|
|
638
|
+
res.status(400).json({ error: 'pauseAfterKeyframes must be a boolean when provided' });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
631
642
|
const story = await readProjectStory(project);
|
|
632
643
|
const job = createJobFn({ story, project });
|
|
633
|
-
|
|
644
|
+
const pipelineOptions = { forceRestart: forceRestart === true };
|
|
645
|
+
if (pauseAfterKeyframes !== undefined) {
|
|
646
|
+
pipelineOptions.pauseAfterKeyframes = pauseAfterKeyframes;
|
|
647
|
+
}
|
|
648
|
+
setImmediate(() => runPipelineFn(job.id, pipelineOptions));
|
|
634
649
|
|
|
635
650
|
res.status(202).json({
|
|
636
651
|
jobId: job.id,
|
package/src/api/server.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { pathToFileURL } from 'node:url';
|
|
3
5
|
import { assertRequiredApiEnv, env } from '../config/env.js';
|
|
4
6
|
import { createJobsRouter } from './routes/jobs.js';
|
|
5
7
|
import { createProjectAssetsRouter } from './routes/projectAssets.js';
|
|
6
8
|
import { createProjectsRouter } from './routes/projects.js';
|
|
7
9
|
import { createWebhookRouter } from './routes/webhooks.js';
|
|
8
|
-
import {
|
|
10
|
+
import { createAuthGuards } from './middleware/auth.js';
|
|
9
11
|
import { buildOpenApiSpec } from './openapi/spec.js';
|
|
10
12
|
|
|
11
13
|
const swaggerUiHtml = `<!doctype html>
|
|
@@ -42,11 +44,29 @@ function getRequestBaseUrl(req, { fallbackPort = 3000 } = {}) {
|
|
|
42
44
|
return `http://localhost:${fallbackPort}`;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
function hasDashboardBundle(dashboardDir) {
|
|
48
|
+
if (!dashboardDir || typeof dashboardDir !== 'string') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return fs.existsSync(path.join(dashboardDir, 'index.html'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createApiApp({ baseUrl, dashboardDir, auth } = {}) {
|
|
56
|
+
const authOptions = {
|
|
57
|
+
previewMode: Boolean(auth?.previewMode),
|
|
58
|
+
allowUnauthenticatedExposedPreview: Boolean(auth?.allowUnauthenticatedExposedPreview)
|
|
59
|
+
};
|
|
60
|
+
const shouldRequireApiToken = !authOptions.previewMode;
|
|
61
|
+
if (shouldRequireApiToken) {
|
|
62
|
+
assertRequiredApiEnv();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const guards = createAuthGuards(authOptions);
|
|
66
|
+
const dashboardEnabled = hasDashboardBundle(dashboardDir);
|
|
47
67
|
|
|
48
68
|
const app = express();
|
|
49
|
-
app.set('trust proxy',
|
|
69
|
+
app.set('trust proxy', !authOptions.previewMode);
|
|
50
70
|
app.use(express.json({ limit: '2mb' }));
|
|
51
71
|
|
|
52
72
|
app.get('/health', (_req, res) => {
|
|
@@ -67,16 +87,30 @@ export function createApiApp({ baseUrl } = {}) {
|
|
|
67
87
|
});
|
|
68
88
|
|
|
69
89
|
app.use('/webhooks', createWebhookRouter());
|
|
70
|
-
app.use('/projects', requireBearerTokenOrAccessToken, createProjectAssetsRouter());
|
|
71
|
-
app.use(requireBearerToken);
|
|
90
|
+
app.use('/projects', guards.requireBearerTokenOrAccessToken, createProjectAssetsRouter());
|
|
91
|
+
app.use(guards.requireBearerToken);
|
|
72
92
|
app.use('/jobs', createJobsRouter());
|
|
73
93
|
app.use('/projects', createProjectsRouter());
|
|
74
94
|
|
|
95
|
+
if (dashboardEnabled) {
|
|
96
|
+
app.use(express.static(dashboardDir));
|
|
97
|
+
app.get('/', (_req, res) => {
|
|
98
|
+
res.sendFile(path.join(dashboardDir, 'index.html'));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
return app;
|
|
76
103
|
}
|
|
77
104
|
|
|
78
|
-
export function startApiServer({ port = env.port, baseUrl } = {}) {
|
|
79
|
-
const app = createApiApp({ baseUrl });
|
|
105
|
+
export function startApiServer({ port = env.port, host, baseUrl, dashboardDir, auth } = {}) {
|
|
106
|
+
const app = createApiApp({ baseUrl, dashboardDir, auth });
|
|
107
|
+
|
|
108
|
+
if (host) {
|
|
109
|
+
return app.listen(port, host, () => {
|
|
110
|
+
console.log(`rilo api listening on ${host}:${port}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
80
114
|
return app.listen(port, () => {
|
|
81
115
|
console.log(`rilo api listening on :${port}`);
|
|
82
116
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { startApiServer } from '../../api/server.js';
|
|
6
|
+
import { openPath } from './openHome.js';
|
|
7
|
+
|
|
8
|
+
function parsePort(value, fallback) {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
15
|
+
throw new Error(`Invalid port: ${value}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveDashboardDir() {
|
|
22
|
+
const commandDir = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
return path.resolve(commandDir, '../../../frontend/dist');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveWorkerEntryPoint() {
|
|
27
|
+
const commandDir = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
return path.resolve(commandDir, '../../worker/processor.js');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildWorkerProcess() {
|
|
32
|
+
const workerPath = resolveWorkerEntryPoint();
|
|
33
|
+
return spawn(process.execPath, [workerPath], {
|
|
34
|
+
stdio: 'inherit'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printUnsafeExposeWarning(url) {
|
|
39
|
+
console.warn('WARNING: running preview in exposed mode without auth.');
|
|
40
|
+
console.warn(`WARNING: dashboard and API are reachable from ${url}`);
|
|
41
|
+
console.warn('WARNING: use only on trusted networks or isolated containers.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveOpenUrl(host, port) {
|
|
45
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
46
|
+
return `http://127.0.0.1:${port}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `http://${host}:${port}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function startPreview({ args = process.argv.slice(3) } = {}) {
|
|
53
|
+
const expose = args.includes('--expose');
|
|
54
|
+
const unsafeNoAuth = args.includes('--unsafe-no-auth');
|
|
55
|
+
const noOpen = args.includes('--no-open');
|
|
56
|
+
|
|
57
|
+
if (!expose && unsafeNoAuth) {
|
|
58
|
+
throw new Error('--unsafe-no-auth can only be used with --expose');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (expose && !unsafeNoAuth) {
|
|
62
|
+
throw new Error('Exposed preview requires --unsafe-no-auth');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const portArgIndex = args.indexOf('--port');
|
|
66
|
+
const portArg = portArgIndex === -1 ? null : args[portArgIndex + 1] || null;
|
|
67
|
+
const port = parsePort(portArg, 3000);
|
|
68
|
+
|
|
69
|
+
const hostArgIndex = args.indexOf('--host');
|
|
70
|
+
const hostArg = hostArgIndex === -1 ? null : args[hostArgIndex + 1] || null;
|
|
71
|
+
const host = hostArg || (expose ? '0.0.0.0' : '127.0.0.1');
|
|
72
|
+
|
|
73
|
+
const dashboardDir = resolveDashboardDir();
|
|
74
|
+
if (!fs.existsSync(path.join(dashboardDir, 'index.html'))) {
|
|
75
|
+
throw new Error('Dashboard bundle not found. Run `npm run frontend:build` first.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const apiServer = startApiServer({
|
|
79
|
+
port,
|
|
80
|
+
host,
|
|
81
|
+
dashboardDir,
|
|
82
|
+
auth: {
|
|
83
|
+
previewMode: true,
|
|
84
|
+
allowUnauthenticatedExposedPreview: unsafeNoAuth
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const worker = buildWorkerProcess();
|
|
89
|
+
const openUrl = resolveOpenUrl(host, port);
|
|
90
|
+
|
|
91
|
+
const stopAll = () => {
|
|
92
|
+
if (!worker.killed) {
|
|
93
|
+
worker.kill('SIGTERM');
|
|
94
|
+
}
|
|
95
|
+
apiServer.close();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
process.once('SIGINT', () => {
|
|
99
|
+
stopAll();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
process.once('SIGTERM', () => {
|
|
104
|
+
stopAll();
|
|
105
|
+
process.exit(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
worker.once('exit', (code) => {
|
|
109
|
+
apiServer.close(() => {
|
|
110
|
+
if (typeof code === 'number' && code !== 0) {
|
|
111
|
+
process.exit(code);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (unsafeNoAuth) {
|
|
117
|
+
printUnsafeExposeWarning(openUrl);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Dashboard: ${openUrl}`);
|
|
121
|
+
console.log(`Docs: ${openUrl}/docs`);
|
|
122
|
+
|
|
123
|
+
if (!noOpen) {
|
|
124
|
+
await openPath(openUrl);
|
|
125
|
+
}
|
|
126
|
+
}
|