Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/app/dev.rs

17.2 KiB, 7 runs

created by r1870400018:945, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::app::constant;
2
3use oxedyne_fe2o3_core::prelude::*;
4
5use std::{
6 fs,
7 path::Path,
8};
9
10
11pub fn setup(app_root: &Path) -> Outcome<String> {
12
13 // Check if this looks like an existing website.
14 let existing_indicators = [
15 "www/public/index.html",
16 "www/src/index.html",
17 "www/public/main.js",
18 "www/src/js",
19 "package.json",
20 ];
21
22 let mut existing_files = Vec::new();
23 for &indicator in &existing_indicators {
24 let path = app_root.join(indicator);
25 if res!(fs::exists(&path)) {
26 existing_files.push(indicator);
27 }
28 }
29
30 if !existing_files.is_empty() {
31 info!("Detected existing website with: {:?}", existing_files);
32 return Ok(fmt!("Existing website detected, skipping dev initialisation. \
33 Found: {}", existing_files.join(", ")));
34 }
35
36 // Only create new structure if nothing exists.
37 for dir in constant::INIT_TREE_HALT.iter() {
38 let dir_path = app_root.join(dir);
39 if res!(fs::exists(&dir_path)) {
40 return Ok(fmt!("Directory {:?} exists, skipping dev setup.", dir_path));
41 }
42 }
43
44 info!("No existing website detected, creating development structure.");
45
46 // Halt initialisation if certain existing directories are detected.
47 for dir in constant::INIT_TREE_HALT.iter() {
48 let dir_path = app_root.join(dir);
49 if res!(fs::exists(&dir_path)) {
50 return Ok(fmt!("{:?} exists, bypassing dev initialisation.", dir_path));
51 }
52 }
53
54 info!("Creating development tree because no existing web code was detected.");
55 for dir in constant::DEV_TREE_CREATE.iter() {
56 let dir_path = app_root.join(dir);
57 res!(fs::create_dir_all(&dir_path));
58 info!(" Created: {}", dir_path.display());
59 }
60
61 // Create HTML files.
62 let html_index = r#"<!DOCTYPE html>
63<html lang="en">
64<head>
65 <meta charset="UTF-8">
66 <meta name="viewport" content="width=device-width, initial-scale=1.0">
67 <title>Database Test Interface</title>
68 <link rel="stylesheet" href="/styles.css">
69</head>
70<body>
71 <div class="container">
72
73 <img src="/assets/img/logo.svg" alt="Hematite Rust Library" height="30"/>
74
75 <h2>Database Test Interface</h2>
76
77 <div class="input-group">
78 <label>Key:</label>
79 <input type="text" id="keyInput" placeholder="Enter key">
80 </div>
81
82 <div class="input-group">
83 <label>Value:</label>
84 <input type="text" id="valueInput" placeholder="Enter value">
85 </div>
86
87 <div class="input-group">
88 <button id="storeBtn">Store</button>
89 <button id="retrieveBtn">Retrieve</button>
90 </div>
91
92 <div id="status"></div>
93 </div>
94
95 <script type="module" src="/bundles/js/main.bundle.js"></script>
96</body>
97</html>"#;
98
99 let html_admin = r#"<!DOCTYPE html>
100<html lang="en">
101<head>
102 <meta charset="UTF-8">
103 <meta name="viewport" content="width=device-width, initial-scale=1.0">
104 <title>Admin Interface</title>
105 <link rel="stylesheet" href="/styles.css">
106</head>
107<body>
108 <div class="container">
109 <img src="/assets/img/logo.svg" alt="Hematite Rust Library" height="30"/>
110 <h2>Admin Interface</h2>
111 <!-- Admin-specific content here -->
112 <div id="status"></div>
113 </div>
114 <script type="module" src="/bundles/js/admin.bundle.js"></script>
115</body>
116</html>
117"#;
118
119 // Create WebSocket utility module.
120 let js_websocket = r#"export class WebSocketManager {
121 constructor(statusCallback) {
122 this.ws = null;
123 this.reconnectAttempts = 0;
124 this.MAX_RECONNECT_ATTEMPTS = 3;
125 this.reconnectTimeout = null;
126 this.statusCallback = statusCallback;
127 }
128
129 connect() {
130 if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
131
132 if (this.reconnectTimeout) {
133 clearTimeout(this.reconnectTimeout);
134 this.reconnectTimeout = null;
135 }
136
137 const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
138 const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
139
140 try {
141 this.ws = new WebSocket(wsUrl);
142
143 this.ws.onopen = () => {
144 console.log('WebSocket connected');
145 this.statusCallback('Connected to server.');
146 this.reconnectAttempts = 0;
147 };
148
149 this.ws.onclose = (event) => {
150 this.ws = null;
151 console.log('WebSocket closed:', event.code, event.reason || '<no reason>');
152
153 // Only attempt reconnect if we haven't exceeded attempts and it wasn't a clean closure
154 if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS && event.code !== 1000) {
155 this.reconnectAttempts++;
156 const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 10000);
157 this.statusCallback(`Connection lost. Retry attempt ${this.reconnectAttempts} in ${delay/1000} seconds...`);
158 this.reconnectTimeout = setTimeout(() => this.connect(), delay);
159 } else {
160 const msg = event.code === 1000
161 ? 'Connection closed normally.'
162 : 'Unable to connect to server. Please refresh the page to try again.';
163 this.statusCallback(msg);
164 }
165 };
166
167 this.ws.onerror = (error) => {
168 console.error('WebSocket error:', error);
169 this.statusCallback('Connection error occurred.');
170 };
171
172 } catch (error) {
173 console.error('Failed to create WebSocket:', error);
174 this.statusCallback('Failed to create connection.');
175 }
176 }
177
178 send(message) {
179 if (this.ws && this.ws.readyState === WebSocket.OPEN) {
180 try {
181 this.ws.send(message);
182 return true;
183 } catch (error) {
184 console.error('Send error:', error);
185 this.statusCallback('Failed to send message.');
186 return false;
187 }
188 }
189 return false;
190 }
191
192 onMessage(callback) {
193 if (this.ws) {
194 this.ws.onmessage = callback;
195 }
196 }
197
198 close() {
199 if (this.reconnectTimeout) {
200 clearTimeout(this.reconnectTimeout);
201 this.reconnectTimeout = null;
202 }
203
204 if (this.ws) {
205 try {
206 if (this.ws.readyState === WebSocket.OPEN) {
207 this.ws.close(1000, 'Normal closure');
208 this.statusCallback('Connection closed cleanly.');
209 }
210 } catch (error) {
211 console.error('Error during WebSocket closure:', error);
212 } finally {
213 this.ws = null;
214 }
215 }
216 }
217}"#;
218
219 // Create main application module.
220 //let js_main_index = r#"import { WebSocketManager } from '../../utils/websocket.mjs';
221 let js_main_index = r#"import { WebSocketManager } from '@utils/websocket.mjs';
222
223class App {
224 constructor() {
225 console.log('App initialising...');
226
227 // Bind methods to instance
228 this.storeData = this.storeData.bind(this);
229 this.retrieveData = this.retrieveData.bind(this);
230 this.showStatus = this.showStatus.bind(this);
231
232 this.wsManager = new WebSocketManager((msg) => {
233 console.log('Status update:', msg);
234 this.showStatus(msg);
235 });
236
237 // Initialize immediately
238 this.init();
239 }
240
241 init() {
242 // Check if DOM is loaded
243 if (document.readyState === 'loading') {
244 document.addEventListener('DOMContentLoaded', () => this.setupAll());
245 } else {
246 this.setupAll();
247 }
248 }
249
250 setupAll() {
251 console.log('Setting up all components...');
252 this.setupWebSocket();
253 this.setupWindowEvents();
254 this.setupButtons();
255 }
256
257 showStatus(message) {
258 console.log('Status message:', message);
259 const status = document.getElementById('status');
260 if (status) {
261 status.textContent = message;
262 } else {
263 console.error('Status element not found.');
264 }
265 }
266
267 setupButtons() {
268 console.log('Setting up buttons...');
269 const storeBtn = document.getElementById('storeBtn');
270 const retrieveBtn = document.getElementById('retrieveBtn');
271
272 if (storeBtn) {
273 console.log('Store button found, adding listener.');
274 storeBtn.onclick = () => {
275 console.log('Store button clicked.');
276 this.storeData();
277 };
278 } else {
279 console.error('Store button not found.');
280 }
281
282 if (retrieveBtn) {
283 console.log('Retrieve button found, adding listener.');
284 retrieveBtn.onclick = () => {
285 console.log('Retrieve button clicked.');
286 this.retrieveData();
287 };
288 } else {
289 console.error('Retrieve button not found.');
290 }
291 }
292
293 setupWebSocket() {
294 console.log('Setting up WebSocket...');
295 this.wsManager.connect();
296 this.wsManager.onMessage((event) => {
297 const response = event.data;
298 console.log('Received WebSocket message:', response);
299
300 if (response.startsWith('data')) {
301 const value = response.split(' ')[1].replace(/['"]/g, '');
302 const valueInput = document.getElementById('valueInput');
303 if (valueInput) {
304 valueInput.value = value;
305 this.showStatus('Data retrieved successfully.');
306 }
307 } else {
308 this.showStatus(`Server response: ${response}`);
309 }
310 });
311 }
312
313 setupWindowEvents() {
314 window.addEventListener('beforeunload', () => {
315 console.log('Window closing, cleaning up...');
316 this.wsManager.close();
317 });
318 }
319
320 storeData() {
321 console.log('storeData called');
322 const keyInput = document.getElementById('keyInput');
323 const valueInput = document.getElementById('valueInput');
324
325 if (!keyInput || !valueInput) {
326 console.error('Input elements not found.');
327 return;
328 }
329
330 const key = keyInput.value.trim();
331 const value = valueInput.value.trim();
332
333 if (!key || !value) {
334 this.showStatus('Both key and value are required');
335 return;
336 }
337
338 const message = `insert (t2|[(str|${key}),(str|${value})])`;
339 console.log('Sending message:', message);
340
341 if (!this.wsManager.send(message)) {
342 console.log('WebSocket not ready, attempting to reconnect...');
343 this.showStatus('Connecting to server...');
344 this.wsManager.connect();
345 setTimeout(() => this.storeData(), 1000);
346 }
347 }
348
349 retrieveData() {
350 console.log('retrieveData called');
351 const keyInput = document.getElementById('keyInput');
352
353 if (!keyInput) {
354 console.error('Key input element not found.');
355 return;
356 }
357
358 const key = keyInput.value.trim();
359
360 if (!key) {
361 this.showStatus('Key is required for retrieval.');
362 return;
363 }
364
365 const message = `get_data (str|${key})`;
366 console.log('Sending message:', message);
367
368 if (!this.wsManager.send(message)) {
369 console.log('WebSocket not ready, attempting to reconnect...');
370 this.showStatus('Connecting to server...');
371 this.wsManager.connect();
372 setTimeout(() => this.retrieveData(), 1000);
373 }
374 }
375}
376
377// Create instance immediately and expose it globally
378console.log('Creating App instance...');
379window.app = new App();"#;
380
381 // Create admin module.
382 let js_admin_index = r#"import { WebSocketManager } from '../../utils/websocket.mjs';
383
384class AdminApp {
385 constructor() {
386 console.log('Admin app initialising...');
387
388 this.showStatus = this.showStatus.bind(this);
389
390 this.wsManager = new WebSocketManager((msg) => {
391 console.log('Status update:', msg);
392 this.showStatus(msg);
393 });
394
395 this.init();
396 }
397
398 init() {
399 if (document.readyState === 'loading') {
400 document.addEventListener('DOMContentLoaded', () => this.setupAll());
401 } else {
402 this.setupAll();
403 }
404 }
405
406 setupAll() {
407 console.log('Setting up admin components...');
408 this.setupWebSocket();
409 this.setupWindowEvents();
410 }
411
412 // Re-use other methods from main App but customize for admin...
413 showStatus(message) {
414 console.log('Status message:', message);
415 const status = document.getElementById('status');
416 if (status) {
417 status.textContent = message;
418 }
419 }
420
421 setupWebSocket() {
422 console.log('Setting up WebSocket...');
423 this.wsManager.connect();
424 this.wsManager.onMessage((event) => {
425 const response = event.data;
426 console.log('Received WebSocket message:', response);
427 this.showStatus(`Server response: ${response}`);
428 });
429 }
430
431 setupWindowEvents() {
432 window.addEventListener('beforeunload', () => {
433 console.log('Window closing, cleaning up...');
434 this.wsManager.close();
435 });
436 }
437}
438
439console.log('Creating AdminApp instance...');
440window.adminApp = new AdminApp();
441"#;
442
443 // Create base SCSS file.
444 let base_styles = r#"// Import components
445@use 'common' as *;
446
447// Base styles
448.container {
449 margin: 20px;
450 padding: 10px;
451}"#;
452
453 // Create common styles.
454 let common_styles = r#"$primary-color: #4CAF50;
455$hover-color: #45a049;
456$border-color: #ccc;
457$status-bg: #f8f8f8;
458
459@mixin button-style {
460 padding: 5px 15px;
461 margin-right: 10px;
462 background: $primary-color;
463 color: white;
464 border: none;
465 cursor: pointer;
466 &:hover {
467 background: $hover-color;
468 }
469}"#;
470
471 // Create input component styles.
472 let input_styles = r#"@use '../common' as *;
473
474.input-group {
475 margin-bottom: 15px;
476
477 label {
478 display: inline-block;
479 width: 50px;
480 margin-right: 10px;
481 }
482
483 input {
484 padding: 5px;
485 margin-right: 10px;
486 border: 1px solid $border-color;
487 width: 200px;
488 }
489
490 button {
491 @include button-style;
492 }
493}"#;
494
495 // Create status component styles.
496 let status_styles = r#"@use '../common' as *;
497
498#status {
499 margin-top: 20px;
500 padding: 10px;
501 background: $status-bg;
502 border-left: 4px solid $primary-color;
503}"#;
504
505 // Create main page styles.
506 let main_styles = r#"@use './common' as *;
507@use 'components/input';
508@use 'components/status';
509
510.container {
511 margin: 20px;
512 padding: 10px;
513}
514"#;
515
516 // Create admin page styles.
517 let admin_styles = r#"@use './common' as *;
518@use 'components/input';
519@use 'components/status';
520
521.container {
522 margin: 20px;
523 padding: 10px;
524}
525"#;
526
527 // Create status component styles.
528 let logo_svg = include_str!("logo.svg");
529
530 // Write all files.
531 let files = [
532 ("www/public/index.html", html_index),
533 ("www/public/admin.html", html_admin),
534 ("www/public/assets/img/logo.svg", logo_svg),
535 ("www/src/js/utils/websocket.mjs", js_websocket),
536 ("www/src/js/pages/main/index.mjs", js_main_index),
537 ("www/src/js/pages/admin/index.mjs", js_admin_index),
538 ("www/src/styles/base.scss", base_styles),
539 ("www/src/styles/_common.scss", common_styles),
540 ("www/src/styles/components/_input.scss", input_styles),
541 ("www/src/styles/components/_status.scss", status_styles),
542 ("www/src/styles/main.scss", main_styles),
543 ("www/src/styles/admin.scss", admin_styles),
544 ];
545
546 for (path, content) in files.iter() {
547 let file_path = app_root.join(path);
548 if let Some(parent) = file_path.parent() {
549 res!(fs::create_dir_all(parent));
550 }
551 res!(fs::write(&file_path, content));
552 info!(" Created: {}", file_path.display());
553 }
554
555 Ok(fmt!(""))
556}
557
558pub fn ensure_compatibility(app_root: &Path) -> Outcome<()> {
559 info!("Ensuring compatibility with existing website structure...");
560
561 // Create any missing directories that Steel needs.
562 let required_dirs = [
563 "tls/dev",
564 "tls/prod",
565 "www/logs",
566 ];
567
568 for dir in &required_dirs {
569 let dir_path = app_root.join(dir);
570 if !dir_path.exists() {
571 res!(fs::create_dir_all(&dir_path));
572 info!("Created required directory: {}", dir);
573 }
574 }
575
576 // Create minimal missing files if needed
577 let index_path = app_root.join("www/public/index.html");
578 if !index_path.exists() {
579 warn!("No index.html found in www/public/. Creating minimal placeholder.");
580 let minimal_html = r#"<!DOCTYPE html>
581<html lang="en">
582<head>
583 <meta charset="UTF-8">
584 <meta name="viewport" content="width=device-width, initial-scale=1.0">
585 <title>Steel Server</title>
586</head>
587<body>
588 <h1>Welcome</h1>
589 <p>Steel server is running. Please add your content to www/public/</p>
590</body>
591</html>"#;
592 res!(fs::write(&index_path, minimal_html));
593 info!("Created minimal index.html");
594 }
595
596 Ok(())
597}