Záznamy v UI

This commit is contained in:
2026-04-20 21:23:00 +02:00
parent d3a68795c4
commit 9e7e868e7d
38 changed files with 433 additions and 187 deletions

260
package-lock.json generated
View File

@@ -11,7 +11,6 @@
"dependencies": {
"@kitajs/html": "^4.2.13",
"@kitajs/ts-html-plugin": "^4.1.4",
"@logdna/tail-file": "^4.0.2",
"@minify-html/node": "^0.18.1",
"apache-crypt": "^1.2.6",
"async-mutex": "^0.5.0",
@@ -30,6 +29,7 @@
"postgres-migrations": "^5.3.0",
"prettier": "^3.8.1",
"read": "^5.0.1",
"read-last-lines": "^1.8.0",
"sass": "^1.98.0",
"sharp": "^0.34.5",
"superjson": "^2.2.6",
@@ -96,16 +96,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/cli/node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -162,16 +152,6 @@
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
@@ -219,16 +199,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
@@ -251,16 +221,6 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-create-regexp-features-plugin": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz",
@@ -279,16 +239,6 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
@@ -1597,16 +1547,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/preset-env/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/preset-modules": {
"version": "0.1.6-no-external-plugins",
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
@@ -2961,15 +2901,6 @@
"typescript": "^5.9.3"
}
},
"node_modules/@logdna/tail-file": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@logdna/tail-file/-/tail-file-4.0.2.tgz",
"integrity": "sha512-aEZF3LanJoewsQ9J1CRk5wvOqvQvw4lhxitSMXz3sQPgfezamzwW+pGvKXxvht7EvJEYSlYPxfSjL6T2bDSEkg==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/@minify-html/node": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@minify-html/node/-/node-0.18.1.tgz",
@@ -3898,6 +3829,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
@@ -4026,6 +3970,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -4119,16 +4069,6 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
@@ -4164,9 +4104,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
"integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
"version": "2.10.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
"integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4675,9 +4615,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.339",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz",
"integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==",
"version": "1.5.340",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz",
"integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
"dev": true,
"license": "ISC"
},
@@ -5543,9 +5483,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -5889,9 +5829,9 @@
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5923,6 +5863,18 @@
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
@@ -6089,16 +6041,6 @@
"node": ">=6"
}
},
"node_modules/make-dir/node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
@@ -6195,6 +6137,17 @@
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -6296,6 +6249,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -6579,13 +6541,13 @@
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
"node": ">=6"
}
},
"node_modules/postcss": {
@@ -6695,6 +6657,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/postcss-cli/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/postcss-cli/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -7101,6 +7076,25 @@
"pify": "^2.3.0"
}
},
"node_modules/read-cache/node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/read-last-lines": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/read-last-lines/-/read-last-lines-1.8.0.tgz",
"integrity": "sha512-oPL0cnZkhsO2xF7DBrdzVhXSNajPP5TzzCim/2IAjeGb17ArLLTRriI/ceV6Rook3L27mvbrOvLlf9xYYnaftQ==",
"license": "MIT",
"dependencies": {
"mz": "^2.7.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -7316,15 +7310,13 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
@@ -7422,6 +7414,18 @@
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7541,16 +7545,13 @@
}
},
"node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=6"
}
},
"node_modules/source-map-js": {
@@ -7680,6 +7681,27 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
}
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/timestring": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/timestring/-/timestring-7.0.0.tgz",

View File

