teknolis / resources /views /dashboard.blade.php
Codex
Deploy latest assistant, DeepSeek defaults, and automation updates
74acf19
Raw
History Blame Contribute Delete
84.9 kB
@php
$healthScore = (int) round(collect($platformProgress)->avg('completion') ?: 0);
$cronTone = match ($cronStatus['state']) {
'healthy' => 'db-pill--healthy',
'critical' => 'db-pill--critical',
default => 'db-pill--warning',
};
$cronServiceLabel = match ($cronStatus['service_status']) {
'active' => 'Active',
'inactive' => 'Inactive',
'failed' => 'Failed',
default => 'Unknown',
};
@endphp
<x-app-layout>
<style>
@import url('https://fonts.bunny.net/css?family=manrope:400,500,600,700,800&family=space-grotesk:500,700&display=swap');
.db-page,
.db-page * {
box-sizing: border-box;
}
.db-page {
--db-radius-outer: 15px;
--db-radius-inner: 10px;
padding: 28px 18px 48px;
color: #e6edf7;
font-family: "Manrope", sans-serif;
}
.db-container {
width: min(1280px, 100%);
margin: 0 auto;
}
.db-header-copy {
display: flex;
flex-direction: column;
gap: 8px;
}
.db-header-kicker {
display: inline-flex;
align-items: center;
gap: 10px;
color: #9debdc;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.db-header-kicker::before {
content: "";
width: 8px;
height: 8px;
border-radius: 999px;
background: linear-gradient(180deg, #43d9bd, #18a38d);
box-shadow: 0 0 18px rgba(67, 217, 189, 0.72);
}
.db-header-copy h2 {
margin: 0;
font-family: "Space Grotesk", sans-serif;
font-size: clamp(2rem, 4vw, 2.8rem);
letter-spacing: -0.04em;
}
.db-header-copy p {
margin: 0;
max-width: 760px;
color: rgba(226, 235, 247, 0.68);
font-size: 14px;
line-height: 1.8;
}
.db-hero {
display: grid;
gap: 22px;
grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr);
padding: 28px;
border-radius: var(--db-radius-outer);
border: 1px solid rgba(255, 255, 255, 0.08);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.03)),
rgba(5, 12, 20, 0.56);
box-shadow: 0 28px 90px rgba(2, 8, 15, 0.36);
}
.db-kicker {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(67, 217, 189, 0.2);
background: rgba(67, 217, 189, 0.08);
color: #abf7e8;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.db-kicker::before {
content: "";
width: 8px;
height: 8px;
border-radius: 999px;
background: #43d9bd;
box-shadow: 0 0 18px rgba(67, 217, 189, 0.72);
}
.db-title {
margin: 18px 0 14px;
max-width: 10ch;
color: #f8fbff;
font-family: "Space Grotesk", sans-serif;
font-size: clamp(2.2rem, 3.6vw, 4.2rem);
line-height: 1;
letter-spacing: -0.05em;
}
.db-copy {
max-width: 660px;
margin: 0;
color: rgba(226, 235, 247, 0.74);
font-size: 16px;
line-height: 1.78;
}
.db-chip-row,
.db-inline-badges,
.db-actions,
.db-step-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.db-chip,
.db-pill {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-radius: var(--db-radius-inner);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
color: #dbe6f4;
font-size: 13px;
font-weight: 700;
}
.db-chip::before,
.db-pill::before {
content: "";
width: 7px;
height: 7px;
border-radius: 999px;
background: currentColor;
opacity: 0.7;
}
.db-hero-panel,
.db-card {
border-radius: var(--db-radius-outer);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 17, 26, 0.68);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.db-hero-panel {
padding: 22px;
background:
radial-gradient(circle at top, rgba(255, 255, 255, 0.07), transparent 42%),
linear-gradient(180deg, rgba(17, 34, 51, 0.96), rgba(10, 19, 31, 0.98));
}
.db-window-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
color: rgba(219, 232, 247, 0.74);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.db-window-dots {
display: flex;
gap: 8px;
}
.db-window-dots span {
width: 10px;
height: 10px;
border-radius: 999px;
}
.db-window-dots span:first-child {
background: #ff7a59;
}
.db-window-dots span:nth-child(2) {
background: #f5b96b;
}
.db-window-dots span:last-child {
background: #43d9bd;
}
.db-panel-copy strong {
display: block;
color: #f8fbff;
font-size: 1.18rem;
font-weight: 800;
}
.db-panel-copy p {
margin: 8px 0 0;
color: rgba(226, 235, 247, 0.68);
font-size: 14px;
line-height: 1.72;
}
.db-grid-stats,
.db-grid-platforms,
.db-grid-health,
.db-grid-recent {
display: grid;
gap: 18px;
}
.db-grid-stats {
grid-template-columns: repeat(5, minmax(0, 1fr));
margin-top: 22px;
}
.db-grid-platforms {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.db-grid-health {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.db-stat-card,
.db-health-card {
padding: 20px;
border-radius: var(--db-radius-inner);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.db-stat-card span,
.db-health-card span {
display: block;
color: rgba(226, 235, 247, 0.54);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.db-stat-card strong {
display: block;
margin-top: 18px;
color: #ffffff;
font-size: 1.95rem;
font-weight: 800;
letter-spacing: -0.04em;
}
.db-stat-card p,
.db-health-card p {
margin: 8px 0 0;
color: rgba(226, 235, 247, 0.68);
font-size: 13px;
line-height: 1.6;
}
.db-card {
padding: 22px;
}
.db-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.db-section-tag {
display: inline-flex;
margin-bottom: 10px;
color: #8be6d6;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.db-card-title {
margin: 0;
color: #f5fbff;
font-size: 1.32rem;
font-weight: 800;
letter-spacing: -0.03em;
}
.db-card-desc {
margin: 6px 0 0;
color: rgba(219, 232, 247, 0.68);
font-size: 14px;
line-height: 1.72;
}
.db-pill--healthy,
.db-step--done,
.db-state--healthy {
color: #abf7e8;
background: rgba(67, 217, 189, 0.12);
}
.db-pill--warning,
.db-state--ready,
.db-state--warning {
color: #ffd9a8;
background: rgba(245, 185, 107, 0.12);
}
.db-pill--critical,
.db-state--critical {
color: #ffd0c3;
background: rgba(255, 122, 89, 0.12);
}
.db-state--pending {
color: rgba(226, 235, 247, 0.74);
background: rgba(255, 255, 255, 0.08);
}
.db-progress-card {
padding: 20px;
border-radius: var(--db-radius-inner);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.db-progress-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.db-progress-head h4 {
margin: 0;
color: #f8fbff;
font-size: 1.02rem;
font-weight: 800;
letter-spacing: -0.03em;
}
.db-progress-head p {
margin: 7px 0 0;
color: rgba(226, 235, 247, 0.58);
font-size: 13px;
line-height: 1.68;
}
.db-progress-value {
color: #ffffff;
font-size: 1.42rem;
font-weight: 800;
letter-spacing: -0.04em;
}
.db-progress-bar {
margin-top: 16px;
overflow: hidden;
height: 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
}
.db-progress-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #43d9bd, #f5b96b);
}
.db-metric-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: 16px;
}
.db-metric {
padding: 12px 14px;
border-radius: var(--db-radius-inner);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.db-metric span {
display: block;
color: rgba(226, 235, 247, 0.5);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.db-metric strong {
display: block;
margin-top: 10px;
color: #f8fbff;
font-size: 1rem;
font-weight: 800;
}
.db-step-list {
margin-top: 16px;
}
.db-step {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
border-radius: var(--db-radius-inner);
background: rgba(255, 255, 255, 0.08);
color: rgba(226, 235, 247, 0.72);
font-size: 12px;
font-weight: 700;
}
.db-step::before {
content: "";
width: 7px;
height: 7px;
border-radius: 999px;
background: currentColor;
opacity: 0.75;
}
.db-note {
margin-top: 14px;
color: rgba(226, 235, 247, 0.72);
font-size: 13px;
line-height: 1.72;
}
.db-health-callout {
margin-top: 18px;
border-radius: var(--db-radius-inner);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 18px;
background: rgba(255, 255, 255, 0.04);
}
.db-code {
display: inline-flex;
margin-top: 8px;
padding: 10px 14px;
border-radius: var(--db-radius-inner);
background: rgba(255, 255, 255, 0.06);
color: #f8fbff;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
word-break: break-all;
}
.db-table {
overflow: hidden;
border-radius: var(--db-radius-outer);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
}
.db-table table {
width: 100%;
border-collapse: collapse;
}
.db-table th,
.db-table td {
padding: 16px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
vertical-align: top;
}
.db-table th {
color: rgba(226, 235, 247, 0.54);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.db-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 999px;
padding: 13px 18px;
text-decoration: none;
font-size: 13px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.db-button:hover {
transform: translateY(-1px);
}
.db-button--primary {
background: linear-gradient(135deg, #43d9bd, #149f88);
color: #041118;
box-shadow: 0 18px 32px rgba(67, 217, 189, 0.2);
}
.db-button--ghost {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
color: #f5fbff;
}
/* Creative Workspace Styles (Freepik-like) */
.workspace-hero {
margin-top: 40px;
}
.workspace-hero-title {
color: #f8fbff;
font-family: "Space Grotesk", sans-serif;
font-size: 2rem;
font-weight: 800;
margin: 0 0 28px;
letter-spacing: -0.03em;
}
.category-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 28px;
}
.category-btn {
padding: 10px 18px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
color: #e6edf7;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.category-btn:hover {
background: rgba(67, 217, 189, 0.15);
border-color: rgba(67, 217, 189, 0.3);
color: #abf7e8;
}
.category-btn.active {
background: linear-gradient(135deg, #43d9bd, #149f88);
border-color: #43d9bd;
color: #041118;
}
.cat-divider {
width: 1px;
height: 26px;
background: rgba(255,255,255,0.12);
align-self: center;
flex-shrink: 0;
margin: 0 4px;
}
.cat-tool-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 8px 13px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(255,255,255,0.05);
color: rgba(230,237,247,0.75);
font-size: 12px;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
transition: all 0.18s ease;
}
.cat-tool-btn:hover {
background: rgba(67,217,189,0.12);
border-color: rgba(67,217,189,0.35);
color: #abf7e8;
}
.cat-tool-btn span {
font-size: 11px;
}
.quick-actions {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin-bottom: 36px;
}
.action-card {
padding: 20px;
border-radius: var(--db-radius-inner);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(17, 34, 51, 0.6);
text-decoration: none;
color: inherit;
transition: all 0.24s ease;
display: flex;
flex-direction: column;
gap: 12px;
}
.action-card:hover {
border-color: rgba(67, 217, 189, 0.4);
background: rgba(17, 34, 51, 0.9);
transform: translateY(-2px);
box-shadow: 0 12px 32px rgba(67, 217, 189, 0.15);
}
.action-card-icon {
width: 48px;
height: 48px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(67, 217, 189, 0.15), rgba(245, 185, 107, 0.1));
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.action-card-title {
color: #f8fbff;
font-weight: 700;
font-size: 14px;
}
.action-card-desc {
color: rgba(226, 235, 247, 0.66);
font-size: 12px;
line-height: 1.5;
}
.action-card-badge {
display: inline-block;
background: rgba(67, 217, 189, 0.2);
color: #abf7e8;
padding: 4px 8px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
width: fit-content;
}
.recent-section-title {
color: #f8fbff;
font-family: "Space Grotesk", sans-serif;
font-size: 1.4rem;
font-weight: 800;
margin: 0 0 20px;
letter-spacing: -0.03em;
}
.video-composer-shell {
margin-bottom: 26px;
padding: 14px 16px;
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(180deg, rgba(14, 18, 24, 0.98), rgba(11, 14, 19, 0.98));
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
}
.video-composer-form {
display: grid;
gap: 14px;
}
.video-composer-main {
display: grid;
grid-template-columns: 178px minmax(0, 1fr);
gap: 18px;
align-items: stretch;
}
.video-frame-rail {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.video-frame-box {
position: relative;
display: grid;
justify-items: center;
align-content: center;
width: 100%;
min-height: 122px;
padding: 12px 10px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.03);
text-align: center;
cursor: pointer;
overflow: hidden;
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.video-frame-box::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(7, 10, 15, 0.04), rgba(7, 10, 15, 0.52));
opacity: 0;
transition: opacity 0.18s ease;
pointer-events: none;
}
.video-frame-box:hover {
border-color: rgba(67, 217, 189, 0.34);
background: rgba(255, 255, 255, 0.05);
transform: translateY(-1px);
}
.video-frame-box input {
display: none;
}
.video-frame-plus {
position: relative;
z-index: 2;
color: rgba(230, 237, 247, 0.72);
font-size: 38px;
line-height: 1;
font-weight: 300;
transition: opacity 0.18s ease, transform 0.18s ease;
}
.video-frame-label {
position: relative;
z-index: 2;
margin-top: 10px;
color: rgba(230, 237, 247, 0.58);
font-size: 12px;
font-weight: 600;
line-height: 1.3;
transition: color 0.18s ease, transform 0.18s ease;
}
.video-frame-name {
margin-top: 6px;
max-width: 100%;
color: #abf7e8;
font-size: 10px;
font-weight: 700;
line-height: 1.35;
word-break: break-word;
}
.video-frame-name:empty {
display: none;
}
.video-frame-preview {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.18s ease;
pointer-events: none;
}
.video-frame-box.is-filled {
justify-items: start;
align-content: end;
padding: 12px;
}
.video-frame-box.is-filled::after {
opacity: 1;
}
.video-frame-box.is-filled .video-frame-preview {
opacity: 1;
}
.video-frame-box.is-filled .video-frame-plus {
opacity: 0;
transform: scale(0.8);
}
.video-frame-box.is-filled .video-frame-label {
margin-top: 0;
color: #f8fbff;
transform: translateY(0);
}
.video-composer-content {
display: grid;
gap: 14px;
min-width: 0;
}
.video-composer-prompt-wrap {
display: block;
min-width: 0;
}
.video-composer-prompt {
width: 100%;
min-height: 122px;
padding: 8px 0 0;
border: 0;
background: transparent;
color: #f8fbff;
font: inherit;
font-size: 15px;
line-height: 1.65;
resize: none;
outline: none;
}
.video-composer-prompt::placeholder {
color: rgba(230, 237, 247, 0.32);
}
.video-composer-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
min-width: 0;
}
.video-composer-status {
display: grid;
gap: 10px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
}
.video-composer-status[data-state="processing"] {
border-color: rgba(120, 95, 255, 0.28);
background: rgba(70, 56, 173, 0.12);
}
.video-composer-status[data-state="success"] {
border-color: rgba(67, 217, 189, 0.24);
background: rgba(67, 217, 189, 0.1);
}
.video-composer-status[data-state="error"] {
border-color: rgba(248, 113, 113, 0.24);
background: rgba(127, 29, 29, 0.16);
}
.video-composer-status-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.video-composer-status-title {
color: #f8fbff;
font-size: 13px;
font-weight: 800;
}
.video-composer-status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 24px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: rgba(230, 237, 247, 0.88);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.video-composer-status-copy {
color: rgba(230, 237, 247, 0.62);
font-size: 12px;
line-height: 1.65;
}
.video-composer-status-track {
height: 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.video-composer-status-fill {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #7f5cff, #43d9bd);
transition: width 0.2s ease;
}
.video-composer-status-link {
display: none;
width: fit-content;
color: #abf7e8;
font-size: 12px;
font-weight: 700;
text-decoration: none;
}
.video-composer-status-link.is-visible {
display: inline-flex;
}
.video-composer-toast-stack {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 80;
display: grid;
gap: 10px;
pointer-events: none;
}
.video-composer-toast {
min-width: min(320px, calc(100vw - 36px));
max-width: 360px;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(9, 14, 22, 0.96);
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.3);
color: #f8fbff;
pointer-events: auto;
}
.video-composer-toast[data-tone="success"] {
border-color: rgba(67, 217, 189, 0.24);
}
.video-composer-toast[data-tone="error"] {
border-color: rgba(248, 113, 113, 0.28);
}
.video-composer-toast[data-tone="processing"] {
border-color: rgba(120, 95, 255, 0.28);
}
.video-composer-toast strong {
display: block;
font-size: 13px;
font-weight: 800;
}
.video-composer-toast span {
display: block;
margin-top: 6px;
color: rgba(230, 237, 247, 0.68);
font-size: 12px;
line-height: 1.6;
}
.video-composer-meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.video-provider-select-wrap {
position: relative;
display: inline-flex;
align-items: center;
min-height: 34px;
padding: 0 12px 0 36px;
border-radius: 10px;
border: 1px solid rgba(120, 95, 255, 0.3);
background: rgba(70, 56, 173, 0.18);
overflow: hidden;
}
.video-provider-select-wrap::before {
content: '⬡';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #eef4ff;
font-size: 12px;
line-height: 1;
pointer-events: none;
}
.video-provider-select {
min-width: 116px;
border: 0;
background: transparent;
color: #eef4ff;
font: inherit;
font-size: 12px;
font-weight: 700;
outline: none;
appearance: none;
cursor: pointer;
padding-right: 20px;
}
.video-provider-select option {
background: #131924;
color: #eef4ff;
}
.video-provider-select-caret {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: rgba(238, 244, 255, 0.72);
font-size: 10px;
pointer-events: none;
}
.video-meta-pill {
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 34px;
padding: 0 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: #eef4ff;
font-size: 12px;
font-weight: 700;
}
.video-meta-pill--primary {
background: rgba(70, 56, 173, 0.18);
border-color: rgba(120, 95, 255, 0.3);
}
.video-meta-ico {
color: rgba(230, 237, 247, 0.56);
font-size: 12px;
line-height: 1;
}
.video-composer-actions {
display: flex;
align-items: center;
gap: 14px;
margin-left: auto;
}
.video-credit {
display: inline-flex;
align-items: center;
gap: 6px;
color: #f8fbff;
font-size: 14px;
font-weight: 800;
}
.video-credit span {
color: rgba(230, 237, 247, 0.32);
text-decoration: line-through;
font-weight: 700;
}
.video-create-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 46px;
padding: 0 22px;
border: 0;
border-radius: 10px;
background: #f5f7ff;
color: #0d1220;
font: inherit;
font-size: 16px;
font-weight: 800;
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.video-create-btn:hover {
transform: translateY(-1px);
box-shadow: 0 12px 30px rgba(255, 255, 255, 0.14);
}
.video-create-btn::before {
content: '+';
font-size: 18px;
line-height: 1;
}
.asset-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.asset-card {
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
overflow: hidden;
transition: all 0.2s ease;
}
.asset-card-link {
display: block;
color: inherit;
text-decoration: none;
}
.asset-card:hover {
border-color: rgba(67, 217, 189, 0.3);
box-shadow: 0 8px 24px rgba(67, 217, 189, 0.1);
transform: translateY(-2px);
}
.asset-card-image {
width: 100%;
height: 140px;
background: linear-gradient(135deg, rgba(67, 217, 189, 0.1), rgba(245, 185, 107, 0.05));
display: flex;
align-items: center;
justify-content: center;
color: rgba(226, 235, 247, 0.4);
font-size: 12px;
}
.asset-card-info {
padding: 12px;
}
.asset-card-title {
color: #e6edf7;
font-size: 12px;
font-weight: 700;
margin: 0 0 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.asset-card-meta {
color: rgba(226, 235, 247, 0.5);
font-size: 11px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: rgba(226, 235, 247, 0.54);
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state-text {
font-size: 14px;
margin-bottom: 8px;
}
@media (max-width: 1180px) {
.quick-actions {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.asset-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
}
@media (max-width: 860px) {
.workspace-hero-title,
.recent-section-title {
font-size: 1.6rem;
}
.quick-actions {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.asset-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.video-composer-main {
grid-template-columns: 164px minmax(0, 1fr);
}
.action-card {
padding: 16px;
}
}
@media (max-width: 600px) {
.category-buttons {
overflow-x: auto;
padding-bottom: 8px;
}
.category-btn {
white-space: nowrap;
}
.video-composer-shell {
padding: 12px;
}
.video-composer-main {
grid-template-columns: 1fr;
}
.video-frame-rail {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.video-composer-prompt {
min-height: 112px;
}
.video-composer-bottom {
align-items: stretch;
}
.video-composer-actions {
width: 100%;
justify-content: space-between;
}
.quick-actions {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
}
</style>
<x-slot name="header">
<div class="db-header-copy">
<span class="db-header-kicker">Operations cockpit</span>
<h2>Dashboard</h2>
<p>Ringkasan progress automation, kesehatan scheduler, dan readiness tiap platform dalam satu layar yang lebih jelas.</p>
</div>
</x-slot>
<div class="db-page">
<div class="db-container space-y-6">
<!-- Creative Workspace Section (Freepik-style) -->
<section class="workspace-hero">
<div class="category-buttons">
<button class="category-btn active">All</button>
<button class="category-btn">New</button>
<button class="category-btn">Image</button>
<button class="category-btn">Video</button>
<button class="category-btn">Audio</button>
<button class="category-btn">3D</button>
<button class="category-btn">Others</button>
<span class="cat-divider"></span>
<a href="{{ route('app.index') }}" class="cat-tool-btn" title="Open app">🧩 <span>App</span></a>
<a href="{{ route('app.index') }}#image-lab" class="cat-tool-btn" title="Generate Visual">🎨 <span>Image</span></a>
<a href="{{ route('app.index') }}#video-lab" class="cat-tool-btn" title="Generate Video">🎬 <span>Video</span></a>
<a href="{{ route('app.index') }}#voice-lab" class="cat-tool-btn" title="Text to Voice">🎙️ <span>Voice</span></a>
<a href="{{ route('app.index') }}#face-lab" class="cat-tool-btn" title="Face Swap Style">🪄 <span>Face</span></a>
<a href="{{ route('social.index') }}" class="cat-tool-btn" title="Social Publish">📤 <span>Publish</span></a>
</div>
<div class="video-composer-shell">
<form method="POST" action="{{ route('tools.video') }}" class="video-composer-form" enctype="multipart/form-data">
@csrf
<input type="hidden" name="video_provider" id="dashboard-video-provider-input" value="gemini">
<input type="hidden" name="video_model" value="veo">
<input type="hidden" name="video_quality" value="720p">
<input type="hidden" name="video_duration" value="8s">
<input type="hidden" name="aspect_ratio" value="16:9">
<input type="hidden" name="veo_variant" value="veo-3">
<input type="hidden" name="replicate_i2v_model" value="wavespeedai/wan-2.1-i2v-480p">
<input type="hidden" name="video_audio" value="off">
<div class="video-composer-main">
<div class="video-frame-rail">
<label class="video-frame-box" for="dashboard-start-frame-input">
<input id="dashboard-start-frame-input" type="file" name="source_image_file" accept="image/jpeg,image/png,image/webp">
<img class="video-frame-preview" id="dashboard-start-frame-preview" alt="Start frame preview">
<span class="video-frame-plus">+</span>
<span class="video-frame-label">Start Frame</span>
<span class="video-frame-name" id="dashboard-start-frame-name"></span>
</label>
<label class="video-frame-box" for="dashboard-end-frame-input">
<input id="dashboard-end-frame-input" type="file" name="end_image_file" accept="image/jpeg,image/png,image/webp">
<img class="video-frame-preview" id="dashboard-end-frame-preview" alt="End frame preview">
<span class="video-frame-plus">+</span>
<span class="video-frame-label">End Frame</span>
<span class="video-frame-name" id="dashboard-end-frame-name"></span>
</label>
</div>
<div class="video-composer-content">
<div class="video-composer-prompt-wrap">
<textarea name="prompt" class="video-composer-prompt" placeholder="Describe the video content you want to generate, e.g., &quot;A child flying a kite in the park, golden sunlight, camera tilting up.&quot;">{{ old('prompt') }}</textarea>
</div>
</div>
</div>
<div class="video-composer-status" id="dashboard-video-status" data-state="idle">
<div class="video-composer-status-head">
<strong class="video-composer-status-title" id="dashboard-video-status-title">Ready to render</strong>
<span class="video-composer-status-badge" id="dashboard-video-status-badge">Idle</span>
</div>
<div class="video-composer-status-copy" id="dashboard-video-status-copy">Upload start frame, isi prompt, lalu jalankan render. Kalau provider gagal, error akan muncul langsung di dashboard tanpa meninggalkan hasil palsu.</div>
<div class="video-composer-status-track" aria-hidden="true">
<span class="video-composer-status-fill" id="dashboard-video-status-fill"></span>
</div>
<a href="{{ route('app.index') }}#result-lab" class="video-composer-status-link" id="dashboard-video-status-link">Open result lab</a>
</div>
<div class="video-composer-bottom">
<div class="video-composer-meta">
<label class="video-provider-select-wrap" for="dashboard-video-provider-select" aria-label="Choose video provider">
<select id="dashboard-video-provider-select" class="video-provider-select">
<option value="runway">Runway</option>
<option value="gemini" selected>Gemini</option>
<option value="openai">OpenAI</option>
<option value="huggingface">Hugging Face</option>
<option value="replicate">Replicate WAN 2.1</option>
</select>
<span class="video-provider-select-caret">▾</span>
</label>
<span class="video-meta-pill"><span class="video-meta-ico">◔</span><span id="dashboard-video-quality-pill">720p</span></span>
<span class="video-meta-pill"><span class="video-meta-ico">◷</span><span>8s</span></span>
<span class="video-meta-pill"><span class="video-meta-ico">▭</span><span>16:9</span></span>
</div>
<div class="video-composer-actions">
<div class="video-credit">🤍 120 <span>320</span></div>
<button type="submit" class="video-create-btn">Create</button>
</div>
</div>
</form>
</div>
<h3 class="recent-section-title">Recent projects</h3>
@if ($recentProjects->count() > 0)
<div class="asset-grid">
@foreach ($recentProjects as $project)
@php
$typeIcon = match($project->type) {
'video' => '🎬',
'audio' => '🎙️',
default => '🖼️',
};
$typeLabel = match($project->type) {
'video' => 'Video',
'audio' => 'Audio',
default => 'Image',
};
$thumb = $project->thumbnail_url ?: $project->url;
$isVideo = $project->type === 'video';
$isAudio = $project->type === 'audio';
$meta = is_array($project->meta) ? $project->meta : [];
$editorKey = 'image';
$editorHash = '#image-lab';
$editorLabel = 'Open image lab';
$assetId = $project->type === 'image' ? (string) $project->id : '';
$sourceAssetId = '';
$sourceThumb = '';
if ($isAudio) {
$editorKey = 'voice';
$editorHash = '#voice-lab';
$editorLabel = 'Open voice lab';
} elseif (($meta['template_slug'] ?? '') === 'movie-poster' || $project->provider === 'poster') {
$editorKey = 'poster';
$editorHash = '#image-lab';
$editorLabel = 'Open movie poster canvas';
$sourceAssetId = (string) ($meta['source_asset_id'] ?? $assetId);
} elseif (in_array((string) $project->provider, ['replicate-tryon', 'fal-tryon'], true) || ($meta['source'] ?? '') === 'tools_tryon_async_poll') {
$editorKey = 'tryon';
$editorHash = '#image-lab';
$editorLabel = 'Open try on canvas';
$sourceAssetId = (string) ($meta['source_asset_id'] ?? $assetId);
} elseif ($isVideo || in_array((string) $project->provider, ['replicate-i2v', 'freepik-runway-i2v', 'hf-motion', 'huggingface-motion', 'openai-sora', 'gemini-veo', 'runway'], true)) {
$editorKey = 'video';
$editorHash = '#video-lab';
$editorLabel = 'Open video canvas';
$sourceAssetId = (string) ($meta['source_asset_id'] ?? '');
$sourceThumb = (string) ($project->thumbnail_url ?: '');
}
$editorRouteParams = array_filter([
'editor' => $editorKey,
'asset' => $assetId !== '' ? $assetId : null,
'source_asset_id' => $sourceAssetId !== '' ? $sourceAssetId : null,
'source_thumb' => $sourceThumb !== '' ? $sourceThumb : null,
], fn ($value) => $value !== null && $value !== '');
$editorHref = route('app.index', $editorRouteParams).$editorHash;
@endphp
<div class="asset-card" title="{{ $project->prompt ?? $typeLabel }}">
<a href="{{ $editorHref }}" class="asset-card-link" aria-label="{{ $editorLabel }}">
<div class="asset-card-image" style="position:relative; overflow:hidden;">
@if ($isAudio)
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:8px;color:rgba(226,235,247,0.5);">
<span style="font-size:28px;">🎙️</span>
<audio src="{{ $project->url }}" controls style="width:90%;opacity:0.85;"></audio>
</div>
@elseif ($isVideo && $thumb)
<img src="{{ $thumb }}" alt="video" style="width:100%;height:100%;object-fit:cover;">
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;">
<div style="width:36px;height:36px;background:rgba(0,0,0,0.55);border-radius:50%;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>
</div>
</div>
@elseif ($thumb)
<img src="{{ $thumb }}" alt="{{ $typeLabel }}" style="width:100%;height:100%;object-fit:cover;">
@else
<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:28px;">{{ $typeIcon }}</div>
@endif
</div>
<div class="asset-card-info">
<div class="asset-card-title" style="display:flex;align-items:center;gap:5px;">
<span>{{ $typeIcon }}</span>
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
{{ $project->prompt ? \Illuminate\Support\Str::limit($project->prompt, 28) : $typeLabel }}
</span>
</div>
<div class="asset-card-meta">
{{ strtoupper($project->provider ?? 'AI') }} • {{ $project->created_at->format('M d') }}
</div>
</div>
</a>
</div>
@endforeach
</div>
<div style="text-align:center;margin-top:14px;">
<a href="{{ route('app.index') }}" style="font-size:13px;color:rgba(160,185,230,0.8);text-decoration:none;">
Open AI App →
</a>
</div>
@else
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">No projects yet</div>
<p style="color: rgba(226, 235, 247, 0.5); font-size: 12px;">Start creating by choosing an action above</p>
</div>
@endif
</section>
<section class="workspace-hero">
<h2 class="workspace-hero-title">What would you like to create today?</h2>
<div class="quick-actions">
<a href="{{ route('app.index') }}#face-lab" class="action-card">
<div class="action-card-icon">🎭</div>
<div class="action-card-title">Character Restyle</div>
<div class="action-card-desc">Push satu portrait ke look baru yang lebih editorial dengan referensi image tambahan.</div>
<span class="action-card-badge">Face Lab</span>
</a>
<a href="{{ route('app.index') }}#video-lab" class="action-card">
<div class="action-card-icon">🎬</div>
<div class="action-card-title">Image to Video</div>
<div class="action-card-desc">Animate start frame jadi motion clip dengan prompt sinematik dan output siap review.</div>
<span class="action-card-badge">Video Lab</span>
</a>
<a href="{{ route('app.index') }}#image-lab" class="action-card">
<div class="action-card-icon">🖼️</div>
<div class="action-card-title">Campaign Visual</div>
<div class="action-card-desc">Generate key visual, product hero, atau concept frame untuk kebutuhan social dan ads.</div>
<span class="action-card-badge">Image Lab</span>
</a>
<a href="{{ route('app.index') }}#voice-lab" class="action-card">
<div class="action-card-icon">🎙️</div>
<div class="action-card-title">Voice Draft</div>
<div class="action-card-desc">Buat VO hook, explainer, atau narasi singkat yang langsung bisa dipreview.</div>
<span class="action-card-badge">Voice Lab</span>
</a>
</div>
</section>
<section class="db-hero">
<div>
<span class="db-kicker">Automation Pulse</span>
<h1 class="db-title">Automation Ops Board</h1>
<p class="db-copy">Dashboard sekarang jadi pusat observasi yang lebih premium buat lihat readiness tiap platform, queue health, dan status cronjob yang menentukan apakah scheduled publish benar-benar jalan di server.</p>
<div class="db-chip-row" style="margin-top: 22px;">
<span class="db-chip">{{ $stats['ready_platforms'] }} platform ready</span>
<span class="db-chip">{{ $healthScore }}% average progress</span>
<span class="db-pill {{ $cronTone }}">{{ $cronStatus['headline'] }}</span>
</div>
<div class="db-actions" style="margin-top: 24px;">
<a href="{{ route('social.index') }}" class="db-button db-button--primary">Open Social Automation</a>
<a href="{{ route('social.credentials.index') }}" class="db-button db-button--ghost">Manage Credentials</a>
</div>
</div>
<div class="db-hero-panel">
<div class="db-window-bar">
<div class="db-window-dots">
<span></span>
<span></span>
<span></span>
</div>
<span>Automation heartbeat</span>
</div>
<div class="db-panel-copy">
<strong>{{ $cronStatus['headline'] }}</strong>
<p>{{ $cronStatus['detail'] }}</p>
</div>
<div class="db-grid-health" style="margin-top: 18px;">
<div class="db-health-card">
<span>Cron Service</span>
<p>{{ $cronServiceLabel }}</p>
</div>
<div class="db-health-card">
<span>Schedule Entry</span>
<p>{{ $cronStatus['schedule_entry_found'] ? 'Detected' : 'Missing' }}</p>
</div>
<div class="db-health-card">
<span>Runner</span>
<p>{{ $cronStatus['runner_detected'] ? 'Detected' : 'Idle' }}</p>
</div>
<div class="db-health-card">
<span>Next Queue Focus</span>
<p>{{ $nextScheduledPost?->platform ? ucfirst($nextScheduledPost->platform) : 'No pending post' }}</p>
</div>
</div>
</div>
</section>
<section class="db-grid-stats">
<article class="db-stat-card">
<span>Scheduled</span>
<strong>{{ $stats['scheduled'] }}</strong>
<p>Item yang siap atau menunggu jalur publish terjadwal.</p>
</article>
<article class="db-stat-card">
<span>Posted</span>
<strong>{{ $stats['posted'] }}</strong>
<p>Total post yang sudah pernah berhasil terkirim.</p>
</article>
<article class="db-stat-card">
<span>Failed</span>
<strong>{{ $stats['failed'] }}</strong>
<p>Butuh pengecekan token, media requirement, atau retry.</p>
</article>
<article class="db-stat-card">
<span>Approval Queue</span>
<strong>{{ $stats['approval'] }}</strong>
<p>Draft yang masih tertahan manual approval.</p>
</article>
<article class="db-stat-card">
<span>Ready Platforms</span>
<strong>{{ $stats['ready_platforms'] }}</strong>
<p>Platform dengan token yang sudah siap dipakai automation.</p>
</article>
</section>
<section class="db-card">
<div class="db-card-head">
<div>
<span class="db-section-tag">Recurring Engine</span>
<h3 class="db-card-title">Campaign KPI and alert center</h3>
<p class="db-card-desc">Monitor performa recurring campaign, lihat lane brand/team yang paling aktif, dan tangkap failure scheduler sebelum queue harian ikut terganggu.</p>
</div>
<div class="db-inline-badges">
<span class="db-pill {{ $recurringStats['unread_alerts'] > 0 ? 'db-pill--critical' : 'db-pill--healthy' }}">
{{ $recurringStats['unread_alerts'] }} unread alerts
</span>
<span class="db-pill {{ $recurringStats['approval_queue'] > 0 ? 'db-pill--warning' : 'db-pill--healthy' }}">
{{ $recurringStats['approval_queue'] }} awaiting approval
</span>
</div>
</div>
<div class="db-grid-health">
<article class="db-health-card">
<span>Active Campaigns</span>
<p>{{ $recurringStats['active'] }}</p>
</article>
<article class="db-health-card">
<span>Due Today</span>
<p>{{ $recurringStats['due_today'] }}</p>
</article>
<article class="db-health-card">
<span>Generated This Week</span>
<p>{{ $recurringStats['generated_this_week'] }}</p>
</article>
<article class="db-health-card">
<span>Failing Campaigns</span>
<p>{{ $recurringStats['failing_campaigns'] }}</p>
</article>
</div>
<div class="db-grid-platforms" style="margin-top: 18px;">
<article class="db-progress-card">
<div class="db-progress-head">
<div>
<h4>Brand / Team Approval Lanes</h4>
<p>Recurring campaign sekarang bisa pakai approval lane khusus per brand dan team.</p>
</div>
<span class="db-pill db-pill--warning">{{ $recurringLaneSummary->count() }} lanes</span>
</div>
<div class="db-table" style="margin-top: 16px;">
<table>
<thead>
<tr>
<th>Lane</th>
<th>Active</th>
<th>Manual</th>
<th>Next Run</th>
</tr>
</thead>
<tbody>
@forelse ($recurringLaneSummary as $lane)
<tr>
<td>{{ $lane['label'] }}</td>
<td>{{ $lane['active'] }}/{{ $lane['count'] }}</td>
<td>{{ $lane['manual'] }}</td>
<td>{{ optional($lane['next_occurrence_at'])->format('d M Y H:i') ?: '-' }}</td>
</tr>
@empty
<tr>
<td colspan="4" style="color: rgba(226, 235, 247, 0.58);">Belum ada recurring campaign lane yang tersimpan.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</article>
<article class="db-progress-card">
<div class="db-progress-head">
<div>
<h4>Recent Recurring Alerts</h4>
<p>Alert dibuat otomatis saat recurring scheduler gagal membuat occurrence baru.</p>
</div>
<span class="db-pill {{ $recurringStats['unread_alerts'] > 0 ? 'db-pill--critical' : 'db-pill--healthy' }}">
{{ $recurringStats['unread_alerts'] > 0 ? 'Needs attention' : 'Quiet' }}
</span>
</div>
<div class="db-table" style="margin-top: 16px;">
<table>
<thead>
<tr>
<th>Campaign</th>
<th>Message</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@forelse ($recentRecurringAlerts as $alert)
<tr>
<td>{{ data_get($alert->context, 'campaign_title') ?: ('Campaign #'.$alert->social_recurring_campaign_id) }}</td>
<td>{{ \Illuminate\Support\Str::limit($alert->message, 90) }}</td>
<td>{{ $alert->read_at ? 'resolved' : 'unread' }}</td>
</tr>
@empty
<tr>
<td colspan="3" style="color: rgba(226, 235, 247, 0.58);">Belum ada recurring alert. Scheduler masih bersih.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</article>
</div>
<div class="db-table" style="margin-top: 18px;">
<table>
<thead>
<tr>
<th>Campaign</th>
<th>Lane</th>
<th>Approval</th>
<th>Next Run</th>
</tr>
</thead>
<tbody>
@forelse ($recentRecurringCampaigns as $campaign)
<tr>
<td>{{ $campaign->title ?: 'Untitled recurring campaign' }}</td>
<td>{{ $campaign->brand_label ?: 'General' }} / {{ $campaign->team_label ?: 'Shared' }}</td>
<td>{{ strtoupper($campaign->approval_mode) }}</td>
<td>{{ optional($campaign->next_occurrence_at)->format('d M Y H:i') ?: '-' }}</td>
</tr>
@empty
<tr>
<td colspan="4" style="color: rgba(226, 235, 247, 0.58);">Belum ada recurring campaign yang bisa ditampilkan.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</section>
<section class="db-card">
<div class="db-card-head">
<div>
<span class="db-section-tag">Platform Progress</span>
<h3 class="db-card-title">Readiness by platform</h3>
<p class="db-card-desc">Setiap platform sekarang punya progress yang menggabungkan credential readiness, aktivitas queue, dan jejak publish supaya gampang kelihatan mana yang sudah live dan mana yang masih butuh perhatian.</p>
</div>
</div>
<div class="db-grid-platforms">
@foreach ($platforms as $key => $label)
@php
$progress = $platformProgress[$key];
$stateClass = match ($progress['state_tone']) {
'healthy' => 'db-state--healthy',
'critical' => 'db-state--critical',
'ready', 'warning' => 'db-state--warning',
default => 'db-state--pending',
};
@endphp
<article class="db-progress-card">
<div class="db-progress-head">
<div>
<h4>{{ $label }}</h4>
<p>{{ $progress['next_action'] }}</p>
</div>
<span class="db-pill {{ $stateClass }}">{{ $progress['state'] }}</span>
</div>
<div style="margin-top: 16px; display: flex; align-items: center; justify-content: space-between; gap: 14px;">
<div class="db-progress-value">{{ $progress['completion'] }}%</div>
<div class="db-inline-badges">
<span class="db-pill {{ $progress['credential_ready'] ? 'db-pill--healthy' : 'db-state--pending' }}">
{{ $progress['credential_ready'] ? 'Token ready' : 'Token missing' }}
</span>
</div>
</div>
<div class="db-progress-bar">
<div class="db-progress-fill" style="width: {{ $progress['completion'] }}%;"></div>
</div>
<div class="db-metric-grid">
<div class="db-metric">
<span>Scheduled</span>
<strong>{{ $progress['counts']['scheduled'] }}</strong>
</div>
<div class="db-metric">
<span>Posted</span>
<strong>{{ $progress['counts']['posted'] }}</strong>
</div>
<div class="db-metric">
<span>Failed</span>
<strong>{{ $progress['counts']['failed'] }}</strong>
</div>
<div class="db-metric">
<span>Approval</span>
<strong>{{ $progress['counts']['approval'] }}</strong>
</div>
</div>
<div class="db-step-list">
@foreach ($progress['steps'] as $step)
<span class="db-step {{ $step['done'] ? 'db-step--done' : '' }}">{{ $step['label'] }}</span>
@endforeach
</div>
<p class="db-note">
@if ($progress['last_activity_at'])
Aktivitas terakhir {{ $progress['last_activity_at']->format('d M Y H:i') }} dengan status {{ $progress['last_status'] ?? 'unknown' }}.
@else
Belum ada aktivitas queue untuk platform ini.
@endif
</p>
</article>
@endforeach
</div>
</section>
<section class="db-card">
<div class="db-card-head">
<div>
<span class="db-section-tag">Cron Scheduler</span>
<h3 class="db-card-title">Cron Scheduler Status</h3>
<p class="db-card-desc">Status ini dibaca dari server saat halaman dibuka, jadi kamu bisa lihat apakah scheduler Laravel benar-benar punya jalur eksekusi otomatis atau belum.</p>
</div>
<span class="db-pill {{ $cronTone }}">{{ $cronStatus['state'] }}</span>
</div>
<div class="db-grid-health">
<article class="db-health-card">
<span>Cron Service</span>
<p>{{ $cronServiceLabel }}</p>
</article>
<article class="db-health-card">
<span>Entry Source</span>
<p>{{ $cronStatus['schedule_entry_source'] ?? 'Belum ditemukan' }}</p>
</article>
<article class="db-health-card">
<span>Runner Command</span>
<p>{{ $cronStatus['runner_command'] ?? 'Tidak ada runner artisan yang sedang aktif' }}</p>
</article>
<article class="db-health-card">
<span>Checked At</span>
<p>{{ $cronStatus['checked_at']->format('d M Y H:i:s') }}</p>
</article>
</div>
<div class="db-health-callout">
<strong style="display: block; color: #f8fbff; font-size: 1rem; font-weight: 800;">Expected cron entry</strong>
<p class="db-card-desc" style="margin-top: 8px;">Kalau mau automation scheduled jalan stabil, server ini seharusnya punya entry cron berikut.</p>
<span class="db-code">{{ $cronStatus['expected_cron'] }}</span>
<p class="db-card-desc" style="margin-top: 16px;">Laravel command yang dijadwalkan oleh aplikasi saat ini:</p>
<span class="db-code">{{ $cronStatus['publish_command'] }}</span>
</div>
</section>
<section class="db-card">
<div class="db-card-head">
<div>
<span class="db-section-tag">Recent Queue</span>
<h3 class="db-card-title">Latest automation items</h3>
<p class="db-card-desc">Snapshot singkat item terakhir biar kamu bisa lihat platform mana yang paling aktif tanpa harus pindah ke halaman queue penuh.</p>
</div>
</div>
<div class="db-table">
<table>
<thead>
<tr>
<th>Platform</th>
<th>Status</th>
<th>Scheduled</th>
<th>Caption</th>
</tr>
</thead>
<tbody>
@forelse ($recentPosts as $post)
<tr>
<td>{{ ucfirst($post->platform) }}</td>
<td>{{ $post->status }}</td>
<td>{{ optional($post->scheduled_at)->format('d M Y H:i') ?: '-' }}</td>
<td>{{ \Illuminate\Support\Str::limit($post->caption, 100) }}</td>
</tr>
@empty
<tr>
<td colspan="4" style="color: rgba(226, 235, 247, 0.58);">Belum ada item queue. Mulai dari halaman Social Automation untuk bikin draft atau scheduled post pertama.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</section>
</div>
</div>
<div class="video-composer-toast-stack" id="dashboard-video-toast-stack" aria-live="polite"></div>
<script>
(() => {
const bindFramePreview = (inputId, labelId, previewId) => {
const input = document.getElementById(inputId);
const label = document.getElementById(labelId);
const preview = document.getElementById(previewId);
const frameBox = input ? input.closest('.video-frame-box') : null;
if (!input || !label || !preview || !frameBox) {
return;
}
let objectUrl = '';
input.addEventListener('change', () => {
const file = input.files && input.files[0] ? input.files[0] : null;
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
objectUrl = '';
}
if (!file) {
preview.removeAttribute('src');
label.textContent = '';
frameBox.classList.remove('is-filled');
return;
}
if (file.type.startsWith('image/')) {
objectUrl = URL.createObjectURL(file);
preview.src = objectUrl;
label.textContent = '';
frameBox.classList.add('is-filled');
return;
}
preview.removeAttribute('src');
label.textContent = file.name;
frameBox.classList.remove('is-filled');
});
};
bindFramePreview('dashboard-start-frame-input', 'dashboard-start-frame-name', 'dashboard-start-frame-preview');
bindFramePreview('dashboard-end-frame-input', 'dashboard-end-frame-name', 'dashboard-end-frame-preview');
const providerSelect = document.getElementById('dashboard-video-provider-select');
const providerInput = document.getElementById('dashboard-video-provider-input');
const qualityPill = document.getElementById('dashboard-video-quality-pill');
const videoForm = document.querySelector('.video-composer-form');
const videoStatus = document.getElementById('dashboard-video-status');
const videoStatusTitle = document.getElementById('dashboard-video-status-title');
const videoStatusBadge = document.getElementById('dashboard-video-status-badge');
const videoStatusCopy = document.getElementById('dashboard-video-status-copy');
const videoStatusFill = document.getElementById('dashboard-video-status-fill');
const videoStatusLink = document.getElementById('dashboard-video-status-link');
const toastStack = document.getElementById('dashboard-video-toast-stack');
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
const submitButton = videoForm ? videoForm.querySelector('.video-create-btn') : null;
let videoPollTimer = null;
let videoProgressTimer = null;
let visualProgress = 0;
const showToast = (title, message, tone = 'processing') => {
if (!toastStack) {
return;
}
const toast = document.createElement('div');
toast.className = 'video-composer-toast';
toast.setAttribute('data-tone', tone);
toast.innerHTML = `<strong>${title}</strong><span>${message}</span>`;
toastStack.appendChild(toast);
window.setTimeout(() => {
toast.remove();
}, 4200);
};
const clearVideoTimers = () => {
if (videoPollTimer) {
window.clearTimeout(videoPollTimer);
videoPollTimer = null;
}
if (videoProgressTimer) {
window.clearInterval(videoProgressTimer);
videoProgressTimer = null;
}
};
const setVideoBusy = (busy) => {
if (!submitButton) {
return;
}
submitButton.disabled = busy;
submitButton.textContent = busy ? 'Rendering...' : 'Create';
submitButton.style.opacity = busy ? '0.7' : '1';
};
const setVideoStatus = (state, title, copy, progress = 0, linkHref = '', linkLabel = 'Open result lab') => {
if (!videoStatus || !videoStatusTitle || !videoStatusBadge || !videoStatusCopy || !videoStatusFill || !videoStatusLink) {
return;
}
videoStatus.setAttribute('data-state', state);
videoStatusTitle.textContent = title;
videoStatusBadge.textContent = state === 'processing'
? 'Rendering'
: (state === 'success' ? 'Success' : (state === 'error' ? 'Failed' : 'Idle'));
videoStatusCopy.textContent = copy;
videoStatusFill.style.width = `${Math.max(0, Math.min(100, progress))}%`;
if (linkHref) {
videoStatusLink.href = linkHref;
videoStatusLink.textContent = linkLabel;
videoStatusLink.classList.add('is-visible');
} else {
videoStatusLink.classList.remove('is-visible');
}
};
const beginVisualProgress = (startAt = 12) => {
clearVideoTimers();
visualProgress = startAt;
setVideoStatus('processing', 'Uploading assets', 'Source frame sedang dikirim ke server dan render video sedang dipersiapkan.', visualProgress);
videoProgressTimer = window.setInterval(() => {
visualProgress = Math.min(92, visualProgress + (visualProgress < 48 ? 8 : (visualProgress < 76 ? 4 : 2)));
setVideoStatus('processing', 'Rendering video', 'Job video sedang berjalan. Dashboard akan memberi tahu kalau provider gagal atau hasil sudah siap.', visualProgress);
}, 1500);
};
const pollVideoResult = async (pollUrl) => {
try {
const response = await fetch(pollUrl, {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
});
const data = await response.json();
if (data.status === 'processing') {
videoPollTimer = window.setTimeout(() => pollVideoResult(pollUrl), 3500);
return;
}
clearVideoTimers();
if (data.status === 'succeeded') {
setVideoBusy(false);
setVideoStatus('success', 'Video ready', data.notice || 'Render video selesai dan hasilnya sudah siap dibuka.', 100, '{{ route('app.index') }}#result-lab');
showToast('Video ready', data.notice || 'Render video selesai.', 'success');
return;
}
setVideoBusy(false);
setVideoStatus('error', 'Render failed', data.error || 'Provider gagal menyelesaikan render video.', Math.max(visualProgress, 24));
showToast('Render failed', data.error || 'Provider gagal menyelesaikan render video.', 'error');
} catch (error) {
clearVideoTimers();
setVideoBusy(false);
setVideoStatus('error', 'Polling interrupted', 'Status akhir video tidak bisa dibaca. Cek result lab atau coba ulangi render.', Math.max(visualProgress, 24), '{{ route('app.index') }}#result-lab');
showToast('Polling interrupted', 'Status akhir video tidak bisa dibaca. Cek result lab atau coba ulangi render.', 'error');
}
};
if (providerSelect && providerInput) {
const syncProviderMeta = () => {
providerInput.value = providerSelect.value || 'gemini';
if (qualityPill) {
qualityPill.textContent = providerInput.value === 'replicate' ? '480p' : '720p';
}
};
syncProviderMeta();
providerSelect.addEventListener('change', () => {
syncProviderMeta();
});
}
if (videoForm) {
videoForm.addEventListener('submit', async (event) => {
event.preventDefault();
clearVideoTimers();
setVideoBusy(true);
beginVisualProgress(16);
try {
const response = await fetch(videoForm.action, {
method: 'POST',
body: new FormData(videoForm),
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken,
},
credentials: 'same-origin',
});
const data = await response.json();
if (!response.ok || data.status === 'failed') {
clearVideoTimers();
setVideoBusy(false);
setVideoStatus('error', 'Render failed', data.error || 'Video tidak berhasil dibuat.', Math.max(visualProgress, 18));
showToast('Render failed', data.error || 'Video tidak berhasil dibuat.', 'error');
return;
}
if (data.status === 'processing' && data.poll_url) {
setVideoStatus('processing', 'Render queued', data.notice || 'Video job sudah masuk queue render.', Math.max(visualProgress, 28));
showToast('Render queued', data.notice || 'Video job sudah masuk queue render.', 'processing');
videoPollTimer = window.setTimeout(() => pollVideoResult(data.poll_url), 3000);
return;
}
clearVideoTimers();
setVideoBusy(false);
setVideoStatus('success', 'Video ready', data.notice || 'Video selesai dibuat.', 100, '{{ route('app.index') }}#result-lab');
showToast('Video ready', data.notice || 'Video selesai dibuat.', 'success');
} catch (error) {
clearVideoTimers();
setVideoBusy(false);
setVideoStatus('error', 'Request failed', 'Permintaan render tidak bisa dikirim. Cek koneksi atau coba ulangi.', Math.max(visualProgress, 18));
showToast('Request failed', 'Permintaan render tidak bisa dikirim. Cek koneksi atau coba ulangi.', 'error');
}
});
}
})();
</script>
</x-app-layout>