@masuidrive/ticket
Version:
Real-time ticket tracking viewer with Vite + Express
329 lines (326 loc) • 13.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnifiedServerSSE = void 0;
const http_1 = require("http");
const path_1 = __importDefault(require("path"));
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const fileService_1 = require("./src/fileService");
const fileWatcher_1 = require("./src/fileWatcher");
// IMPORTANT: Port 4932 is REQUIRED for this project - DO NOT CHANGE
// This port was explicitly specified by the project requirements
// Changing this port will break the application functionality
const DEFAULT_PORT = 4932;
const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = parseInt(process.env.PORT || DEFAULT_PORT.toString(), 10);
class UnifiedServerSSE {
constructor(projectRoot, hostname, port) {
this.sseClients = new Map();
this.clientIdCounter = 0;
this.projectRoot = projectRoot || process.cwd();
this.hostname = hostname || 'localhost';
this.port = port || parseInt(process.env.PORT || DEFAULT_PORT.toString(), 10);
// Vite build directory - handle both direct tsx execution and compiled js execution
if (__dirname.includes('/server/dist')) {
// Compiled JS execution (npx package)
this.distPath = path_1.default.join(__dirname, '..', '..', 'dist');
}
else {
// Direct tsx execution (development)
this.distPath = path_1.default.join(__dirname, '..', 'dist');
}
this.expressApp = (0, express_1.default)();
this.fileService = new fileService_1.FileService(this.projectRoot);
this.fileWatcher = new fileWatcher_1.FileWatcher(this.projectRoot);
this.setupMiddleware();
this.setupExpressRoutes();
this.setupFileWatcher();
}
setupMiddleware() {
this.expressApp.use((0, cors_1.default)());
this.expressApp.use(express_1.default.json());
// Serve static files from Vite build directory
this.expressApp.use(express_1.default.static(this.distPath));
}
setupExpressRoutes() {
// Express API routes
this.expressApp.get('/api/express/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
this.expressApp.get('/api/express/ticket', async (req, res) => {
try {
const content = await this.fileService.readTicketFile();
if (!content) {
return res.status(404).json({
error: 'Ticket file not found',
message: 'No current-ticket.md file exists in the project root'
});
}
res.json(content);
}
catch (error) {
console.error('Error reading ticket file:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Failed to read ticket file'
});
}
});
this.expressApp.get('/api/express/ticket/exists', async (req, res) => {
try {
const exists = await this.fileService.fileExists('current-ticket.md');
res.json({ exists });
}
catch (error) {
console.error('Error checking file existence:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Failed to check file existence'
});
}
});
this.expressApp.get('/api/system-info', async (req, res) => {
try {
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
// Get current directory
const currentDir = this.projectRoot;
// Get git branch
let gitBranch = '';
try {
const { stdout } = await execAsync('git branch --show-current', { cwd: currentDir });
gitBranch = stdout.trim();
}
catch (gitError) {
// Git command failed, branch might not exist or not a git repo
gitBranch = '';
}
res.json({
currentDir,
gitBranch
});
}
catch (error) {
console.error('Error getting system info:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Failed to get system info'
});
}
});
// SSE endpoint
this.expressApp.get('/api/ticket-stream', async (req, res) => {
// Set SSE headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Disable Nginx buffering
// Generate unique client ID
const clientId = `client-${++this.clientIdCounter}`;
console.log(`SSE client connected: ${clientId}`);
// Store client
this.sseClients.set(clientId, { id: clientId, res });
// Send initial update notification if file exists
try {
const exists = await this.fileService.fileExists('current-ticket.md');
if (exists) {
this.sendSSEMessage(res, 'update', { ticket: 'current-ticket.md' });
}
}
catch (error) {
console.error('Error sending initial notification:', error);
}
// Send periodic heartbeat to keep connection alive
const heartbeatInterval = setInterval(() => {
this.sendSSEMessage(res, 'heartbeat', { timestamp: Date.now() });
}, 30000); // Every 30 seconds
// Handle client disconnect
req.on('close', () => {
console.log(`SSE client disconnected: ${clientId}`);
clearInterval(heartbeatInterval);
this.sseClients.delete(clientId);
});
});
}
setupFileWatcher() {
this.fileWatcher.watchFile('current-ticket.md');
this.fileWatcher.on('fileChanged', async () => {
console.log('Ticket file changed');
await this.broadcastUpdate();
});
this.fileWatcher.on('fileAdded', async () => {
console.log('Ticket file added');
await this.broadcastUpdate();
});
this.fileWatcher.on('fileRemoved', () => {
console.log('Ticket file removed');
this.broadcast('removed', { ticket: 'current-ticket.md' });
});
this.fileWatcher.on('error', (error) => {
console.error('File watcher error:', error);
});
}
sendSSEMessage(res, eventType, data) {
const message = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
res.write(message);
}
async broadcastUpdate() {
try {
// Send only update notification, not the content
this.broadcast('update', { ticket: 'current-ticket.md' });
}
catch (error) {
console.error('Error broadcasting update:', error);
}
}
broadcast(eventType, data) {
this.sseClients.forEach((client) => {
try {
this.sendSSEMessage(client.res, eventType, data);
}
catch (error) {
console.error(`Error sending to client ${client.id}:`, error);
this.sseClients.delete(client.id);
}
});
}
async start() {
// For production, check if dist directory exists
if (!dev) {
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
if (!fs.existsSync(this.distPath)) {
throw new Error(`Production build directory not found at ${this.distPath}. Please run 'npm run build' first.`);
}
}
// Serve index.html for SPA routes (fallback)
this.expressApp.use((req, res, next) => {
// Skip API routes
if (req.path.startsWith('/api/')) {
return res.status(404).json({ error: 'API endpoint not found' });
}
// Skip files with extensions (likely static assets)
if (path_1.default.extname(req.path)) {
return res.status(404).send('File not found');
}
// Serve index.html for all other routes (SPA fallback)
res.sendFile(path_1.default.join(this.distPath, 'index.html'), (err) => {
if (err) {
console.error('Error serving index.html:', err);
res.status(500).send('Error loading application');
}
});
});
this.server = (0, http_1.createServer)(this.expressApp);
this.server.listen(this.port, this.hostname, () => {
console.log(`> Ready on http://${this.hostname}:${this.port}`);
console.log(`> Express API available at:
- GET /api/express/health
- GET /api/express/ticket
- GET /api/express/ticket/exists
- GET /api/system-info
- SSE /api/ticket-stream`);
});
}
stop() {
this.fileWatcher.stop();
// Close all SSE connections
this.sseClients.forEach((client) => {
client.res.end();
});
this.sseClients.clear();
this.server.close();
}
}
exports.UnifiedServerSSE = UnifiedServerSSE;
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
let projectRoot = process.cwd();
for (let i = 0; i < args.length; i++) {
if ((args[i] === '-d' || args[i] === '--dir') && args[i + 1]) {
projectRoot = args[i + 1];
i++;
}
else if (args[i] === '-h' || args[i] === '--help') {
showHelp();
process.exit(0);
}
}
return { projectRoot };
}
function showHelp() {
console.log(`
Unified Ticket Viewer Server with SSE - Next.js + Express
Usage: tsx server/unified-server-sse.ts [options]
Options:
-d, --dir <path> Project directory containing current-ticket.md
-h, --help Show this help message
Environment Variables:
PORT Server port (default: ${DEFAULT_PORT})
NODE_ENV Set to 'production' for production mode
`);
}
async function main() {
const { projectRoot } = parseArgs();
console.log(`Starting Unified Server with SSE...`);
console.log(`Project root: ${projectRoot}`);
console.log(`Port: ${port}`);
console.log(`Mode: ${dev ? 'development' : 'production'}`);
const server = new UnifiedServerSSE(projectRoot);
await server.start();
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down server...');
server.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\nShutting down server...');
server.stop();
process.exit(0);
});
}
// Run if this is the main module
if (require.main === module) {
main().catch(console.error);
}
//# sourceMappingURL=unified-server.js.map