@@ -56,7 +56,6 @@
"dependencies": {
"@kitajs/html": "^4.2.13",
"@kitajs/ts-html-plugin": "^4.1.4",
"@logdna/tail-file": "^4.0.2",
"@minify-html/node": "^0.18.1",
"apache-crypt": "^1.2.6",
"async-mutex": "^0.5.0",
@@ -75,6 +74,7 @@
"postgres-migrations": "^5.3.0",
"prettier": "^3.8.1",
"read": "^5.0.1",
"read-last-lines": "^1.8.0",
"sass": "^1.98.0",
"sharp": "^0.34.5",
"superjson": "^2.2.6",

View File

@@ -1,7 +1,7 @@
import { loadConfig } from "../src/config.js";
import { initDb, postgres } from "../src/db/client.js";
import { createUser } from "../src/db/user.js";
import { czchanLog } from "../src/lib/logger.js";
import { czchanLog } from "../src/lib/log.js";
import bcrypt from "bcrypt";
import { read } from "read";

View File

@@ -1,4 +1,4 @@
import { czchanLog } from "../src/lib/logger.js";
import { czchanLog } from "../src/lib/log.js";
import { readdir, mkdir, writeFile } from "fs/promises";
import { parse } from "path";
@@ -16,7 +16,7 @@ await (async () => {
await czchanLog(
"build",
"info",
`Kód ${statusCode}: nalezeno ${images.length} obrázků `,
`Nalezeny obrázky pro kód ${statusCode}: celkem ${images.length} `,
);
errorImages[statusCode] = images.sort();

View File

@@ -9,7 +9,7 @@ import { cachePost } from "./db/cache/post.js";
import { cacheRestriction } from "./db/cache/restriction.js";
import { cacheUser } from "./db/cache/user.js";
import { postgres } from "./db/client.js";
import { czchanLog } from "./lib/logger.js";
import { czchanLog } from "./lib/log.js";
import type {
User,
Banner,

View File

@@ -1,5 +1,5 @@
import type { CzchanConfig } from "../config.js";
import { czchanLog } from "../lib/logger.js";
import { czchanLog } from "../lib/log.js";
import { Pool } from "pg";
import { migrate } from "postgres-migrations";

View File

@@ -1,6 +1,6 @@
import { readAllBans } from "../db/ban.js";
import { createCIDRIndex, createIndexedCIDR } from "./cidr.js";
import { czchanLog } from "./logger.js";
import { czchanLog } from "./log.js";
import { randomUUID } from "crypto";
import { readdir, readFile } from "fs/promises";
import ipaddr from "ipaddr.js";

106
src/lib/log.ts Normal file
View File

@@ -0,0 +1,106 @@
import { CzchanError } from "./error.js";
import { appendFile, readdir, stat } from "node:fs/promises";
import { join, parse } from "node:path";
import readLastLines from "read-last-lines";
type Log = {
name: string;
created: Date;
modified: Date;
size: number;
};
type LogLine = {
timestamp: Date;
log: string;
level: "info" | "warn" | "error";
message: string;
actor: string;
};
const czchanLog = async (
log: string,
level: "info" | "warn" | "error",
message: string,
actor?: string,
) => {
const now = new Date();
const timestamp = now.getTime();
const consoleLogLine = `[${now.toISOString()}] [${level}] [${log}] (${actor ?? "system"}) ${message}`;
const fileLogLine = `${JSON.stringify({
timestamp,
log,
level,
message,
actor: actor ?? "system",
})}\n`;
switch (level) {
case "info":
console.info(consoleLogLine);
break;
case "warn":
console.warn(consoleLogLine);
break;
case "error":
console.error(consoleLogLine);
break;
}
await appendFile(join("logs", `${log}.jsonl`), fileLogLine);
await appendFile(join("logs", `all.jsonl`), fileLogLine);
};
const readLogs = async () => {
const result = [];
const logs = (await readdir("logs")).map((log) => parse(log).name).sort();
for (const log of logs) {
const stats = await stat(join("logs", `${log}.jsonl`));
const name = log;
const created = stats.birthtime;
const modified = stats.mtime;
const size = stats.size;
result.push({ name, created, modified, size });
}
return result;
};
const readLogPage = async (log: string, page: number): Promise<LogLine[]> => {
const limit = 100;
const offset = page * limit;
let lines: string;
try {
lines = await readLastLines.read(
join("logs", `${log}.jsonl`),
offset + limit,
);
} catch (_) {
throw new CzchanError(`Záznam ${log} neexistuje.`, 404);
}
const splitLines = lines.split(/\r\n|\r|\n/g).filter(Boolean);
const end = Math.min(splitLines.length, splitLines.length - offset + limit);
if (end <= 0) {
return [];
}
return splitLines
.slice(-end)
.slice(-limit)
.map((line) => {
const entry = JSON.parse(line);
entry.timestamp = new Date(entry.timestamp);
return entry as LogLine;
})
.reverse();
};
export type { Log, LogLine };
export { czchanLog, readLogs, readLogPage };

View File

@@ -1,30 +0,0 @@
import { appendFile } from "node:fs/promises";
import { join } from "node:path";
const czchanLog = async (
log: string,
level: "info" | "warn" | "error",
message: string,
actor?: string,
) => {
const now = new Date();
const consoleLogLine = `[${now.toISOString()}] [${level}] [${log}] (${actor ?? "system"}) ${message}`;
const fileLogLine = `${now.getTime()},${log},${level},${message},${actor ?? "system"}\n`;
switch (level) {
case "info":
console.info(consoleLogLine);
break;
case "warn":
console.warn(consoleLogLine);
break;
case "error":
console.error(consoleLogLine);
break;
}
await appendFile(join("logs", `${log}.csv`), fileLogLine);
await appendFile(join("logs", `all.csv`), fileLogLine);
};
export { czchanLog };

View File

@@ -1,5 +1,5 @@
import { redis } from "../cache.js";
import { czchanLog } from "./logger.js";
import { czchanLog } from "./log.js";
import minifyHTML from "@minify-html/node";
import { readdir, readFile } from "node:fs/promises";
import { join, parse } from "node:path";

View File

@@ -1,7 +1,7 @@
import { redis } from "../cache.js";
import { createBan, readBans } from "../db/ban.js";
import { rectifyCIDR } from "./ip.js";
import { czchanLog } from "./logger.js";
import { czchanLog } from "./log.js";
import { randomUUID } from "crypto";
import ipaddr from "ipaddr.js";
import SuperJSON from "superjson";

View File

@@ -3,7 +3,7 @@ import { loadConfig } from "./config.js";
import { initDb } from "./db/client.js";
import { CzchanError } from "./lib/error.js";
import { initIPLists } from "./lib/ip.js";
import { czchanLog } from "./lib/logger.js";
import { czchanLog } from "./lib/log.js";
import { initPages } from "./lib/page.js";
import { pruneBans } from "./tasks/prune_bans.js";
import { pruneCaptchas } from "./tasks/prune_captchas.js";

View File

@@ -1,6 +1,6 @@
import { deleteBan } from "../db/ban.js";
import { postgres } from "../db/client.js";
import { czchanLog } from "../lib/logger.js";
import { czchanLog } from "../lib/log.js";
import type { Ban } from "../schema/tables.js";
const pruneBans = async () => {

View File

@@ -1,5 +1,5 @@
import { redis } from "../cache.js";
import { czchanLog } from "../lib/logger.js";
import { czchanLog } from "../lib/log.js";
import { readdir, unlink } from "fs/promises";
import { join, parse } from "path";

View File

@@ -5,7 +5,7 @@ import {
removeUsageFromArray,
} from "../db/file_record.js";
import { readPosts } from "../db/post.js";
import { czchanLog } from "../lib/logger.js";
import { czchanLog } from "../lib/log.js";
import { bytesToSize } from "../lib/util.js";
import type { Banner, Post } from "../schema/tables.js";
import { unlink } from "fs/promises";

View File

@@ -20,7 +20,7 @@ import {
updatePostLooped,
} from "../../../db/post.js";
import { CzchanError } from "../../../lib/error.js";
import { czchanLog } from "../../../lib/logger.js";
import { czchanLog } from "../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
import type { Board, Post } from "../../../schema/tables.js";
import { ZPostActionsForm } from "../../../schema/validation/post.js";

View File

@@ -5,7 +5,7 @@ import {
updateBanAppealResponse,
} from "../../../../db/ban.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import type { Ban } from "../../../../schema/tables.js";
import { BanActionsForm } from "../../../../schema/validation/ban.js";

View File

@@ -1,6 +1,6 @@
import { deleteBanner, readBanners } from "../../../../db/banner.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import type { Banner } from "../../../../schema/tables.js";
import { BannerActionsForm } from "../../../../schema/validation/banner.js";

View File

@@ -5,7 +5,7 @@ import {
updateBoardName,
} from "../../../../db/board.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import type { Board } from "../../../../schema/tables.js";
import {

View File

@@ -2,7 +2,7 @@ import { createBanner } from "../../../../db/banner.js";
import { addUsageToArray, readFileRecord } from "../../../../db/file_record.js";
import { CzchanError } from "../../../../lib/error.js";
import { processFiles } from "../../../../lib/file.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import type { FileRecord } from "../../../../schema/tables.js";
import type { Request, Response } from "express";

View File

@@ -1,6 +1,6 @@
import { createBoard } from "../../../../db/board.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import { CreateBoardForm } from "../../../../schema/validation/board.js";
import type { Request, Response } from "express";

View File

@@ -1,7 +1,7 @@
import { readBoard } from "../../../../db/board.js";
import { createFilter } from "../../../../db/filter.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import { CreateFilterForm } from "../../../../schema/validation/filter.js";
import type { Request, Response } from "express";

View File

@@ -1,7 +1,7 @@
import { createNews } from "../../../../db/news.js";
import { CzchanError } from "../../../../lib/error.js";
import { formatContent } from "../../../../lib/formatting.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import { CreateNewsForm } from "../../../../schema/validation/news.js";
import type { Request, Response } from "express";

View File

@@ -1,7 +1,7 @@
import { readBoard } from "../../../../db/board.js";
import { createRestriction } from "../../../../db/restriction.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import { CreateRestrictionForm } from "../../../../schema/validation/restriction.js";
import type { Request, Response } from "express";

View File

@@ -1,6 +1,6 @@
import { createUser } from "../../../../db/user.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import {
canUser,
canUserApplyRole,

View File

@@ -4,7 +4,7 @@ import {
updateFilterPriority,
} from "../../../../db/filter.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import type { Filter } from "../../../../schema/tables.js";
import {

View File

@@ -1,6 +1,6 @@
import { deleteNews, readNews } from "../../../../db/news.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import type { News } from "../../../../schema/tables.js";
import { NewsActionsForm } from "../../../../schema/validation/news.js";

View File

@@ -3,7 +3,7 @@ import {
readRestrictions,
} from "../../../../db/restriction.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import type { Restriction } from "../../../../schema/tables.js";
import { RestrictionActionsForm } from "../../../../schema/validation/restriction.js";

View File

@@ -1,6 +1,6 @@
import { readBoard, updateBoardConfig } from "../../../../db/board.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import {
PrimaryBoardConfigForm,

View File

@@ -1,7 +1,7 @@
import { readNewsPost, updateNews } from "../../../../db/news.js";
import { CzchanError } from "../../../../lib/error.js";
import { formatContent } from "../../../../lib/formatting.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../../lib/permissions.js";
import { UpdateNewsContentForm } from "../../../../schema/validation/news.js";
import type { Request, Response } from "express";

View File

@@ -1,6 +1,6 @@
import { readUser, updateUserPermissions } from "../../../../db/user.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import {
canUser,
canUserActOn,

View File

@@ -6,7 +6,7 @@ import {
updateUserRank,
} from "../../../../db/user.js";
import { CzchanError } from "../../../../lib/error.js";
import { czchanLog } from "../../../../lib/logger.js";
import { czchanLog } from "../../../../lib/log.js";
import {
canUser,
canUserActOn,

101
src/web/routes/mod/log.tsx Normal file
View File

@@ -0,0 +1,101 @@
import { CzchanError } from "../../../lib/error.js";
import { type LogLine, readLogPage } from "../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
import { zPage } from "../../../schema/validation/common.js";
import Page from "../../components/chrome/page.js";
import PageBody from "../../components/chrome/page_body.js";
import PageHead from "../../components/chrome/page_head.js";
import Datetime from "../../components/primitives/datetime.js";
import { getCtx, type TemplateCtx } from "../../ctx.js";
import logs from "./logs.js";
import { type PropsWithChildren } from "@kitajs/html";
import type { Request, Response } from "express";
export default async (req: Request, res: Response) => {
const ctx = await getCtx(req);
if (!canUser(ctx.user, CzchanPerm.VIEW_LOGS)) {
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
}
const log = req.params.log.toString();
const page = zPage.parse(req.query.page ?? "1");
const lines = await readLogPage(log, page);
const html = Template(ctx, page, log, lines);
res.send(html);
};
const Template = (
ctx: TemplateCtx,
page: number,
log: string,
lines: LogLine[],
) => (
<Page>
<PageHead ctx={ctx} title={`Záznam (${log})`} description="" />
<PageBody ctx={ctx}>
<h1 class="title center" safe>
Záznam ({log})
</h1>
<hr />
<LogNav log={log} page={page} />
<hr />
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Datum</th>
<th>Záznam</th>
<th>Úroveň</th>
<th>Zpráva</th>
<th>Činitel</th>
</tr>
</thead>
<tbody>
{lines.map((line) => (
<tr>
<td>
<Datetime ctx={ctx} date={line.timestamp} />
</td>
<td safe>{line.log}</td>
<td>{line.level}</td>
<td safe>{line.message}</td>
<td safe>{line.actor}</td>
</tr>
))}
{logs.length === 0 && (
<tr>
<td class="center" colspan="5">
Nic tady není...
</td>
</tr>
)}
</tbody>
</table>
</div>
<hr />
<LogNav log={log} page={page} />
<hr />
</PageBody>
</Page>
);
const LogNav = ({
log,
page,
}: PropsWithChildren<{ log: string; page: number }>) => (
<nav class="pagination">
[
{page === 1 ? (
<span>Předchozí</span>
) : (
<a href={`/mod/log/${log}?page=${page - 1}`}>Předchozí</a>
)}
] [
<b>
<a href={`/mod/log/${log}?page=${page}`}>{page}</a>
</b>
] [<a href={`/mod/log/${log}?page=${page + 1}`}>Další</a>]
</nav>
);

View File

@@ -1,9 +1,12 @@
import { CzchanError } from "../../../lib/error.js";
import { type Log, readLogs } from "../../../lib/log.js";
import { canUser, CzchanPerm } from "../../../lib/permissions.js";
import { bytesToSize } from "../../../lib/util.js";
import Page from "../../components/chrome/page.js";
import PageBody from "../../components/chrome/page_body.js";
import PageHead from "../../components/chrome/page_head.js";
import ModNav from "../../components/navigation/mod_nav.js";
import Datetime from "../../components/primitives/datetime.js";
import { getCtx, type TemplateCtx } from "../../ctx.js";
import type { Request, Response } from "express";
@@ -14,20 +17,58 @@ export default async (req: Request, res: Response) => {
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
}
const html = Template(ctx);
const logs = await readLogs();
const html = Template(ctx, logs);
res.send(html);
};
const Template = (ctx: TemplateCtx) => (
const Template = (ctx: TemplateCtx, logs: Log[]) => (
<Page>
<PageHead ctx={ctx} title="Záznamy" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Záznamy</h1>
<p class="subtitle center">[TBI]</p>
<hr />
<ModNav ctx={ctx} />
<hr />
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Záznam</th>
<th>Vytvořen</th>
<th>Upraven</th>
<th>Velikost</th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr>
<td>
<a href={`/mod/log/${log.name}`} safe>
{log.name}
</a>
</td>
<td>
<Datetime ctx={ctx} date={log.created} />
</td>
<td>
<Datetime ctx={ctx} date={log.modified} />
</td>
<td safe>{bytesToSize(log.size)}</td>
</tr>
))}
{logs.length === 0 && (
<tr>
<td class="center" colspan="4">
Nic tady není...
</td>
</tr>
)}
</tbody>
</table>
</div>
<hr />
</PageBody>
</Page>
);

View File

@@ -15,14 +15,18 @@ export default async (req: Request, res: Response) => {
throw new CzchanError(`Stránka ${pageName} neexistuje`, 404);
}
const html = Template(ctx, pageHtml);
const html = Template(ctx, pageName, pageHtml);
res.send(html);
};
const Template = (ctx: TemplateCtx, pageHtml: string) => (
const Template = (ctx: TemplateCtx, pageName: string, pageHtml: string) => (
<Page>
<PageHead ctx={ctx} title="Přihlásit se" description="" />
<PageHead
ctx={ctx}
title={`${ctx.config.site.ui.name} - Stránka (${pageName})`}
description=""
/>
<PageBody ctx={ctx}>{pageHtml as "safe"}</PageBody>
</Page>
);

View File

@@ -24,6 +24,7 @@ import modEditUserPermissions from "../routes/mod/edit/user_permissions.js";
import modFilters from "../routes/mod/filters.js";
import modIndex from "../routes/mod/index.js";
import modIP from "../routes/mod/ip.js";
import modLog from "../routes/mod/log.js";
import modLogs from "../routes/mod/logs.js";
import modNews from "../routes/mod/news.js";
import modPostInfo from "../routes/mod/post_info.js";
@@ -45,6 +46,7 @@ const registerModRoutes = (router: Router) => {
router.get("/mod/restrictions", e(modRestrictions));
router.get("/mod/filters", e(modFilters));
router.get("/mod/logs", e(modLogs));
router.get("/mod/log/:log", e(modLog));
// Mod info
router.get("/mod/ip/:ip", e(modIP));

View File

@@ -1,5 +1,5 @@
import { CzchanError } from "../../lib/error.js";
import { czchanLog } from "../../lib/logger.js";
import { czchanLog } from "../../lib/log.js";
import Page from "../components/chrome/page.js";
import PageBody from "../components/chrome/page_body.js";
import PageHead from "../components/chrome/page_head.js";

View File

@@ -20,7 +20,7 @@ const autoUpdate = () => {
const infoContainer = $("#update-info-container");
infoContainer.append(
'<hr><div class="updater"><span id="update-info"></span> <a href="javascript:void(0)" id="update">[Aktualizovat]</a> <label>[ Auto? <input type="checkbox" id="do-update" checked=""> ]</label></div>',
'<hr><nav class="updater"><span id="update-info"></span> <a href="javascript:void(0)" id="update">[Aktualizovat]</a> <label>[ Auto? <input type="checkbox" id="do-update" checked=""> ]</label></nav>',
);
let seconds = 0;