← back to Port Viewer
public/index.html
2985 lines
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Port Viewer - Active Ports</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 40px;
animation: fadeIn 0.6s ease-in;
}
h1 {
font-size: 3em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.2em;
opacity: 0.9;
}
.stats {
display: flex;
justify-content: center;
gap: 30px;
margin: 20px 0;
flex-wrap: wrap;
}
.stat-box {
background: rgba(255,255,255,0.2);
backdrop-filter: blur(10px);
padding: 15px 30px;
border-radius: 15px;
color: white;
font-weight: bold;
}
.stat-number {
font-size: 2em;
display: block;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 25px;
animation: fadeIn 0.8s ease-in;
}
.card {
background: white;
border-radius: 20px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
overflow: hidden;
animation: slideUp 0.5s ease-out;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 5px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.card.active-user-app {
border: 3px solid #00b894;
box-shadow: 0 10px 30px rgba(0, 184, 148, 0.3);
}
.card.active-user-app::before {
background: linear-gradient(90deg, #00b894 0%, #00a085 100%);
height: 8px;
}
.card.offline-app {
border: 3px solid #d63031;
background: #ffe5e5;
box-shadow: 0 10px 30px rgba(214, 48, 49, 0.3);
}
.card.offline-app::before {
background: linear-gradient(90deg, #d63031 0%, #c0392b 100%);
height: 8px;
}
.card.loading {
border: 3px solid #0984e3;
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
box-shadow: 0 10px 30px rgba(9, 132, 227, 0.3);
position: relative;
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 0.8s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(9, 132, 227, 0.15);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 15px;
border-radius: 20px;
z-index: 10;
}
.loading-overlay .big-spinner {
width: 60px;
height: 60px;
border: 5px solid rgba(9, 132, 227, 0.2);
border-radius: 50%;
border-top-color: #0984e3;
animation: spin 1s linear infinite;
}
.loading-text {
color: #0984e3;
font-weight: 600;
font-size: 1.2em;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 40px rgba(0,0,0,0.3);
}
.port-number {
font-size: 3em;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.port-info {
margin: 10px 0;
}
.info-label {
font-size: 0.9em;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 5px;
}
.info-value {
font-size: 1.1em;
color: #333;
font-weight: 600;
word-break: break-all;
}
.protocol-badge {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9em;
font-weight: bold;
margin-top: 10px;
}
.protocol-tcp {
background: #e3f2fd;
color: #1976d2;
}
.protocol-tcp6 {
background: #f3e5f5;
color: #7b1fa2;
}
.protocol-udp {
background: #e8f5e9;
color: #388e3c;
}
.loading {
text-align: center;
color: white;
font-size: 1.5em;
margin-top: 50px;
}
.spinner {
border: 4px solid rgba(255,255,255,0.3);
border-top: 4px solid white;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
.refresh-btn {
position: fixed;
bottom: 30px;
right: 30px;
background: white;
color: #667eea;
border: none;
padding: 15px 30px;
border-radius: 50px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
transition: all 0.3s ease;
}
.refresh-btn:hover {
transform: scale(1.1);
box-shadow: 0 8px 30px rgba(0,0,0,0.4);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
background: rgba(255,255,255,0.9);
color: #d32f2f;
padding: 20px;
border-radius: 15px;
text-align: center;
margin: 20px;
}
.app-name {
font-size: 1.3em;
font-weight: bold;
color: #764ba2;
margin: 15px 0;
display: flex;
align-items: center;
gap: 10px;
}
.website-links {
margin: 10px 0;
}
.website-link {
display: inline-block;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
text-decoration: none;
font-size: 0.9em;
margin: 5px 5px 5px 0;
transition: all 0.3s ease;
position: relative;
}
.website-link:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.web-view-container {
margin-top: 15px;
border-top: 2px solid #e0e0e0;
padding-top: 15px;
}
.web-view-toggle {
background: linear-gradient(90deg, #00b894 0%, #00a085 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
margin-bottom: 10px;
width: 100%;
transition: all 0.3s ease;
font-size: 0.9em;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.web-view-toggle:hover {
transform: scale(1.02);
box-shadow: 0 5px 15px rgba(0, 184, 148, 0.4);
}
.web-view-toggle.active {
background: linear-gradient(90deg, #ff6b6b 0%, #ee5a6f 100%);
}
.web-view-iframe-container {
display: block;
width: 100%;
height: 300px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
margin-top: 10px;
position: relative;
background: white;
animation: slideDown 0.3s ease-out;
}
.web-view-iframe-container.active {
display: block;
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
}
to {
opacity: 1;
max-height: 400px;
}
}
.web-view-iframe {
width: 100%;
height: 100%;
border: none;
background: white;
}
.web-view-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
color: #667eea;
}
.web-view-error {
padding: 20px;
text-align: center;
color: #d63031;
font-weight: bold;
}
.fullscreen-btn {
position: absolute;
top: 10px;
right: 10px;
background: rgba(102, 126, 234, 0.9);
color: white;
border: none;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 0.85em;
font-weight: bold;
z-index: 10;
transition: all 0.3s ease;
}
.fullscreen-btn:hover {
background: rgba(102, 126, 234, 1);
transform: scale(1.05);
}
.preview-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 1200px;
height: 80%;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
z-index: 1000;
display: none;
flex-direction: column;
overflow: hidden;
}
.preview-container.active {
display: flex;
}
.preview-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-title {
font-size: 1.5em;
font-weight: bold;
}
.close-preview {
background: rgba(255,255,255,0.3);
border: none;
color: white;
font-size: 1.5em;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
}
.close-preview:hover {
background: rgba(255,255,255,0.5);
transform: rotate(90deg);
}
.preview-iframe {
flex: 1;
border: none;
background: white;
}
.preview-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 999;
display: none;
}
.preview-overlay.active {
display: block;
}
.docker-badge {
background: #e3f2fd;
color: #1976d2;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.8em;
display: inline-block;
margin-top: 5px;
}
.open-icon {
font-size: 0.8em;
opacity: 0.8;
}
.directory-info {
background: #f5f5f5;
padding: 10px;
border-radius: 10px;
margin: 10px 0;
font-size: 0.9em;
color: #666;
font-family: monospace;
word-break: break-all;
}
.restart-btn {
background: linear-gradient(90deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
margin-top: 10px;
width: 100%;
transition: all 0.3s ease;
font-size: 0.9em;
}
.restart-btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(238, 90, 111, 0.4);
}
.restart-btn:active {
transform: scale(0.95);
}
.restart-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.system-badge {
background: #ffeaa7;
color: #d63031;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.8em;
display: inline-block;
margin-top: 5px;
}
.user-app-indicator {
background: #55efc4;
color: #00b894;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.8em;
display: inline-block;
margin-top: 5px;
}
.last-port-info {
background: #dfe6e9;
padding: 8px 12px;
border-radius: 10px;
margin: 5px 0;
font-size: 0.85em;
color: #2d3436;
}
.offline-badge {
background: #d63031;
color: white;
padding: 8px 15px;
border-radius: 15px;
font-size: 0.9em;
display: inline-block;
margin: 10px 0;
font-weight: bold;
}
.offline-app .port-number {
color: #d63031;
opacity: 0.7;
}
.offline-app .info-value,
.offline-app .info-label {
opacity: 0.6;
}
.active-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background: #00b894;
margin-left: 10px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.add-dir-btn {
position: fixed;
bottom: 100px;
right: 30px;
background: linear-gradient(90deg, #00b894 0%, #00a085 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 50px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
box-shadow: 0 5px 20px rgba(0, 184, 148, 0.4);
transition: all 0.3s ease;
}
.add-dir-btn:hover {
transform: scale(1.1);
box-shadow: 0 8px 30px rgba(0, 184, 148, 0.5);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 2000;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 20px;
padding: 40px;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
animation: slideUp 0.3s ease-out;
}
.modal-header {
font-size: 2em;
font-weight: bold;
color: #667eea;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-weight: bold;
color: #333;
margin-bottom: 8px;
font-size: 0.95em;
}
.form-input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 1em;
transition: border-color 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: #667eea;
}
.form-buttons {
display: flex;
gap: 10px;
margin-top: 30px;
}
.btn-submit {
flex: 1;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px;
border-radius: 10px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-submit:hover {
transform: scale(1.05);
}
.btn-cancel {
flex: 1;
background: #ddd;
color: #333;
border: none;
padding: 15px;
border-radius: 10px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-cancel:hover {
background: #ccc;
}
.ask-server-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.ask-header {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.ask-subtitle {
color: #666;
margin-bottom: 20px;
font-size: 0.95em;
}
.ask-input-container {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.ask-input {
flex: 1;
padding: 15px 20px;
border: 2px solid #ddd;
border-radius: 50px;
font-size: 1em;
transition: border-color 0.3s ease;
}
.ask-input:focus {
outline: none;
border-color: #667eea;
}
.ask-btn {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 50px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.ask-btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.ask-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.ask-response {
background: #f8f9fa;
border-radius: 15px;
padding: 20px;
margin-top: 20px;
display: none;
animation: slideUp 0.3s ease-out;
}
.ask-response.show {
display: block;
}
.response-header {
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
font-size: 1.1em;
}
.response-content {
background: white;
padding: 15px;
border-radius: 10px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
color: #333;
}
.response-error {
background: #ffe5e5;
color: #d63031;
padding: 15px;
border-radius: 10px;
font-weight: bold;
}
.quick-queries {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 15px;
}
.quick-query-btn {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
border: 2px solid #667eea;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.85em;
cursor: pointer;
transition: all 0.3s ease;
}
.quick-query-btn:hover {
background: #667eea;
color: white;
}
.directories-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.directories-header {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.dir-header-actions {
display: flex;
gap: 10px;
}
.dir-add-btn {
background: linear-gradient(90deg, #00b894 0%, #00a085 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
font-size: 0.9em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.dir-add-btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(0, 184, 148, 0.4);
}
.dir-scan-btn {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
font-size: 0.9em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.dir-scan-btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.directories-list {
margin-top: 20px;
}
.dir-item {
background: white;
border-radius: 15px;
padding: 20px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
border: 2px solid #e0e0e0;
}
.dir-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.dir-item.active {
border-color: #00b894;
background: linear-gradient(to right, rgba(0, 184, 148, 0.05), white);
}
.dir-item.offline {
border-color: #d63031;
background: linear-gradient(to right, rgba(214, 48, 49, 0.05), white);
}
.dir-info {
flex: 1;
}
.dir-path {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #333;
font-size: 1.1em;
margin-bottom: 5px;
}
.dir-meta {
display: flex;
gap: 15px;
font-size: 0.85em;
color: #666;
margin-bottom: 10px;
}
.dir-iframe-container {
width: 100%;
height: 250px;
border-radius: 8px;
overflow: hidden;
background: white;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
margin-top: 10px;
}
.dir-iframe {
width: 100%;
height: 100%;
border: none;
}
.dir-status {
display: inline-flex;
align-items: center;
gap: 5px;
font-weight: bold;
}
.dir-status.active {
color: #00b894;
}
.dir-status.offline {
color: #d63031;
}
.dir-actions {
display: flex;
gap: 10px;
}
.dir-action-btn {
padding: 8px 16px;
border: none;
border-radius: 10px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.85em;
}
.dir-action-btn.start {
background: linear-gradient(90deg, #00b894 0%, #00a085 100%);
color: white;
}
.dir-action-btn.restart {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
}
.dir-action-btn.delete {
background: #ff6b6b;
color: white;
}
.dir-action-btn.stop {
background: #e74c3c;
color: white;
}
.dir-action-btn.assign {
background: #f39c12;
color: white;
}
.dir-action-btn.open {
background: linear-gradient(90deg, #00b894 0%, #00a085 100%);
color: white;
}
.dir-action-btn:hover {
transform: scale(1.05);
}
.dir-action-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.empty-directories {
text-align: center;
padding: 40px;
color: #999;
}
.status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-dot.active {
background: #00b894;
}
.status-dot.offline {
background: #d63031;
animation: none;
}
.email-settings-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.email-header {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.email-content {
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.email-toggle {
display: flex;
align-items: center;
gap: 10px;
}
.toggle-switch {
position: relative;
width: 60px;
height: 30px;
background: #ccc;
border-radius: 30px;
cursor: pointer;
transition: background 0.3s;
}
.toggle-switch.active {
background: #00b894;
}
.toggle-switch::after {
content: '';
position: absolute;
width: 24px;
height: 24px;
background: white;
border-radius: 50%;
top: 3px;
left: 3px;
transition: left 0.3s;
}
.toggle-switch.active::after {
left: 33px;
}
.email-input-group {
flex: 1;
min-width: 300px;
}
.email-input-group label {
display: block;
font-weight: bold;
color: #333;
margin-bottom: 5px;
font-size: 0.9em;
}
.email-input-group input {
width: 100%;
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 1em;
transition: border-color 0.3s;
}
.email-input-group input:focus {
outline: none;
border-color: #667eea;
}
.save-email-btn {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 25px;
border-radius: 10px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.save-email-btn:hover {
transform: scale(1.05);
}
.email-status {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
border-radius: 10px;
background: #e8f5e9;
color: #2e7d32;
font-weight: bold;
font-size: 0.9em;
}
.email-status.disabled {
background: #ffebee;
color: #c62828;
}
.login-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
}
.login-overlay.hidden {
display: none;
}
.login-box {
background: white;
border-radius: 20px;
padding: 50px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
text-align: center;
max-width: 400px;
width: 90%;
animation: slideUp 0.5s ease-out;
}
.login-logo {
font-size: 4em;
margin-bottom: 20px;
}
.login-title {
font-size: 2em;
font-weight: bold;
color: #667eea;
margin-bottom: 10px;
}
.login-subtitle {
color: #666;
margin-bottom: 30px;
}
.login-input {
width: 100%;
padding: 15px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 1em;
margin-bottom: 20px;
transition: border-color 0.3s;
}
.login-input:focus {
outline: none;
border-color: #667eea;
}
.login-btn {
width: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px;
border-radius: 10px;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.login-btn:hover {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.login-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.login-error {
background: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 10px;
margin-bottom: 20px;
display: none;
}
.login-error.show {
display: block;
}
.logout-btn {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid white;
padding: 10px 20px;
border-radius: 20px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
z-index: 1000;
}
.logout-btn:hover {
background: white;
color: #667eea;
}
.live-domains-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
padding: 30px;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
color: white;
}
.domains-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
margin-top: 20px;
}
.domain-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
gap: 12px;
}
.domain-card:hover {
transform: translateY(-5px);
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.4);
}
.domain-card.live {
border-color: #00b894;
background: rgba(0, 184, 148, 0.2);
}
.domain-status {
width: 12px;
height: 12px;
border-radius: 50%;
background: #00b894;
box-shadow: 0 0 10px #00b894;
animation: pulse 2s infinite;
}
.domain-name {
flex: 1;
font-weight: 600;
font-size: 1.1em;
}
.domain-link {
color: white;
text-decoration: none;
opacity: 0.8;
transition: opacity 0.3s;
}
.domain-link:hover {
opacity: 1;
}
.domain-iframe-container {
width: 100%;
height: 300px;
border-radius: 8px;
overflow: hidden;
background: white;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.domain-iframe {
width: 100%;
height: 100%;
border: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.all-dirs-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.all-dirs-header {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.all-dirs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
margin-top: 20px;
}
.all-dir-card {
background: white;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 15px;
transition: all 0.3s ease;
cursor: pointer;
}
.all-dir-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
border-color: #667eea;
}
.all-dir-card.tracked {
background: linear-gradient(to right, rgba(0, 184, 148, 0.05), white);
border-color: #00b894;
}
.all-dir-name {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #333;
font-size: 0.95em;
margin-bottom: 10px;
word-break: break-all;
}
.all-dir-path {
font-family: 'Courier New', monospace;
font-size: 0.75em;
color: #999;
margin-bottom: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.all-dir-status {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.75em;
font-weight: bold;
margin-bottom: 10px;
}
.all-dir-status.tracked {
background: #e8f5e9;
color: #2e7d32;
}
.all-dir-status.untracked {
background: #fff3cd;
color: #856404;
}
.all-dir-actions {
display: flex;
gap: 5px;
}
.all-dir-btn {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
font-size: 0.8em;
transition: all 0.3s ease;
}
.all-dir-btn:hover {
transform: scale(1.05);
}
.all-dir-btn.add {
background: linear-gradient(90deg, #00b894 0%, #00a085 100%);
color: white;
}
.all-dir-btn.tracked {
background: #e0e0e0;
color: #666;
cursor: default;
}
.loading-dirs {
text-align: center;
padding: 40px;
color: #999;
}
</style>
</head>
<body>
<!-- Login Overlay -->
<div class="login-overlay" id="loginOverlay">
<div class="login-box">
<div class="login-logo">🔒</div>
<div class="login-title">Port Viewer</div>
<div class="login-subtitle">Enter password to access</div>
<div class="login-error" id="loginError">Invalid password</div>
<form onsubmit="handleLogin(event)">
<input
type="password"
class="login-input"
id="loginPassword"
placeholder="Password"
autocomplete="current-password"
required
/>
<button type="submit" class="login-btn" id="loginBtn">
🔓 Login
</button>
</form>
</div>
</div>
<button class="logout-btn" id="logoutBtn" onclick="handleLogout()" style="display: none;">
🚪 Logout
</button>
<div class="container">
<header>
<h1>🚀 Port Viewer</h1>
<p class="subtitle">Active Network Ports & Services</p>
<div class="stats" id="stats"></div>
</header>
<!-- Live Domains Section -->
<div class="live-domains-section">
<div class="all-dirs-header">
<span>🌐 Live Domains</span>
<button class="dir-scan-btn" onclick="loadDomains()">🔄 Refresh</button>
</div>
<div id="domainsContainer">
<div class="loading-dirs">
<div class="spinner"></div>
<p>Loading domains...</p>
</div>
</div>
</div>
<!-- All Server Directories Section -->
<div class="all-dirs-section">
<div class="all-dirs-header">
<span>📂 All Server Directories</span>
<button class="dir-scan-btn" onclick="loadAllServerDirectories()">🔄 Refresh</button>
</div>
<div id="allDirsContainer">
<div class="loading-dirs">
<div class="spinner"></div>
<p>Scanning server directories...</p>
</div>
</div>
</div>
<!-- Email Alert Settings -->
<div class="email-settings-section">
<div class="email-header">📧 Email Alerts</div>
<div class="email-content">
<div class="email-toggle">
<span style="font-weight: bold;">Alerts:</span>
<div class="toggle-switch" id="emailToggle" onclick="toggleEmailAlerts()"></div>
</div>
<div class="email-input-group">
<label>Email Address</label>
<input type="email" id="emailRecipient" placeholder="your@email.com" />
</div>
<button class="save-email-btn" onclick="saveEmailConfig()">💾 Save Settings</button>
<div class="email-status" id="emailStatus">
<span>⏱️ Monitoring every 60 seconds</span>
</div>
</div>
</div>
<!-- Server Directories Section -->
<div class="directories-section">
<div class="directories-header">
<span>📁 Server Directories</span>
<div class="dir-header-actions">
<button class="dir-scan-btn" onclick="scanAllDirectories()">🔍 Scan All</button>
<button class="dir-add-btn" onclick="openAddDirectoryModal()">➕ Add Directory</button>
</div>
</div>
<div class="directories-list" id="directoriesList">
<div class="empty-directories">
Loading directories...
</div>
</div>
</div>
<!-- Ask the Server Section -->
<div class="ask-server-section">
<div class="ask-header">💬 Ask the Server</div>
<div class="ask-subtitle">
👋 Ask me anything about this server! Try: "What's the uptime?", "Show memory usage", "How many ports are open?", or type "help" for more commands.
</div>
<div class="ask-input-container">
<input
type="text"
class="ask-input"
id="askInput"
placeholder="Ask about uptime, memory, CPU, disk, ports, processes..."
onkeypress="if(event.key === 'Enter') askServer()"
/>
<button class="ask-btn" id="askBtn" onclick="askServer()">Send 🚀</button>
</div>
<div class="quick-queries">
<button class="quick-query-btn" onclick="quickQuery('uptime')">⏱️ Uptime</button>
<button class="quick-query-btn" onclick="quickQuery('memory')">💾 Memory</button>
<button class="quick-query-btn" onclick="quickQuery('cpu')">🖥️ CPU</button>
<button class="quick-query-btn" onclick="quickQuery('disk')">💿 Disk</button>
<button class="quick-query-btn" onclick="quickQuery('ports')">🔌 Ports</button>
<button class="quick-query-btn" onclick="quickQuery('processes')">⚙️ Processes</button>
<button class="quick-query-btn" onclick="quickQuery('help')">❓ Help</button>
</div>
<div class="ask-response" id="askResponse"></div>
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Loading ports...</p>
</div>
<div id="error" style="display: none;"></div>
<div class="cards-grid" id="cards"></div>
</div>
<button class="refresh-btn" onclick="loadPorts()">🔄 Refresh</button>
<!-- Add Directory Modal -->
<div class="modal" id="addDirModal">
<div class="modal-content">
<div class="modal-header">📁 Add Directory</div>
<form id="addDirForm" onsubmit="handleAddDirectory(event)">
<div class="form-group">
<label class="form-label">Directory Path *</label>
<input type="text" class="form-input" id="dirPath" placeholder="/root/my-app" required>
</div>
<div class="form-group">
<label class="form-label">Port Number (optional)</label>
<input type="number" class="form-input" id="dirPort" placeholder="3000">
</div>
<div class="form-group">
<label class="form-label">App Name (optional)</label>
<input type="text" class="form-input" id="dirAppName" placeholder="My App">
</div>
<div class="form-buttons">
<button type="button" class="btn-cancel" onclick="closeAddDirectoryModal()">Cancel</button>
<button type="submit" class="btn-submit">Add Directory</button>
</div>
</form>
</div>
</div>
<!-- Assign Port Modal -->
<div class="modal" id="assignPortModal">
<div class="modal-content">
<div class="modal-header">🔢 Assign Port</div>
<form id="assignPortForm" onsubmit="handleAssignPort(event)">
<div class="form-group">
<label class="form-label">Directory</label>
<input type="text" class="form-input" id="assignDirPath" readonly>
</div>
<div class="form-group">
<label class="form-label">Port Number *</label>
<input type="number" class="form-input" id="assignPort" placeholder="3000" required>
</div>
<div class="form-group">
<label class="form-label">App Name (optional)</label>
<input type="text" class="form-input" id="assignAppName" placeholder="My App">
</div>
<div class="form-buttons">
<button type="button" class="btn-cancel" onclick="closeAssignPortModal()">Cancel</button>
<button type="submit" class="btn-submit">Assign Port</button>
</div>
</form>
</div>
</div>
<!-- Scan Directories Modal -->
<div class="modal" id="scanDirModal">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">🔍 All Server Directories</div>
<div id="scanDirList" style="max-height: 400px; overflow-y: auto; margin: 20px 0;">
<div style="text-align: center; padding: 20px;">
<div class="spinner"></div>
<p>Scanning directories...</p>
</div>
</div>
<div class="form-buttons">
<button type="button" class="btn-cancel" onclick="closeScanDirModal()">Close</button>
</div>
</div>
</div>
<div class="preview-overlay" id="previewOverlay" onclick="closePreview()"></div>
<div class="preview-container" id="previewContainer">
<div class="preview-header">
<div class="preview-title" id="previewTitle"></div>
<button class="close-preview" onclick="closePreview()">×</button>
</div>
<iframe class="preview-iframe" id="previewIframe"></iframe>
</div>
<script>
// Authentication Management
let authToken = localStorage.getItem('authToken');
let serverInfo = { serverIp: 'localhost', port: 6000 }; // Default values
// Fetch server info on page load
fetch('/api/server-info')
.then(response => response.json())
.then(info => {
serverInfo = info;
})
.catch(err => console.error('Failed to fetch server info:', err));
// Check if authenticated on page load
if (!authToken) {
document.getElementById('loginOverlay').classList.remove('hidden');
} else {
document.getElementById('loginOverlay').classList.add('hidden');
document.getElementById('logoutBtn').style.display = 'block';
initializeApp();
}
async function handleLogin(event) {
event.preventDefault();
const password = document.getElementById('loginPassword').value;
const btn = document.getElementById('loginBtn');
const errorDiv = document.getElementById('loginError');
btn.disabled = true;
btn.textContent = '🔄 Logging in...';
errorDiv.classList.remove('show');
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ password })
});
const result = await response.json();
if (result.success && result.token) {
authToken = result.token;
localStorage.setItem('authToken', authToken);
document.getElementById('loginOverlay').classList.add('hidden');
document.getElementById('logoutBtn').style.display = 'block';
document.getElementById('loginPassword').value = '';
initializeApp();
} else {
errorDiv.classList.add('show');
btn.disabled = false;
btn.textContent = '🔓 Login';
}
} catch (err) {
errorDiv.textContent = 'Connection error. Please try again.';
errorDiv.classList.add('show');
btn.disabled = false;
btn.textContent = '🔓 Login';
}
}
async function handleLogout() {
if (!confirm('Are you sure you want to logout?')) return;
try {
await fetch('/api/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
} catch (err) {
console.error('Logout error:', err);
}
authToken = null;
localStorage.removeItem('authToken');
document.getElementById('loginOverlay').classList.remove('hidden');
document.getElementById('logoutBtn').style.display = 'none';
}
// Wrapper for authenticated fetch
async function authFetch(url, options = {}) {
if (!options.headers) options.headers = {};
options.headers['Authorization'] = `Bearer ${authToken}`;
const response = await fetch(url, options);
if (response.status === 401) {
// Session expired
authToken = null;
localStorage.removeItem('authToken');
document.getElementById('loginOverlay').classList.remove('hidden');
document.getElementById('logoutBtn').style.display = 'none';
throw new Error('Session expired. Please login again.');
}
return response;
}
function initializeApp() {
loadPorts();
loadDirectories();
loadEmailConfig();
loadDomains();
loadAllServerDirectories();
// Auto-refresh every 2 minutes
setInterval(() => {
loadPorts();
loadDirectories();
loadDomains();
}, 120000);
}
// Load live domains
async function loadDomains() {
const container = document.getElementById('domainsContainer');
container.innerHTML = `
<div class="loading-dirs">
<div class="spinner"></div>
<p>Loading domains...</p>
</div>
`;
try {
const response = await authFetch('/api/domains');
const data = await response.json();
if (data.domains && data.domains.length > 0) {
const grid = document.createElement('div');
grid.className = 'domains-container';
data.domains.forEach(domain => {
const card = document.createElement('div');
card.className = 'domain-card live';
const proxyUrl = `/api/proxy/${encodeURIComponent('https://' + domain)}`;
card.innerHTML = `
<div style="display: flex; align-items: center; gap: 12px;">
<div class="domain-status"></div>
<div class="domain-name">${domain}</div>
<a href="https://${domain}" target="_blank" rel="noopener noreferrer" class="domain-link">🔗</a>
</div>
<div class="domain-iframe-container">
<iframe class="domain-iframe" src="${proxyUrl}" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
</div>
`;
grid.appendChild(card);
});
container.innerHTML = '';
container.appendChild(grid);
} else {
container.innerHTML = `
<div class="loading-dirs">
🌐 No domains configured
</div>
`;
}
} catch (err) {
container.innerHTML = `
<div class="loading-dirs" style="color: #fff3cd;">
❌ Error: ${err.message}
</div>
`;
}
}
// Load all server directories
async function loadAllServerDirectories() {
const container = document.getElementById('allDirsContainer');
container.innerHTML = `
<div class="loading-dirs">
<div class="spinner"></div>
<p>Scanning server directories...</p>
</div>
`;
try {
// Fetch all directories
const scanResponse = await authFetch('/api/scan-directories');
const scanResult = await scanResponse.json();
// Fetch tracked ports to know which are already tracked
const portsResponse = await authFetch('/api/ports');
const ports = await portsResponse.json();
// Fetch directory data to get lastSeen timestamps
const dirDataResponse = await authFetch('/api/directories');
const directoryData = await dirDataResponse.json();
const trackedDirs = new Set();
ports.forEach(port => {
if (port.directory && port.isUserApp) {
trackedDirs.add(port.directory);
}
});
if (scanResult.directories && scanResult.directories.length > 0) {
// Sort directories by most recently accessed
const sortedDirs = scanResult.directories.sort((a, b) => {
const aData = directoryData[a];
const bData = directoryData[b];
// If both have lastSeen, sort by timestamp (most recent first)
if (aData?.lastSeen && bData?.lastSeen) {
return new Date(bData.lastSeen) - new Date(aData.lastSeen);
}
// Tracked directories come before untracked
if (aData?.lastSeen) return -1;
if (bData?.lastSeen) return 1;
// Both untracked - maintain alphabetical order
return a.localeCompare(b);
});
const grid = document.createElement('div');
grid.className = 'all-dirs-grid';
sortedDirs.forEach(dir => {
const isTracked = trackedDirs.has(dir);
const dirName = dir.split('/').pop();
const dirData = directoryData[dir];
const lastPort = dirData?.lastPort || 'N/A';
const card = document.createElement('div');
card.className = `all-dir-card ${isTracked ? 'tracked' : ''}`;
card.innerHTML = `
<div class="all-dir-name">📁 ${dirName}</div>
<div class="all-dir-path" title="${dir}">${dir}</div>
<div class="all-dir-status ${isTracked ? 'tracked' : 'untracked'}">
${isTracked ? '✅ Tracked' : '⚠️ Not Tracked'}
</div>
${dirData?.lastPort ? `<div style="font-size: 0.75em; color: #666; margin: 5px 0;">Last Port: ${lastPort}</div>` : ''}
<div class="all-dir-actions">
${isTracked
? '<button class="all-dir-btn tracked" disabled>Already Tracked</button>'
: `<button class="all-dir-btn add" onclick="quickAddDirectory(event, '${dir}', '${dirName}')">➕ Add & Start</button>`
}
</div>
`;
grid.appendChild(card);
});
container.innerHTML = '';
container.appendChild(grid);
} else {
container.innerHTML = `
<div class="loading-dirs">
📂 No Node.js projects found on the server
</div>
`;
}
} catch (err) {
container.innerHTML = `
<div class="loading-dirs" style="color: #d63031;">
❌ Error: ${err.message}
</div>
`;
}
}
async function quickAddDirectory(event, directory, appName) {
// Fetch directory data to check if there's a last port
const dirDataResponse = await authFetch('/api/directories');
const directoryData = await dirDataResponse.json();
const lastPort = directoryData[directory]?.lastPort;
// Suggest an available port if no last port
let suggestedPort = lastPort || '3000';
if (!lastPort) {
try {
const portResponse = await authFetch('/api/suggest-port');
const portData = await portResponse.json();
suggestedPort = portData.port;
} catch (err) {
console.error('Failed to get suggested port:', err);
}
}
const button = event.target;
const card = button.closest('.all-dir-card');
const originalButtonHTML = button.innerHTML;
const port = prompt(`Enter port number for ${appName} (suggested: ${suggestedPort}):`, suggestedPort);
if (!port) return;
// Start loading spinner IMMEDIATELY after OK is clicked
button.disabled = true;
button.innerHTML = '⏳ Adding<span class="loading-spinner"></span>';
card.classList.add('loading');
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="big-spinner"></div>
<div class="loading-text">🚀 Adding & starting ${appName}...</div>
`;
card.appendChild(overlay);
try {
// Add the directory
overlay.querySelector('.loading-text').textContent = `📝 Adding ${appName}...`;
const addResponse = await authFetch('/api/add-directory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
directory: directory,
port: port,
appName: appName
})
});
const addResult = await addResponse.json();
if (addResult.success) {
// Now start it
overlay.querySelector('.loading-text').textContent = `🚀 Starting ${appName}...`;
button.innerHTML = '⏳ Starting<span class="loading-spinner"></span>';
const startResponse = await authFetch(`/api/restart/${encodeURIComponent(directory)}`);
const startResult = await startResponse.json();
if (startResult.success) {
overlay.querySelector('.loading-text').textContent = `✅ ${appName} started successfully!`;
button.innerHTML = '✅ Started!';
button.style.background = '#00b894';
// ALWAYS open the app
setTimeout(() => {
const url = `http://${serverInfo.serverIp}:${port}`;
window.open(url, '_blank');
}, 1500);
// Reload everything
setTimeout(() => {
card.classList.remove('loading');
overlay.remove();
loadAllServerDirectories();
loadDirectories();
loadPorts();
}, 3000);
} else {
overlay.querySelector('.loading-text').textContent = `❌ Failed to start`;
button.innerHTML = '❌ Failed';
button.style.background = '#d63031';
setTimeout(() => {
card.classList.remove('loading');
overlay.remove();
button.innerHTML = originalButtonHTML;
button.style.background = '';
button.disabled = false;
}, 3000);
alert(`✅ ${appName} added but failed to start: ${startResult.message || startResult.error}`);
}
} else {
card.classList.remove('loading');
overlay.remove();
button.innerHTML = originalButtonHTML;
button.disabled = false;
alert(`❌ Error: ${addResult.error}`);
}
} catch (err) {
card.classList.remove('loading');
if (overlay && overlay.parentNode) overlay.remove();
button.innerHTML = originalButtonHTML;
button.disabled = false;
alert(`❌ Error: ${err.message}`);
}
}
async function loadPorts() {
const loading = document.getElementById('loading');
const cards = document.getElementById('cards');
const error = document.getElementById('error');
const stats = document.getElementById('stats');
loading.style.display = 'block';
cards.innerHTML = '';
error.style.display = 'none';
try {
const response = await authFetch('/api/ports');
const ports = await response.json();
loading.style.display = 'none';
if (ports.error) {
error.innerHTML = `<div class="error">${ports.error}</div>`;
error.style.display = 'block';
return;
}
// Display stats
const tcpCount = ports.filter(p => p.protocol.includes('TCP')).length;
const udpCount = ports.filter(p => p.protocol.includes('UDP')).length;
stats.innerHTML = `
<div class="stat-box">
<span class="stat-number">${ports.length}</span>
<div>Total Ports</div>
</div>
<div class="stat-box">
<span class="stat-number">${tcpCount}</span>
<div>TCP</div>
</div>
<div class="stat-box">
<span class="stat-number">${udpCount}</span>
<div>UDP</div>
</div>
`;
// Display cards
ports.forEach((port, index) => {
const card = document.createElement('div');
card.className = 'card';
card.style.animationDelay = `${index * 0.05}s`;
// Add special classes for user apps
if (port.isUserApp && port.directory) {
if (port.isOffline) {
card.className += ' offline-app';
} else {
card.className += ' active-user-app';
}
}
const protocolClass = `protocol-${port.protocol.toLowerCase()}`;
let websiteLinksHTML = '';
let webViewHTML = '';
if (port.websites && port.websites.length > 0) {
websiteLinksHTML = '<div class="website-links">';
const firstWebsite = port.websites[0];
const firstUrl = firstWebsite.includes('localhost') || firstWebsite.includes('127.0.0.1')
? `http://localhost:${port.port}`
: `https://${firstWebsite}`;
port.websites.forEach(website => {
const url = website.includes('localhost') || website.includes('127.0.0.1')
? `http://localhost:${port.port}`
: `https://${website}`;
websiteLinksHTML += `
<a href="${url}" class="website-link" target="_blank" rel="noopener noreferrer"
onmouseover="showPreview('${url}', '${website}')"
onmouseout="hidePreviewDelayed()">
${website} <span class="open-icon">↗</span>
</a>
`;
});
websiteLinksHTML += '</div>';
// Add web view container for the first website - always visible
// Use direct URL for better CSS loading, fallback to proxy if needed
const directUrl = firstUrl;
const proxyUrl = `/api/proxy/${encodeURIComponent(firstUrl)}`;
webViewHTML = `
<div class="web-view-container">
<div class="web-view-iframe-container active" id="webview-${port.port}">
<button class="fullscreen-btn" onclick="openFullscreen('${firstUrl}', '${firstWebsite}')">⛶ Fullscreen</button>
<iframe class="web-view-iframe" id="iframe-${port.port}" src="${directUrl}" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
</div>
</div>
`;
}
let dockerHTML = '';
if (port.docker) {
dockerHTML = `<div class="docker-badge">🐳 ${port.docker.image}</div>`;
}
let directoryHTML = '';
let restartButtonHTML = '';
let appTypeHTML = '';
let statusHTML = '';
if (port.isUserApp && port.directory) {
if (port.isOffline) {
appTypeHTML = '<div class="offline-badge">❌ OFFLINE</div>';
statusHTML = `<div class="last-port-info">Last Port: ${port.port}</div>`;
restartButtonHTML = `<button class="restart-btn" onclick="restartApp('${port.directory}', '${port.port}')">🔄 Start App</button>`;
} else {
appTypeHTML = '<span class="user-app-indicator">👤 User App <span class="active-indicator"></span></span>';
restartButtonHTML = `<button class="restart-btn" onclick="restartApp('${port.directory}', '${port.port}')">🔄 Restart App</button>`;
}
directoryHTML = `<div class="directory-info">📁 ${port.directory}</div>`;
} else if (!port.isUserApp && port.port) {
const sysPort = parseInt(port.port);
if ([22, 53, 80, 443, 111, 123, 514].includes(sysPort)) {
appTypeHTML = '<span class="system-badge">⚙️ System Service</span>';
}
}
let protocolBadge = port.protocol !== 'N/A' ? `<span class="protocol-badge ${protocolClass}">${port.protocol}</span>` : '';
card.innerHTML = `
<div class="port-number">:${port.port}</div>
${port.appName ? `<div class="app-name">📱 ${port.appName}</div>` : ''}
${appTypeHTML}
${statusHTML}
${websiteLinksHTML}
${webViewHTML}
${dockerHTML}
${directoryHTML}
<div class="port-info">
<div class="info-label">Address</div>
<div class="info-value">${port.address}</div>
</div>
<div class="port-info">
<div class="info-label">Process</div>
<div class="info-value">${port.process}</div>
</div>
${protocolBadge}
${restartButtonHTML}
`;
cards.appendChild(card);
});
if (ports.length === 0) {
cards.innerHTML = '<div class="error">No listening ports found</div>';
}
} catch (err) {
loading.style.display = 'none';
error.innerHTML = `<div class="error">Failed to load ports: ${err.message}</div>`;
error.style.display = 'block';
}
}
// Email configuration functions
async function loadEmailConfig() {
try {
const response = await authFetch('/api/email-config');
const config = await response.json();
document.getElementById('emailRecipient').value = config.recipient || '';
const toggle = document.getElementById('emailToggle');
const status = document.getElementById('emailStatus');
if (config.enabled) {
toggle.classList.add('active');
status.classList.remove('disabled');
status.innerHTML = '<span>✅ Alerts Enabled - Monitoring every 60 seconds</span>';
} else {
toggle.classList.remove('active');
status.classList.add('disabled');
status.innerHTML = '<span>❌ Alerts Disabled</span>';
}
} catch (err) {
console.error('Failed to load email config:', err);
}
}
function toggleEmailAlerts() {
const toggle = document.getElementById('emailToggle');
toggle.classList.toggle('active');
}
async function saveEmailConfig() {
const enabled = document.getElementById('emailToggle').classList.contains('active');
const recipient = document.getElementById('emailRecipient').value.trim();
if (!recipient) {
alert('Please enter an email address');
return;
}
try {
const response = await authFetch('/api/email-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled, recipient })
});
const result = await response.json();
if (result.success) {
alert('✅ Email settings saved successfully!');
loadEmailConfig();
} else {
alert(`❌ Error: ${result.error}`);
}
} catch (err) {
alert(`❌ Error: ${err.message}`);
}
}
// Load and display directories
async function loadDirectories() {
try {
const response = await authFetch('/api/ports');
const ports = await response.json();
// Extract unique directories and their status
const directories = {};
ports.forEach(port => {
if (port.directory && port.isUserApp) {
directories[port.directory] = {
path: port.directory,
appName: port.appName || path.basename(port.directory),
port: port.port,
isActive: !port.isOffline,
process: port.process,
lastSeen: new Date().toISOString()
};
}
});
displayDirectories(directories);
} catch (err) {
console.error('Failed to load directories:', err);
}
}
function displayDirectories(directories) {
const container = document.getElementById('directoriesList');
const dirArray = Object.values(directories);
if (dirArray.length === 0) {
container.innerHTML = `
<div class="empty-directories">
📂 No directories tracked yet. Click "Add Directory" to add one!
</div>
`;
return;
}
container.innerHTML = dirArray.map(dir => {
const statusClass = dir.isActive ? 'active' : 'offline';
const statusText = dir.isActive ? 'ONLINE' : 'OFFLINE';
let actionButtons = '';
if (dir.isActive) {
actionButtons = `
<button class="dir-action-btn open" onclick="openApp('${dir.port}')">🌐 Open</button>
<button class="dir-action-btn restart" onclick="restartDirectory('${dir.path}', '${dir.port}')">🔄 Restart</button>
<button class="dir-action-btn stop" onclick="stopPort('${dir.port}')">⏹️ Stop</button>
`;
} else {
actionButtons = `
<button class="dir-action-btn start" onclick="restartDirectory('${dir.path}', '${dir.port}')">▶️ Start</button>
`;
}
const iframeHTML = dir.isActive ? `
<div class="dir-iframe-container">
<iframe class="dir-iframe" src="/api/proxy/${encodeURIComponent('http://' + serverInfo.serverIp + ':' + dir.port)}" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
</div>
` : '';
return `
<div class="dir-item ${statusClass}">
<div class="dir-info">
<div class="dir-path">📁 ${dir.path}</div>
<div class="dir-meta">
<span class="dir-status ${statusClass}">
<span class="status-dot ${statusClass}"></span>
${statusText}
</span>
<span>Port: ${dir.port}</span>
<span>App: ${dir.appName}</span>
</div>
${iframeHTML}
</div>
<div class="dir-actions">
${actionButtons}
<button class="dir-action-btn assign" onclick="openAssignPortModal('${dir.path}', '${dir.port}', '${dir.appName}')">🔢 Assign</button>
<button class="dir-action-btn delete" onclick="deleteDirectory('${dir.path}')">🗑️ Remove</button>
</div>
</div>
`;
}).join('');
}
async function deleteDirectory(directory) {
if (!confirm(`Are you sure you want to remove ${directory} from tracking?\n\nThis won't delete the actual directory, just stop tracking it.`)) {
return;
}
try {
const response = await authFetch(`/api/delete-directory/${encodeURIComponent(directory)}`);
const result = await response.json();
if (result.success) {
loadDirectories();
loadPorts();
} else {
alert(`❌ Error: ${result.error}`);
}
} catch (err) {
alert(`❌ Error: ${err.message}`);
}
}
async function restartDirectory(directory, port) {
if (!confirm(`Are you sure you want to restart the app in ${directory}?`)) {
return;
}
const button = event.target;
const originalText = button.textContent;
button.disabled = true;
button.innerHTML = 'Starting<span class="loading-spinner"></span>';
try {
const response = await authFetch(`/api/restart/${encodeURIComponent(directory)}`);
const result = await response.json();
if (result.success) {
button.innerHTML = '✅ Started!';
button.style.background = '#00b894';
// Always open the URL in a new tab
const portToOpen = result.lastPort || port;
if (portToOpen) {
setTimeout(() => {
const url = `http://${serverInfo.serverIp}:${portToOpen}`;
window.open(url, '_blank');
}, 1500);
}
// Reload directories after a delay
setTimeout(() => {
loadDirectories();
loadPorts();
button.textContent = originalText;
button.style.background = '';
button.disabled = false;
}, 3000);
} else {
button.textContent = '❌ Failed';
button.style.background = '#d63031';
setTimeout(() => {
button.textContent = originalText;
button.style.background = '';
button.disabled = false;
}, 3000);
alert(`❌ Failed to restart: ${result.error || result.message}`);
}
} catch (err) {
button.textContent = '❌ Error';
button.disabled = false;
setTimeout(() => {
button.textContent = originalText;
}, 3000);
alert(`❌ Error: ${err.message}`);
}
}
function openApp(port) {
const url = `http://${serverInfo.serverIp}:${port}`;
window.open(url, '_blank');
}
async function stopPort(port) {
if (!confirm(`Are you sure you want to stop the process on port ${port}?`)) {
return;
}
try {
const response = await authFetch(`/api/stop-port/${port}`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
alert(`✅ Stopped process on port ${port}`);
loadDirectories();
loadPorts();
} else {
alert(`❌ Error: ${result.error || 'Failed to stop process'}`);
}
} catch (err) {
alert(`❌ Error: ${err.message}`);
}
}
function openAssignPortModal(directory, currentPort, appName) {
document.getElementById('assignDirPath').value = directory;
document.getElementById('assignPort').value = currentPort || '';
document.getElementById('assignAppName').value = appName || '';
document.getElementById('assignPortModal').classList.add('active');
}
function closeAssignPortModal() {
document.getElementById('assignPortModal').classList.remove('active');
document.getElementById('assignPortForm').reset();
}
async function handleAssignPort(event) {
event.preventDefault();
const directory = document.getElementById('assignDirPath').value;
const port = document.getElementById('assignPort').value;
const appName = document.getElementById('assignAppName').value.trim();
try {
const response = await authFetch('/api/assign-port', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
directory: directory,
port: port,
appName: appName || undefined
})
});
const result = await response.json();
if (result.success) {
closeAssignPortModal();
alert(`✅ Port ${port} assigned to ${directory}`);
loadDirectories();
loadPorts();
} else {
alert(`❌ Error: ${result.error}`);
}
} catch (err) {
alert(`❌ Error: ${err.message}`);
}
}
async function scanAllDirectories() {
document.getElementById('scanDirModal').classList.add('active');
document.getElementById('scanDirList').innerHTML = `
<div style="text-align: center; padding: 20px;">
<div class="spinner"></div>
<p>Scanning directories...</p>
</div>
`;
try {
const response = await authFetch('/api/scan-directories');
const result = await response.json();
if (result.directories && result.directories.length > 0) {
const listHTML = result.directories.map(dir => `
<div class="dir-item" style="margin-bottom: 10px;">
<div class="dir-info" style="flex: 1;">
<div class="dir-path" style="font-size: 0.95em;">📁 ${dir}</div>
</div>
<div class="dir-actions">
<button class="dir-action-btn" style="padding: 6px 12px; font-size: 0.8em;" onclick="addScannedDirectory('${dir}')">➕ Add</button>
</div>
</div>
`).join('');
document.getElementById('scanDirList').innerHTML = listHTML;
} else {
document.getElementById('scanDirList').innerHTML = `
<div class="empty-directories">
No Node.js projects found
</div>
`;
}
} catch (err) {
document.getElementById('scanDirList').innerHTML = `
<div class="error">Error: ${err.message}</div>
`;
}
}
function closeScanDirModal() {
document.getElementById('scanDirModal').classList.remove('active');
}
async function addScannedDirectory(directory) {
// Suggest an available port
let suggestedPort = '3000';
try {
const portResponse = await authFetch('/api/suggest-port');
const portData = await portResponse.json();
suggestedPort = portData.port;
} catch (err) {
console.error('Failed to get suggested port:', err);
}
const port = prompt(`Enter port number for ${directory} (suggested: ${suggestedPort}):`, suggestedPort);
if (!port) return;
try {
const response = await authFetch('/api/add-directory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
directory: directory,
port: port,
appName: directory.split('/').pop()
})
});
const result = await response.json();
if (result.success) {
alert(`✅ Directory added: ${directory}`);
closeScanDirModal();
loadDirectories();
loadPorts();
} else {
alert(`❌ Error: ${result.error}`);
}
} catch (err) {
alert(`❌ Error: ${err.message}`);
}
}
// Close modals when clicking outside
document.getElementById('assignPortModal').addEventListener('click', (e) => {
if (e.target.id === 'assignPortModal') {
closeAssignPortModal();
}
});
document.getElementById('scanDirModal').addEventListener('click', (e) => {
if (e.target.id === 'scanDirModal') {
closeScanDirModal();
}
});
// Ask the Server functionality
async function askServer() {
const input = document.getElementById('askInput');
const btn = document.getElementById('askBtn');
const responseDiv = document.getElementById('askResponse');
const query = input.value.trim();
if (!query) return;
btn.disabled = true;
btn.textContent = 'Thinking...';
responseDiv.innerHTML = '';
responseDiv.classList.remove('show');
try {
const response = await authFetch('/api/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.success) {
responseDiv.innerHTML = `
<div class="response-header">📊 ${result.query || query}</div>
<div class="response-content">${result.response}</div>
`;
} else {
responseDiv.innerHTML = `
<div class="response-error">❌ ${result.error}</div>
`;
}
responseDiv.classList.add('show');
} catch (err) {
responseDiv.innerHTML = `
<div class="response-error">❌ Error: ${err.message}</div>
`;
responseDiv.classList.add('show');
} finally {
btn.disabled = false;
btn.textContent = 'Send 🚀';
}
}
function quickQuery(type) {
const input = document.getElementById('askInput');
const queries = {
'uptime': 'What is the uptime?',
'memory': 'Show memory usage',
'cpu': 'Show CPU usage',
'disk': 'Show disk usage',
'ports': 'Show open ports',
'processes': 'Show top processes',
'help': 'help'
};
input.value = queries[type] || type;
askServer();
}
// Preview functionality
let previewTimeout = null;
let hideTimeout = null;
function showPreview(url, title) {
clearTimeout(hideTimeout);
previewTimeout = setTimeout(() => {
const overlay = document.getElementById('previewOverlay');
const container = document.getElementById('previewContainer');
const iframe = document.getElementById('previewIframe');
const titleEl = document.getElementById('previewTitle');
titleEl.textContent = title;
iframe.src = url;
overlay.classList.add('active');
container.classList.add('active');
}, 500); // Show preview after 500ms hover
}
function hidePreviewDelayed() {
clearTimeout(previewTimeout);
hideTimeout = setTimeout(() => {
closePreview();
}, 300);
}
function closePreview() {
const overlay = document.getElementById('previewOverlay');
const container = document.getElementById('previewContainer');
const iframe = document.getElementById('previewIframe');
overlay.classList.remove('active');
container.classList.remove('active');
// Clear iframe after animation
setTimeout(() => {
if (!container.classList.contains('active')) {
iframe.src = '';
}
}, 300);
}
// Prevent closing when hovering over preview
document.getElementById('previewContainer').addEventListener('mouseenter', () => {
clearTimeout(hideTimeout);
});
document.getElementById('previewContainer').addEventListener('mouseleave', () => {
closePreview();
});
// Add Directory Modal Functions
function openAddDirectoryModal() {
document.getElementById('addDirModal').classList.add('active');
}
function closeAddDirectoryModal() {
document.getElementById('addDirModal').classList.remove('active');
document.getElementById('addDirForm').reset();
}
async function handleAddDirectory(event) {
event.preventDefault();
const directory = document.getElementById('dirPath').value.trim();
const port = document.getElementById('dirPort').value.trim();
const appName = document.getElementById('dirAppName').value.trim();
try {
const response = await authFetch('/api/add-directory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
directory: directory,
port: port || undefined,
appName: appName || undefined
})
});
const result = await response.json();
if (result.success) {
closeAddDirectoryModal();
alert(`✅ Directory added: ${directory}`);
loadPorts();
loadDirectories();
} else {
alert(`❌ Error: ${result.error}`);
}
} catch (err) {
alert(`❌ Error: ${err.message}`);
}
}
// Close modal when clicking outside
document.getElementById('addDirModal').addEventListener('click', (e) => {
if (e.target.id === 'addDirModal') {
closeAddDirectoryModal();
}
});
// Web View Toggle Functions
function toggleWebView(port, url) {
const container = document.getElementById(`webview-${port}`);
const iframe = document.getElementById(`iframe-${port}`);
const button = event.target.closest('.web-view-toggle');
if (container.classList.contains('active')) {
// Hide the web view
container.classList.remove('active');
button.classList.remove('active');
button.innerHTML = '<span>👁️ Show Live Preview</span>';
iframe.src = ''; // Clear iframe to stop loading
} else {
// Show the web view
container.classList.add('active');
button.classList.add('active');
button.innerHTML = '<span>👁️ Hide Live Preview</span>';
// Load iframe if not already loaded
if (!iframe.src) {
iframe.src = url;
// Hide loading spinner when iframe loads
iframe.onload = function() {
const loading = container.querySelector('.web-view-loading');
if (loading) loading.style.display = 'none';
};
// Handle iframe errors
iframe.onerror = function() {
const loading = container.querySelector('.web-view-loading');
if (loading) {
loading.innerHTML = '<div class="web-view-error">❌ Failed to load preview</div>';
}
};
}
}
}
function openFullscreen(url, title) {
const overlay = document.getElementById('previewOverlay');
const container = document.getElementById('previewContainer');
const iframe = document.getElementById('previewIframe');
const titleEl = document.getElementById('previewTitle');
titleEl.textContent = title;
iframe.src = url;
overlay.classList.add('active');
container.classList.add('active');
}
// Restart app function
async function restartApp(directory, port) {
if (!confirm(`Are you sure you want to restart the app in ${directory}?`)) {
return;
}
const button = event.target;
const card = button.closest('.card');
button.disabled = true;
button.innerHTML = '⏳ Starting<span class="loading-spinner"></span>';
// Add loading state to card
card.classList.add('loading');
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="big-spinner"></div>
<div class="loading-text">🚀 Starting app...</div>
`;
card.appendChild(overlay);
try {
const response = await authFetch(`/api/restart/${encodeURIComponent(directory)}`);
const result = await response.json();
if (result.success) {
overlay.querySelector('.loading-text').textContent = '✅ Started successfully!';
button.innerHTML = '✅ Started!';
button.style.background = 'linear-gradient(90deg, #00b894 0%, #00a085 100%)';
// ALWAYS open the URL in a new tab using server IP
const portToOpen = result.lastPort || port;
if (portToOpen) {
setTimeout(() => {
const url = `http://${serverInfo.serverIp}:${portToOpen}`;
window.open(url, '_blank');
}, 1500);
}
setTimeout(() => {
card.classList.remove('loading');
overlay.remove();
button.textContent = '🔄 Restart App';
button.style.background = 'linear-gradient(90deg, #ff6b6b 0%, #ee5a6f 100%)';
button.disabled = false;
// Reload ports after restart
setTimeout(() => {
loadPorts();
loadDirectories();
}, 2000);
}, 3000);
} else {
overlay.querySelector('.loading-text').textContent = '❌ Failed to start';
button.textContent = '❌ Failed';
button.style.background = '#d63031';
setTimeout(() => {
card.classList.remove('loading');
overlay.remove();
button.textContent = '🔄 Restart App';
button.style.background = 'linear-gradient(90deg, #ff6b6b 0%, #ee5a6f 100%)';
button.disabled = false;
}, 3000);
alert(`Failed to restart: ${result.error || result.message}`);
}
} catch (err) {
card.classList.remove('loading');
if (overlay && overlay.parentNode) overlay.remove();
button.textContent = '❌ Error';
button.disabled = false;
alert(`Error: ${err.message}`);
setTimeout(() => {
button.textContent = '🔄 Restart App';
}, 3000);
}
}
</script>
</body>
</html>