| @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: |
| 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: |
| 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: |
| 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: |
| box-shadow: 0 0 18px rgba(67, 217, 189, 0.72); |
| } |
|
|
| .db-title { |
| margin: 18px 0 14px; |
| max-width: 10ch; |
| color: |
| 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: |
| 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: |
| } |
|
|
| .db-window-dots span:nth-child(2) { |
| background: |
| } |
|
|
| .db-window-dots span:last-child { |
| background: |
| } |
|
|
| .db-panel-copy strong { |
| display: block; |
| color: |
| 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: |
| 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: |
| font-size: 11px; |
| font-weight: 800; |
| letter-spacing: 0.14em; |
| text-transform: uppercase; |
| } |
|
|
| .db-card-title { |
| margin: 0; |
| color: |
| 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: |
| background: rgba(67, 217, 189, 0.12); |
| } |
|
|
| .db-pill--warning, |
| .db-state--ready, |
| .db-state--warning { |
| color: |
| background: rgba(245, 185, 107, 0.12); |
| } |
|
|
| .db-pill--critical, |
| .db-state--critical { |
| color: |
| 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: |
| 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: |
| 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: |
| 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: |
| 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: |
| 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: |
| } |
|
|
| |
| .workspace-hero { |
| margin-top: 40px; |
| } |
|
|
| .workspace-hero-title { |
| color: |
| 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: |
| 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: |
| } |
|
|
| .category-btn.active { |
| background: linear-gradient(135deg, #43d9bd, #149f88); |
| border-color: |
| color: |
| } |
|
|
| .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: |
| } |
|
|
| .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: |
| 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: |
| padding: 4px 8px; |
| border-radius: 6px; |
| font-size: 10px; |
| font-weight: 700; |
| width: fit-content; |
| } |
|
|
| .recent-section-title { |
| color: |
| 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: |
| 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: |
| 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: |
| 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: |
| 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: |
| 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: |
| 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: |
| font-size: 12px; |
| line-height: 1; |
| pointer-events: none; |
| } |
|
|
| .video-provider-select { |
| min-width: 116px; |
| border: 0; |
| background: transparent; |
| color: |
| font: inherit; |
| font-size: 12px; |
| font-weight: 700; |
| outline: none; |
| appearance: none; |
| cursor: pointer; |
| padding-right: 20px; |
| } |
|
|
| .video-provider-select option { |
| background: |
| color: |
| } |
|
|
| .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: |
| 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: |
| 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: |
| color: |
| 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: |
| 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') }} |
| <a href="{{ route('app.index') }}#video-lab" class="cat-tool-btn" title="Generate Video">🎬 <span>Video</span></a> |
| <a href="{{ route('app.index') }} |
| <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., "A child flying a kite in the park, golden sunlight, camera tilting up."">{{ 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') }} |
| </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') }} |
| <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') }} |
| <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') }} |
| <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') }} |
| <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: |
| <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> |
|
|