mega-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* UserController — HTTP 컨트롤러(ADR-074: 베이스 없음, 정적 메서드만). 모델을 직접 import 하지 않고
|
|
4
|
+
* `ctx.services.user`(자동 DI, ADR-148)로 서비스를 거친다(ADR-022). 핸들러는 도메인 데이터만 반환하고
|
|
5
|
+
* envelope(`{ ok, data, meta }`)는 프레임워크가 감싼다(ADR-018/147).
|
|
6
|
+
*/
|
|
7
|
+
export class UserController {
|
|
8
|
+
/** GET /users — 목록 @param {any} _req @param {any} _reply @param {any} ctx */
|
|
9
|
+
static async index(_req, _reply, ctx) {
|
|
10
|
+
return ctx.services.user.list()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** GET /users/:id — 단건(없으면 404 에러 envelope) @param {any} req @param {any} _reply @param {any} ctx */
|
|
14
|
+
static async show(req, _reply, ctx) {
|
|
15
|
+
return ctx.services.user.get(req.params.id)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** POST /users — 생성(201) @param {any} req @param {any} reply @param {any} ctx */
|
|
19
|
+
static async create(req, reply, ctx) {
|
|
20
|
+
const user = await ctx.services.user.create(req.body)
|
|
21
|
+
reply.code(201)
|
|
22
|
+
return user
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** PUT /users/:id — 수정 @param {any} req @param {any} _reply @param {any} ctx */
|
|
26
|
+
static async update(req, _reply, ctx) {
|
|
27
|
+
return ctx.services.user.update(req.params.id, req.body ?? {})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** DELETE /users/:id — 삭제 @param {any} req @param {any} _reply @param {any} ctx */
|
|
31
|
+
static async destroy(req, _reply, ctx) {
|
|
32
|
+
return ctx.services.user.remove(req.params.id)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* WebController — 관리 UI(MPA) HTTP 컨트롤러(ADR-074: 베이스 없음, 정적 메서드만).
|
|
4
|
+
*
|
|
5
|
+
* JSON REST API(`/users`, user-controller.js)와 같은 `ctx.services.user`(자동 DI, ADR-148)를 거치되,
|
|
6
|
+
* 도메인 데이터를 반환하는 대신 EJS 뷰를 서버사이드 렌더한다(ADR-011/136). 폼 제출(urlencoded)은
|
|
7
|
+
* `@fastify/formbody`(ADR-151)가 `req.body` 로 파싱하고, 생성/수정 후엔 PRG(Post/Redirect/Get)로
|
|
8
|
+
* 목록으로 리다이렉트한다. 검증/충돌 에러는 폼을 에러 표시와 함께 다시 렌더한다(JSON envelope 대신).
|
|
9
|
+
*
|
|
10
|
+
* CSRF(ADR-051, 디폴트 ON)는 폼 `_csrf` 토큰을 요구하므로, 폼을 담은 뷰는 `reply.generateCsrf()`로
|
|
11
|
+
* 토큰을 발급해 hidden input 으로 심는다.
|
|
12
|
+
*/
|
|
13
|
+
import { MegaNotFoundError, MegaValidationError, MegaConflictError } from 'mega-framework/errors'
|
|
14
|
+
import { currentUser } from '../middleware/web-auth.js'
|
|
15
|
+
|
|
16
|
+
/** 알림 쿼리(?notice=) → 로케일 키 화이트리스트(임의 t() 조회 방지). */
|
|
17
|
+
const NOTICE_KEYS = new Set(['created', 'updated', 'deleted'])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 검증/충돌 에러에서 어떤 필드가 잘못됐는지 계산한다(폼 is-invalid 표시용).
|
|
21
|
+
* @param {unknown} err
|
|
22
|
+
* @returns {{ name: boolean, email: boolean }}
|
|
23
|
+
*/
|
|
24
|
+
function invalidFields(err) {
|
|
25
|
+
if (err instanceof MegaValidationError) {
|
|
26
|
+
const d = /** @type {any} */ (err).details ?? {}
|
|
27
|
+
// user-service 는 details.{name,email} = "값이 있는가"(true=정상) 로 준다 → 없으면 invalid.
|
|
28
|
+
return { name: !d.name, email: !d.email }
|
|
29
|
+
}
|
|
30
|
+
if (err instanceof MegaConflictError) {
|
|
31
|
+
return { name: false, email: true } // 이메일 중복.
|
|
32
|
+
}
|
|
33
|
+
return { name: false, email: false }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class WebController {
|
|
37
|
+
/** GET / — 랜딩 페이지(공개). @param {any} req @param {any} reply @param {any} ctx */
|
|
38
|
+
static async home(req, reply, ctx) {
|
|
39
|
+
// 로그인 상태면 navbar 로그아웃 폼(POST+CSRF)이 토큰을 쓰므로 공개 페이지에서도 발급한다.
|
|
40
|
+
return ctx.render('home', { title: ctx.t('home_title', '사용자 CRUD 관리'), currentUser: currentUser(req), csrfToken: reply.generateCsrf() })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** GET /admin/users — 목록(+ ?notice= 알림, 삭제 폼 CSRF 토큰). @param {any} req @param {any} reply @param {any} ctx */
|
|
44
|
+
static async list(req, reply, ctx) {
|
|
45
|
+
const users = await ctx.services.user.list()
|
|
46
|
+
const key = String(req.query?.notice ?? '')
|
|
47
|
+
const notice = NOTICE_KEYS.has(key) ? ctx.t(`notice_${key}`) : null
|
|
48
|
+
return ctx.render('users/list', {
|
|
49
|
+
title: ctx.t('users_title', '사용자 목록'),
|
|
50
|
+
users,
|
|
51
|
+
notice,
|
|
52
|
+
currentUser: currentUser(req),
|
|
53
|
+
csrfToken: reply.generateCsrf(),
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** GET /admin/users/new — 신규 폼. @param {any} req @param {any} reply @param {any} ctx */
|
|
58
|
+
static async newForm(req, reply, ctx) {
|
|
59
|
+
return ctx.render('users/new', {
|
|
60
|
+
title: ctx.t('new_title', '사용자 추가'),
|
|
61
|
+
values: {},
|
|
62
|
+
invalid: null,
|
|
63
|
+
currentUser: currentUser(req),
|
|
64
|
+
csrfToken: reply.generateCsrf(),
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** POST /admin/users — 생성 → 목록 리다이렉트(PRG). 검증/충돌 시 폼 재렌더. @param {any} req @param {any} reply @param {any} ctx */
|
|
69
|
+
static async create(req, reply, ctx) {
|
|
70
|
+
try {
|
|
71
|
+
await ctx.services.user.create(req.body ?? {})
|
|
72
|
+
return reply.redirect('/admin/users?notice=created')
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err instanceof MegaValidationError || err instanceof MegaConflictError) {
|
|
75
|
+
reply.code(err instanceof MegaConflictError ? 409 : 400)
|
|
76
|
+
return ctx.render('users/new', {
|
|
77
|
+
title: ctx.t('new_title', '사용자 추가'),
|
|
78
|
+
values: req.body ?? {},
|
|
79
|
+
invalid: invalidFields(err),
|
|
80
|
+
error: err.message,
|
|
81
|
+
currentUser: currentUser(req),
|
|
82
|
+
csrfToken: reply.generateCsrf(),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
throw err // 미지 에러는 프레임워크 글로벌 핸들러로.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** GET /admin/users/:id/edit — 수정 폼(사전 채움). 없으면 목록으로. @param {any} req @param {any} reply @param {any} ctx */
|
|
90
|
+
static async editForm(req, reply, ctx) {
|
|
91
|
+
try {
|
|
92
|
+
const user = await ctx.services.user.get(req.params.id)
|
|
93
|
+
return ctx.render('users/edit', {
|
|
94
|
+
title: ctx.t('edit_title', '사용자 수정'),
|
|
95
|
+
values: user,
|
|
96
|
+
invalid: null,
|
|
97
|
+
currentUser: currentUser(req),
|
|
98
|
+
csrfToken: reply.generateCsrf(),
|
|
99
|
+
})
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err instanceof MegaNotFoundError) return reply.redirect('/admin/users')
|
|
102
|
+
throw err
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** POST /admin/users/:id — 수정 → 목록 리다이렉트(PRG). 검증/충돌 시 폼 재렌더, 없으면 목록으로. @param {any} req @param {any} reply @param {any} ctx */
|
|
107
|
+
static async update(req, reply, ctx) {
|
|
108
|
+
try {
|
|
109
|
+
await ctx.services.user.update(req.params.id, req.body ?? {})
|
|
110
|
+
return reply.redirect('/admin/users?notice=updated')
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err instanceof MegaValidationError || err instanceof MegaConflictError) {
|
|
113
|
+
reply.code(err instanceof MegaConflictError ? 409 : 400)
|
|
114
|
+
return ctx.render('users/edit', {
|
|
115
|
+
title: ctx.t('edit_title', '사용자 수정'),
|
|
116
|
+
values: { id: req.params.id, ...(req.body ?? {}) },
|
|
117
|
+
invalid: invalidFields(err),
|
|
118
|
+
error: err.message,
|
|
119
|
+
currentUser: currentUser(req),
|
|
120
|
+
csrfToken: reply.generateCsrf(),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
if (err instanceof MegaNotFoundError) return reply.redirect('/admin/users')
|
|
124
|
+
throw err
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** POST /admin/users/:id/delete — 삭제 → 목록 리다이렉트(PRG). 없으면 그대로 목록으로. @param {any} req @param {any} reply @param {any} ctx */
|
|
129
|
+
static async destroy(req, reply, ctx) {
|
|
130
|
+
try {
|
|
131
|
+
await ctx.services.user.remove(req.params.id)
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (!(err instanceof MegaNotFoundError)) throw err
|
|
134
|
+
}
|
|
135
|
+
return reply.redirect('/admin/users?notice=deleted')
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* WorkerController — /demo/worker CPU 워커 풀 데모 UI HTTP 컨트롤러(ADR-074/124, ADR-157).
|
|
4
|
+
*
|
|
5
|
+
* `ctx.workers['hash']`(boot 가 config.workers 로 배선) 으로 SHA-256 N회 반복 같은 CPU-bound 작업을 worker_threads
|
|
6
|
+
* 풀에서 돌린다. 계산은 AJAX(JSON)로 실행하고(폼 아닌 JSON → CSRF 토큰 면제 + Origin 검증, ADR-051), 페이지의
|
|
7
|
+
* 하트비트 ping 이 계산 도중에도 계속 응답받는 걸 보여줘 **메인 스레드 non-block** 을 시연한다.
|
|
8
|
+
*/
|
|
9
|
+
import { currentUser } from '../middleware/web-auth.js'
|
|
10
|
+
import { HashWorker } from '../workers/hash-worker.js'
|
|
11
|
+
|
|
12
|
+
/** 반복 횟수 기본/최소/최대 — 과도한 입력으로 워커를 장시간 점유하지 않게 상한을 둔다. 기본값은 계산이
|
|
13
|
+
* 몇 초 걸리도록 잡아, 실행 중에도 하트비트가 계속 도는(메인 스레드 non-block) 모습을 눈으로 보게 한다. */
|
|
14
|
+
const ROUNDS_DEFAULT = 5_000_000
|
|
15
|
+
const ROUNDS_MIN = 100_000
|
|
16
|
+
const ROUNDS_MAX = 20_000_000
|
|
17
|
+
/** 워커 task 타임아웃(ms) — 상한 반복도 여유 있게 끝나도록. */
|
|
18
|
+
const RUN_TIMEOUT_MS = 60_000
|
|
19
|
+
|
|
20
|
+
export class WorkerController {
|
|
21
|
+
/** GET /demo/worker — 실행 폼 + 풀 정보 + 하트비트 영역 렌더. @param {any} req @param {any} reply @param {any} ctx */
|
|
22
|
+
static async index(req, reply, ctx) {
|
|
23
|
+
return ctx.render('worker/index', {
|
|
24
|
+
title: ctx.t('worker_title', { defaultValue: 'CPU 워커 데모 (MegaWorker)' }),
|
|
25
|
+
pool: { name: HashWorker.name, mode: HashWorker.mode, poolSize: HashWorker.poolSize },
|
|
26
|
+
defaultRounds: ROUNDS_DEFAULT,
|
|
27
|
+
currentUser: currentUser(req),
|
|
28
|
+
csrfToken: reply.generateCsrf(),
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* POST /demo/worker/run — CPU-bound 해시를 워커 풀에서 실행하고 결과(JSON)를 돌려준다. JSON 요청이라 CSRF
|
|
34
|
+
* 토큰 없이 Origin 검증만 거친다(ADR-051). @param {any} req @param {any} reply @param {any} ctx
|
|
35
|
+
*/
|
|
36
|
+
static async run(req, reply, ctx) {
|
|
37
|
+
const body = req.body ?? {}
|
|
38
|
+
const requested = Number(body.rounds)
|
|
39
|
+
const rounds = Number.isFinite(requested)
|
|
40
|
+
? Math.min(ROUNDS_MAX, Math.max(ROUNDS_MIN, Math.floor(requested)))
|
|
41
|
+
: ROUNDS_DEFAULT
|
|
42
|
+
const startedAt = Date.now()
|
|
43
|
+
const out = await ctx.workers['hash'].run('sha256Loop', { input: 'mega', rounds }, { timeoutMs: RUN_TIMEOUT_MS })
|
|
44
|
+
const ms = Date.now() - startedAt
|
|
45
|
+
ctx.log?.debug?.({ rounds, ms }, 'worker-demo.run')
|
|
46
|
+
// 프레임워크가 JSON 응답을 { ok, data, meta } 엔벨로프로 감싸므로(정본 응답 포맷) 본문은 data 에 담긴다.
|
|
47
|
+
return reply.send({ digest: out.digest, rounds: out.rounds, ms })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* GET /demo/worker/ping — 즉시 응답하는 하트비트(JSON). 워커 계산이 도는 동안에도 이 응답이 끊김 없이
|
|
52
|
+
* 빨라야 메인 스레드가 안 막힌 것이다. @param {any} _req @param {any} reply @param {any} _ctx
|
|
53
|
+
*/
|
|
54
|
+
static async ping(_req, reply, _ctx) {
|
|
55
|
+
return reply.send({ ok: true, at: new Date().toISOString() })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* WsController — /demo/ws 실시간 채팅 데모 페이지(MPA) HTTP 컨트롤러(ADR-158, ADR-074 정적 메서드만).
|
|
4
|
+
*
|
|
5
|
+
* 채팅 자체는 WebSocket(/ws/chat)으로 흐르므로 이 컨트롤러는 셸 페이지만 렌더한다 — 접속 스크립트
|
|
6
|
+
* (ws-chat.js)와 WASM MegaSocket 이 브라우저에서 ASP E: 프레임으로 연결한다. 페이지는 `webRequireAuth`
|
|
7
|
+
* 뒤라 currentUser 가 보장된다.
|
|
8
|
+
*
|
|
9
|
+
* ASP 공유-키: WASM 클라이언트도 서버와 같은 masterSecret 으로 키를 유도해야 하므로(공유-키 구조),
|
|
10
|
+
* 서버 env(ASP_MASTER_SECRET)를 페이지에 주입한다. CSP(script-src 'self')가 인라인 스크립트를 막으므로
|
|
11
|
+
* 시크릿은 **data 속성**으로 심고 외부 스크립트(ws-chat.js)가 읽는다(인라인 주입 불가).
|
|
12
|
+
*/
|
|
13
|
+
import { currentUser } from '../middleware/web-auth.js'
|
|
14
|
+
|
|
15
|
+
export class WsController {
|
|
16
|
+
/** GET /demo/ws — 채팅 셸 렌더(로그인 필요). @param {any} req @param {any} reply @param {any} ctx */
|
|
17
|
+
static async index(req, reply, ctx) {
|
|
18
|
+
return ctx.render('ws/index', {
|
|
19
|
+
title: ctx.t('ws_title', { defaultValue: '실시간 채팅 (WebSocket + ASP)' }),
|
|
20
|
+
currentUser: currentUser(req),
|
|
21
|
+
// navbar 로그아웃 폼(POST+CSRF)이 토큰을 쓴다(로그인 상태 페이지).
|
|
22
|
+
csrfToken: reply.generateCsrf(),
|
|
23
|
+
// WS 엔드포인트 경로 — 클라가 location.host 와 합쳐 ws(s):// URL 을 만든다.
|
|
24
|
+
wsPath: '/ws/chat',
|
|
25
|
+
// WASM MegaSocket 의 ASP masterSecret(공유-키). data 속성으로 전달.
|
|
26
|
+
aspSecret: process.env.ASP_MASTER_SECRET ?? '',
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { MegaJob } from 'mega-framework'
|
|
3
|
+
|
|
4
|
+
/** 'flaky' 모드가 성공하는 시도 번호 — 1번째 시도는 실패(throw)하고 2번째 시도에 성공한다(재시도 시연). */
|
|
5
|
+
const FLAKY_SUCCEED_ON = 2
|
|
6
|
+
/** 시도 카운터 키 TTL(초) — 데모 잡은 짧게 살고 사라진다. */
|
|
7
|
+
const ATTEMPT_TTL = 600
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* EmailJob — /demo/jobs 데모 잡(ADR-028/119). `mega worker` 프로세스(ecosystem instances:2)가 소비한다
|
|
11
|
+
* (config.jobs). 실제 메일은 보내지 않고 발송을 **시뮬레이션**하며, payload.mode 로 세 가지 흐름을 시연한다:
|
|
12
|
+
*
|
|
13
|
+
* - `ok` : 1번째 시도에 바로 성공(sent).
|
|
14
|
+
* - `flaky` : 1번째 시도는 throw(일시 실패) → MegaJobQueue 가 재시도(p-retry, static retries/backoff) →
|
|
15
|
+
* 2번째 시도에 성공. "일시 오류는 그냥 throw 하면 재시도된다"(MegaJob 정본)를 보여준다.
|
|
16
|
+
* - `fail` : 매 시도 throw(영구 실패) → 재시도 소진 후 DLQ(`<subject>.dlq`)로 격리된다.
|
|
17
|
+
*
|
|
18
|
+
* 각 시도를 'demo' 캐시(redis)에 기록해(시도 카운터 + 이벤트 LIST) 웹 페이지가 처리 타임라인을 보여준다.
|
|
19
|
+
* NATS durable consumer group(같은 durable 이름)이라 instances:2 워커가 메시지를 자연 분산 처리한다(중복 X).
|
|
20
|
+
*/
|
|
21
|
+
export class EmailJob extends MegaJob {
|
|
22
|
+
static subject = 'demo.email'
|
|
23
|
+
static bus = 'jobs'
|
|
24
|
+
static concurrency = 2
|
|
25
|
+
// 추가 재시도 2회(첫 시도 포함 최대 3회). 데모 체감용으로 백오프를 짧게 둔다(기본 1s~30s 대신 0.5s~2s).
|
|
26
|
+
static retries = 2
|
|
27
|
+
static backoff = { type: 'exponential', initial: 500, max: 2000 }
|
|
28
|
+
|
|
29
|
+
/** 이벤트 LIST 키(LPUSH → 최신이 앞). 웹이 LRANGE 로 읽는다. */
|
|
30
|
+
static EVENTS_KEY = 'demo:jobs:events'
|
|
31
|
+
/** 이벤트 보관 최대 건수. */
|
|
32
|
+
static EVENTS_MAX = 30
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 메시지 1건 처리. throw 하면 MegaJobQueue 가 재시도/DLQ 를 담당하므로 일시·영구 실패는 그냥 throw 한다.
|
|
36
|
+
* @param {{ id: string, to: string, subject?: string, mode: 'ok'|'flaky'|'fail' }} payload
|
|
37
|
+
* @param {Record<string, any>} ctx - worker 프로세스 컨텍스트(ctx.cache('demo') 글로벌 키).
|
|
38
|
+
* @returns {Promise<{ id: string, status: 'sent', attempt: number }>}
|
|
39
|
+
* @throws {Error} flaky 의 1번째 시도, fail 의 모든 시도 — 재시도/DLQ 트리거.
|
|
40
|
+
*/
|
|
41
|
+
async run(payload, ctx) {
|
|
42
|
+
const { id, to, mode } = payload
|
|
43
|
+
const redis = ctx.cache('demo').native
|
|
44
|
+
// 시도 카운터를 원자적으로 올린다 — 재시도(같은 메시지 재실행)마다 1씩 증가해 흐름을 추적한다.
|
|
45
|
+
const attempt = await redis.incr(`demo:jobs:attempt:${id}`)
|
|
46
|
+
await redis.expire(`demo:jobs:attempt:${id}`, ATTEMPT_TTL)
|
|
47
|
+
ctx.log?.debug?.({ id, mode, attempt }, 'email-job.run')
|
|
48
|
+
|
|
49
|
+
if (mode === 'flaky' && attempt < FLAKY_SUCCEED_ON) {
|
|
50
|
+
await EmailJob.#logEvent(redis, { id, to, mode, attempt, status: 'retry' })
|
|
51
|
+
throw new Error(`EmailJob ${id}: simulated transient failure on attempt ${attempt} (will retry)`)
|
|
52
|
+
}
|
|
53
|
+
if (mode === 'fail') {
|
|
54
|
+
await EmailJob.#logEvent(redis, { id, to, mode, attempt, status: 'failed' })
|
|
55
|
+
throw new Error(`EmailJob ${id}: simulated permanent failure on attempt ${attempt} (bound for DLQ)`)
|
|
56
|
+
}
|
|
57
|
+
// ok, 또는 flaky 가 충분히 재시도된 뒤 → 발송 성공.
|
|
58
|
+
await EmailJob.#logEvent(redis, { id, to, mode, attempt, status: 'sent' })
|
|
59
|
+
return { id, status: 'sent', attempt }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 처리 이벤트 1건을 redis LIST 머리에 넣고 최근 N건만 남긴다.
|
|
64
|
+
* @param {any} redis - ioredis 핸들(ctx.cache('demo').native).
|
|
65
|
+
* @param {{ id: string, to: string, mode: string, attempt: number, status: 'retry'|'failed'|'sent' }} event
|
|
66
|
+
* @returns {Promise<void>}
|
|
67
|
+
*/
|
|
68
|
+
static async #logEvent(redis, event) {
|
|
69
|
+
await redis.lpush(EmailJob.EVENTS_KEY, JSON.stringify({ ...event, at: new Date().toISOString() }))
|
|
70
|
+
await redis.ltrim(EmailJob.EVENTS_KEY, 0, EmailJob.EVENTS_MAX - 1)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nav_metrics": "Metrics",
|
|
3
|
+
"nav_tracing": "Tracing",
|
|
4
|
+
"nav_logs": "Logs",
|
|
5
|
+
"nav_upload": "Upload",
|
|
6
|
+
"nav_docs": "API Docs",
|
|
7
|
+
"nav_guide": "Guide",
|
|
8
|
+
"nav_perf": "Performance",
|
|
9
|
+
"nav_demo": "Demo",
|
|
10
|
+
"perf_title": "Performance Benchmark",
|
|
11
|
+
"perf_subtitle": "Measures throughput and latency of core framework surfaces (HTTP, crypto, DB, cache, session) directly with Node's built-in perf_hooks.",
|
|
12
|
+
"perf_form_title": "Run Benchmark",
|
|
13
|
+
"perf_form_desc": "Pick a scenario and iteration count; per-iteration latency is collected to compute percentiles.",
|
|
14
|
+
"perf_scenario_label": "Scenario",
|
|
15
|
+
"perf_iterations_label": "Iterations",
|
|
16
|
+
"perf_concurrency_label": "Concurrency",
|
|
17
|
+
"perf_payload_label": "Payload (bytes)",
|
|
18
|
+
"perf_limit_hint": "Leave concurrency/payload blank to use per-scenario defaults. Each scenario has iteration/concurrency caps for safety; exceeding them is clamped automatically and flagged in the result.",
|
|
19
|
+
"perf_run_btn": "Run",
|
|
20
|
+
"perf_clear_btn": "Clear results",
|
|
21
|
+
"perf_error": "Run failed:",
|
|
22
|
+
"perf_results_title": "Results",
|
|
23
|
+
"perf_results_desc": "Each run prepends a row. Lower time (ms) is better; higher RPS is better.",
|
|
24
|
+
"perf_results_empty": "No runs yet.",
|
|
25
|
+
"perf_col_scenario": "Scenario",
|
|
26
|
+
"perf_col_iter": "Iter",
|
|
27
|
+
"perf_col_conc": "Conc",
|
|
28
|
+
"perf_col_duration": "Duration(ms)",
|
|
29
|
+
"perf_col_rps": "RPS",
|
|
30
|
+
"perf_col_avg": "Avg(ms)",
|
|
31
|
+
"perf_col_min": "Min",
|
|
32
|
+
"perf_col_max": "Max",
|
|
33
|
+
"perf_col_okfail": "OK/Fail",
|
|
34
|
+
"guide_index_title": "Guide",
|
|
35
|
+
"guide_index_subtitle": "A collection of MEGA-FRAMEWORK usage guides.",
|
|
36
|
+
"guide_index_welcome": "Pick a topic to read. Code samples are pre-highlighted on the server.",
|
|
37
|
+
"guide_toc": "Contents",
|
|
38
|
+
"guide_back": "Back to list",
|
|
39
|
+
"metrics_title": "Metrics",
|
|
40
|
+
"metrics_subtitle": "Prometheus /metrics counters shown as human-friendly cards (this web process).",
|
|
41
|
+
"metrics_disabled": "Metrics are disabled. Check health.exposeMetrics in mega.config.js.",
|
|
42
|
+
"metrics_http_title": "HTTP requests",
|
|
43
|
+
"metrics_http_desc": "Cumulative request count by status class.",
|
|
44
|
+
"metrics_http_total": "Total requests",
|
|
45
|
+
"metrics_process_title": "Process",
|
|
46
|
+
"metrics_process_desc": "Memory, uptime and CPU usage of this web process.",
|
|
47
|
+
"metrics_process_heap": "Heap used",
|
|
48
|
+
"metrics_process_rss": "RSS",
|
|
49
|
+
"metrics_process_uptime": "Uptime",
|
|
50
|
+
"metrics_process_cpu": "CPU time",
|
|
51
|
+
"metrics_jobs_title": "Job queue",
|
|
52
|
+
"metrics_jobs_desc": "Cumulative job processing events (linked to the jobs demo).",
|
|
53
|
+
"metrics_jobs_enqueued": "Enqueued",
|
|
54
|
+
"metrics_jobs_processed": "Processed",
|
|
55
|
+
"metrics_jobs_retried": "Retried",
|
|
56
|
+
"metrics_jobs_dlq": "DLQ",
|
|
57
|
+
"metrics_ws_title": "WebSocket",
|
|
58
|
+
"metrics_ws_desc": "Cumulative WS messages (linked to the chat demo).",
|
|
59
|
+
"metrics_ws_total": "Total messages",
|
|
60
|
+
"metrics_ws_empty": "No WS messages yet.",
|
|
61
|
+
"metrics_routes_title": "Requests by route",
|
|
62
|
+
"metrics_routes_desc": "Top routes by request count.",
|
|
63
|
+
"metrics_routes_empty": "No requests recorded yet.",
|
|
64
|
+
"metrics_routes_route": "Route",
|
|
65
|
+
"metrics_routes_count": "Count",
|
|
66
|
+
"metrics_raw_hint": "Raw (Prometheus scrape format):",
|
|
67
|
+
"tracing_title": "Distributed tracing",
|
|
68
|
+
"tracing_subtitle": "Links the current request's trace_id and recent traces to Zipkin.",
|
|
69
|
+
"tracing_notice_generated": "Trace generated. It will appear in Zipkin shortly.",
|
|
70
|
+
"tracing_disabled": "Tracing is disabled. Set MEGA_OTEL_ENABLED=true in .env and restart.",
|
|
71
|
+
"tracing_current_title": "Current request trace",
|
|
72
|
+
"tracing_current_desc": "The trace_id of the request that opened this page (also sent as the x-trace-id response header).",
|
|
73
|
+
"tracing_trace_id": "trace_id",
|
|
74
|
+
"tracing_span_id": "span_id",
|
|
75
|
+
"tracing_open_zipkin": "Open in Zipkin",
|
|
76
|
+
"tracing_no_current": "No current trace (tracing disabled).",
|
|
77
|
+
"tracing_generate": "Generate trace",
|
|
78
|
+
"tracing_generate_hint": "Runs a DB ping inside a user span to build a multi-level span tree.",
|
|
79
|
+
"tracing_zipkin_title": "Zipkin",
|
|
80
|
+
"tracing_zipkin_desc": "Traces are exported via OTLP → collector → Zipkin.",
|
|
81
|
+
"tracing_service": "Service name",
|
|
82
|
+
"tracing_ui": "Zipkin UI",
|
|
83
|
+
"tracing_export_hint": "Export has some latency, so a freshly generated trace appears in Zipkin after a short delay.",
|
|
84
|
+
"tracing_recent_title": "Recent traces",
|
|
85
|
+
"tracing_recent_desc": "Recently recorded trace_ids.",
|
|
86
|
+
"tracing_recent_empty": "No traces recorded yet.",
|
|
87
|
+
"tracing_recent_at": "Time",
|
|
88
|
+
"tracing_recent_route": "Route",
|
|
89
|
+
"tracing_view": "View",
|
|
90
|
+
"logs_title": "Structured logging",
|
|
91
|
+
"logs_subtitle": "Emit pino logs and see trace_id correlation and secret masking.",
|
|
92
|
+
"logs_notice_emitted": "Log emitted. Check the structured output in the server console.",
|
|
93
|
+
"logs_emit_title": "Emit log",
|
|
94
|
+
"logs_emit_desc": "Sends one log line at the chosen level through the real logger.",
|
|
95
|
+
"logs_level": "Level",
|
|
96
|
+
"logs_message": "Message",
|
|
97
|
+
"logs_message_ph": "e.g. a demo log line",
|
|
98
|
+
"logs_emit_btn": "emit",
|
|
99
|
+
"logs_redact_hint": "token/password/secret fields in the log payload are masked as [Redacted] in console output, and the active trace_id is attached automatically (ADR-141). Check the server console.",
|
|
100
|
+
"logs_recent_title": "Recent emits",
|
|
101
|
+
"logs_recent_desc": "Safe metadata of emitted logs (secrets excluded).",
|
|
102
|
+
"logs_recent_empty": "No logs emitted yet.",
|
|
103
|
+
"logs_recent_at": "Time",
|
|
104
|
+
"logs_recent_trace": "trace_id",
|
|
105
|
+
"upload_title": "File upload",
|
|
106
|
+
"upload_subtitle": "Saves multipart uploads to a project folder and shows metadata + download links.",
|
|
107
|
+
"upload_form_title": "Upload",
|
|
108
|
+
"upload_form_desc": "Upload image/PDF/text files (gated by MIME, size and count).",
|
|
109
|
+
"upload_file_label": "Choose files",
|
|
110
|
+
"upload_constraints": "Allowed: image/PDF/text · max 5MB · up to 3 at a time",
|
|
111
|
+
"upload_submit": "Upload",
|
|
112
|
+
"upload_error": "Upload failed:",
|
|
113
|
+
"upload_recent_title": "Recent uploads",
|
|
114
|
+
"upload_recent_desc": "Metadata of saved files (click a filename to download).",
|
|
115
|
+
"upload_recent_empty": "No files uploaded yet.",
|
|
116
|
+
"upload_dir_hint": "Saved under (relative to project root):",
|
|
117
|
+
"upload_recent_at": "Time",
|
|
118
|
+
"upload_recent_name": "Filename",
|
|
119
|
+
"upload_recent_path": "Saved path",
|
|
120
|
+
"upload_recent_mime": "MIME",
|
|
121
|
+
"upload_recent_size": "Size",
|
|
122
|
+
"nav_home": "Home",
|
|
123
|
+
"nav_users": "Users",
|
|
124
|
+
"theme_toggle": "Toggle theme",
|
|
125
|
+
"footer_built": "Built with MEGA-FRAMEWORK · Bootstrap 5",
|
|
126
|
+
"home_title": "User CRUD admin",
|
|
127
|
+
"home_subtitle": "Create, read, update, and delete a postgres-backed user resource through a Bootstrap 5 admin UI. The same service layer is also exposed as a JSON REST API.",
|
|
128
|
+
"home_cta_manage": "Manage users",
|
|
129
|
+
"home_cta_api": "JSON API (/users)",
|
|
130
|
+
"home_api_note": "Every request follows the canonical layer flow:",
|
|
131
|
+
"users_title": "Users",
|
|
132
|
+
"users_new": "Add user",
|
|
133
|
+
"users_empty": "No users yet.",
|
|
134
|
+
"col_id": "ID",
|
|
135
|
+
"col_name": "Name",
|
|
136
|
+
"col_email": "Email",
|
|
137
|
+
"col_created": "Created",
|
|
138
|
+
"col_actions": "Actions",
|
|
139
|
+
"action_edit": "Edit",
|
|
140
|
+
"action_delete": "Delete",
|
|
141
|
+
"delete_confirm_title": "Confirm delete",
|
|
142
|
+
"delete_confirm_body": "Really delete?",
|
|
143
|
+
"delete_confirm_cancel": "Cancel",
|
|
144
|
+
"delete_confirm_ok": "Delete",
|
|
145
|
+
"new_title": "Add user",
|
|
146
|
+
"edit_title": "Edit user",
|
|
147
|
+
"field_name": "Name",
|
|
148
|
+
"field_name_ph": "e.g. Ada Lovelace",
|
|
149
|
+
"field_name_required": "Please enter a name.",
|
|
150
|
+
"field_email": "Email",
|
|
151
|
+
"field_email_ph": "e.g. ada@example.com",
|
|
152
|
+
"field_email_required": "Please enter a valid email.",
|
|
153
|
+
"btn_create": "Create",
|
|
154
|
+
"btn_save": "Save",
|
|
155
|
+
"btn_cancel": "Cancel",
|
|
156
|
+
"notice_created": "User created.",
|
|
157
|
+
"notice_updated": "User updated.",
|
|
158
|
+
"notice_deleted": "User deleted.",
|
|
159
|
+
"field_password": "Password",
|
|
160
|
+
"field_password_hint": "Use at least 8 characters.",
|
|
161
|
+
"login_title": "Sign in",
|
|
162
|
+
"login_failed": "Incorrect email or password.",
|
|
163
|
+
"login_locked": "Too many attempts. Please try again later.",
|
|
164
|
+
"login_no_account": "Don't have an account?",
|
|
165
|
+
"register_title": "Sign up",
|
|
166
|
+
"register_have_account": "Already have an account?",
|
|
167
|
+
"btn_login": "Sign in",
|
|
168
|
+
"btn_logout": "Sign out",
|
|
169
|
+
"btn_register": "Sign up",
|
|
170
|
+
"auth_notice_logged_out": "You have been signed out.",
|
|
171
|
+
"auth_notice_registered": "Your account has been created.",
|
|
172
|
+
"nav_notes": "Notes (Mongo)",
|
|
173
|
+
"nav_redis": "Cache (Redis)",
|
|
174
|
+
"nav_ws": "Chat (WS+ASP)",
|
|
175
|
+
"notes_title": "Notes (MongoDB)",
|
|
176
|
+
"notes_subtitle": "Create, read, update, and delete a notes collection through the MongoDB document adapter.",
|
|
177
|
+
"notes_new": "Add note",
|
|
178
|
+
"notes_empty": "No notes yet.",
|
|
179
|
+
"notes_new_title": "Add note",
|
|
180
|
+
"notes_edit_title": "Edit note",
|
|
181
|
+
"notes_field_title": "Title",
|
|
182
|
+
"notes_field_title_ph": "e.g. Meeting memo",
|
|
183
|
+
"notes_field_title_required": "Please enter a title.",
|
|
184
|
+
"notes_field_body": "Body",
|
|
185
|
+
"notes_field_body_ph": "Write the content (optional).",
|
|
186
|
+
"notes_notice_created": "Note created.",
|
|
187
|
+
"notes_notice_updated": "Note updated.",
|
|
188
|
+
"notes_notice_deleted": "Note deleted.",
|
|
189
|
+
"redis_title": "Redis demo",
|
|
190
|
+
"redis_subtitle": "Demonstrates a visit counter (atomic INCR/EXPIRE) and a cached query result (GET/SET/TTL/DEL) via the Redis adapter.",
|
|
191
|
+
"redis_visits_title": "Visit counter",
|
|
192
|
+
"redis_visits_desc": "Each time you open this page the counter is atomically incremented. The daily counter auto-expires after 2 days.",
|
|
193
|
+
"redis_visits_total": "Total visits",
|
|
194
|
+
"redis_visits_today": "Today",
|
|
195
|
+
"redis_cache_title": "Cached query result",
|
|
196
|
+
"redis_cache_desc": "Caches the user count in Redis for 30s. A hit skips SQL; a miss recomputes from SQL.",
|
|
197
|
+
"redis_cache_hit": "Cache hit",
|
|
198
|
+
"redis_cache_miss": "Cache miss",
|
|
199
|
+
"redis_cache_ttl": "TTL left",
|
|
200
|
+
"redis_cache_reload": "Reload",
|
|
201
|
+
"redis_cache_clear": "Clear cache",
|
|
202
|
+
"redis_notice_cache_cleared": "Cache cleared.",
|
|
203
|
+
"home_demo_notes": "Mongo notes demo",
|
|
204
|
+
"home_demo_redis": "Redis cache demo",
|
|
205
|
+
"home_demo_ws": "WebSocket chat demo",
|
|
206
|
+
"ws_title": "Realtime chat (WebSocket + ASP)",
|
|
207
|
+
"ws_subtitle": "Connects to /ws/chat with the WASM MegaSocket. Messages are ASP-encrypted (E: frames), encrypted/decrypted in the browser, and broadcast to the whole channel by the server.",
|
|
208
|
+
"ws_asp_badge": "ASP encrypted",
|
|
209
|
+
"ws_asp_badge_title": "Every message is encrypted with AES-256-GCM (E: frames) in both directions.",
|
|
210
|
+
"ws_status_connecting": "Connecting…",
|
|
211
|
+
"ws_status_open": "Connected",
|
|
212
|
+
"ws_status_closed": "Disconnected",
|
|
213
|
+
"ws_status_error": "Error",
|
|
214
|
+
"ws_online": "{n} online",
|
|
215
|
+
"ws_presence_join": "{user} joined.",
|
|
216
|
+
"ws_presence_leave": "{user} left.",
|
|
217
|
+
"ws_empty": "No messages yet. Be the first to say hi.",
|
|
218
|
+
"ws_input_placeholder": "Type a message…",
|
|
219
|
+
"ws_send": "Send",
|
|
220
|
+
"ws_encrypted_note": "Messages are encrypted with ASP (AES-256-GCM) E: frames in transit.",
|
|
221
|
+
"nav_cron": "Scheduler (Cron)",
|
|
222
|
+
"nav_jobs": "Job Queue (NATS)",
|
|
223
|
+
"nav_worker": "Workers (Threads)",
|
|
224
|
+
"home_demo_cron": "Scheduler demo",
|
|
225
|
+
"home_demo_jobs": "Job queue demo",
|
|
226
|
+
"home_demo_worker": "CPU worker demo",
|
|
227
|
+
"cron_title": "Scheduler demo (MegaSchedule)",
|
|
228
|
+
"cron_subtitle": "The mega scheduler process bumps a Redis counter atomically every 30 seconds. Duplicate runs across instances are prevented by a distributed lock (redlock leader election).",
|
|
229
|
+
"cron_counter_title": "Run counter",
|
|
230
|
+
"cron_counter_desc": "Each scheduled run (or manual trigger below) increments a Redis counter by one.",
|
|
231
|
+
"cron_counter_total": "Total runs",
|
|
232
|
+
"cron_run_now": "Run now",
|
|
233
|
+
"cron_run_hint": "Runs the same job once immediately without waiting for the cron time (manual trigger).",
|
|
234
|
+
"cron_next_title": "Next run times",
|
|
235
|
+
"cron_next_desc": "The next five scheduled run times computed by croner (pure calculation, no timer).",
|
|
236
|
+
"cron_next_soonest": "soonest",
|
|
237
|
+
"cron_history_title": "Recent run history",
|
|
238
|
+
"cron_history_desc": "The last 10 runs are kept in a Redis LIST (LPUSH + LTRIM). Scheduled and manual runs stack together.",
|
|
239
|
+
"cron_history_empty": "No runs yet. Start the scheduler process or trigger a run manually above.",
|
|
240
|
+
"cron_history_at": "Run time",
|
|
241
|
+
"cron_history_source": "Source",
|
|
242
|
+
"cron_source_schedule": "schedule",
|
|
243
|
+
"cron_source_manual": "manual",
|
|
244
|
+
"cron_notice_triggered": "Ran the scheduled job once immediately.",
|
|
245
|
+
"jobs_title": "Job queue demo (MegaJob)",
|
|
246
|
+
"jobs_subtitle": "Enqueue an EmailJob to NATS JetStream and the mega worker process consumes it. Transient failures retry; permanent failures are isolated to the DLQ.",
|
|
247
|
+
"jobs_enqueue_title": "Enqueue an email job",
|
|
248
|
+
"jobs_enqueue_desc": "Nothing is actually sent — only processing is simulated. The mode demonstrates success / retry / permanent failure.",
|
|
249
|
+
"jobs_enqueue_btn": "Enqueue",
|
|
250
|
+
"jobs_reload": "Reload",
|
|
251
|
+
"jobs_field_to": "Recipient",
|
|
252
|
+
"jobs_field_mode": "Mode",
|
|
253
|
+
"jobs_mode_ok": "Success",
|
|
254
|
+
"jobs_mode_flaky": "Retry",
|
|
255
|
+
"jobs_mode_fail": "Permanent failure",
|
|
256
|
+
"jobs_mode_ok_hint": "succeeds on the first attempt",
|
|
257
|
+
"jobs_mode_flaky_hint": "1st attempt fails → retry → 2nd succeeds",
|
|
258
|
+
"jobs_mode_fail_hint": "every attempt fails → DLQ after retries are exhausted",
|
|
259
|
+
"jobs_dlq_title": "DLQ (isolated jobs)",
|
|
260
|
+
"jobs_dlq_desc": "Where permanently failed jobs land after retries are exhausted (a NATS stream). For analysis and reprocessing.",
|
|
261
|
+
"jobs_dlq_empty": "No jobs have reached the DLQ yet.",
|
|
262
|
+
"jobs_dlq_failed_at": "Failed at",
|
|
263
|
+
"jobs_dlq_deliveries": "Deliveries",
|
|
264
|
+
"jobs_dlq_error": "Error",
|
|
265
|
+
"jobs_dlq_payload": "Payload",
|
|
266
|
+
"jobs_events_title": "Processing events",
|
|
267
|
+
"jobs_events_desc": "The timeline the worker logs per attempt (newest first). Retries show the same job multiple times.",
|
|
268
|
+
"jobs_events_empty": "No jobs processed yet. Enqueue one above and start the mega worker process.",
|
|
269
|
+
"jobs_events_at": "Time",
|
|
270
|
+
"jobs_events_id": "Job ID",
|
|
271
|
+
"jobs_events_attempt": "Attempt",
|
|
272
|
+
"jobs_events_status": "Status",
|
|
273
|
+
"jobs_status_sent": "sent",
|
|
274
|
+
"jobs_status_retry": "retry",
|
|
275
|
+
"jobs_status_failed": "failed",
|
|
276
|
+
"jobs_notice_enqueued": "Job enqueued. Once a worker processes it, it appears in the events below.",
|
|
277
|
+
"worker_title": "CPU worker demo (MegaWorker)",
|
|
278
|
+
"worker_subtitle": "Runs CPU-bound work like N rounds of SHA-256 in a worker_threads pool. A heartbeat confirms the server still answers other requests instantly while computing (main thread non-blocking).",
|
|
279
|
+
"worker_run_title": "Run a hash computation",
|
|
280
|
+
"worker_run_desc": "Chain-hashes SHA-256 for the given number of rounds — on a worker thread, not the main thread.",
|
|
281
|
+
"worker_run_btn": "Run on worker",
|
|
282
|
+
"worker_rounds_label": "Rounds",
|
|
283
|
+
"worker_pool_mode": "mode",
|
|
284
|
+
"worker_pool_size": "pool size",
|
|
285
|
+
"worker_error": "Run failed:",
|
|
286
|
+
"worker_result_rounds": "Rounds",
|
|
287
|
+
"worker_result_ms": "Elapsed",
|
|
288
|
+
"worker_result_digest": "Final hash",
|
|
289
|
+
"worker_heartbeat_title": "Main-thread heartbeat",
|
|
290
|
+
"worker_heartbeat_desc": "Pings the server every second. If this latency stays small while the worker computes, the main thread is not blocked.",
|
|
291
|
+
"worker_heartbeat_count": "pings",
|
|
292
|
+
"worker_heartbeat_latency": "recent round-trip",
|
|
293
|
+
"worker_heartbeat_last": "last ping",
|
|
294
|
+
"worker_heartbeat_proof": "If the computation ran on the main thread, these pings would freeze until it finished. On a worker thread, they keep ticking.",
|
|
295
|
+
"server": {
|
|
296
|
+
"internal": "Internal server error"
|
|
297
|
+
},
|
|
298
|
+
"validation": {
|
|
299
|
+
"failed": "Validation failed."
|
|
300
|
+
},
|
|
301
|
+
"auth": {
|
|
302
|
+
"required": "Authentication required"
|
|
303
|
+
},
|
|
304
|
+
"upload": {
|
|
305
|
+
"unsupported_media_type": "Unsupported media type.",
|
|
306
|
+
"too_large": "File exceeds the size limit.",
|
|
307
|
+
"not_found": "File not found."
|
|
308
|
+
},
|
|
309
|
+
"guide": {
|
|
310
|
+
"not_found": "Guide not found."
|
|
311
|
+
},
|
|
312
|
+
"csrf": {
|
|
313
|
+
"invalid_token": "Invalid csrf token",
|
|
314
|
+
"missing_secret": "Missing csrf secret"
|
|
315
|
+
}
|
|
316
|
+
}
|