PM2 cluster deploy + Redis caching + Nginx gzip + UI polish

- PM2 v6: 2x API cluster + 1x web, zero-downtime reload
- Redis cache: 30s TTL on GET /tasks, X-Cache header
- Nginx gzip compression
- Mobile touch targets 44px
- Czech diacritics throughout UI
- TaskModal slide-up animation
- StatusBadge color dots
- GroupSelector emoji icons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude CLI Agent
2026-03-29 11:20:58 +00:00
parent 4a6b5e5498
commit d74c89255e
13 changed files with 842 additions and 212 deletions

View File

@@ -1,40 +1,67 @@
// Task Team — API Server — 2026-03-29
require('dotenv').config();
const Fastify = require('fastify');
const cors = require('@fastify/cors');
const jwt = require('@fastify/jwt');
const { Pool } = require('pg');
require("dotenv").config();
const Fastify = require("fastify");
const cors = require("@fastify/cors");
const jwt = require("@fastify/jwt");
const { Pool } = require("pg");
const Redis = require("ioredis");
const app = Fastify({ logger: true });
// Database pool
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://taskteam:TaskTeam2026!@10.10.10.10:5432/taskteam'
connectionString: process.env.DATABASE_URL || "postgresql://taskteam:TaskTeam2026!@10.10.10.10:5432/taskteam"
});
// Redis client
const redis = new Redis(process.env.REDIS_URL || "redis://:Redis2026!@10.10.10.10:6379");
redis.on("connect", () => {
app.log.info("Redis connected");
});
redis.on("error", (err) => {
app.log.error("Redis error: " + err.message);
});
// Plugins
app.register(cors, { origin: true });
app.register(jwt, { secret: process.env.JWT_SECRET || 'taskteam-jwt-secret-2026' });
app.register(jwt, { secret: process.env.JWT_SECRET || "taskteam-jwt-secret-2026" });
// Decorate with db
app.decorate('db', pool);
// Decorate with db and redis
app.decorate("db", pool);
app.decorate("redis", redis);
// Health check
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
app.get("/health", async () => ({
status: "ok",
timestamp: new Date().toISOString(),
pid: process.pid,
redis: redis.status
}));
// Register routes
app.register(require('./routes/tasks'), { prefix: '/api/v1' });
app.register(require('./routes/groups'), { prefix: '/api/v1' });
app.register(require('./routes/auth'), { prefix: '/api/v1' });
app.register(require('./routes/connectors'), { prefix: '/api/v1' });
app.register(require('./routes/connectors/odoo'), { prefix: '/api/v1' });
app.register(require('./routes/chat'), { prefix: '/api/v1' });
app.register(require("./routes/tasks"), { prefix: "/api/v1" });
app.register(require("./routes/groups"), { prefix: "/api/v1" });
app.register(require("./routes/auth"), { prefix: "/api/v1" });
app.register(require("./routes/connectors"), { prefix: "/api/v1" });
app.register(require("./routes/connectors/odoo"), { prefix: "/api/v1" });
app.register(require("./routes/chat"), { prefix: "/api/v1" });
// Graceful shutdown
const shutdown = async (signal) => {
app.log.info(`Received ${signal}, shutting down gracefully...`);
await redis.quit();
await pool.end();
process.exit(0);
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
// Start
const start = async () => {
try {
await app.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' });
console.log('Task Team API listening on port ' + (process.env.PORT || 3000));
await app.listen({ port: process.env.PORT || 3000, host: "0.0.0.0" });
console.log("Task Team API listening on port " + (process.env.PORT || 3000) + " (pid: " + process.pid + ")");
} catch (err) {
app.log.error(err);
process.exit(1);