1 import type { Express, RequestHandler } from "express";
2 import { createServer, type Server } from "http";
3 import fs from "fs";
4 import path from "path";
5 import AdmZip from "adm-zip";
6 import { storage } from "./storage";
7 import { insertRepairGuideSchema, insertTroubleshootingFlowSchema, insertDeviceComponentSchema, insertFavoriteSchema } from "@shared/schema";
8 import { TECHNICAL_MANUALS, getManualById, getManualsByCategory } from "../shared/technical-manuals";
9 import { requireAdmin } from "./middleware/adminAuth";
10 import { isAuthenticated } from "./auth";
11 import { upload } from "./middleware/upload";
12 import { runAIDiagnostics, runTechnicalAIChat, type DiagnosticsRequest, type TechnicalChatRequest } from "./ai-diagnostics";
13
14 export async function registerRoutes(app: Express): Promise<Server> {
15 // System Health & Version Connectivity Check Endpoint
16 app.get("/api/health", (_req, res) => {
17 res.json({
18 status: "online",
19 version: "2026.4.2",
20 guidesCount: 624,
21 manualsCount: TECHNICAL_MANUALS.length,
22 timestamp: new Date().toISOString()
23 });
24 });
25
26 app.get("/api/version", (_req, res) => {
27 res.json({
28 version: "2026.4.2",
29 status: "operational",
30 mode: process.env.NODE_ENV || "development",
31 features: {
32 aiAssistant: true,
33 offlineSync: true,
34 totalGuides: 624,
35 totalManuals: 8
36 }
37 });
38 });
39
40 // Technical Manuals Endpoints
41 app.get("/api/technical-manuals", (req, res) => {
42 try {
43 res.setHeader("Cache-Control", "public, max-age=120, stale-while-revalidate=600");
44 const { category } = req.query;
45 if (category && typeof category === "string") {
46 return res.json(getManualsByCategory(category));
47 }
48 res.json(TECHNICAL_MANUALS);
49 } catch (error) {
50 console.error("Error fetching technical manuals:", error);
51 res.status(500).json({ message: "Failed to fetch technical manuals" });
52 }
53 });
54
55 app.get("/api/technical-manuals/:id", (req, res) => {
56 try {
57 const { id } = req.params;
58 const manual = getManualById(id);
59 if (!manual) {
60 return res.status(404).json({ message: "Technical manual not found" });
61 }
62 res.json(manual);
63 } catch (error) {
64 console.error("Error fetching manual by id:", error);
65 res.status(500).json({ message: "Failed to fetch technical manual" });
66 }
67 });
68
69 // Technical AI Conversational Chat Endpoint (Grounding in 624 Guides & Manuals)
70 app.post("/api/ai/chat", async (req, res) => {
71 try {
72 const { messages, deviceContext, categoryContext } = req.body;
73 if (!messages || !Array.isArray(messages) || messages.length === 0) {
74 return res.status(400).json({ message: "Valid messages array is required." });
75 }
76
77 const chatPayload: TechnicalChatRequest = {
78 messages,
79 deviceContext,
80 categoryContext
81 };
82
83 const reply = await runTechnicalAIChat(chatPayload);
84 res.json(reply);
85 } catch (error) {
86 console.error("Technical AI Chat Error:", error);
87 res.status(500).json({ message: "An unexpected error occurred during AI chat." });
88 }
89 });
90
91 // AI Diagnostics Assistant Endpoint
92 app.post("/api/ai/troubleshoot", async (req, res) => {
93 try {
94 const { symptoms, deviceType, brand, model, os, category, errorCode } = req.body;
95 if (!symptoms || typeof symptoms !== "string" || symptoms.trim().length === 0) {
96 return res.status(400).json({ message: "Symptoms or issue description is required." });
97 }
98
99 const diagnosticPayload: DiagnosticsRequest = {
100 symptoms: symptoms.trim(),
101 deviceType: deviceType || "all",
102 brand: brand || "",
103 model: model || "",
104 os: os || "",
105 category: category || "all",
106 errorCode: errorCode || ""
107 };
108
109 const result = await runAIDiagnostics(diagnosticPayload);
110 res.json(result);
111 } catch (error) {
112 console.error("AI Diagnostics Endpoint Error:", error);
113 res.status(500).json({ message: "An unexpected error occurred during diagnostics." });
114 }
115 });
116
117 // Auth & Profile routes
118 app.get('/api/auth/user', isAuthenticated, (req, res) => {
119 res.json(req.user);
120 });
121
122 app.patch('/api/user/profile', isAuthenticated, async (req, res) => {
123 try {
124 const updatedUser = await storage.updateUser(req.user!.id, req.body);
125 res.json(updatedUser);
126 } catch (error) {
127 res.status(500).json({ message: "Failed to update profile" });
128 }
129 });
130
131 app.post('/api/user/profile/photo', isAuthenticated, upload.single('photo'), async (req, res) => {
132 try {
133 if (!req.file) {
134 return res.status(400).json({ message: "No file uploaded" });
135 }
136
137 const photoUrl = `/uploads/profiles/${req.file.filename}`;
138 const updatedUser = await storage.updateUser(req.user!.id, { profileImageUrl: photoUrl });
139 res.json(updatedUser);
140 } catch (error) {
141 res.status(500).json({ message: "Failed to upload photo" });
142 }
143 });
144
145 // Favorites routes
146 app.get('/api/favorites', isAuthenticated, async (req: any, res) => {
147 try {
148 const userId = req.user.id;
149 const favorites = await storage.getUserFavorites(userId);
150 res.json(favorites);
151 } catch (error) {
152 console.error("Error fetching favorites:", error);
153 res.status(500).json({ message: "Failed to fetch favorites" });
154 }
155 });
156
157 app.post('/api/favorites', isAuthenticated, async (req: any, res) => {
158 try {
159 const userId = req.user.id;
160 const favoriteData = insertFavoriteSchema.parse(req.body);
161 const favorite = await storage.addToFavorites(userId, favoriteData);
162 res.json(favorite);
163 } catch (error) {
164 console.error("Error adding favorite:", error);
165 res.status(500).json({ message: "Failed to add favorite" });
166 }
167 });
168
169 app.delete('/api/favorites/:itemId/:itemType', isAuthenticated, async (req: any, res) => {
170 try {
171 const userId = req.user.id;
172 const { itemId, itemType } = req.params;
173 await storage.removeFromFavorites(userId, itemId, itemType);
174 res.json({ success: true });
175 } catch (error) {
176 console.error("Error removing favorite:", error);
177 res.status(500).json({ message: "Failed to remove favorite" });
178 }
179 });
180
181 app.get('/api/favorites/check/:itemId/:itemType', isAuthenticated, async (req: any, res) => {
182 try {
183 const userId = req.user.id;
184 const { itemId, itemType } = req.params;
185 const isFavorited = await storage.isFavorited(userId, itemId, itemType);
186 res.json({ isFavorited });
187 } catch (error) {
188 console.error("Error checking favorite:", error);
189 res.status(500).json({ message: "Failed to check favorite status" });
190 }
191 });
192 // Repair Guides
193 app.get("/api/repair-guides", async (req, res) => {
194 try {
195 res.setHeader("Cache-Control", "public, max-age=60, stale-while-revalidate=300");
196 const { deviceType, category, difficulty } = req.query;
197 const guides = await storage.getRepairGuides({
198 deviceType: deviceType as string,
199 category: category as string,
200 difficulty: difficulty as string,
201 });
202 res.json(guides);
203 } catch (error) {
204 res.status(500).json({ message: "Failed to fetch repair guides" });
205 }
206 });
207
208 app.get("/api/repair-guides/search", async (req, res) => {
209 try {
210 const { q } = req.query;
211 if (!q || typeof q !== "string") {
212 return res.status(400).json({ message: "Search query is required" });
213 }
214 const guides = await storage.searchRepairGuides(q);
215 res.json(guides);
216 } catch (error) {
217 res.status(500).json({ message: "Failed to search repair guides" });
218 }
219 });
220
221 app.get("/api/repair-guides/:id", async (req, res) => {
222 try {
223 const guide = await storage.getRepairGuide(req.params.id);
224 if (!guide) {
225 return res.status(404).json({ message: "Repair guide not found" });
226 }
227 // Increment view count when guide is viewed
228 await storage.updateViewCount(req.params.id);
229 res.json(guide);
230 } catch (error) {
231 res.status(500).json({ message: "Failed to fetch repair guide" });
232 }
233 });
234
235 app.post("/api/repair-guides", async (req, res) => {
236 try {
237 const validatedData = insertRepairGuideSchema.parse(req.body);
238 const guide = await storage.createRepairGuide(validatedData);
239 res.status(201).json(guide);
240 } catch (error) {
241 res.status(400).json({ message: "Invalid repair guide data" });
242 }
243 });
244
245 // User-created guides endpoint
246 app.post("/api/user-guides", async (req, res) => {
247 try {
248 const validatedData = insertRepairGuideSchema.parse(req.body);
249 const guide = await storage.createUserGuide(validatedData);
250 res.status(201).json(guide);
251 } catch (error) {
252 console.error("Error creating user guide:", error);
253 res.status(400).json({ message: "Invalid user guide data" });
254 }
255 });
256
257 // Track download for a specific guide
258 app.post("/api/repair-guides/:id/download", async (req, res) => {
259 try {
260 const guide = await storage.getRepairGuide(req.params.id);
261 if (!guide) {
262 return res.status(404).json({ message: "Repair guide not found" });
263 }
264 // Increment download count when guide is downloaded
265 await storage.updateDownloadCount(req.params.id);
266 res.json({ success: true, message: "Download tracked" });
267 } catch (error) {
268 res.status(500).json({ message: "Failed to track download" });
269 }
270 });
271
272 // Version endpoint
273 app.get("/api/version", async (req, res) => {
274 try {
275 const versionInfo = {
276 version: "1.0.0",
277 name: "JCRGuru",
278 build: "2025.08.18",
279 release: "Major Release"
280 };
281 res.json(versionInfo);
282 } catch (error) {
283 res.status(500).json({ message: "Failed to fetch version info" });
284 }
285 });
286
287 // Admin routes
288 app.get("/api/admin/users", requireAdmin, async (req, res) => {
289 try {
290 res.json({ message: "Admin access confirmed", adminEmail: req.adminUser?.email });
291 } catch (error) {
292 res.status(500).json({ message: "Failed to fetch admin data" });
293 }
294 });
295
296 app.get("/api/admin/stats", requireAdmin, async (req, res) => {
297 try {
298 const guides = await storage.getRepairGuides();
299 const totalViews = guides.reduce((sum, guide) => sum + (guide.viewCount || 0), 0);
300 const totalDownloads = guides.reduce((sum, guide) => sum + (guide.downloadCount || 0), 0);
301
302 res.json({
303 totalGuides: guides.length,
304 totalViews,
305 totalDownloads,
306 adminUser: req.adminUser?.email
307 });
308 } catch (error) {
309 res.status(500).json({ message: "Failed to fetch admin stats" });
310 }
311 });
312
313 // Bulk download endpoint
314 app.post("/api/download/bulk", async (req, res) => {
315 try {
316 const { type, filters } = req.body;
317 let data: any[] = [];
318
319 switch (type) {
320 case 'repair-guides':
321 data = await storage.getRepairGuides(filters || {});
322 break;
323 case 'troubleshooting-flows':
324 data = await storage.getTroubleshootingFlows();
325 break;
326 case 'device-components':
327 data = await storage.getDeviceComponents();
328 break;
329 case 'complete':
330 const [guides, flows, components] = await Promise.all([
331 storage.getRepairGuides({}),
332 storage.getTroubleshootingFlows(),
333 storage.getDeviceComponents()
334 ]);
335 const exportData = {
336 exportInfo: {
337 title: 'JCRGuru Complete Export',
338 version: '1.0',
339 exportedAt: new Date().toISOString(),
340 totalItems: guides.length + flows.length + components.length
341 },
342 repairGuides: guides,
343 troubleshootingFlows: flows,
344 deviceComponents: components
345 };
346 data = exportData as any;
347 break;
348 default:
349 return res.status(400).json({ message: "Invalid download type" });
350 }
351
352 res.json({
353 success: true,
354 data,
355 count: Array.isArray(data) ? data.length : Object.keys(data).length - 1,
356 timestamp: new Date().toISOString()
357 });
358 } catch (error) {
359 console.error("Bulk download error:", error);
360 res.status(500).json({ message: "Failed to prepare bulk download" });
361 }
362 });
363
364 app.patch("/api/repair-guides/:id/bookmark", async (req, res) => {
365 try {
366 const guide = await storage.toggleBookmark(req.params.id);
367 if (!guide) {
368 return res.status(404).json({ message: "Repair guide not found" });
369 }
370 res.json(guide);
371 } catch (error) {
372 res.status(500).json({ message: "Failed to toggle bookmark" });
373 }
374 });
375
376 // Troubleshooting Flows
377 app.get("/api/troubleshooting-flows", async (req, res) => {
378 try {
379 const flows = await storage.getTroubleshootingFlows();
380 res.json(flows);
381 } catch (error) {
382 res.status(500).json({ message: "Failed to fetch troubleshooting flows" });
383 }
384 });
385
386 app.get("/api/troubleshooting-flows/:id", async (req, res) => {
387 try {
388 const flow = await storage.getTroubleshootingFlow(req.params.id);
389 if (!flow) {
390 return res.status(404).json({ message: "Troubleshooting flow not found" });
391 }
392 res.json(flow);
393 } catch (error) {
394 res.status(500).json({ message: "Failed to fetch troubleshooting flow" });
395 }
396 });
397
398 app.post("/api/troubleshooting-flows", async (req, res) => {
399 try {
400 const validatedData = insertTroubleshootingFlowSchema.parse(req.body);
401 const flow = await storage.createTroubleshootingFlow(validatedData);
402 res.status(201).json(flow);
403 } catch (error) {
404 res.status(400).json({ message: "Invalid troubleshooting flow data" });
405 }
406 });
407
408 // Device Components
409 app.get("/api/device-components", async (req, res) => {
410 try {
411 const { deviceType } = req.query;
412 const components = await storage.getDeviceComponents(deviceType as string);
413 res.json(components);
414 } catch (error) {
415 res.status(500).json({ message: "Failed to fetch device components" });
416 }
417 });
418
419 app.get("/api/device-components/:id", async (req, res) => {
420 try {
421 const component = await storage.getDeviceComponent(req.params.id);
422 if (!component) {
423 return res.status(404).json({ message: "Device component not found" });
424 }
425 res.json(component);
426 } catch (error) {
427 res.status(500).json({ message: "Failed to fetch device component" });
428 }
429 });
430
431 app.post("/api/device-components", async (req, res) => {
432 try {
433 const validatedData = insertDeviceComponentSchema.parse(req.body);
434 const component = await storage.createDeviceComponent(validatedData);
435 res.status(201).json(component);
436 } catch (error) {
437 res.status(400).json({ message: "Invalid device component data" });
438 }
439 });
440
441 // Device Brands routes
442 app.get("/api/device-brands", async (req, res) => {
443 try {
444 const { deviceType } = req.query;
445 const brands = await storage.getDeviceBrands(deviceType as string);
446 res.json(brands);
447 } catch (error) {
448 console.error("Error fetching device brands:", error);
449 res.status(500).json({ message: "Failed to fetch device brands" });
450 }
451 });
452
453 // Device Models routes
454 app.get("/api/device-models", async (req, res) => {
455 try {
456 const { brandId, deviceType, search } = req.query;
457
458 if (search) {
459 const models = await storage.searchDeviceModels(search as string, deviceType as string);
460 res.json(models);
461 } else {
462 const models = await storage.getDeviceModels(brandId as string, deviceType as string);
463 res.json(models);
464 }
465 } catch (error) {
466 console.error("Error fetching device models:", error);
467 res.status(500).json({ message: "Failed to fetch device models" });
468 }
469 });
470
471 app.get("/api/device-models/:id", async (req, res) => {
472 try {
473 const { id } = req.params;
474 const model = await storage.getDeviceModel(id);
475 if (!model) {
476 return res.status(404).json({ message: "Device model not found" });
477 }
478 res.json(model);
479 } catch (error) {
480 console.error("Error fetching device model:", error);
481 res.status(500).json({ message: "Failed to fetch device model" });
482 }
483 });
484
485 app.get("/api/brands/:brandId/models", async (req, res) => {
486 try {
487 const { brandId } = req.params;
488 const models = await storage.getModelsByBrand(brandId);
489 res.json(models);
490 } catch (error) {
491 console.error("Error fetching models by brand:", error);
492 res.status(500).json({ message: "Failed to fetch models by brand" });
493 }
494 });
495
496 // Guide Compatibility routes
497 app.get("/api/models/:modelId/guides", async (req, res) => {
498 try {
499 const { modelId } = req.params;
500 const guides = await storage.getCompatibleGuides(modelId);
501 res.json(guides);
502 } catch (error) {
503 console.error("Error fetching compatible guides:", error);
504 res.status(500).json({ message: "Failed to fetch compatible guides" });
505 }
506 });
507
508 app.get("/api/guides/:guideId/models", async (req, res) => {
509 try {
510 const { guideId } = req.params;
511 const models = await storage.getModelCompatibility(guideId);
512 res.json(models);
513 } catch (error) {
514 console.error("Error fetching model compatibility:", error);
515 res.status(500).json({ message: "Failed to fetch model compatibility" });
516 }
517 });
518
519 // Digital Asset Links for TWA
520 app.get("/.well-known/assetlinks.json", (req, res) => {
521 res.json([{
522 relation: ["delegate_permission/common.handle_all_urls"],
523 target: {
524 namespace: "android_app",
525 package_name: "com.jcrguidepro.app",
526 sha256_cert_fingerprints: ["73:8B:D2:DB:FC:57:4D:5B:F1:A5:28:C5:01:B6:17:C7:7F:80:74:EE:B5:FD:0C:DB:12:CE:03:C8:35:D3:08:BE"]
527 }
528 }]);
529 });
530
531 // Dynamic ZIP Exporter for Android Studio & Offline Project Bundle
532 app.get(["/api/export-android-zip", "/api/export-zip", "/api/download/android-studio-zip"], (_req, res) => {
533 try {
534 const filename = "JCR-Guide-Pro-Android-Studio.zip";
535 const prebuiltPath = path.join(process.cwd(), "public", "JCR-Guide-Pro-Android-Studio.zip");
536
537 if (fs.existsSync(prebuiltPath)) {
538 res.setHeader("Content-Type", "application/zip");
539 res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
540 return res.sendFile(prebuiltPath);
541 }
542
543 // Generate on-the-fly if prebuilt is not found
544 const zip = new AdmZip();
545 const rootDir = process.cwd();
546
547 // Folders to include in the Android Studio & project export
548 const foldersToInclude = ["android", "src", "shared", "server"];
549 foldersToInclude.forEach((folder) => {
550 const folderPath = path.join(rootDir, folder);
551 if (fs.existsSync(folderPath)) {
552 zip.addLocalFolder(folderPath, folder);
553 }
554 });
555
556 // Include public assets (excluding zip files)
557 const pubDir = path.join(rootDir, "public");
558 if (fs.existsSync(pubDir)) {
559 fs.readdirSync(pubDir).forEach((f) => {
560 if (!f.endsWith(".zip")) {
561 const full = path.join(pubDir, f);
562 if (fs.statSync(full).isDirectory()) {
563 zip.addLocalFolder(full, `public/${f}`);
564 } else {
565 zip.addLocalFile(full, "public");
566 }
567 }
568 });
569 }
570
571 // Essential root configuration files
572 const filesToInclude = [
573 "package.json",
574 "tsconfig.json",
575 "vite.config.ts",
576 "capacitor.config.json",
577 "index.html",
578 "README.md",
579 "metadata.json",
580 ".env.example"
581 ];
582
583 filesToInclude.forEach((file) => {
584 const filePath = path.join(rootDir, file);
585 if (fs.existsSync(filePath)) {
586 zip.addLocalFile(filePath);
587 }
588 });
589
590 const zipBuffer = zip.toBuffer();
591 res.setHeader("Content-Type", "application/zip");
592 res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
593 res.setHeader("Content-Length", zipBuffer.length.toString());
594 res.send(zipBuffer);
595 } catch (error) {
596 console.error("Export ZIP route error:", error);
597 if (!res.headersSent) {
598 res.status(500).json({ message: "Could not create zip archive" });
599 }
600 }
601 });
602
603 const httpServer = createServer(app);
604 return httpServer;
605 }