lobstakit-cloud 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * LobstaKit Cloud — Common JavaScript utilities
3
+ */
4
+
5
+ /**
6
+ * Show a toast notification
7
+ * @param {string} message - Message to display
8
+ * @param {string} type - Type: 'success', 'error', 'info', 'warning'
9
+ * @param {number} duration - Duration in ms (default 3000)
10
+ */
11
+ function showToast(message, type = 'info', duration = 3000) {
12
+ const container = document.getElementById('toast-container');
13
+ if (!container) return;
14
+
15
+ const toast = document.createElement('div');
16
+
17
+ const colors = {
18
+ success: 'bg-green-500',
19
+ error: 'bg-red-500',
20
+ warning: 'bg-yellow-500',
21
+ info: 'bg-blue-500'
22
+ };
23
+
24
+ const icons = {
25
+ success: '✓',
26
+ error: '✗',
27
+ warning: '⚠',
28
+ info: 'ℹ'
29
+ };
30
+
31
+ toast.className = `toast flex items-center space-x-3 px-4 py-3 rounded-lg shadow-lg ${colors[type] || colors.info} text-white min-w-64`;
32
+ toast.innerHTML = `
33
+ <span class="font-bold">${icons[type] || icons.info}</span>
34
+ <span>${escapeHtml(message)}</span>
35
+ `;
36
+
37
+ container.appendChild(toast);
38
+
39
+ // Auto-remove
40
+ setTimeout(() => {
41
+ toast.classList.add('hiding');
42
+ setTimeout(() => toast.remove(), 300);
43
+ }, duration);
44
+
45
+ // Click to dismiss
46
+ toast.addEventListener('click', () => {
47
+ toast.classList.add('hiding');
48
+ setTimeout(() => toast.remove(), 300);
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Escape HTML to prevent XSS
54
+ * @param {string} text - Raw text
55
+ * @returns {string} Escaped text
56
+ */
57
+ function escapeHtml(text) {
58
+ const div = document.createElement('div');
59
+ div.textContent = text;
60
+ return div.innerHTML;
61
+ }
62
+
63
+ /**
64
+ * Format a timestamp
65
+ * @param {string|number} timestamp - ISO string or unix timestamp
66
+ * @returns {string} Formatted string
67
+ */
68
+ function formatTime(timestamp) {
69
+ const date = new Date(timestamp);
70
+ return date.toLocaleString();
71
+ }
72
+
73
+ /**
74
+ * Debounce a function
75
+ * @param {Function} func - Function to debounce
76
+ * @param {number} wait - Wait time in ms
77
+ * @returns {Function} Debounced function
78
+ */
79
+ function debounce(func, wait) {
80
+ let timeout;
81
+ return function executedFunction(...args) {
82
+ const later = () => {
83
+ clearTimeout(timeout);
84
+ func(...args);
85
+ };
86
+ clearTimeout(timeout);
87
+ timeout = setTimeout(later, wait);
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Get CSRF token — not used in Express cloud version
93
+ * @returns {null}
94
+ */
95
+ function getCsrfToken() {
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Make an API call with error handling
101
+ * @param {string} url - API endpoint
102
+ * @param {object} options - Fetch options
103
+ * @returns {Promise<object>} Response data
104
+ */
105
+ async function apiCall(url, options = {}) {
106
+ try {
107
+ const headers = {
108
+ 'Content-Type': 'application/json',
109
+ ...options.headers
110
+ };
111
+
112
+ const response = await fetch(url, {
113
+ headers,
114
+ ...options
115
+ });
116
+
117
+ const data = await response.json();
118
+
119
+ if (!response.ok) {
120
+ throw new Error(data.error || `HTTP ${response.status}`);
121
+ }
122
+
123
+ return data;
124
+ } catch (error) {
125
+ console.error(`API Error (${url}):`, error);
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Copy text to clipboard
132
+ * @param {string} text - Text to copy
133
+ */
134
+ async function copyToClipboard(text) {
135
+ try {
136
+ await navigator.clipboard.writeText(text);
137
+ showToast('Copied to clipboard', 'success');
138
+ } catch (error) {
139
+ // Fallback for older browsers
140
+ const textarea = document.createElement('textarea');
141
+ textarea.value = text;
142
+ document.body.appendChild(textarea);
143
+ textarea.select();
144
+ document.execCommand('copy');
145
+ document.body.removeChild(textarea);
146
+ showToast('Copied to clipboard', 'success');
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Format bytes to human readable
152
+ * @param {number} bytes - Number of bytes
153
+ * @returns {string} Formatted string
154
+ */
155
+ function formatBytes(bytes) {
156
+ if (bytes === 0) return '0 B';
157
+ const k = 1024;
158
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
159
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
160
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
161
+ }
162
+
163
+ /**
164
+ * Sleep/delay utility
165
+ * @param {number} ms - Milliseconds to wait
166
+ * @returns {Promise} Resolves after delay
167
+ */
168
+ function sleep(ms) {
169
+ return new Promise(resolve => setTimeout(resolve, ms));
170
+ }
171
+
172
+ /**
173
+ * Toggle password field visibility
174
+ * @param {string} inputId - ID of the input field
175
+ * @param {HTMLElement} btn - The toggle button
176
+ */
177
+ function toggleVisibility(inputId, btn) {
178
+ const input = document.getElementById(inputId);
179
+ if (input.type === 'password') {
180
+ input.type = 'text';
181
+ btn.textContent = 'Hide';
182
+ } else {
183
+ input.type = 'password';
184
+ btn.textContent = 'Show';
185
+ }
186
+ }
187
+
188
+ // Global error handler for unhandled promise rejections
189
+ window.addEventListener('unhandledrejection', (event) => {
190
+ console.error('Unhandled promise rejection:', event.reason);
191
+ // Don't show toast for network errors during polling
192
+ if (!event.reason?.message?.includes('fetch')) {
193
+ showToast('An error occurred', 'error');
194
+ }
195
+ });
196
+
197
+ // Log LobstaKit initialization
198
+ console.log('%c🛠️ LobstaKit Cloud', 'font-size: 16px; font-weight: bold; color: #DC2626;');
@@ -0,0 +1,93 @@
1
+ /**
2
+ * LobstaKit Cloud — Login Page
3
+ */
4
+
5
+ // Check if already authenticated
6
+ async function checkAuth() {
7
+ const token = localStorage.getItem('lobstakit_token');
8
+ if (token) {
9
+ try {
10
+ const res = await fetch('/api/auth/status', {
11
+ headers: { 'Authorization': `Bearer ${token}` }
12
+ });
13
+ const data = await res.json();
14
+ if (data.authenticated) {
15
+ window.location.href = '/manage.html';
16
+ return;
17
+ }
18
+ } catch (e) {
19
+ // Token check failed — continue to login
20
+ }
21
+ // Token expired/invalid
22
+ localStorage.removeItem('lobstakit_token');
23
+ }
24
+
25
+ // Check if setup is complete
26
+ try {
27
+ const statusRes = await fetch('/api/auth/status');
28
+ const status = await statusRes.json();
29
+ if (!status.passwordSet) {
30
+ window.location.href = '/index.html'; // Go to setup
31
+ return;
32
+ }
33
+ // Pre-fill email if stored
34
+ if (status.email) {
35
+ const emailEl = document.getElementById('email');
36
+ if (emailEl) {
37
+ emailEl.value = status.email;
38
+ // Focus password field instead since email is pre-filled
39
+ const pwEl = document.getElementById('password');
40
+ if (pwEl) pwEl.focus();
41
+ }
42
+ }
43
+ } catch (e) {
44
+ // Can't reach server — stay on login page
45
+ }
46
+ }
47
+
48
+ async function login() {
49
+ const email = document.getElementById('email').value.trim();
50
+ const password = document.getElementById('password').value;
51
+ const errorEl = document.getElementById('login-error');
52
+ const btn = document.getElementById('login-btn');
53
+ errorEl.classList.add('hidden');
54
+
55
+ if (!email || !email.includes('@')) {
56
+ errorEl.textContent = 'Please enter a valid email address';
57
+ errorEl.classList.remove('hidden');
58
+ return;
59
+ }
60
+
61
+ if (!password) {
62
+ errorEl.textContent = 'Password is required';
63
+ errorEl.classList.remove('hidden');
64
+ return;
65
+ }
66
+
67
+ btn.disabled = true;
68
+ btn.textContent = 'Signing in...';
69
+
70
+ try {
71
+ const res = await fetch('/api/auth/login', {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ email, password })
75
+ });
76
+ const data = await res.json();
77
+ if (data.status === 'ok') {
78
+ localStorage.setItem('lobstakit_token', data.token);
79
+ window.location.href = '/manage.html';
80
+ } else {
81
+ errorEl.textContent = data.error || 'Login failed';
82
+ errorEl.classList.remove('hidden');
83
+ }
84
+ } catch (e) {
85
+ errorEl.textContent = 'Connection error';
86
+ errorEl.classList.remove('hidden');
87
+ } finally {
88
+ btn.disabled = false;
89
+ btn.textContent = 'Sign In';
90
+ }
91
+ }
92
+
93
+ document.addEventListener('DOMContentLoaded', checkAuth);