diff --git a/internal/server/server.go b/internal/server/server.go
index b851d7a5..f1215f58 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -850,6 +850,31 @@ func (s *Server) handleImageGenerations(c *gin.Context) {
}
}
+ // ponytail: per-model valid size sets. Add new models here.
+ if req.Size != nil {
+ validSizes := map[string][]string{
+ "gpt-image": {"1024x1024", "1024x1536", "1536x1024", "auto"},
+ "dall-e-3": {"1024x1024", "1024x1792", "1792x1024", "auto"},
+ "dall-e-2": {"256x256", "512x512", "1024x1024", "auto"},
+ }
+ for prefix, sizes := range validSizes {
+ if strings.HasPrefix(req.Model, prefix) {
+ valid := false
+ for _, s := range sizes {
+ if *req.Size == s {
+ valid = true
+ break
+ }
+ }
+ if !valid {
+ *req.Size = "1024x1024"
+ log.Printf("[WARN] Unsupported size for %s, clamped to 1024x1024", req.Model)
+ }
+ break
+ }
+ }
+ }
+
provider, ok := s.providers[providerName]
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Provider %s not enabled or supported", providerName)})
diff --git a/static/css/dashboard.css b/static/css/dashboard.css
index d88517d0..8f388351 100644
--- a/static/css/dashboard.css
+++ b/static/css/dashboard.css
@@ -1395,8 +1395,150 @@ body {
border: 1px solid var(--bg2);
}
-/* Settings: Warning Card */
+/* Warning Card */
.warning-card {
border: 1px dashed var(--warning);
background: rgba(215, 153, 33, 0.08);
}
+
+/* ==================== MOBILE BAREBONES ==================== */
+@media (max-width: 767px) {
+ .sidebar {
+ transform: translateX(-100%);
+ width: 280px;
+ z-index: 2000;
+ }
+ .sidebar.mobile-visible {
+ transform: translateX(0);
+ }
+ .sidebar.collapsed {
+ width: 280px;
+ }
+ .sidebar.collapsed .menu-item span,
+ .sidebar.collapsed .menu-title,
+ .sidebar.collapsed .user-details {
+ display: block;
+ }
+ .sidebar.collapsed .menu-item {
+ justify-content: flex-start;
+ padding: 0.75rem var(--spacing-lg);
+ }
+ .sidebar.collapsed .menu-item i {
+ font-size: inherit;
+ }
+ .sidebar.collapsed .sidebar-footer {
+ justify-content: space-between;
+ padding: var(--spacing-lg);
+ }
+
+ .sidebar-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.6);
+ z-index: 1999;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s;
+ }
+ .sidebar-backdrop.visible {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .main-content {
+ padding-left: 0 !important;
+ }
+
+ .mobile-menu-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg1);
+ border: 1px solid var(--bg3);
+ color: var(--fg1);
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 1rem;
+ flex-shrink: 0;
+ }
+ .mobile-menu-btn:active {
+ background: var(--bg2);
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+ .grid-2 {
+ grid-template-columns: 1fr;
+ }
+ .grid-3 {
+ grid-template-columns: 1fr;
+ }
+
+ .chart-container {
+ height: 280px;
+ }
+
+ .table-container {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .content-body {
+ padding: var(--spacing-md);
+ }
+ .top-bar {
+ padding: 0 var(--spacing-md);
+ gap: 0.5rem;
+ }
+ .top-bar .page-title h2 {
+ font-size: 1rem;
+ }
+ .top-bar-actions {
+ gap: var(--spacing-sm);
+ }
+
+ .status-indicator .status-text {
+ display: none;
+ }
+
+ .monitoring-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .card-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ .card-actions {
+ width: 100%;
+ }
+
+ .login-card {
+ padding: 2rem 1.25rem;
+ }
+
+ .period-selector {
+ overflow-x: auto;
+ flex-wrap: nowrap;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .modal-content {
+ width: 95%;
+ margin: 1rem;
+ }
+}
+
+@media (max-width: 767px) {
+ .mobile-menu-btn { display: inline-flex; }
+}
+@media (min-width: 768px) {
+ .mobile-menu-btn { display: none; }
+ .sidebar-backdrop { display: none; }
+}
+
+/* ponytail: single-breakpoint responsive layer, no card-based table fallback
+ add table→card reflow when tables get too wide to scroll horizontally */
diff --git a/static/index.html b/static/index.html
index e45fe4a6..02546e3b 100644
--- a/static/index.html
+++ b/static/index.html
@@ -4,7 +4,7 @@
GopherGate - Admin Dashboard
-
+
@@ -51,7 +51,7 @@
GopherGate
@@ -135,11 +135,17 @@
+
+
+
+
-
Overview
+ Overview
diff --git a/static/js/dashboard.js b/static/js/dashboard.js
index 7420b681..92227fb2 100644
--- a/static/js/dashboard.js
+++ b/static/js/dashboard.js
@@ -60,23 +60,75 @@ class Dashboard {
const toggleBtn = document.getElementById('sidebar-toggle');
const sidebar = document.querySelector('.sidebar');
const logoutBtn = document.getElementById('logout-btn');
-
+ const backdrop = document.getElementById('sidebar-backdrop');
+ const mobileBtn = document.getElementById('mobile-menu-btn');
+
+ const isMobile = () => window.innerWidth < 768;
+
+ const closeMobileNav = () => {
+ if (sidebar) sidebar.classList.remove('mobile-visible');
+ if (backdrop) backdrop.classList.remove('visible');
+ };
+
+ const toggleMobileNav = () => {
+ if (!sidebar || !backdrop) return;
+ const opening = !sidebar.classList.contains('mobile-visible');
+ sidebar.classList.toggle('mobile-visible', opening);
+ backdrop.classList.toggle('visible', opening);
+ document.body.style.overflow = opening && isMobile() ? 'hidden' : '';
+ };
+
if (toggleBtn && sidebar) {
toggleBtn.onclick = () => {
- sidebar.classList.toggle('collapsed');
- localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
+ if (isMobile()) {
+ toggleMobileNav();
+ } else {
+ sidebar.classList.toggle('collapsed');
+ localStorage.setItem('sidebar_collapsed', sidebar.classList.contains('collapsed'));
+ }
};
-
- if (localStorage.getItem('sidebar_collapsed') === 'true') {
+
+ if (!isMobile() && localStorage.getItem('sidebar_collapsed') === 'true') {
sidebar.classList.add('collapsed');
}
}
+ if (mobileBtn) {
+ mobileBtn.onclick = toggleMobileNav;
+ }
+
+ if (backdrop) {
+ backdrop.onclick = closeMobileNav;
+ }
+
+ // Close sidebar on page navigation (mobile)
+ const menuItems = document.querySelectorAll('.menu-item');
+ menuItems.forEach(item => {
+ const origClick = item.onclick;
+ item.onclick = (e) => {
+ if (isMobile()) closeMobileNav();
+ if (origClick) origClick(e);
+ };
+ });
+
if (logoutBtn) {
logoutBtn.onclick = () => {
+ if (isMobile()) closeMobileNav();
window.authManager.logout();
};
}
+
+ // Handle resize
+ let resizeTimer;
+ window.addEventListener('resize', () => {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(() => {
+ if (!isMobile()) {
+ closeMobileNav();
+ document.body.style.overflow = '';
+ }
+ }, 200);
+ });
}
setupRefresh() {