lets deploy parallel workers to speed up the development process. make the app fetch current settings, app data from android to display in the application drawer. the navigation gestures and other daily use settings like the ones in notification bar should comply with current android settings. help me build an apk ready pwa stack that can be tested on samsung s23 ultra
3a08226 verified | class ArchServerPanel extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: 'open' }); | |
| this.services = [ | |
| { name: 'Plex', port: 32400, status: 'unknown' }, | |
| { name: 'Nextcloud', port: 8080, status: 'unknown' }, | |
| { name: 'Pi-hole', port: 80, status: 'unknown' }, | |
| { name: 'SSH', port: 22, status: 'unknown' }, | |
| { name: 'Home Assistant', port: 8123, status: 'unknown' } | |
| ]; | |
| } | |
| connectedCallback() { | |
| this.render(); | |
| this.checkServices(); | |
| } | |
| render() { | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| width: 100vw; | |
| background: #020617; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .header { | |
| padding: calc(env(safe-area-inset-top, 24px) + 24px) 24px 24px; | |
| border-bottom: 1px solid #1e293b; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .title { | |
| font-size: 18px; | |
| color: #06b6d4; | |
| font-weight: 700; | |
| } | |
| .subtitle { | |
| font-size: 11px; | |
| color: #64748b; | |
| margin-top: 4px; | |
| } | |
| .close-btn { | |
| width: 32px; | |
| height: 32px; | |
| border: 1px solid #334155; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #94a3b8; | |
| cursor: pointer; | |
| background: transparent; | |
| } | |
| .close-btn:active { | |
| background: #334155; | |
| color: #e2e8f0; | |
| } | |
| .content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px; | |
| } | |
| .section { | |
| margin-bottom: 32px; | |
| } | |
| .section-title { | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| color: #475569; | |
| margin-bottom: 16px; | |
| border-bottom: 1px solid #1e293b; | |
| padding-bottom: 8px; | |
| } | |
| .service-item { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 16px; | |
| background: rgba(15, 23, 42, 0.6); | |
| border: 1px solid #1e293b; | |
| border-radius: 12px; | |
| margin-bottom: 12px; | |
| } | |
| .service-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .service-icon { | |
| width: 40px; | |
| height: 40px; | |
| background: rgba(51, 65, 85, 0.5); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #06b6d4; | |
| } | |
| .service-name { | |
| font-size: 14px; | |
| color: #e2e8f0; | |
| margin-bottom: 2px; | |
| } | |
| .service-port { | |
| font-size: 11px; | |
| color: #64748b; | |
| } | |
| .status { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 12px; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| } | |
| .status.online { | |
| color: #22c55e; | |
| } | |
| .status.online .status-dot { | |
| background: #22c55e; | |
| box-shadow: 0 0 8px #22c55e; | |
| } | |
| .status.offline { | |
| color: #ef4444; | |
| } | |
| .status.offline .status-dot { | |
| background: #ef4444; | |
| } | |
| .status.checking { | |
| color: #eab308; | |
| } | |
| .status.checking .status-dot { | |
| background: #eab308; | |
| animation: pulse 1s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| .config-input { | |
| width: 100%; | |
| background: rgba(15, 23, 42, 0.8); | |
| border: 1px solid #334155; | |
| border-radius: 8px; | |
| padding: 12px; | |
| color: #e2e8f0; | |
| font-family: inherit; | |
| font-size: 13px; | |
| margin-top: 8px; | |
| } | |
| .config-input:focus { | |
| outline: none; | |
| border-color: #06b6d4; | |
| } | |
| .save-btn { | |
| width: 100%; | |
| background: #06b6d4; | |
| color: #0f172a; | |
| border: none; | |
| border-radius: 8px; | |
| padding: 12px; | |
| font-family: inherit; | |
| font-weight: 700; | |
| margin-top: 12px; | |
| cursor: pointer; | |
| } | |
| .save-btn:active { | |
| opacity: 0.9; | |
| } | |
| </style> | |
| <div class="header"> | |
| <div> | |
| <div class="title">Server Config</div> | |
| <div class="subtitle">~/homelab/services</div> | |
| </div> | |
| <button class="close-btn" id="close-btn"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="content"> | |
| <div class="section"> | |
| <div class="section-title">Services</div> | |
| <div id="services-list"></div> | |
| </div> | |
| <div class="section"> | |
| <div class="section-title">Configuration</div> | |
| <input type="text" class="config-input" id="server-url" placeholder="http://192.168.1.100:3000"> | |
| <button class="save-btn" id="save-config">Save Configuration</button> | |
| </div> | |
| </div> | |
| `; | |
| this.setupInteractions(); | |
| this.renderServices(); | |
| } | |
| setupInteractions() { | |
| const closeBtn = this.shadowRoot.getElementById('close-btn'); | |
| if (closeBtn) { | |
| closeBtn.addEventListener('click', () => { | |
| this.dispatchEvent(new CustomEvent('toggle-server-panel', { | |
| bubbles: true, | |
| composed: true | |
| })); | |
| }); | |
| } | |
| const saveBtn = this.shadowRoot.getElementById('save-config'); | |
| const urlInput = this.shadowRoot.getElementById('server-url'); | |
| // Load saved config | |
| const saved = localStorage.getItem('serverEndpoint'); | |
| if (saved && urlInput) urlInput.value = saved; | |
| if (saveBtn && urlInput) { | |
| saveBtn.addEventListener('click', () => { | |
| const url = urlInput.value; | |
| localStorage.setItem('serverEndpoint', url); | |
| // Show feedback | |
| saveBtn.textContent = 'Saved!'; | |
| setTimeout(() => { | |
| saveBtn.textContent = 'Save Configuration'; | |
| if (window.launcher) { | |
| window.launcher.serverEndpoint = url; | |
| window.launcher.checkServerStatus(); | |
| } | |
| }, 1000); | |
| }); | |
| } | |
| // Listen for server status updates | |
| window.addEventListener('server-status', (e) => { | |
| this.updateServerStatus(e.detail); | |
| }); | |
| } | |
| renderServices() { | |
| const container = this.shadowRoot.getElementById('services-list'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| this.services.forEach(service => { | |
| const el = document.createElement('div'); | |
| el.className = 'service-item'; | |
| el.innerHTML = ` | |
| <div class="service-info"> | |
| <div class="service-icon"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect> | |
| <rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect> | |
| <line x1="6" y1="6" x2="6.01" y2="6"></line> | |
| <line x1="6" y1="18" x2="6.01" y2="18"></line> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="service-name">${service.name}</div> | |
| <div class="service-port">:${service.port}</div> | |
| </div> | |
| </div> | |
| <div class="status checking" id="status-${service.name}"> | |
| <div class="status-dot"></div> | |
| <span>checking</span> | |
| </div> | |
| `; | |
| container.appendChild(el); | |
| }); | |
| } | |
| checkServices() { | |
| // Simulate checking services (in real use, these would be actual fetch requests) | |
| setTimeout(() => { | |
| this.services.forEach(service => { | |
| const statusEl = this.shadowRoot.getElementById(`status-${service.name}`); | |
| if (statusEl) { | |
| const isOnline = Math.random() > 0.3; // Simulate random status | |
| statusEl.className = `status ${isOnline ? 'online' : 'offline'}`; | |
| statusEl.innerHTML = ` | |
| <div class="status-dot"></div> | |
| <span>${isOnline ? 'online' : 'offline'}</span> | |
| `; | |
| } | |
| }); | |
| }, 1500); | |
| } | |
| updateServerStatus(status) { | |
| // Could update specific UI elements based on main server status | |
| console.log('Server panel received status:', status); | |
| } | |
| } | |
| customElements.define('arch-server-panel', ArchServerPanel); |