diff --git a/_backend_logs.txt b/_backend_logs.txt new file mode 100644 index 0000000..7d8be45 --- /dev/null +++ b/_backend_logs.txt @@ -0,0 +1,100 @@ + at Readable.push (node:internal/streams/readable:392:5) { + query: 'DELETE FROM `notifications` WHERE (`is_read` = ? AND `createdAt` < ?)', + parameters: [ + true, + 2026-01-17T17:00:00.024Z + ], + driverError: Error: Unknown column 'createdAt' in 'WHERE' + at Packet.asError (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packets/packet.js:740:17) + at Query.execute (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/commands/command.js:29:26) + at PoolConnection.handlePacket (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:477:34) + at PacketParser.onPacket (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:93:12) + at PacketParser.executeStart (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packet_parser.js:75:16) + at Socket. (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:100:25) + at Socket.emit (node:events:519:28) + at addChunk (node:internal/streams/readable:561:12) + at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) + at Readable.push (node:internal/streams/readable:392:5) { + code: 'ER_BAD_FIELD_ERROR', + errno: 1054, + sqlState: '42S22', + sqlMessage: "Unknown column 'createdAt' in 'WHERE'", + sql: "DELETE FROM `notifications` WHERE (`is_read` = true AND `createdAt` < '2026-01-18 00:00:00.024')" + }, + code: 'ER_BAD_FIELD_ERROR', + errno: 1054, + sqlState: '42S22', + sqlMessage: "Unknown column 'createdAt' in 'WHERE'", + sql: "DELETE FROM `notifications` WHERE (`is_read` = true AND `createdAt` < '2026-01-18 00:00:00.024')" +} +[Nest] 1 - 02/17/2026, 12:00:00 AM LOG [FileCleanupService] No expired files found. +[Nest] 1 - 02/17/2026, 12:00:00 AM ERROR [NotificationCleanupService] Failed to cleanup notifications +[Nest] 1 - 02/17/2026, 12:00:00 AM ERROR [NotificationCleanupService] QueryFailedError: Unknown column 'createdAt' in 'WHERE' + at Query.onResult (/app/node_modules/.pnpm/typeorm@0.3.27_ioredis@5.8.2_mysql2@3.15.3_redis@4.7.1_reflect-metadata@0.2.2_ts-node@1_cb81dfd56f1203fe00eb0fec5dfcce08/node_modules/typeorm/driver/mysql/MysqlQueryRunner.js:168:37) + at Query.execute (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/commands/command.js:36:14) + at PoolConnection.handlePacket (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:477:34) + at PacketParser.onPacket (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:93:12) + at PacketParser.executeStart (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packet_parser.js:75:16) + at Socket. (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:100:25) + at Socket.emit (node:events:519:28) + at addChunk (node:internal/streams/readable:561:12) + at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) + at Readable.push (node:internal/streams/readable:392:5) { + query: 'DELETE FROM `notifications` WHERE (`is_read` = ? AND `createdAt` < ?)', + parameters: [ + true, + 2026-01-17T17:00:00.038Z + ], + driverError: Error: Unknown column 'createdAt' in 'WHERE' + at Packet.asError (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packets/packet.js:740:17) + at Query.execute (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/commands/command.js:29:26) + at PoolConnection.handlePacket (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:477:34) + at PacketParser.onPacket (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:93:12) + at PacketParser.executeStart (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/packet_parser.js:75:16) + at Socket. (/app/node_modules/.pnpm/mysql2@3.15.3/node_modules/mysql2/lib/base/connection.js:100:25) + at Socket.emit (node:events:519:28) + at addChunk (node:internal/streams/readable:561:12) + at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) + at Readable.push (node:internal/streams/readable:392:5) { + code: 'ER_BAD_FIELD_ERROR', + errno: 1054, + sqlState: '42S22', + sqlMessage: "Unknown column 'createdAt' in 'WHERE'", + sql: "DELETE FROM `notifications` WHERE (`is_read` = true AND `createdAt` < '2026-01-18 00:00:00.038')" + }, + code: 'ER_BAD_FIELD_ERROR', + errno: 1054, + sqlState: '42S22', + sqlMessage: "Unknown column 'createdAt' in 'WHERE'", + sql: "DELETE FROM `notifications` WHERE (`is_read` = true AND `createdAt` < '2026-01-18 00:00:00.038')" +} +[Nest] 1 - 02/17/2026, 4:40:11 AM WARN [HttpExceptionFilter] ⚠️ HTTP 404 Error on GET /: "Cannot GET /" +[Nest] 1 - 02/17/2026, 7:29:09 AM WARN [HttpExceptionFilter] ⚠️ HTTP 404 Error on GET /: "Cannot GET /" +[Nest] 1 - 02/17/2026, 11:58:32 AM WARN [HttpExceptionFilter] ⚠️ HTTP 404 Error on GET /: "Cannot GET /" +[Nest] 1 - 02/17/2026, 3:48:07 PM WARN [HttpExceptionFilter] ⚠️ HTTP 404 Error on GET /robots.txt: "Cannot GET /robots.txt" +[Nest] 1 - 02/17/2026, 3:57:31 PM WARN [HttpExceptionFilter] ⚠️ HTTP 401 Error on GET /api/dashboard/stats: "Unauthorized" +[Nest] 1 - 02/17/2026, 3:57:31 PM WARN [HttpExceptionFilter] ⚠️ HTTP 401 Error on GET /api/dashboard/pending: "Unauthorized" +[Nest] 1 - 02/17/2026, 3:57:31 PM WARN [HttpExceptionFilter] ⚠️ HTTP 401 Error on GET /api/dashboard/activity: "Unauthorized" +[Nest] 1 - 02/17/2026, 3:57:31 PM WARN [HttpExceptionFilter] ⚠️ HTTP 401 Error on GET /api/notifications/unread: "Unauthorized" +[Nest] 1 - 02/17/2026, 3:57:32 PM DEBUG [DashboardService] Getting dashboard stats for user 1 +[Nest] 1 - 02/17/2026, 3:57:36 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:37 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:39 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/rfas?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:40 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/rfas?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:41 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:43 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:44 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/projects?isActive=true: "You do not have permission: project.view" +[Nest] 1 - 02/17/2026, 3:57:45 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/projects?isActive=true: "You do not have permission: project.view" +[Nest] 1 - 02/17/2026, 3:57:49 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/rfas?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:50 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/rfas?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:51 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/rfas?page=1&revisionStatus=ALL: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:52 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/rfas?page=1&revisionStatus=ALL: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:53 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:57:54 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[OrganizationService] Found 18 organizations +[Nest] 1 - 02/17/2026, 3:57:55 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/projects?isActive=true: "You do not have permission: project.view" +[Nest] 1 - 02/17/2026, 3:57:56 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/projects?isActive=true: "You do not have permission: project.view" +[Nest] 1 - 02/17/2026, 3:58:04 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:58:05 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/correspondences?page=1&revisionStatus=CURRENT: "You do not have permission: document.view" +[Nest] 1 - 02/17/2026, 3:58:22 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/projects?isActive=true: "You do not have permission: project.view" +[Nest] 1 - 02/17/2026, 3:58:23 PM WARN [HttpExceptionFilter] ⚠️ HTTP 403 Error on GET /api/projects?isActive=true: "You do not have permission: project.view" diff --git a/backend/src/modules/notification/notification-cleanup.service.ts b/backend/src/modules/notification/notification-cleanup.service.ts index 0ca71ba..616d22a 100644 --- a/backend/src/modules/notification/notification-cleanup.service.ts +++ b/backend/src/modules/notification/notification-cleanup.service.ts @@ -10,7 +10,7 @@ export class NotificationCleanupService { constructor( @InjectRepository(Notification) - private notificationRepo: Repository, + private notificationRepo: Repository ) {} /** @@ -26,10 +26,14 @@ export class NotificationCleanupService { dateThreshold.setDate(dateThreshold.getDate() - daysAgo); try { - const result = await this.notificationRepo.delete({ - isRead: true, - createdAt: LessThan(dateThreshold), - }); + const result = await this.notificationRepo + .createQueryBuilder() + .delete() + .from(Notification) + .where('is_read = :isRead', { isRead: true }) + // Use column name 'created_at' explicitly + .andWhere('created_at < :dateThreshold', { dateThreshold }) + .execute(); this.logger.log(`Deleted ${result.affected} old read notifications.`); } catch (error) { diff --git a/specs/07-database/fix-project-permissions.sql b/specs/07-database/fix-project-permissions.sql new file mode 100644 index 0000000..f2fc6eb --- /dev/null +++ b/specs/07-database/fix-project-permissions.sql @@ -0,0 +1,29 @@ +-- Fix Project Permissions +-- File: specs/07-database/fix-project-permissions.sql +-- 1. Ensure project.view permission exists +INSERT IGNORE INTO permissions ( + permission_id, + permission_name, + description, + module, + is_active + ) +VALUES ( + 202, + 'project.view', + 'ดูรายการโครงการ', + 'project', + 1 + ); +-- 2. Grant project.view to Superadmin (Role 1) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (1, 202); +-- 3. Grant project.view to Organization Admin (Role 2) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (2, 202); +-- 4. Grant project.view to Project Manager (Role 6) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (6, 202); +-- 5. Grant project.view to Viewer (Role 5) +INSERT IGNORE INTO role_permissions (role_id, permission_id) +VALUES (5, 202); \ No newline at end of file diff --git a/specs/08-infrastructure/05_monitoring.md b/specs/08-infrastructure/05_monitoring.md index 54722a5..9b391e0 100644 --- a/specs/08-infrastructure/05_monitoring.md +++ b/specs/08-infrastructure/05_monitoring.md @@ -297,6 +297,9 @@ services: <<: [*restart_policy, *default_logging] image: gcr.io/cadvisor/cadvisor:v0.47.2 container_name: cadvisor + privileged: true + devices: + - /dev/kmsg deploy: resources: limits: @@ -313,6 +316,7 @@ services: - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro + - /dev/disk/:/dev/disk:ro healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/healthz"] interval: 30s @@ -421,6 +425,20 @@ services: - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro - /sys/fs/cgroup:/sys/fs/cgroup:ro + + mysqld-exporter: + image: prom/mysqld-exporter:v0.15.0 + container_name: mysqld-exporter + restart: unless-stopped + user: root + command: + - '--config.my-cnf=/etc/mysql/my.cnf' + ports: + - "9104:9104" + networks: + - lcbp3 + volumes: + - "/share/np-dms/monitoring/mysqld-exporter/.my.cnf:/etc/mysql/my.cnf:ro" ``` --- @@ -484,12 +502,12 @@ scrape_configs: host: 'qnap' metrics_path: '/metrics' - # MariaDB Exporter (optional - QNAP) - # - job_name: 'mariadb' - # static_configs: - # - targets: ['192.168.10.8:9104'] - # labels: - # host: 'qnap' + # MariaDB Exporter (QNAP) + - job_name: 'mariadb' + static_configs: + - targets: ['192.168.10.8:9104'] + labels: + host: 'qnap' ``` --- @@ -518,15 +536,16 @@ scrape_configs: ### Recommended Dashboards to Import -| Dashboard ID | Name | Purpose | -| :----------- | :--------------------------- | :------------------ | -| 1860 | Node Exporter Full | Host system metrics | ป | -| 14282 | cAdvisor exporter | Container metrics | ป | -| 11074 | Node Exporter for Prometheus | Node overview | -| 893 | Docker and Container | Docker overview | -| 7362 | MySQL | MySQL view | -| 1214 | Redis | Redis view | -| 14204 | Elasticsearch | Elasticsearch view | +| Dashboard ID | Name | Purpose | +| :----------- | :--------------------------- | :----------------------------- | +| 1860 | Node Exporter Full | Host system metrics | +| 14282 | cAdvisor exporter | Container metrics | +| 11074 | Node Exporter for Prometheus | Node overview | +| 893 | Docker and Container | Docker overview | +| 7362 | MySQL | MySQL view | +| 1214 | Redis | Redis view | +| 14204 | Elasticsearch | Elasticsearch view | +| 13106 | MySQL/MariaDB Overview | Detailed MySQL/MariaDB metrics | ### Import Dashboard via Grafana UI @@ -546,7 +565,7 @@ scrape_configs: | :--- | :------------------------------------------------------------------------------------------------- | :----- | | 1 | SSH เข้า ASUSTOR ได้ (`ssh admin@192.168.10.9`) | ✅ | | 2 | Docker Network `lcbp3` สร้างแล้ว (ดูหัวข้อ [สร้าง Docker Network](#-สร้าง-docker-network-ทำครั้งแรกครั้งเดียว)) | ✅ | -| 3 | สร้าง Directories และกำหนดสิทธิ์แล้ว (ดูหัวข้อ [กำหนดสิทธิ](#กำหนดสิทธิ-บน-asustor)) | ✅ | +| 3 | สร้าง Directories และกำหนดสิทธิ์แล้ว (ดูหัวข้อ [กำหนดสิทธิ](#กำหนดสิทธิ-บน-asustor)) | ✅ | | 4 | สร้าง `prometheus.yml` แล้ว (ดูหัวข้อ [Prometheus Configuration](#prometheus-configuration)) | ✅ | | 5 | สร้าง `promtail-config.yml` แล้ว (ดูหัวข้อ [Step 1.2](#step-12-สร้าง-promtail-configyml)) | ✅ | @@ -635,6 +654,41 @@ scrape_configs: target_label: 'stream' EOF +# ขั้นตอนการเตรียมระบบที่ QNAP (ก่อน Deploy Stack) + +### 1. สร้าง Monitoring User ใน MariaDB +รันคำสั่ง SQL นี้ผ่าน **phpMyAdmin** หรือ `docker exec`: +```sql +CREATE USER 'exporter'@'%' IDENTIFIED BY 'Center2025' WITH MAX_USER_CONNECTIONS 3; +GRANT PROCESS, REPLICATION CLIENT, SELECT, SLAVE MONITOR ON *.* TO 'exporter'@'%'; +FLUSH PRIVILEGES; +``` + +### 2. สร้างไฟล์คอนฟิก .my.cnf บน QNAP +เพื่อให้ `mysqld-exporter` อ่านรหัสผ่านที่มีตัวอักษรพิเศษได้ถูกต้อง: + +1. **SSH เข้า QNAP** (หรือใช้ File Station สร้าง Folder): + ```bash + ssh admin@192.168.10.8 + ``` +2. **สร้าง Directory สำหรับเก็บ Config**: + ```bash + mkdir -p /share/np-dms/monitoring/mysqld-exporter + ``` +3. **สร้างไฟล์ .my.cnf**: + ```bash + cat > /share/np-dms/monitoring/mysqld-exporter/.my.cnf << 'EOF' +[client] +user=exporter +password=Center2025 +host=mariadb +EOF + ``` +4. **กำหนดสิทธิ์ไฟล์** (เพื่อให้ Container อ่านไฟล์ได้): + ```bash + chmod 644 /share/np-dms/monitoring/mysqld-exporter/.my.cnf + ``` + # ตรวจสอบ cat /volume1/np-dms/monitoring/promtail/config/promtail-config.yml ``` diff --git a/specs/08-infrastructure/MariaDB_setting.md b/specs/08-infrastructure/MariaDB_setting.md index 26d239a..b437aee 100644 --- a/specs/08-infrastructure/MariaDB_setting.md +++ b/specs/08-infrastructure/MariaDB_setting.md @@ -51,6 +51,11 @@ docker exec -it mariadb mysql -u root -p CREATE USER 'gitea'@'%' IDENTIFIED BY 'Center#2025'; GRANT ALL PRIVILEGES ON gitea.* TO 'gitea'@'%'; FLUSH PRIVILEGES; + +docker exec -it mariadb mysql -u root -p + CREATE USER 'exporter'@'%' IDENTIFIED BY 'Center#2025' WITH MAX_USER_CONNECTIONS 3; + GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%'; + FLUSH PRIVILEGES; ``` ## Docker file