mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
809 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db6cb6cf7d | |||
| babeeb75b2 | |||
| e0187605e7 | |||
| e2861f1e59 | |||
| 01aa4d9984 | |||
| 555372f175 | |||
| bd5fd409d1 | |||
| f61d82e13f | |||
| f39588fff7 | |||
| 0716f271df | |||
| 1085c3e561 | |||
| 93907e6d96 | |||
| d03fd63c64 | |||
| 3ff43af787 | |||
| 7f2fef8e8e | |||
| 7658170865 | |||
| b1f8a505a5 | |||
| 104da58646 | |||
| ed6e97e8c9 | |||
| 0b25274874 | |||
| bb8ab41bfe | |||
| b43a5fb343 | |||
| 554caa5018 | |||
| ffe7403043 | |||
| a26d0ece59 | |||
| a4277f7ed0 | |||
| f75aaf9cef | |||
| 4e1ecf41ee | |||
| f23fb5313c | |||
| 18489c5339 | |||
| 4044bae26d | |||
| 02686caa78 | |||
| 797aef80d6 | |||
| a8841ad8a3 | |||
| 0b2cd5fc8a | |||
| 9d349716e5 | |||
| 92d068ef49 | |||
| c6e3620151 | |||
| d92165d0f8 | |||
| a56bbead8d | |||
| db3fb923f0 | |||
| bb9d88a095 | |||
| 912da6b8f4 | |||
| 3b07623346 | |||
| 702ceca9b0 | |||
| 21f2433919 | |||
| 4e7791b104 | |||
| f79c24c80d | |||
| 087c2f25fb | |||
| 36f2ae1f69 | |||
| dcd830eb82 | |||
| 80637fd4aa | |||
| a7f0072f32 | |||
| a39e676a23 | |||
| fbc926036e | |||
| 5b508d19bc | |||
| 589281ce13 | |||
| 36aec8c519 | |||
| 978bfb05f2 | |||
| 3ffd0538a1 | |||
| 932a05a9ee | |||
| 15c62fa40f | |||
| a5afd38520 | |||
| 5138f7fb6e | |||
| 1bed0538db | |||
| bd582eaad3 | |||
| 5803ccadca | |||
| 4f16b06a0f | |||
| fad23e2a01 | |||
| 4b84e47d89 | |||
| 9e020c10f4 | |||
| 6fd47963ec | |||
| df9b36b242 | |||
| f9fe74ef94 | |||
| 823ea6eeb7 | |||
| 6c7606d3b0 | |||
| d2e9437cfa | |||
| a549eb5d1b | |||
| 648f8180e7 | |||
| 9afb8947ac | |||
| 0c8338e2a5 | |||
| df94755c6b | |||
| 204419fccb | |||
| a973494bc8 | |||
| ba459e61ad | |||
| f92fc8ff73 | |||
| 91cb4850ca | |||
| fb7939a4dc | |||
| 1f352591a4 | |||
| 0085621900 | |||
| 0eb789d89a | |||
| 7f21b27bb8 | |||
| 7abd6788de | |||
| 681b116e09 | |||
| 04b1d0716b | |||
| 668f12a0eb | |||
| 394149a3d6 | |||
| 57c8ff28ab | |||
| 5b29af9e74 | |||
| 9f5b90cb93 | |||
| e4f001acf7 | |||
| 604dbccaea | |||
| 6b7d0335b7 | |||
| b55dd29f02 | |||
| dcd7918a08 | |||
| 38e3a538ef | |||
| 19c2275a7b | |||
| a53251454e | |||
| 8f39a0f649 | |||
| 674c310b67 | |||
| a550afca60 | |||
| e64d955ecb | |||
| 57fda67ed7 | |||
| 6d5a42aeb2 | |||
| 5d9160c36d | |||
| b465f48c6e | |||
| 8c16d99f55 | |||
| fdedbb78cf | |||
| 416beca1a6 | |||
| e261d3620e | |||
| 1089133b97 | |||
| 90de4ae4cb | |||
| 4e263284c8 | |||
| 343368631f | |||
| f5f40e1438 | |||
| 74f6601f5d | |||
| c4e03c1a83 | |||
| bb9124db0d | |||
| ce52506c95 | |||
| f20b74cb36 | |||
| f3a5839aae | |||
| 0896aed91e | |||
| d8ae54da53 | |||
| 66bf012c87 | |||
| 08fc54d2ea | |||
| b36e68f7c8 | |||
| 045f32b7ce | |||
| 7ae869d98a | |||
| efb04a4238 | |||
| e6e586f6de | |||
| e13d7fb1f4 | |||
| 23b0bae018 | |||
| e581346f84 | |||
| 555fd3a8a2 | |||
| 7a29d2f2ca | |||
| 7a3fe20083 | |||
| c16c119d65 | |||
| 048a12899d | |||
| 5fd0e691b3 | |||
| 28932f8df5 | |||
| 4da29ac4e1 | |||
| 396fc5e3c3 | |||
| af8daf98f6 | |||
| 2e91f90057 | |||
| 3213ab8efa | |||
| f2308d9bd1 | |||
| 623465ff50 | |||
| 7c224b3886 | |||
| 166073fbd4 | |||
| 373cbd5917 | |||
| 2c27859197 | |||
| bb5d58b675 | |||
| caca29d354 | |||
| 35f0adb235 | |||
| 8bcff45d87 | |||
| 3ad256a626 | |||
| c4b9181a0a | |||
| 1ba40fadf6 | |||
| 20f1481a01 | |||
| 828747876b | |||
| cbe0ea683f | |||
| 561864c625 | |||
| 3abdb5c17a | |||
| f2ef19e3d8 | |||
| 78b18a59ec | |||
| c28581b51d | |||
| f9486927de | |||
| 86645d9ea3 | |||
| feea1d76cd | |||
| 2e95f31d93 | |||
| 18cf8f51b7 | |||
| 9af842ebc0 | |||
| 3d157f1c20 | |||
| b970c5bba1 | |||
| 7e7c0eb429 | |||
| f0ab8f25e7 | |||
| 87896ce2a9 | |||
| fae9f38d90 | |||
| 1df62bc315 | |||
| 9199b807d5 | |||
| 3363e90007 | |||
| 78bad84f0e | |||
| 50482aa21d | |||
| 339d653bab | |||
| 585db91263 | |||
| cbc48d2ddc | |||
| 1c6bed1297 | |||
| bf126e7151 | |||
| e5e244e2dc | |||
| 4b23e729c1 | |||
| c9c675da28 | |||
| 1c485ac376 | |||
| 9a8bf8431b | |||
| 3aedb9be11 | |||
| 58f0e9f98a | |||
| 88072b7de7 | |||
| 9129e9606c | |||
| b088a79143 | |||
| 14d46cb3de | |||
| 5989e5b8cb | |||
| 7b3afa4467 | |||
| d758de0752 | |||
| e6424b1d66 | |||
| 5a95c81574 | |||
| d170ba69b7 | |||
| d04ee0a933 | |||
| 444fe5890a | |||
| d1eb9266ea | |||
| 275c9d7ff4 | |||
| 552bdc09ad | |||
| 09afbbd17d | |||
| 6ea045d152 | |||
| 8fd8aa4bcf | |||
| 366c2c0a22 | |||
| 21f06a4143 | |||
| d99da146bd | |||
| 290113d553 | |||
| 1f370b3eea | |||
| 17ed56c682 | |||
| 2377a4faa6 | |||
| 32081056c7 | |||
| 5f86ca27e1 | |||
| c58530cda3 | |||
| 9b99bec50b | |||
| 8b189b686e | |||
| 430733bd41 | |||
| 6699887a75 | |||
| 8a498e902d | |||
| eed71d3633 | |||
| 228808a267 | |||
| ee1adf8330 | |||
| 9e8b0962a0 | |||
| 72a28cac61 | |||
| c759ade7b9 | |||
| 7392b968ef | |||
| 7f328023ea | |||
| 4ff1604044 | |||
| be0745f2f7 | |||
| cdc0de9405 | |||
| 47c00c2837 | |||
| baa9449b8c | |||
| 1c05e2d7da | |||
| a20125a6ed | |||
| 845132fc58 | |||
| 038c52ad7d | |||
| 6aa43fa261 | |||
| 6ed95ed8dc | |||
| ba41c6f2a7 | |||
| 96fd8d0874 | |||
| 9a0ca6cde4 | |||
| 2d7d6a3d77 | |||
| 08f72e5aeb | |||
| 52c76fc664 | |||
| 4d0e1357a6 | |||
| e92c8a0975 | |||
| 331cd5b59a | |||
| 601186dcc0 | |||
| 3295b7b081 | |||
| 34605fe3b0 | |||
| 045ce12c8b | |||
| 98c723cfa5 | |||
| 24872d2dc5 | |||
| 00511da24c | |||
| d851610432 | |||
| 6c15d48b58 | |||
| 8bd26cca92 | |||
| 31655679e6 | |||
| accdd7698c | |||
| 8f187aa917 | |||
| c77985fa92 | |||
| 2749b8017c | |||
| c4aff9f81b | |||
| c4739e0257 | |||
| 93f91ab332 | |||
| 8a0e1e7a61 | |||
| 463d1c8ddf | |||
| 9c8d4bcb89 | |||
| d300f3cdae | |||
| b14a5c9f5f | |||
| 2abb266e3b | |||
| e714fe8fe7 | |||
| b7023b7b1f | |||
| 3936edb017 | |||
| 71a9157abf | |||
| 3b571f404c | |||
| 6ff6e595a6 | |||
| 28bfd8cd17 | |||
| af829fa73e | |||
| d0d8e60a8f | |||
| c4f094572d | |||
| b4aeb5134b | |||
| 4608b03659 | |||
| b51fc19671 | |||
| 2dce9814ac | |||
| 9f2ab70ded | |||
| bc7c23c1f0 | |||
| d6b68e3638 | |||
| 07091110f5 | |||
| 6b88cf11a1 | |||
| 0149b3e61a | |||
| 0d31187ab4 | |||
| 9187fdd399 | |||
| d5dba78c7c | |||
| bb8e6229d3 | |||
| 5dea324626 | |||
| 0a616c1c94 | |||
| a188696a8e | |||
| c69d9fdf4e | |||
| 83e807ac24 | |||
| 5dea28e6a6 | |||
| a1bba1e69c | |||
| b056266adf | |||
| 249c9e8e4f | |||
| ebc30b674f | |||
| bf079798d3 | |||
| 480cf64738 | |||
| 65f039b7bd | |||
| 5c7b62e9f7 | |||
| 5e6e891ea6 | |||
| 82c904608b | |||
| f5260ceb86 | |||
| abb32fdd6f | |||
| 97aef5c5f5 | |||
| c3eb8ae7f4 | |||
| 72f74f9164 | |||
| 958676e5a7 | |||
| 51134670e5 | |||
| ecea2b220d | |||
| 212f4d5e7a | |||
| 2edb09bd13 | |||
| 6b9f073cb7 | |||
| 9f933a0f1c | |||
| 8a6f721bb4 | |||
| eeadcca6ed | |||
| 8e89c10906 | |||
| 833d16351c | |||
| 0c10552fa7 | |||
| 443afe2f6e | |||
| db48d98da7 | |||
| bf3150434a | |||
| 71c4bd26d2 | |||
| ca4f7040e8 | |||
| 1cfa861d7e | |||
| 007baefe80 | |||
| 7d77f4d996 | |||
| 3d5a62c085 | |||
| 24ffef997b | |||
| 7d5275152c | |||
| 93167c2358 | |||
| 734d3ad280 | |||
| 753668dfa5 | |||
| 351817338d | |||
| 7c12218489 | |||
| 7c71298227 | |||
| 167db6d7e6 | |||
| 74834f3559 | |||
| e13ca5c639 | |||
| a51806d62b | |||
| 57238dfb72 | |||
| a2be642af0 | |||
| aec2d23c2f | |||
| 11a11162da | |||
| 12a7be497e | |||
| 9b69da66a5 | |||
| ee831f96bb | |||
| 5542790145 | |||
| 3b1091a34f | |||
| eebe6541c3 | |||
| e308089cfa | |||
| 5132d6894e | |||
| 006d1e1fa0 | |||
| 4c6dc24355 | |||
| acf227a947 | |||
| 3b05ca73e5 | |||
| 236d7ea585 | |||
| 87408d22c4 | |||
| ea6754775d | |||
| 3210f4d406 | |||
| 16ac97a9c9 | |||
| 8857a51dde | |||
| fc2abe7f16 | |||
| f04a930e13 | |||
| 1de1e2d6ba | |||
| 0208e4baf9 | |||
| 21af2a6d02 | |||
| 2fbe301a51 | |||
| 6d5382982c | |||
| a20b4ee570 | |||
| 909ff9e070 | |||
| 7d3e265ece | |||
| 78d8f311c0 | |||
| c52ba95fd0 | |||
| 9462a0331a | |||
| 4e3d1838c0 | |||
| aad7b5c6ea | |||
| abec24d65b | |||
| e0c3d1864c | |||
| bd25c6fe05 | |||
| 4ec5e14431 | |||
| c9b8c5bec3 | |||
| 223883adf7 | |||
| 83c2d7ff79 | |||
| cf33f5a2f6 | |||
| 92b4596924 | |||
| ce9b748570 | |||
| 87a9069ab4 | |||
| 3e6a76c8c3 | |||
| 3509955374 | |||
| 225711f95d | |||
| 5436647cd3 | |||
| fbd9da3b33 | |||
| edb6705ac8 | |||
| 382a2a5bcc | |||
| b5967607d2 | |||
| 03bbb6c3b6 | |||
| 6054be0ff1 | |||
| ff7355bcc2 | |||
| 23a9124e03 | |||
| ac41807cdd | |||
| d95bb13e6e | |||
| b9d58ba3c1 | |||
| a648b29f31 | |||
| e702feb04f | |||
| ce83355e58 | |||
| 683beed3b7 | |||
| c9095036eb | |||
| 29ec935d67 | |||
| 339015a093 | |||
| b8b5b4170f | |||
| 8a104e4480 | |||
| 89e1a5e88a | |||
| 8e5746d068 | |||
| 1df7c6560a | |||
| 0ff4546379 | |||
| ab036c968d | |||
| 625ece2e7b | |||
| 41842cc1b9 | |||
| e63992a7a5 | |||
| 7ae5d8f1b1 | |||
| 0a34f2138a | |||
| 38b326fe72 | |||
| 8785c609a9 | |||
| 1a67560137 | |||
| 6c7534b9b6 | |||
| ae6c2c985f | |||
| 940331b87f | |||
| e6b5600a47 | |||
| ce2a6ab756 | |||
| 6af91af639 | |||
| e1ff156b91 | |||
| b56d20ac35 | |||
| 9ee7a7b861 | |||
| f73ffce3f0 | |||
| 0997967787 | |||
| 3476ec2b29 | |||
| e0cf568aa9 | |||
| c12d7f8345 | |||
| 9ba115d5ab | |||
| 3756bbfd06 | |||
| 9096bcc641 | |||
| c4d04854e5 | |||
| d95fc0005b | |||
| 2b9093184a | |||
| 831376054b | |||
| 885ee0b6d6 | |||
| 511d0f26dd | |||
| 5263698625 | |||
| 7859f9564e | |||
| 149456682d | |||
| 8c040997da | |||
| 868fed0176 | |||
| c7f44a48da | |||
| 6300bff4e7 | |||
| 3e6b1ffdbb | |||
| 408fd5318b | |||
| cfe419f6df | |||
| 5403726099 | |||
| 057c14909f | |||
| 37b18c06bb | |||
| 3ad1eb9a03 | |||
| a1064771f8 | |||
| 463ee286bf | |||
| a7ae6e72ef | |||
| 36c67f60dd | |||
| bb718b04a6 | |||
| b6ea060054 | |||
| 5dba54c048 | |||
| 415276163f | |||
| 017b9e2e84 | |||
| 5740991ee1 | |||
| 06008bd719 | |||
| b53cd485e5 | |||
| 7c1fd39158 | |||
| f7888f1ba4 | |||
| ece5ac37ad | |||
| a51183fb23 | |||
| 7875192fb0 | |||
| eb128ad431 | |||
| ba61ed87ab | |||
| 8b5348081d | |||
| bb7e69ace5 | |||
| 3b8bb60423 | |||
| 93339ce9df | |||
| d30d99d8c2 | |||
| 3468df16e1 | |||
| ccdeff98dc | |||
| 7e182b1df2 | |||
| ff652ad435 | |||
| 1fdafa7966 | |||
| 1f5e683af5 | |||
| db8c881571 | |||
| 14f49c5b3b | |||
| 52286f26a5 | |||
| 45e7567cb6 | |||
| c0edb59884 | |||
| 1e86c429ca | |||
| fbaebdcbcf | |||
| 3f9716b0a4 | |||
| 57d32197cf | |||
| 9e6a3f26c2 | |||
| 23314beb15 | |||
| 64b94da585 | |||
| 1609b4fc50 | |||
| dfffffb101 | |||
| 81250a117a | |||
| 72bd0241e9 | |||
| 63c24f147f | |||
| 88fb7df3f5 | |||
| 320b1a0cc5 | |||
| 08b168ee6e | |||
| d6a87ac81a | |||
| 615dba42b8 | |||
| 3ffec4f1fe | |||
| 1288b463f1 | |||
| 1aed6d0703 | |||
| 69f1e69140 | |||
| ab4ecd29c6 | |||
| 1d76c8a3d3 | |||
| b1c2cedbac | |||
| 60b4b6f207 | |||
| b9f5eb56c5 | |||
| 7b9ae73044 | |||
| 87f2c33eba | |||
| 0a73a8b10a | |||
| af65404fd3 | |||
| 398f891dac | |||
| 6a2375a774 | |||
| 43bbcf4015 | |||
| fadb563337 | |||
| 1ae54b3906 | |||
| 1c069e2ee2 | |||
| ed1099ac11 | |||
| 99d3a7e046 | |||
| 2144acf34d | |||
| 7a8ad8e833 | |||
| 3b45f33692 | |||
| b4a2340ec7 | |||
| 5910e3b930 | |||
| fd58beaa69 | |||
| 5449e422a5 | |||
| dfb1784b6f | |||
| d956c60b48 | |||
| 69e1f2af6e | |||
| 7cc2f5ed4b | |||
| ce2b17a5c9 | |||
| 0fcc60bf65 | |||
| 45ac25055b | |||
| d3e201f2e4 | |||
| 693735dc2e | |||
| a4fa85dd79 | |||
| 29f70860ef | |||
| ae882f6afc | |||
| 57941f67ce | |||
| ab356d516b | |||
| d11b3f1a99 | |||
| 83ba08dd6b | |||
| 34cc00a1f6 | |||
| 7b86429056 | |||
| e67c1b1539 | |||
| e501f8e05d | |||
| 9e40d89317 | |||
| 5d475ab23f | |||
| 01cb39e18e | |||
| 0d0ef25970 | |||
| 6c766e2001 | |||
| 1c73166120 | |||
| fc5dbc85dc | |||
| 622327cdf6 | |||
| b0a4cd5c42 | |||
| 8c95c94472 | |||
| 7dafb2eddc | |||
| 7c9f3eb87f | |||
| ab34dd81f8 | |||
| 4e5fb26be1 | |||
| 6fc80bc0fc | |||
| 292667ac29 | |||
| 8c03058f8d | |||
| 25e73d097c | |||
| 1970cd8891 | |||
| 8ae9b2dec1 | |||
| 1662608984 | |||
| 42693983f1 | |||
| f43048d783 | |||
| b5ec332b4a | |||
| dab1c44b18 | |||
| d9c0bff780 | |||
| beceace2c2 | |||
| 5373636704 | |||
| e8d04eccf6 | |||
| d978c82263 | |||
| 883a458ed3 | |||
| 9009d9df99 | |||
| 81c0fa1243 | |||
| 01221616ae | |||
| be75a0f5e9 | |||
| 1b40839490 | |||
| 80f5780602 | |||
| dcf1817cd6 | |||
| ae251803ad | |||
| f6d9627ceb | |||
| 0cbc6b7b56 | |||
| f0af2d14e4 | |||
| b797039144 | |||
| 86e29e5065 | |||
| fddef22e8e | |||
| ad7551b7af | |||
| a8be878a87 | |||
| 2b1849f1a3 | |||
| 25a6590324 | |||
| fa251dc07f | |||
| 4de93e7e95 | |||
| a5e5f33dc8 | |||
| f09eb18bcc | |||
| 5c65e35f24 | |||
| 8cbe859971 | |||
| 67f38db847 | |||
| 28a72a3592 | |||
| dae334694d | |||
| e57fe3a046 | |||
| 54670c7845 | |||
| 3dbcef1500 | |||
| 24bbad05cb | |||
| cacbda3164 | |||
| da9b1a3a64 | |||
| 394b911eca | |||
| 220778e84f | |||
| c6e343da5d | |||
| 268e5b7478 | |||
| b9ccd16a02 | |||
| 302ad0e8a6 | |||
| e052f435db | |||
| caa1330f39 | |||
| 870d19f566 | |||
| a28f787833 | |||
| 335a51b589 | |||
| 72f9471486 | |||
| f0cd101af5 | |||
| 52876bd77e | |||
| aabe1926bd | |||
| e785227064 | |||
| 0cb88eec3b | |||
| 8cd2914f73 | |||
| f70e2ee8eb | |||
| c60a21ef32 | |||
| 77a57ee609 | |||
| fc15f0e083 | |||
| 6115beece3 | |||
| 54434fbe78 | |||
| 83755bdd25 | |||
| 08f901c5f0 | |||
| 06910fbd4d | |||
| 154e7dd185 | |||
| 8bde3226bf | |||
| e5565145b5 | |||
| 37a667daff | |||
| eda98bacfc | |||
| ee1ee52e13 | |||
| 4060094404 | |||
| d03bf41ad1 | |||
| ba2fe24629 | |||
| 547116090a | |||
| 974826f42d | |||
| 4914b029f9 | |||
| ff864fbaab | |||
| f583ba4938 | |||
| d552f541ac | |||
| d46d02e37a | |||
| 4e0c6e85da | |||
| 1147808366 | |||
| a7c27a60e0 | |||
| 974976bd90 | |||
| 9bbe665984 | |||
| d0ea8551b6 | |||
| 089ee9bc07 | |||
| 786c4f8207 | |||
| 13879aefdd | |||
| d03a8fd7a4 | |||
| c00aae5566 | |||
| e923f4d650 | |||
| 654c07a364 | |||
| 820eeb0aac | |||
| 2a338baa26 | |||
| ec7351272f | |||
| 13dec43ef3 | |||
| 43673e3349 | |||
| 1e77253a63 | |||
| 798af326a2 | |||
| d349a7591e | |||
| 8370145f6c | |||
| 304945acea | |||
| 1dd05fee50 | |||
| 374eda4103 | |||
| 52a8cca3e8 | |||
| 22ef4d75b6 | |||
| 534e6a0ad3 | |||
| 80ed4fc5fa | |||
| 445eb02386 | |||
| 53ae0b89b0 | |||
| c7376b4d8f | |||
| e3e59b7e24 | |||
| 07f59ac581 | |||
| 7176c3d4f4 | |||
| 13366284c6 | |||
| 0f0b447137 | |||
| f7cc28c896 | |||
| fd790003d1 | |||
| fdad329148 | |||
| 73bf5f3fbe | |||
| 103d64a659 | |||
| 92c4d35992 | |||
| 3b1d95b71b | |||
| 4fd9bf2bb6 | |||
| ce96b6f55d | |||
| 54bbc1dd00 | |||
| ffa31bcacc | |||
| 559f5bd257 | |||
| 977e49e1ce | |||
| 30cb707436 | |||
| ae439633b9 | |||
| 7f33f6ddfd | |||
| 8404415a93 | |||
| 5ddddf8b5d | |||
| 927db19cc6 | |||
| eccff16c5f | |||
| 7be9c62c7c | |||
| 76ce14e8b1 | |||
| fd0074c0b0 | |||
| ad01cdb5ac | |||
| 1b4ca67cf0 | |||
| 53936e19d4 | |||
| b58d00c5a6 | |||
| 41db0c45cd | |||
| 5361b551eb | |||
| eb87647466 | |||
| d9b900f28d | |||
| 198bd6c723 | |||
| 1831e6309f | |||
| 007f5149d2 | |||
| 1a094e3511 | |||
| db1ddc1292 | |||
| 535e3bb36a | |||
| 785237e55e | |||
| e6bf47a859 | |||
| 93ae1711f1 | |||
| af97812beb | |||
| 78cf22ade9 | |||
| b4a461d93a | |||
| 5f33c02064 | |||
| 46c1a392b8 | |||
| c6880c5df6 | |||
| 646da94ef9 | |||
| 36ede93dce | |||
| 9ed35589f7 | |||
| e946dd18e0 | |||
| 51cad598c1 | |||
| 0595f23c18 | |||
| b88816706b | |||
| 80edaae023 | |||
| 883c46b064 | |||
| 4248cc31f2 | |||
| 5f81c6d61a | |||
| 00d82cc817 | |||
| a9edfde162 | |||
| 468d7f445d | |||
| 4cc74deece | |||
| f9831490af | |||
| a055a616eb | |||
| 8009a2cbc4 | |||
| 693cc149cf | |||
| d126fe0bce | |||
| 4cc49cce9e | |||
| f7cb4cd805 | |||
| a2bd9883d1 | |||
| 10a079c304 | |||
| 65f12f1082 | |||
| c15f855c72 | |||
| 3d38483022 | |||
| 57dc1e7175 | |||
| ead68c929f |
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: ss-shadcn-svelte
|
||||
description: >
|
||||
Use shadcn-svelte components in SvelteKit projects. Detects whether the current project is a SvelteKit
|
||||
app with shadcn-svelte installed, lists available components, and provides access to full component
|
||||
documentation via the official llms.txt. Helps choose the right UI components for the job — buttons,
|
||||
forms, dialogs, tables, charts, and more — following shadcn-svelte best practices.
|
||||
Use this skill whenever the user is working in a SvelteKit project and wants to: add UI components,
|
||||
build forms, create dialogs or modals, add a data table, use a date picker, build a sidebar or
|
||||
navigation, add charts, use a combobox or select, create an alert or toast notification, or generally
|
||||
build UI with pre-built accessible components. Also trigger when the user mentions "shadcn", "shadcn-svelte",
|
||||
"bits-ui", or asks about available components in their Svelte project.
|
||||
---
|
||||
|
||||
# shadcn-svelte — Component-Aware Svelte UI Assistant
|
||||
|
||||
Use the right shadcn-svelte components when building UI in SvelteKit projects. This skill detects your project setup, shows what's available, and gives you access to full component documentation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The project must be a SvelteKit app with shadcn-svelte initialized:
|
||||
|
||||
```bash
|
||||
# Initialize shadcn-svelte in an existing SvelteKit project
|
||||
npx shadcn-svelte@latest init
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
### Step 1: Detect project setup
|
||||
|
||||
Run the detection script to verify this is a SvelteKit project with shadcn-svelte and see which components are already installed:
|
||||
|
||||
```bash
|
||||
bash <skill-path>/scripts/detect.sh .
|
||||
```
|
||||
|
||||
This will:
|
||||
- Confirm it's a SvelteKit project (checks for `svelte.config.js/ts` and `@sveltejs/kit` in package.json)
|
||||
- Confirm shadcn-svelte is installed (checks for `components.json`, `bits-ui`, or `shadcn-svelte` in package.json)
|
||||
- List all currently installed components in the project's UI directory
|
||||
- Provide the documentation URL
|
||||
|
||||
If the script exits with code 1, the project either isn't SvelteKit or doesn't have shadcn-svelte — do not proceed with shadcn-svelte components in that case.
|
||||
|
||||
### Step 2: Read the component documentation
|
||||
|
||||
The full component documentation for LLMs is available at:
|
||||
|
||||
```
|
||||
https://www.shadcn-svelte.com/llms.txt
|
||||
```
|
||||
|
||||
Fetch this URL to get a structured index of all available components organized by category, with links to individual component documentation pages (in `.md` format).
|
||||
|
||||
When you need to use a specific component, read its individual documentation page from the links provided in `llms.txt`. Each component doc includes:
|
||||
- Import statements and usage examples
|
||||
- Available props, events, and slots
|
||||
- Variants and configuration options
|
||||
- Accessibility information
|
||||
|
||||
### Step 3: Use the right component for the job
|
||||
|
||||
When building UI, follow this decision process:
|
||||
|
||||
1. **Run detection** to confirm shadcn-svelte is available and see installed components
|
||||
2. **Fetch llms.txt** to see all available components
|
||||
3. **Read the specific component docs** for the components you plan to use
|
||||
4. **Check if the component is installed** — if not, add it:
|
||||
```bash
|
||||
npx shadcn-svelte@latest add <component-name>
|
||||
```
|
||||
5. **Import and use the component** following the documentation patterns
|
||||
|
||||
### Component categories
|
||||
|
||||
shadcn-svelte components are organized into these categories:
|
||||
|
||||
| Category | Components |
|
||||
|----------|-----------|
|
||||
| **Layout** | Aspect Ratio, Collapsible, Resizable, Scroll Area, Separator, Sidebar |
|
||||
| **Form & Input** | Button, Calendar, Checkbox, Combobox, Date Picker, Input, Input OTP, Label, Radio Group, Range Calendar, Select, Slider, Switch, Textarea, Toggle, Toggle Group |
|
||||
| **Data Display** | Accordion, Avatar, Badge, Card, Carousel, Chart, Table, Data Table |
|
||||
| **Feedback** | Alert, Alert Dialog, Progress, Skeleton, Sonner (Toast) |
|
||||
| **Overlay** | Context Menu, Dialog, Drawer, Dropdown Menu, Hover Card, Menubar, Popover, Sheet, Tooltip |
|
||||
| **Navigation** | Breadcrumb, Command, Pagination, Tabs |
|
||||
| **Typography** | Typography |
|
||||
|
||||
### Adding new components
|
||||
|
||||
```bash
|
||||
# Add a single component
|
||||
npx shadcn-svelte@latest add button
|
||||
|
||||
# Add multiple components
|
||||
npx shadcn-svelte@latest add button card dialog
|
||||
|
||||
# List all available components
|
||||
npx shadcn-svelte@latest add
|
||||
```
|
||||
|
||||
### Import patterns
|
||||
|
||||
Components are typically imported from the project's `$lib/components/ui` directory:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
</script>
|
||||
```
|
||||
|
||||
Some components use namespace imports (with `* as`) when they have multiple sub-components (Card, Dialog, Sheet, Table, etc.), while simpler components use named imports (Button, Input, Badge, etc.).
|
||||
|
||||
## Important guidelines
|
||||
|
||||
- **Always run detection first** before suggesting shadcn-svelte components
|
||||
- **Always read component docs** before using a component — don't guess at props or patterns
|
||||
- **Check installed components** and add missing ones before importing
|
||||
- **Use the project's configured path** — the components directory may vary based on `components.json` configuration
|
||||
- **Follow Svelte 5 patterns** — shadcn-svelte uses runes (`$state`, `$derived`, `$effect`) and snippet-based composition
|
||||
- **Prefer composition** — shadcn-svelte components are designed to be composed together, not used as monolithic blocks
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# detect.sh — Check if the current project is a SvelteKit project with shadcn-svelte installed.
|
||||
# Exits 0 and prints component info if detected, exits 1 otherwise.
|
||||
|
||||
PROJECT_DIR="${1:-.}"
|
||||
|
||||
# Resolve to absolute path
|
||||
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
||||
|
||||
# --- Step 1: Check for SvelteKit ---
|
||||
|
||||
SVELTEKIT=false
|
||||
|
||||
# Check for svelte.config.js or svelte.config.ts
|
||||
if [[ -f "$PROJECT_DIR/svelte.config.js" ]] || [[ -f "$PROJECT_DIR/svelte.config.ts" ]]; then
|
||||
SVELTEKIT=true
|
||||
fi
|
||||
|
||||
# Also verify package.json has @sveltejs/kit
|
||||
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
||||
if grep -q '"@sveltejs/kit"' "$PROJECT_DIR/package.json" 2>/dev/null; then
|
||||
SVELTEKIT=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SVELTEKIT" != "true" ]]; then
|
||||
echo "NOT_SVELTEKIT"
|
||||
echo "This is not a SvelteKit project. No svelte.config.js/ts found and @sveltejs/kit is not in package.json."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Step 2: Check for shadcn-svelte ---
|
||||
|
||||
SHADCN=false
|
||||
|
||||
# Check for components.json (shadcn-svelte config file)
|
||||
if [[ -f "$PROJECT_DIR/components.json" ]]; then
|
||||
# Verify it's actually a shadcn config (has $schema or style field)
|
||||
if grep -qE '"(\$schema|style)"' "$PROJECT_DIR/components.json" 2>/dev/null; then
|
||||
SHADCN=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for bits-ui in package.json (core dependency of shadcn-svelte)
|
||||
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
||||
if grep -q '"bits-ui"' "$PROJECT_DIR/package.json" 2>/dev/null; then
|
||||
SHADCN=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for shadcn-svelte in package.json
|
||||
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
||||
if grep -q '"shadcn-svelte"' "$PROJECT_DIR/package.json" 2>/dev/null; then
|
||||
SHADCN=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SHADCN" != "true" ]]; then
|
||||
echo "NO_SHADCN_SVELTE"
|
||||
echo "SvelteKit project detected, but shadcn-svelte is not installed."
|
||||
echo "Install it with: npx shadcn-svelte@latest init"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Step 3: Gather installed components ---
|
||||
|
||||
echo "DETECTED"
|
||||
echo "SvelteKit project with shadcn-svelte detected."
|
||||
echo ""
|
||||
|
||||
# Check which components are already installed by scanning the components directory
|
||||
COMPONENTS_DIR=""
|
||||
|
||||
# Try to read the components alias from components.json
|
||||
if [[ -f "$PROJECT_DIR/components.json" ]]; then
|
||||
# Extract the aliases.components path
|
||||
ALIAS_PATH=$(grep -o '"components"[[:space:]]*:[[:space:]]*"[^"]*"' "$PROJECT_DIR/components.json" | head -1 | sed 's/.*"components"[[:space:]]*:[[:space:]]*"//' | sed 's/"//')
|
||||
|
||||
if [[ -n "$ALIAS_PATH" ]]; then
|
||||
# Resolve $lib to src/lib
|
||||
RESOLVED_PATH="${ALIAS_PATH//\$lib/src/lib}"
|
||||
if [[ -d "$PROJECT_DIR/$RESOLVED_PATH/ui" ]]; then
|
||||
COMPONENTS_DIR="$PROJECT_DIR/$RESOLVED_PATH/ui"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: check common locations
|
||||
if [[ -z "$COMPONENTS_DIR" ]]; then
|
||||
for dir in "src/lib/components/ui" "src/lib/ui" "src/components/ui"; do
|
||||
if [[ -d "$PROJECT_DIR/$dir" ]]; then
|
||||
COMPONENTS_DIR="$PROJECT_DIR/$dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "$COMPONENTS_DIR" ]] && [[ -d "$COMPONENTS_DIR" ]]; then
|
||||
echo "Installed components (in $COMPONENTS_DIR):"
|
||||
for comp_dir in "$COMPONENTS_DIR"/*/; do
|
||||
if [[ -d "$comp_dir" ]]; then
|
||||
comp_name=$(basename "$comp_dir")
|
||||
echo " - $comp_name"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Documentation: https://www.shadcn-svelte.com/llms.txt"
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: documentation-writer
|
||||
description: Specialized skill for creating and editing high-quality Kener documentation. MUST be used whenever creating or editing documentation files in the src/routes/(docs)/docs/content/ directory or updating docs.json navigation.
|
||||
---
|
||||
|
||||
# Documentation Writer
|
||||
|
||||
Use this skill for all docs edits in `src/routes/(docs)/docs/content/` and when updating docs navigation in `src/routes/(docs)/docs.json`.
|
||||
|
||||
## Non-negotiable rules
|
||||
|
||||
1. **Be concise**: remove repetition and background that does not help the user complete a task.
|
||||
2. **Be actionable**: prioritize “what to do” over theory.
|
||||
3. **One source of truth**: if another page already has details, link to it instead of duplicating.
|
||||
4. **Preserve structure**: keep valid frontmatter and heading anchor IDs.
|
||||
5. **Keep examples copyable**: minimal, tested-looking, and directly relevant.
|
||||
6. **Search before writing**: always check if the content already exists in some form before adding new sections or pages.
|
||||
7. **Check Relevant Code**: Search the codebase inside `src/` for any relevant code, comments, or tests that can inform the documentation content and ensure accuracy.
|
||||
|
||||
## Docs config model (current)
|
||||
|
||||
`docs.json` is versioned. Sidebar lives inside tabs:
|
||||
|
||||
- `versions[].content.navigation.tabs[].sidebar`
|
||||
- Sidebar groups contain `pages`
|
||||
- Page paths use `content` (legacy `slug` may still appear in older content)
|
||||
|
||||
When adding a new doc page, add it to the appropriate tab sidebar path.
|
||||
|
||||
## Versioned link policy (mandatory)
|
||||
|
||||
- For v4 docs content, internal links MUST use explicit v4 paths: `/docs/v4/...`.
|
||||
- Do not use unversioned shortcuts like `/docs/alerting/...` in v4 pages.
|
||||
- Before finalizing, verify every internal link in edited files resolves to the intended version.
|
||||
|
||||
## Required page format
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Page Title
|
||||
description: One-line summary of user outcome
|
||||
---
|
||||
```
|
||||
|
||||
- Use custom anchors for H2/H3 headings: `## Section {#section}`
|
||||
- Use GitHub admonitions only when needed: `[!NOTE]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`, `[!TIP]`
|
||||
- Prefer short sections and short lists
|
||||
|
||||
## Preferred structure (default)
|
||||
|
||||
1. Short intro (1–2 sentences)
|
||||
2. Quick setup / minimum config
|
||||
3. Required variables/options table
|
||||
4. Verification step
|
||||
5. Top troubleshooting items
|
||||
|
||||
Only add extra sections if they materially improve task completion.
|
||||
|
||||
## Keep docs lean
|
||||
|
||||
Remove or avoid:
|
||||
|
||||
- Multiple near-identical examples
|
||||
- Long conceptual explainers
|
||||
- Platform-by-platform repetition unless behavior differs
|
||||
- Large checklists that restate earlier content
|
||||
|
||||
## Editing workflow
|
||||
|
||||
1. Read the whole target document.
|
||||
2. Compress verbose sections first.
|
||||
3. Keep critical caveats and breaking notes.
|
||||
4. Ensure internal links and anchors still work.
|
||||
5. If adding files, update `docs.json` navigation in the correct version/tab.
|
||||
|
||||
## Review checklist
|
||||
|
||||
- [ ] Title/description frontmatter exists
|
||||
- [ ] Key steps are clear and copyable
|
||||
- [ ] Content is concise and non-duplicative
|
||||
- [ ] Headings keep stable custom anchors
|
||||
- [ ] Navigation updated (if new page)
|
||||
- [ ] Internal links point to correct paths
|
||||
- [ ] v4 pages use `/docs/v4/...` internal links (no unversioned `/docs/...` shortcuts)
|
||||
- [ ] No outdated or irrelevant content remains
|
||||
- [ ] Admonitions used appropriately for important notes
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/ss-shadcn-svelte
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: svelte-code-writer
|
||||
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating or editing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
|
||||
---
|
||||
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
```
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
```
|
||||
|
||||
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
```
|
||||
|
||||
Analyzes Svelte code and suggests fixes for common issues.
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--async` - Enable async Svelte mode (default: false)
|
||||
- `--svelte-version` - Target version: 4 or 5 (default: 5)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Analyze inline code (escape $ as \$)
|
||||
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
|
||||
|
||||
# Analyze a file
|
||||
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
|
||||
|
||||
# Target Svelte 4
|
||||
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
|
||||
```
|
||||
|
||||
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
|
||||
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
|
||||
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component
|
||||
@@ -0,0 +1,361 @@
|
||||
---
|
||||
name: tailwindcss
|
||||
description: Tailwind CSS v4 utility-first styling patterns including responsive design, dark mode, and custom configuration. Use when styling with Tailwind, adding utility classes, configuring Tailwind, setting up dark mode, or customizing the theme.
|
||||
user-invokable: false
|
||||
metadata:
|
||||
category: styling
|
||||
---
|
||||
|
||||
# Tailwind CSS v4 Development Guidelines
|
||||
|
||||
Best practices for using Tailwind CSS v4 utility classes effectively.
|
||||
|
||||
**Note**: Tailwind CSS v4 (released January 2025) uses a CSS-first configuration approach. If you need v3 compatibility, tailwind.config.js is still supported.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Utility-First**: Use utility classes instead of custom CSS
|
||||
2. **Mobile-First**: Design for mobile, then scale up with responsive modifiers
|
||||
3. **Component Extraction**: Extract repeated patterns into components
|
||||
4. **Consistent Spacing**: Use Tailwind's spacing scale
|
||||
5. **Custom Configuration**: Extend the default theme for brand consistency
|
||||
|
||||
## Basic Utilities
|
||||
|
||||
### Layout
|
||||
|
||||
```tsx
|
||||
// Flexbox
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1">Content</div>
|
||||
<div className="flex-shrink-0">Sidebar</div>
|
||||
</div>
|
||||
|
||||
// Grid
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>1</div>
|
||||
<div>2</div>
|
||||
<div>3</div>
|
||||
</div>
|
||||
|
||||
// Positioning
|
||||
<div className="relative">
|
||||
<div className="absolute top-0 right-0">Badge</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
```tsx
|
||||
// Padding and Margin
|
||||
<div className="p-4 m-2"> {/* padding: 1rem, margin: 0.5rem */}
|
||||
<div className="px-6 py-4"> {/* padding-x: 1.5rem, padding-y: 1rem */}
|
||||
<div className="mt-8 mb-4"> {/* margin-top: 2rem, margin-bottom: 1rem */}
|
||||
|
||||
// Space between children
|
||||
<div className="space-y-4"> {/* margin-bottom on all but last child */}
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
```tsx
|
||||
<h1 className="text-4xl font-bold text-gray-900">Heading</h1>
|
||||
<p className="text-base font-normal text-gray-600 leading-relaxed">
|
||||
Paragraph text with comfortable line height.
|
||||
</p>
|
||||
<span className="text-sm font-medium text-blue-600">Label</span>
|
||||
```
|
||||
|
||||
### Colors
|
||||
|
||||
```tsx
|
||||
// Text colors
|
||||
<p className="text-gray-900 dark:text-gray-100">Text</p>
|
||||
|
||||
// Background colors
|
||||
<div className="bg-blue-500 hover:bg-blue-600">Button</div>
|
||||
|
||||
// Border colors
|
||||
<div className="border border-gray-300">Box</div>
|
||||
```
|
||||
|
||||
## Responsive Design
|
||||
|
||||
### Breakpoints
|
||||
|
||||
```tsx
|
||||
// Mobile-first responsive classes
|
||||
<div className="w-full md:w-1/2 lg:w-1/3">
|
||||
{/* Full width on mobile, half on medium screens, third on large */}
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl md:text-4xl lg:text-6xl">
|
||||
{/* Responsive text sizes */}
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Responsive grid */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Container
|
||||
|
||||
```tsx
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Centered container with horizontal padding */}
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Responsive container padding */}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Button
|
||||
|
||||
```tsx
|
||||
<button className="px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
Click me
|
||||
</button>
|
||||
|
||||
// Variants
|
||||
<button className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Secondary
|
||||
</button>
|
||||
```
|
||||
|
||||
### Card
|
||||
|
||||
```tsx
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow-md">
|
||||
<img src="/image.jpg" alt="" className="h-48 w-full object-cover" />
|
||||
<div className="p-6">
|
||||
<h2 className="mb-2 text-xl font-semibold">Card Title</h2>
|
||||
<p className="text-gray-600">Card content goes here.</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Form Input
|
||||
|
||||
```tsx
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
<p className="text-sm text-gray-500">We'll never share your email.</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
## State Variants
|
||||
|
||||
### Hover, Focus, Active
|
||||
|
||||
```tsx
|
||||
<button className="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 focus:ring-2 focus:ring-blue-500">
|
||||
Interactive Button
|
||||
</button>
|
||||
|
||||
<a href="#" className="text-blue-600 hover:text-blue-800 hover:underline">
|
||||
Link
|
||||
</a>
|
||||
```
|
||||
|
||||
### Group Hover
|
||||
|
||||
```tsx
|
||||
<div className="group">
|
||||
<img src="/image.jpg" className="transition-opacity group-hover:opacity-75" />
|
||||
<p className="group-hover:text-blue-600">Hover the container</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
```tsx
|
||||
<button className="disabled:cursor-not-allowed disabled:opacity-50" disabled>
|
||||
Disabled Button
|
||||
</button>
|
||||
```
|
||||
|
||||
## Dark Mode
|
||||
|
||||
```css
|
||||
/* Tailwind v4: Configure in app/globals.css */
|
||||
@import "tailwindcss";
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* Or use class-based: .dark */
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Usage (same as v3)
|
||||
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<h1 className="text-gray-900 dark:text-white">Title</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">Description</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Custom Styles
|
||||
|
||||
### Arbitrary Values
|
||||
|
||||
```tsx
|
||||
<div className="top-[117px]"> {/* Custom top value */}
|
||||
<div className="bg-[#1da1f2]"> {/* Custom color */}
|
||||
<div className="grid-cols-[200px_1fr]"> {/* Custom grid template */}
|
||||
```
|
||||
|
||||
### @apply Directive
|
||||
|
||||
```css
|
||||
/* components/button.css */
|
||||
.btn-primary {
|
||||
@apply rounded-md bg-blue-600 px-4 py-2 font-medium text-white;
|
||||
@apply hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:outline-none;
|
||||
@apply disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Tailwind v4: CSS-First Configuration
|
||||
|
||||
```css
|
||||
/* app/globals.css */
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Custom colors */
|
||||
--color-brand-50: #eff6ff;
|
||||
--color-brand-100: #dbeafe;
|
||||
--color-brand-900: #1e3a8a;
|
||||
|
||||
/* Custom spacing */
|
||||
--spacing-128: 32rem;
|
||||
|
||||
/* Custom fonts */
|
||||
--font-family-sans: "Inter", sans-serif;
|
||||
|
||||
/* Custom breakpoints */
|
||||
--breakpoint-3xl: 1920px;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind v3 Config (Still Supported)
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js (optional in v4)
|
||||
module.exports = {
|
||||
content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: {
|
||||
50: "#eff6ff",
|
||||
100: "#dbeafe",
|
||||
900: "#1e3a8a"
|
||||
}
|
||||
},
|
||||
spacing: {
|
||||
128: "32rem"
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Inter", "sans-serif"]
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")]
|
||||
}
|
||||
```
|
||||
|
||||
## Plugins
|
||||
|
||||
### Official Plugins
|
||||
|
||||
```bash
|
||||
npm install @tailwindcss/forms
|
||||
npm install @tailwindcss/typography
|
||||
npm install @tailwindcss/aspect-ratio
|
||||
npm install @tailwindcss/container-queries
|
||||
```
|
||||
|
||||
```tsx
|
||||
// @tailwindcss/forms
|
||||
<input type="text" className="form-input rounded-md" />
|
||||
|
||||
// @tailwindcss/typography
|
||||
<article className="prose lg:prose-xl">
|
||||
<h1>Article Title</h1>
|
||||
<p>Content...</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Automatic Content Detection
|
||||
|
||||
Tailwind v4 automatically detects and scans all template files - no `content` configuration needed.
|
||||
|
||||
### Build Performance
|
||||
|
||||
Tailwind v4 delivers 3.5x faster full builds (~100ms) compared to v3 using modern CSS features like `@property` and `color-mix()`.
|
||||
|
||||
**Browser Requirements**: Safari 16.4+, Chrome 111+, Firefox 128+
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Centered Content
|
||||
|
||||
```tsx
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div>Centered content</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Sticky Header
|
||||
|
||||
```tsx
|
||||
<header className="sticky top-0 z-50 border-b bg-white">
|
||||
<nav>Navigation</nav>
|
||||
</header>
|
||||
```
|
||||
|
||||
### Grid Layout
|
||||
|
||||
```tsx
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Truncate Text
|
||||
|
||||
```tsx
|
||||
<p className="truncate">This text will be truncated with ellipsis if too long</p>
|
||||
<p className="line-clamp-3">This text will show max 3 lines with ellipsis</p>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Consistent Spacing**: Stick to Tailwind's spacing scale
|
||||
2. **Responsive by Default**: Always consider mobile-first design
|
||||
3. **Extract Components**: Avoid repeating long class lists
|
||||
4. **Use Theme Colors**: Define custom colors in config, not arbitrary values
|
||||
5. **Leverage @apply Sparingly**: Prefer utility classes in JSX
|
||||
6. **Enable Dark Mode**: Plan for dark mode from the start
|
||||
7. **Use Plugins**: Leverage official plugins for common needs
|
||||
8. **Optimize Production**: Ensure purge is configured correctly
|
||||
+47
-2
@@ -1,6 +1,51 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
dist
|
||||
|
||||
# Version control
|
||||
.git
|
||||
.github
|
||||
|
||||
# IDE and editor
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Build outputs (rebuilt inside Docker)
|
||||
build
|
||||
dist
|
||||
.svelte-kit
|
||||
.docs-excluded
|
||||
|
||||
# Environment and secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
*.log
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Docker files (prevent recursive context)
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# Documentation and meta
|
||||
README.md
|
||||
README.template.md
|
||||
AGENTS.md
|
||||
CHANGELOG.md
|
||||
LICENSE
|
||||
check-output.txt
|
||||
|
||||
# AI / tooling config
|
||||
.claude
|
||||
|
||||
# Test artifacts
|
||||
*.test.*
|
||||
*.spec.*
|
||||
@@ -0,0 +1,58 @@
|
||||
# EditorConfig helps maintain consistent coding styles between editors
|
||||
root = true
|
||||
|
||||
# Default settings for all files (e.g. most common, best-practice standard across all filetypes)
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Svelte files
|
||||
[*.svelte]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# JavaScript and TypeScript
|
||||
[*.{js,ts,tsx,cjs,mjs}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# JSON files (package.json, config files, etc.) - per JSON (RFC 8259) specification
|
||||
[*.json,.prettierrc]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# YAML files (e.g., GitHub Actions, Lint configs) - per YAML 1.2 (2009) specification
|
||||
[*.{yaml,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# CSS & PostCSS files
|
||||
[*.{css,postcss}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown files
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
print_width = 180
|
||||
|
||||
# Dockerfile
|
||||
[Dockerfile*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
insert_final_newline = false
|
||||
|
||||
# Ignore binary files
|
||||
[*.{png,jpg,jpeg,gif,ico,svg,woff,woff2,eot,ttf,otf}]
|
||||
charset = unset
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
@@ -0,0 +1,3 @@
|
||||
KENER_SECRET_KEY=some_secret_key_for_kener
|
||||
REDIS_URL=redis://localhost:6379
|
||||
ORIGIN=http://localhost:3000
|
||||
@@ -1,11 +0,0 @@
|
||||
KENER_SECRET_KEY=please_change_me
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
KENER_BASE_PATH=""
|
||||
RESEND_API_KEY=
|
||||
ORIGIN=http://localhost:3000
|
||||
DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||
RESEND_SENDER_EMAIL=Accounts <accounts@resend.dev>
|
||||
|
||||
# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase
|
||||
# DATABASE_URL=mysql://root:password@127.0.0.1:3306/kener
|
||||
@@ -0,0 +1,48 @@
|
||||
# Contributing to Kener
|
||||
|
||||
Thank you for considering contributing to our project! Here are some guidelines to help you get started.
|
||||
|
||||
---
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. Fork the repository and clone it locally.
|
||||
2. Create a new branch for your feature or bug fix:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
3. Make your changes and commit them:
|
||||
```bash
|
||||
git commit -m 'Describe your changes'
|
||||
```
|
||||
4. Push your changes to your fork:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
5. Create a pull request to the `main` branch.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. Create a `.env` file in the root of the project and add the following:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
3. Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is available in the `docs` folder. You can view it by going to [http://localhost:3000/docs/home](http://localhost:3000/docs/home) in your browser.
|
||||
|
||||
## Where to Start
|
||||
|
||||
1. Check out the [roadmap items](https://kener.ing/docs/roadmap/)
|
||||
2. Add language support by following the [i18n guide](https://kener.ing/docs/i18n/)
|
||||
@@ -17,22 +17,20 @@ Steps to reproduce the behavior:
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Version**
|
||||
Which version of kener you are using.
|
||||
|
||||
**Environment**
|
||||
Which environment you are using or where is it deployed. `docker`, `kubernetes`, `bare-metal`, `development`, `pm2` etc
|
||||
|
||||
**Database**
|
||||
Which database you are using. `sqlite`, `mysql`, `postgres`
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: Create Incident Template
|
||||
about: Create Incident Template
|
||||
title: Title of Incident
|
||||
labels: incident
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Your Incident Description goes here. Markdown Supported
|
||||
|
||||
[start_datetime:utcSeconds]
|
||||
[end_datetime:utcSeconds]
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Kener v4 items
|
||||
about: Kener v4 items
|
||||
title: ''
|
||||
labels: kener_v4
|
||||
assignees: rajnandan1
|
||||
|
||||
---
|
||||
|
||||
Kener v4 items
|
||||
@@ -0,0 +1,428 @@
|
||||
# Kener API Development Instructions
|
||||
|
||||
This document provides guidelines for creating new API endpoints in Kener. Follow these patterns to maintain consistency across all APIs.
|
||||
|
||||
## API Architecture Overview
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/routes/(api)/api/
|
||||
├── {resource}/
|
||||
│ ├── +server.ts # GET (list), POST (create)
|
||||
│ └── [{resource}_id]/
|
||||
│ ├── +server.ts # GET, PATCH, DELETE (single resource)
|
||||
│ └── {sub-resource}/
|
||||
│ ├── +server.ts # GET (list), POST (create)
|
||||
│ └── [{sub_id}]/
|
||||
│ └── +server.ts # GET, PATCH, DELETE (single sub-resource)
|
||||
```
|
||||
|
||||
### Key Files
|
||||
- **Types**: `src/lib/types/api.ts` - All API request/response types (snake_case)
|
||||
- **Middleware**: `src/hooks.server.ts` - Authentication and resource validation
|
||||
- **App Locals**: `src/app.d.ts` - TypeScript declarations for `event.locals`
|
||||
- **Repository**: `src/lib/server/db/repositories/*.ts` - Database operations
|
||||
- **DbImpl**: `src/lib/server/db/dbimpl.ts` - Bindings for repository methods
|
||||
|
||||
### Current Locals (set by middleware in `hooks.server.ts`)
|
||||
```typescript
|
||||
interface Locals {
|
||||
user?: SessionUser; // Auth session
|
||||
monitor?: MonitorRecordTyped; // /api/monitors/:monitor_tag/*
|
||||
incident?: IncidentRecord; // /api/incidents/:incident_id/*
|
||||
maintenance?: MaintenanceRecord; // /api/maintenances/:maintenance_id/*
|
||||
page?: PageRecord; // /api/pages/:page_path/*
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Use snake_case for API payloads
|
||||
```typescript
|
||||
// Correct
|
||||
interface CreateMonitorRequest {
|
||||
monitor_tag: string;
|
||||
start_date_time: number;
|
||||
duration_seconds: number;
|
||||
}
|
||||
|
||||
// Wrong
|
||||
interface CreateMonitorRequest {
|
||||
monitorTag: string;
|
||||
startDateTime: number;
|
||||
durationSeconds: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Type Naming Pattern
|
||||
```typescript
|
||||
// List response
|
||||
interface Get{Resource}sListResponse {
|
||||
{resources}: {Resource}Response[];
|
||||
}
|
||||
|
||||
// Single resource response
|
||||
interface Get{Resource}Response {
|
||||
{resource}: {Resource}DetailResponse;
|
||||
}
|
||||
|
||||
// Create request/response
|
||||
interface Create{Resource}Request { ... }
|
||||
interface Create{Resource}Response {
|
||||
{resource}: {Resource}Response;
|
||||
}
|
||||
|
||||
// Update request/response
|
||||
interface Update{Resource}Request { ... }
|
||||
interface Update{Resource}Response {
|
||||
{resource}: {Resource}Response;
|
||||
}
|
||||
|
||||
// Delete response
|
||||
interface Delete{Resource}Response {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Error responses (reuse existing)
|
||||
interface BadRequestResponse { error: { code: string; message: string; } }
|
||||
interface NotFoundResponse { error: { code: string; message: string; } }
|
||||
interface UnauthorizedResponse { error: { code: string; message: string; } }
|
||||
```
|
||||
|
||||
## Middleware Pattern
|
||||
|
||||
### 1. Add Route Regex Pattern in `hooks.server.ts`
|
||||
```typescript
|
||||
const RESOURCE_ID_ROUTE_REGEX = /^\/api\/resources\/(\d+)/;
|
||||
|
||||
function extractResourceId(pathname: string): number | null {
|
||||
const match = pathname.match(RESOURCE_ID_ROUTE_REGEX);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add Validation Block in `handle()` Function
|
||||
```typescript
|
||||
// Validate resource_id exists for /api/resources/:resource_id/* routes
|
||||
const resourceId = extractResourceId(pathname);
|
||||
if (resourceId) {
|
||||
const resource = await db.getResourceById(resourceId);
|
||||
if (!resource) {
|
||||
const errorResponse: NotFoundResponse = {
|
||||
error: {
|
||||
code: "NOT_FOUND",
|
||||
message: `Resource with id '${resourceId}' not found`,
|
||||
},
|
||||
};
|
||||
return json(errorResponse, { status: 404 });
|
||||
}
|
||||
// Store resource in locals for use in endpoints
|
||||
event.locals.resource = resource;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Declare in `app.d.ts`
|
||||
```typescript
|
||||
interface Locals {
|
||||
// Set by hooks.server.ts for /api/resources/:resource_id/* routes
|
||||
resource?: import("$lib/server/types/db").ResourceRecord;
|
||||
}
|
||||
```
|
||||
|
||||
## Endpoint Implementation Pattern
|
||||
|
||||
### GET (List)
|
||||
```typescript
|
||||
import { json, type RequestHandler } from "@sveltejs/kit";
|
||||
import db from "$lib/server/db/db";
|
||||
import type { GetResourcesListResponse, ResourceResponse } from "$lib/types/api";
|
||||
|
||||
function formatDateToISO(date: Date | string): string {
|
||||
if (date instanceof Date) return date.toISOString();
|
||||
const parsed = new Date(date.replace(" ", "T") + "Z");
|
||||
return parsed.toISOString();
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
// Parse query params for filtering
|
||||
const statusParam = url.searchParams.get("status");
|
||||
const pageParam = url.searchParams.get("page");
|
||||
const limitParam = url.searchParams.get("limit");
|
||||
|
||||
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
||||
const limit = limitParam ? Math.min(100, Math.max(1, parseInt(limitParam, 10) || 20)) : 20;
|
||||
|
||||
// Build filter
|
||||
const filter: { status?: string } = {};
|
||||
if (statusParam) filter.status = statusParam;
|
||||
|
||||
// Query database
|
||||
const rawResources = await db.getResourcesPaginated(page, limit, filter);
|
||||
|
||||
// Transform to response format
|
||||
const resources: ResourceResponse[] = rawResources.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
created_at: formatDateToISO(r.created_at),
|
||||
updated_at: formatDateToISO(r.updated_at),
|
||||
}));
|
||||
|
||||
const response: GetResourcesListResponse = { resources };
|
||||
return json(response);
|
||||
};
|
||||
```
|
||||
|
||||
### POST (Create)
|
||||
```typescript
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
let body: CreateResourceRequest;
|
||||
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
const errorResponse: BadRequestResponse = {
|
||||
error: { code: "BAD_REQUEST", message: "Invalid JSON body" },
|
||||
};
|
||||
return json(errorResponse, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || typeof body.name !== "string" || body.name.trim().length === 0) {
|
||||
const errorResponse: BadRequestResponse = {
|
||||
error: { code: "BAD_REQUEST", message: "name is required and must be a non-empty string" },
|
||||
};
|
||||
return json(errorResponse, { status: 400 });
|
||||
}
|
||||
|
||||
// Normalize timestamps using helper
|
||||
const normalizedTimestamp = GetMinuteStartTimestampUTC(body.start_date_time);
|
||||
|
||||
// Create resource
|
||||
const created = await db.createResource({
|
||||
name: body.name.trim(),
|
||||
start_date_time: normalizedTimestamp,
|
||||
});
|
||||
|
||||
// Build response
|
||||
const resourceResponse = await buildResourceResponse(created.id);
|
||||
const response: CreateResourceResponse = { resource: resourceResponse };
|
||||
return json(response, { status: 201 });
|
||||
};
|
||||
```
|
||||
|
||||
### GET (Single) - Uses Middleware
|
||||
```typescript
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
// Resource is validated by middleware and available in locals
|
||||
const resource = locals.resource!;
|
||||
|
||||
const resourceResponse = await buildResourceResponse(resource.id);
|
||||
const response: GetResourceResponse = { resource: resourceResponse };
|
||||
return json(response);
|
||||
};
|
||||
```
|
||||
|
||||
### PATCH (Update) - Uses Middleware
|
||||
```typescript
|
||||
export const PATCH: RequestHandler = async ({ locals, request }) => {
|
||||
const existingResource = locals.resource!;
|
||||
|
||||
let body: UpdateResourceRequest;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "Invalid JSON body" } }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate fields if provided
|
||||
if (body.status !== undefined && !["ACTIVE", "INACTIVE"].includes(body.status)) {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "status must be 'ACTIVE' or 'INACTIVE'" } }, { status: 400 });
|
||||
}
|
||||
|
||||
// Build update data - only include fields present in request
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (body.name !== undefined) updateData.name = body.name.trim();
|
||||
if (body.status !== undefined) updateData.status = body.status;
|
||||
|
||||
// Update if there's data to update
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await db.updateResource(existingResource.id, updateData);
|
||||
}
|
||||
|
||||
const resourceResponse = await buildResourceResponse(existingResource.id);
|
||||
const response: UpdateResourceResponse = { resource: resourceResponse };
|
||||
return json(response);
|
||||
};
|
||||
```
|
||||
|
||||
### DELETE - Uses Middleware
|
||||
```typescript
|
||||
export const DELETE: RequestHandler = async ({ locals }) => {
|
||||
const resource = locals.resource!;
|
||||
|
||||
// Delete related records first (cascade)
|
||||
await db.deleteResourceRelatedRecords(resource.id);
|
||||
|
||||
// Delete the resource itself
|
||||
await db.deleteResource(resource.id);
|
||||
|
||||
const response: DeleteResourceResponse = {
|
||||
message: `Resource with id '${resource.id}' deleted successfully`,
|
||||
};
|
||||
return json(response);
|
||||
};
|
||||
```
|
||||
|
||||
## Timestamp Handling
|
||||
|
||||
### Always normalize timestamps
|
||||
```typescript
|
||||
import { GetMinuteStartTimestampUTC, GetNowTimestampUTC } from "$lib/server/tool";
|
||||
|
||||
// For user-provided timestamps - normalize to minute start
|
||||
const normalizedTs = GetMinuteStartTimestampUTC(body.start_date_time);
|
||||
|
||||
// For current time (when timestamp is optional)
|
||||
const now = GetNowTimestampUTC();
|
||||
|
||||
// For optional timestamp with fallback
|
||||
const timestamp = body.timestamp !== undefined
|
||||
? GetMinuteStartTimestampUTC(body.timestamp)
|
||||
: GetMinuteStartNowTimestampUTC();
|
||||
```
|
||||
|
||||
## Validation Patterns
|
||||
|
||||
### Required Field Validation
|
||||
```typescript
|
||||
if (body.field === undefined || body.field === null) {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "field is required" } }, { status: 400 });
|
||||
}
|
||||
```
|
||||
|
||||
### Type Validation
|
||||
```typescript
|
||||
if (typeof body.count !== "number" || isNaN(body.count) || body.count <= 0) {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "count must be a positive number" } }, { status: 400 });
|
||||
}
|
||||
```
|
||||
|
||||
### Enum Validation
|
||||
```typescript
|
||||
const VALID_STATUSES = ["ACTIVE", "INACTIVE"];
|
||||
if (body.status && !VALID_STATUSES.includes(body.status)) {
|
||||
return json({
|
||||
error: { code: "BAD_REQUEST", message: `status must be one of: ${VALID_STATUSES.join(", ")}` }
|
||||
}, { status: 400 });
|
||||
}
|
||||
```
|
||||
|
||||
### Foreign Key Validation
|
||||
```typescript
|
||||
if (body.monitor_tag) {
|
||||
const monitor = await db.getMonitorByTag(body.monitor_tag);
|
||||
if (!monitor) {
|
||||
return json({
|
||||
error: { code: "BAD_REQUEST", message: `Monitor with tag '${body.monitor_tag}' not found` }
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Array Validation
|
||||
```typescript
|
||||
if (body.items !== undefined) {
|
||||
if (!Array.isArray(body.items)) {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "items must be an array" } }, { status: 400 });
|
||||
}
|
||||
|
||||
for (const item of body.items) {
|
||||
if (!item.tag || typeof item.tag !== "string") {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "Each item must have a valid tag" } }, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding Repository Methods
|
||||
|
||||
### 1. Add Method to Repository Class
|
||||
```typescript
|
||||
// In src/lib/server/db/repositories/{resource}.ts
|
||||
async getResourcesWithDetails(options: {
|
||||
page: number;
|
||||
limit: number;
|
||||
filter?: { status?: string };
|
||||
}): Promise<{ resources: ResourceRecord[]; total: number }> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Declare Method Type in DbImpl
|
||||
```typescript
|
||||
// In src/lib/server/db/dbimpl.ts - declarations section
|
||||
getResourcesWithDetails!: ResourceRepository["getResourcesWithDetails"];
|
||||
```
|
||||
|
||||
### 3. Bind Method in DbImpl Constructor
|
||||
```typescript
|
||||
// In src/lib/server/db/dbimpl.ts - bindResourceMethods()
|
||||
this.getResourcesWithDetails = this.resources.getResourcesWithDetails.bind(this.resources);
|
||||
```
|
||||
|
||||
## Common Imports
|
||||
```typescript
|
||||
import { json, type RequestHandler } from "@sveltejs/kit";
|
||||
import db from "$lib/server/db/db";
|
||||
import type {
|
||||
Get{Resource}Response,
|
||||
Create{Resource}Request,
|
||||
Create{Resource}Response,
|
||||
Update{Resource}Request,
|
||||
Update{Resource}Response,
|
||||
Delete{Resource}Response,
|
||||
BadRequestResponse,
|
||||
NotFoundResponse,
|
||||
} from "$lib/types/api";
|
||||
import { GetMinuteStartTimestampUTC, GetNowTimestampUTC } from "$lib/server/tool";
|
||||
```
|
||||
|
||||
## Response Status Codes
|
||||
- `200` - GET success, PATCH success, DELETE success
|
||||
- `201` - POST success (resource created)
|
||||
- `400` - Bad Request (validation errors)
|
||||
- `401` - Unauthorized (no/invalid token)
|
||||
- `404` - Not Found (resource doesn't exist)
|
||||
- `500` - Internal Server Error
|
||||
|
||||
## Testing with cURL
|
||||
```bash
|
||||
# List
|
||||
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/resources
|
||||
|
||||
# Create
|
||||
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"name":"Test","start_date_time":1735689600}' \
|
||||
http://localhost:3000/api/resources
|
||||
|
||||
# Get single
|
||||
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/resources/1
|
||||
|
||||
# Update
|
||||
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"name":"Updated"}' \
|
||||
http://localhost:3000/api/resources/1
|
||||
|
||||
# Delete
|
||||
curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/resources/1
|
||||
```
|
||||
|
||||
## Checklist for New API
|
||||
|
||||
1. [ ] Define types in `src/lib/types/api.ts`
|
||||
2. [ ] Add middleware validation in `src/hooks.server.ts` (if resource has ID routes)
|
||||
3. [ ] Update `src/app.d.ts` with locals type
|
||||
4. [ ] Create endpoint files in `src/routes/(api)/api/{resource}/`
|
||||
5. [ ] Add repository methods if needed
|
||||
6. [ ] Bind repository methods in DbImpl
|
||||
7. [ ] Test all endpoints with cURL
|
||||
@@ -0,0 +1,144 @@
|
||||
# Kener - AI Coding Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
Kener is an open-source status page application built with **SvelteKit 2.x** (**Svelte 5**) and **Node.js/Express**. It is a **TypeScript-first** codebase providing real-time monitoring, uptime tracking, incident management, and customizable dashboards.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Dual Process Model
|
||||
|
||||
In development, `npm run dev` runs two parallel processes:
|
||||
1. **SvelteKit dev server** (`vite dev`) - serves the frontend with HMR
|
||||
2. **Cron scheduler** (`vite-node src/lib/server/startup.ts`) - runs monitor checks, maintenance scheduling, daily cleanup
|
||||
|
||||
In production, **`scripts/main.ts`** is the single entry point: Express server + SvelteKit handler + migrations + seeds + scheduler startup. Built output runs via `node build/main.js`.
|
||||
|
||||
### Route Groups (SvelteKit)
|
||||
- **`(kener)/`** - Public status page routes
|
||||
- **`(manage)/`** - Admin dashboard (requires authentication)
|
||||
- **`(embed)/`** - Embeddable widgets
|
||||
- **`(docs)/`** - Documentation pages
|
||||
- **`(api)/`** - SvelteKit API routes
|
||||
- **`(account)/`** - Account/auth pages
|
||||
- **`(ext)/`** - External integrations
|
||||
- **`(assets)/`** - Asset serving
|
||||
|
||||
### Core Server Components
|
||||
- **`src/lib/server/controllers/`** - Domain-split controllers (18 TypeScript files): `apiController.ts`, `incidentController.ts`, `monitorsController.ts`, `maintenanceController.ts`, `pagesController.ts`, `userController.ts`, `dashboardController.ts`, `emailController.ts`, `siteDataController.ts`, `validators.ts`, etc.
|
||||
- **`src/lib/server/db/dbimpl.ts`** - Database abstraction layer using Knex.js with repository composition pattern
|
||||
- **`src/lib/server/db/repositories/`** - Domain-driven repositories: `monitors.ts`, `incidents.ts`, `maintenances.ts`, `pages.ts`, `users.ts`, `alerts.ts`, `monitoring.ts`, `images.ts`, `subscriptionSystem.ts`, `emailTemplateConfig.ts`, `monitorAlertConfig.ts`, `site-data.ts`
|
||||
- **`src/lib/server/services/`** - Monitor type implementations (all TypeScript): `apiCall.ts`, `pingCall.ts`, `tcpCall.ts`, `dnsCall.ts`, `sslCall.ts`, `sqlCall.ts`, `heartbeatCall.ts`, `gamedigCall.ts`, `groupCall.ts`, `grpcCall.ts`, `noneCall.ts`
|
||||
- **`src/lib/server/schedulers/`** - Scheduling via `croner`: `appScheduler.ts`, `monitorSchedulers.ts`, `maintenanceScheduler.ts`, `dailyCleanup.ts`, `shutdown.ts`
|
||||
- **`src/lib/server/queues/`** - Job queues via **BullMQ** + **Redis**: `monitorExecuteQueue.ts`, `monitorResponseQueue.ts`, `alertingQueue.ts`, `emailQueue.ts`, `subscriberQueue.ts`
|
||||
- **`src/lib/server/api-server/`** - Express-side API handlers with file-based routing (directory/method pattern: e.g., `monitor-bar/get.ts`)
|
||||
- **`src/lib/server/cron-minute.ts`** - Per-monitor cron execution logic
|
||||
|
||||
### Database
|
||||
- Supports SQLite (default), PostgreSQL, MySQL via **Knex.js**
|
||||
- Connection string format: `sqlite://./path` or `postgresql://...` or `mysql://...`
|
||||
- Migrations in `/migrations/`, seeds in `/seeds/`
|
||||
- Run migrations: `npm run migrate` or auto-runs on `npm start`
|
||||
|
||||
### Build System
|
||||
`npm run build` is a two-step process:
|
||||
1. `scripts/build-sveltekit.js` - Vite build of SvelteKit app (optionally with `--with-docs`)
|
||||
2. `scripts/build-server.js` - esbuild bundles `scripts/main.ts` into `build/main.js`
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev server (SvelteKit + cron scheduler in parallel)
|
||||
npm run build # Production build (SvelteKit then esbuild server bundle)
|
||||
npm run start # Run production build (node build/main.js)
|
||||
npm run check # Svelte + TypeScript type checking
|
||||
npm run prettify # Format all files with Prettier
|
||||
npm run migrate # Run database migrations via Knex
|
||||
npm run seed # Run database seeds
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Svelte 5 + TypeScript conventions
|
||||
- Use **TypeScript** for all code (`.ts`, and `.svelte` with `lang="ts"`).
|
||||
- Use **Svelte 5 runes** (`$state`, `$derived`, `$effect`, `$props()`) in components.
|
||||
- For SvelteKit route typing, use generated `$types` (e.g. `import type { PageServerLoad } from './$types'`).
|
||||
- Avoid packages that hard-require Svelte 4.
|
||||
|
||||
### Monitor Types
|
||||
Defined in `src/lib/server/services/service.ts`. Each type has its own implementation file:
|
||||
```typescript
|
||||
// Supported: API, PING, TCP, DNS, GROUP, SSL, SQL, HEARTBEAT, GAMEDIG, GRPC, NONE
|
||||
```
|
||||
|
||||
### Status Constants
|
||||
Use constants from `src/lib/global-constants.ts`:
|
||||
```typescript
|
||||
// In Svelte/client code:
|
||||
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "$lib/global-constants";
|
||||
|
||||
// In server code (use relative path):
|
||||
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "./global-constants";
|
||||
```
|
||||
|
||||
### API Authentication
|
||||
APIs use Bearer token auth verified via `VerifyAPIKey()`:
|
||||
```typescript
|
||||
import { VerifyAPIKey } from "$lib/server/controllers/apiController";
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
Always use the db singleton, never instantiate Knex directly:
|
||||
```typescript
|
||||
import db from "$lib/server/db/db";
|
||||
const monitor = await db.getMonitorByTag(tag);
|
||||
```
|
||||
|
||||
### Timestamps
|
||||
All timestamps are **UTC seconds** (not milliseconds). Use helpers from `src/lib/server/tool.ts`:
|
||||
```typescript
|
||||
import { GetMinuteStartTimestampUTC, GetNowTimestampUTC } from "$lib/server/tool";
|
||||
```
|
||||
|
||||
### i18n
|
||||
21 locale files in `src/lib/locales/` (en, de, fr, es, hi, ja, ko, zh-CN, zh-TW, pt-BR, ru, etc.). Add new translations by creating `{code}.json` and updating `locales.json`.
|
||||
|
||||
## UI Components
|
||||
|
||||
Uses **shadcn-svelte** components in `src/lib/components/ui/` (40+ components). Import pattern:
|
||||
```typescript
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
```
|
||||
|
||||
Styling: **Tailwind CSS v4** with CSS-based configuration (no `tailwind.config.js`). Theme uses HSL CSS variables defined in `src/routes/layout.css`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required:
|
||||
- `KENER_SECRET_KEY` - Secret key for auth
|
||||
- `ORIGIN` - Site URL (e.g., `http://localhost:3000`)
|
||||
- `REDIS_URL` - Redis connection string (required for BullMQ job queues)
|
||||
|
||||
Optional:
|
||||
- `DATABASE_URL` - Database connection string (defaults to SQLite)
|
||||
- `KENER_BASE_PATH` - Base path for reverse proxy
|
||||
- `PORT` - Server port (default 3000)
|
||||
- `RESEND_API_KEY` / `RESEND_SENDER_EMAIL` - Email notifications
|
||||
|
||||
## File Conventions
|
||||
|
||||
- Server-only code: `src/lib/server/`
|
||||
- Shared utilities: `src/lib/` (except `server/`)
|
||||
- Client utilities: `src/lib/client/`
|
||||
- Route data loading: `+page.server.ts` / `+layout.server.ts`
|
||||
- API endpoints: `+server.ts` files returning `json()`
|
||||
|
||||
## Types & Interfaces
|
||||
|
||||
Place types and interfaces in the appropriate folder based on where they are used:
|
||||
|
||||
- **`src/lib/types/`** - Shared types (safe to import from both server and client code). Use for domain models, DTOs, API response types, and anything needed on both sides.
|
||||
- **`src/lib/server/types/`** - Server-only types (`db.ts`, `auth.ts`, `monitor.ts`, `api-server.ts`). Use for DB models, internal service types, auth/session types.
|
||||
- **`src/lib/client/types/`** - Client-only types (`ui.ts`). Use for UI-specific types, component prop types.
|
||||
|
||||
Always use `import type { ... }` when importing types to avoid accidental runtime imports.
|
||||
@@ -0,0 +1,47 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Track base image versions via .env.build
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
file-patterns:
|
||||
- ".env.build"
|
||||
- "node:*" # Ensures Node.js images are correctly detected
|
||||
|
||||
# Monitor OS package versions in Dockerfile (Debian/Alpine)
|
||||
- package-ecosystem: "gitsubmodule" # Alternative method to track OS packages in Dockerfile
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "os-packages"
|
||||
commit-message:
|
||||
prefix: "os"
|
||||
include: "scope"
|
||||
|
||||
# Monitor Node.js dependencies from package.json
|
||||
# TODO: Uncomment below if we want to begin letting Dependabot monitor & open PRs for Node.js project dependencies
|
||||
# - package-ecosystem: "npm"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# labels:
|
||||
# - "dependencies"
|
||||
# - "npm"
|
||||
# commit-message:
|
||||
# prefix: "npm"
|
||||
# include: "scope"
|
||||
|
||||
# Monitor GitHub Actions dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
commit-message:
|
||||
prefix: "actions"
|
||||
include: "scope"
|
||||
@@ -0,0 +1,98 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release version (for example: 4.0.0)"
|
||||
required: true
|
||||
type: string
|
||||
make_latest:
|
||||
description: "Mark this release as latest"
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
prerelease:
|
||||
description: "Mark as pre-release"
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Bump version, tag, and create release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out default branch
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "Invalid version format: $VERSION"
|
||||
echo "Use semver like 4.0.0 or 4.0.0-rc.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Ensure release tag does not already exist
|
||||
run: |
|
||||
TAG="v${{ inputs.version }}"
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bump package version
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
CURRENT_VERSION=$(node -p 'require("./package.json").version')
|
||||
|
||||
if [ "$CURRENT_VERSION" != "$VERSION" ]; then
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
else
|
||||
echo "package.json already at version $VERSION"
|
||||
fi
|
||||
|
||||
- name: Commit version bump
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add package.json
|
||||
if [ -f package-lock.json ]; then
|
||||
git add package-lock.json
|
||||
fi
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore(release): bump version to $VERSION"
|
||||
fi
|
||||
|
||||
- name: Create and push git tag
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
TAG="v$VERSION"
|
||||
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
git push origin "HEAD:${{ github.event.repository.default_branch }}"
|
||||
git push origin "$TAG"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ inputs.version }}
|
||||
target_commitish: ${{ github.event.repository.default_branch }}
|
||||
generate_release_notes: true
|
||||
make_latest: ${{ inputs.make_latest && 'true' || 'false' }}
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Publish Nightly Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- next/**
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REGISTRY: docker.io
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
|
||||
GITHUB_IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
concurrency:
|
||||
group: nightly-docker-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-push-nightly:
|
||||
name: Build and push nightly Docker images
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [debian, alpine]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3.8.0
|
||||
with:
|
||||
cosign-release: v2.2.4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.6.1
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=nightly,enable=${{ matrix.variant == 'debian' }}
|
||||
type=raw,value=nightly-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=sha,format=short,prefix=nightly-,enable=${{ matrix.variant == 'debian' }}
|
||||
type=sha,format=short,prefix=nightly-alpine-,enable=${{ matrix.variant == 'alpine' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Sign the published Docker images
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
run: |
|
||||
echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Publish Main Docker Image (with Docs)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REGISTRY: docker.io
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
|
||||
GITHUB_IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
concurrency:
|
||||
group: main-docker-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-push-main:
|
||||
name: Build and push main Docker images (with docs)
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [debian, alpine]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3.8.0
|
||||
with:
|
||||
cosign-release: v2.2.4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.6.1
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=main-with-docs,enable=${{ matrix.variant == 'debian' }}
|
||||
type=raw,value=main-with-docs-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=sha,format=short,prefix=main-with-docs-,enable=${{ matrix.variant == 'debian' }}
|
||||
type=sha,format=short,prefix=main-with-docs-alpine-,enable=${{ matrix.variant == 'alpine' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
WITH_DOCS=true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Sign the published Docker images
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
run: |
|
||||
echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
@@ -0,0 +1,146 @@
|
||||
name: Publish Release Docker Images
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REGISTRY: docker.io
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
|
||||
GITHUB_IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
concurrency:
|
||||
group: release-docker-${{ github.event.release.tag_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-push-release:
|
||||
name: Build and push release Docker images
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [debian, alpine]
|
||||
base_path: ["", "/status"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out release tag
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
ref: refs/tags/${{ github.event.release.tag_name || github.ref_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate package version matches release tag
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name || github.ref_name }}"
|
||||
EXPECTED_VERSION="${TAG#v}"
|
||||
PACKAGE_VERSION=$(node -p 'require("./package.json").version')
|
||||
|
||||
if [ "$PACKAGE_VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "package.json version mismatch"
|
||||
echo "release tag: $TAG"
|
||||
echo "expected package.json version: $EXPECTED_VERSION"
|
||||
echo "actual package.json version: $PACKAGE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3.8.0
|
||||
with:
|
||||
cosign-release: v2.2.4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Compute release tags
|
||||
id: vars
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name || github.ref_name }}"
|
||||
NORM_TAG="${TAG#v}"
|
||||
|
||||
if [ "${{ matrix.base_path }}" = "/status" ]; then
|
||||
BASE_SUFFIX="-status"
|
||||
else
|
||||
BASE_SUFFIX=""
|
||||
fi
|
||||
|
||||
WITH_DOCS="false"
|
||||
|
||||
if [ "${{ matrix.variant }}" = "alpine" ]; then
|
||||
VARIANT_SUFFIX="-alpine"
|
||||
else
|
||||
VARIANT_SUFFIX=""
|
||||
fi
|
||||
|
||||
FULL_SUFFIX="${BASE_SUFFIX}${VARIANT_SUFFIX}"
|
||||
|
||||
echo "release_tag=${TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_norm_tag=${NORM_TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "latest_tag=latest${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "with_docs=${WITH_DOCS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "${{ matrix.variant }}" = "debian" ]; then
|
||||
echo "release_tag_debian_alias=${TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT"
|
||||
echo "release_norm_tag_debian_alias=${NORM_TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT"
|
||||
echo "latest_tag_debian_alias=latest${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.6.1
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=${{ steps.vars.outputs.latest_tag }}
|
||||
type=raw,value=${{ steps.vars.outputs.release_tag }}
|
||||
type=raw,value=${{ steps.vars.outputs.release_norm_tag }},enable=${{ steps.vars.outputs.release_norm_tag != steps.vars.outputs.release_tag }}
|
||||
type=raw,value=${{ steps.vars.outputs.latest_tag_debian_alias }},enable=${{ matrix.variant == 'debian' }}
|
||||
type=raw,value=${{ steps.vars.outputs.release_tag_debian_alias }},enable=${{ matrix.variant == 'debian' }}
|
||||
type=raw,value=${{ steps.vars.outputs.release_norm_tag_debian_alias }},enable=${{ matrix.variant == 'debian' && steps.vars.outputs.release_norm_tag_debian_alias != steps.vars.outputs.release_tag_debian_alias }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
WITH_DOCS=${{ steps.vars.outputs.with_docs }}
|
||||
KENER_BASE_PATH=${{ matrix.base_path }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Sign the published Docker images
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
run: |
|
||||
echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
name: Publish Docker image to Dockerhub and GHCR
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "*.*.*"
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ vars.DOCKERHUB_IMAGE_NAME != '' || vars.GHCR_IMAGE_NAME != '' }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Log in to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_IMAGE_NAME != ''
|
||||
}}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && vars.GHCR_IMAGE_NAME != '' }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_IMAGE_NAME }}
|
||||
${{ vars.GHCR_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ endsWith(github.ref, 'main') }}
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
flavor: |
|
||||
latest=false
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
+9
-1
@@ -1,4 +1,6 @@
|
||||
.DS_Store
|
||||
.DS_STORE
|
||||
**/.DS_Store
|
||||
node_modules
|
||||
static/kener
|
||||
build
|
||||
@@ -9,6 +11,7 @@ config/server.yaml
|
||||
/src/lib/.kener
|
||||
/package
|
||||
.env
|
||||
.vscode
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
@@ -26,4 +29,9 @@ uploads/*
|
||||
|
||||
static/uploads/*
|
||||
!static/uploads/upload.dir
|
||||
temp.txt
|
||||
temp.txt
|
||||
temp.js
|
||||
.DS_Store
|
||||
knip-output.txt
|
||||
check-output.txt
|
||||
translation-report.json
|
||||
+2
-1
@@ -19,4 +19,5 @@ config/static/*
|
||||
!config/static/.kener
|
||||
**/*.yaml
|
||||
**/*.yml
|
||||
.github/
|
||||
.github/
|
||||
src/lib/components/ui
|
||||
+69
-7
@@ -1,9 +1,71 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte",
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.ts", "*.tsx", "*.cjs", "*.mjs"],
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.json", ".prettierrc"],
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.yaml", "*.yml"],
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "Dockerfile",
|
||||
"options": {
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
## Available MCP Tools:
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns issues and suggestions.
|
||||
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||
|
||||
### 4. playground-link
|
||||
|
||||
Generates a Svelte Playground link with the provided code.
|
||||
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||
|
||||
## Database compatibility rule
|
||||
|
||||
All database operations — migrations, queries, and repository functions — **MUST** work across all three supported databases: **SQLite**, **PostgreSQL**, and **MySQL**. Use Knex.js schema builder and query builder abstractions; avoid raw SQL unless wrapped in dialect-safe helpers or guarded with `try/catch`. When writing migrations:
|
||||
|
||||
- Use `knex.schema.hasColumn` / `knex.schema.hasTable` guards for idempotency.
|
||||
- Use Knex column types (`.string()`, `.integer()`, `.text()`, etc.) — never raw `ALTER TABLE` unless necessary.
|
||||
- For data-seeding inside migrations, use standard Knex query builder (`.insert()`, `.update()`, `.orderBy()`, `.first()`).
|
||||
- Test that `defaultTo()` values and `notNullable()` constraints work on all three engines.
|
||||
|
||||
## Documentation writing skill
|
||||
|
||||
When the user asks to write or edit documentation, follow the skill file:
|
||||
|
||||
- `.claude/skills/documentation-writer/SKILL.md`
|
||||
|
||||
This is mandatory for docs-related tasks. Prioritize short, clear, action-oriented docs and avoid bloat.
|
||||
@@ -0,0 +1,123 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What is Kener?
|
||||
|
||||
Kener is an open-source status page application built with **SvelteKit 2.x (Svelte 5)** and **Node.js/Express**. It is a **TypeScript-first** codebase providing real-time monitoring, uptime tracking, incident management, and customizable dashboards.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev server (SvelteKit + cron scheduler in parallel)
|
||||
npm run build # Production build (SvelteKit then esbuild server bundle)
|
||||
npm run start # Run production build (node build/main.js)
|
||||
npm run check # Svelte + TypeScript type checking
|
||||
npm run prettify # Format all files with Prettier
|
||||
npm run migrate # Run database migrations via Knex
|
||||
npm run seed # Run database seeds (migrations run automatically first)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Dual Process Model
|
||||
|
||||
In development, `npm run dev` runs two parallel processes:
|
||||
|
||||
1. **SvelteKit dev server** (`vite dev`) - serves the frontend
|
||||
2. **Cron scheduler** (`vite-node src/lib/server/startup.ts`) - runs monitor checks, maintenance scheduler, daily cleanup
|
||||
|
||||
In production, `scripts/main.ts` is the single entry point: Express server + SvelteKit handler + migrations + seeds + scheduler startup.
|
||||
|
||||
### SvelteKit Route Groups
|
||||
|
||||
- **`(kener)/`** - Public status page
|
||||
- **`(manage)/`** - Admin dashboard (authenticated)
|
||||
- **`(embed)/`** - Embeddable widgets
|
||||
- **`(docs)/`** - Documentation pages
|
||||
- **`(api)/`** - SvelteKit API routes; also `src/lib/server/api-server/` for Express-side API handlers (file-based routing: `./action/method.ts`)
|
||||
- **`(account)/`** - Account/auth pages
|
||||
- **`(ext)/`** - External integrations
|
||||
- **`(assets)/`** - Asset serving
|
||||
|
||||
### Database
|
||||
|
||||
- **Knex.js** for query building and migrations. Supports SQLite (default), PostgreSQL, MySQL
|
||||
- Connection configured via `DATABASE_URL` env var: `sqlite://./path`, `postgresql://...`, `mysql://...`
|
||||
- Migrations in `/migrations/`, seeds in `/seeds/`
|
||||
- Always use the db singleton: `import db from "$lib/server/db/db"`
|
||||
|
||||
### Monitor Services
|
||||
|
||||
Each monitor type has a dedicated implementation in `src/lib/server/services/`:
|
||||
|
||||
- Types: API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group, gRPC, None
|
||||
- Scheduled via `src/lib/server/schedulers/` using `croner`
|
||||
- Job queues managed with **BullMQ** + **Redis** (`src/lib/server/queues/`)
|
||||
|
||||
### Build System
|
||||
|
||||
`npm run build` is a two-step process:
|
||||
|
||||
1. `scripts/build-sveltekit.js` - Vite build of SvelteKit app (optionally with `--with-docs`)
|
||||
2. `scripts/build-server.js` - esbuild bundles `scripts/main.ts` into `build/main.js`
|
||||
|
||||
## Key Conventions
|
||||
|
||||
### Svelte 5 + TypeScript
|
||||
|
||||
- Use **TypeScript** for new/modified code
|
||||
- Use **Svelte 5 runes** (`$state`, `$derived`, `$effect`, `$props()`) in new components
|
||||
- Use generated `$types` for SvelteKit route typing (`import type { PageServerLoad } from './$types'`)
|
||||
- Use `import type { ... }` for type imports
|
||||
|
||||
### UI Components
|
||||
|
||||
- **shadcn-svelte** components in `src/lib/components/ui/`
|
||||
- Import: `import { Button } from "$lib/components/ui/button"`
|
||||
- Styling: **Tailwind CSS v4** with HSL CSS variables for theming
|
||||
|
||||
### Timestamps
|
||||
|
||||
All timestamps are **UTC seconds** (not milliseconds). Use helpers from `src/lib/server/tool.ts`.
|
||||
|
||||
### Status Constants
|
||||
|
||||
Constants are exported as a **default export** from `src/lib/global-constants.ts`:
|
||||
|
||||
```typescript
|
||||
// In Svelte/client code or SvelteKit routes:
|
||||
import GC from "$lib/global-constants"
|
||||
// Usage: GC.UP, GC.DOWN, GC.DEGRADED, GC.MAINTENANCE, GC.NO_DATA
|
||||
|
||||
// In server code (use relative path):
|
||||
import GC from "../../global-constants.js"
|
||||
// Usage: GC.UP, GC.DOWN, etc.
|
||||
```
|
||||
|
||||
### API Authentication
|
||||
|
||||
APIs use Bearer token auth: `import { VerifyAPIKey } from "$lib/server/controllers/apiController"`
|
||||
|
||||
### Types Location
|
||||
|
||||
- `src/lib/types/` - Shared types (client + server)
|
||||
- `src/lib/server/types/` - Server-only types
|
||||
- `src/lib/client/types/` - Client-only types
|
||||
|
||||
### i18n
|
||||
|
||||
Locale files in `src/lib/locales/`. Add translations by creating `{code}.json` and updating `locales.json`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required: `KENER_SECRET_KEY`, `ORIGIN`, `REDIS_URL`
|
||||
Optional: `DATABASE_URL` (defaults to SQLite), `KENER_BASE_PATH`, `PORT` (default 3000), `RESEND_API_KEY`, `RESEND_SENDER_EMAIL`
|
||||
|
||||
## Skills
|
||||
|
||||
Read `.claude/skills/` for specialized instructions on:
|
||||
|
||||
- **svelte-code-writer** - Svelte component creation/editing
|
||||
- **documentation-writer** - Editing docs in `src/routes/(docs)/docs/content/`
|
||||
- **tailwindcss** - Tailwind CSS v4 patterns
|
||||
+175
-29
@@ -1,47 +1,193 @@
|
||||
FROM node:23
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Install necessary packages including tzdata for timezone setting
|
||||
RUN apt-get update && apt-get install -y \
|
||||
# =============================================================================
|
||||
# Kener v4 — Status Page Application
|
||||
# Multi-stage, multi-variant (Alpine / Debian) Dockerfile
|
||||
#
|
||||
# Build:
|
||||
# docker build -t kener . # Alpine (default)
|
||||
# docker build -t kener --build-arg VARIANT=debian . # Debian Slim
|
||||
# docker build -t kener --build-arg WITH_DOCS=true . # Include docs
|
||||
#
|
||||
# Run:
|
||||
# docker run -d -p 3000:3000 \
|
||||
# -e KENER_SECRET_KEY=<secret> \
|
||||
# -e ORIGIN=http://localhost:3000 \
|
||||
# -e REDIS_URL=redis://<host>:6379 \
|
||||
# -v kener_db:/app/database \
|
||||
# kener
|
||||
# =============================================================================
|
||||
|
||||
ARG NODE_VERSION=24
|
||||
ARG VARIANT=alpine
|
||||
ARG WITH_DOCS=false
|
||||
ARG KENER_BASE_PATH=
|
||||
|
||||
# =============================================================================
|
||||
# STAGE 1 — BUILDER (installs deps, compiles native modules, builds app)
|
||||
# =============================================================================
|
||||
|
||||
# ---------- Alpine builder ----------
|
||||
FROM node:${NODE_VERSION}-alpine AS builder-alpine
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
python3 \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
tzdata
|
||||
|
||||
# ---------- Debian builder ----------
|
||||
FROM node:${NODE_VERSION}-slim AS builder-debian
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libnode108 \
|
||||
nodejs \
|
||||
python3 \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
make \
|
||||
node-gyp \
|
||||
g++ \
|
||||
tzdata \
|
||||
iputils-ping && \
|
||||
tzdata && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the timezone environment variable
|
||||
ENV TZ=Etc/UTC
|
||||
# ---------- Selected variant ----------
|
||||
FROM builder-${VARIANT} AS builder
|
||||
|
||||
ENV NPM_CONFIG_LOGLEVEL=error
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm install && npm cache clean --force
|
||||
ARG KENER_BASE_PATH
|
||||
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
|
||||
|
||||
# Copy the rest of the application code
|
||||
# 1. Copy package manifests first (maximises layer cache hits)
|
||||
COPY package*.json ./
|
||||
|
||||
# 2. Install ALL dependencies (devDependencies needed for the build step)
|
||||
RUN npm ci --no-fund && \
|
||||
npm cache clean --force
|
||||
|
||||
# 3. Copy the rest of the source tree
|
||||
COPY . .
|
||||
|
||||
# Ensure /app/uploads and /app/database have rw permissions
|
||||
RUN mkdir -p /app/uploads /app/database && \
|
||||
chmod -R 777 /app/uploads /app/database
|
||||
# 4. Create directories that the app expects
|
||||
RUN mkdir -p database
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
# 5. Conditionally remove docs routes before build
|
||||
# (avoids EXDEV rename error in overlayfs; clean .svelte-kit so stale
|
||||
# route types don't persist)
|
||||
ARG WITH_DOCS
|
||||
RUN if [ "$WITH_DOCS" != "true" ]; then \
|
||||
rm -rf src/routes/\(docs\) .svelte-kit; \
|
||||
fi
|
||||
|
||||
# 6. Build: SvelteKit (vite) + server bundle (esbuild)
|
||||
# Use build-with-docs when docs are enabled
|
||||
RUN if [ "$WITH_DOCS" = "true" ]; then \
|
||||
npm run build-with-docs; \
|
||||
else \
|
||||
npm run build; \
|
||||
fi
|
||||
|
||||
# 7. Stage docs runtime files for index-docs (empty dir when docs disabled)
|
||||
RUN mkdir -p /docs-runtime && \
|
||||
if [ "$WITH_DOCS" = "true" ]; then \
|
||||
mkdir -p /docs-runtime/scripts && \
|
||||
mkdir -p /docs-runtime/src/lib && \
|
||||
mkdir -p "/docs-runtime/src/routes/(docs)/docs" && \
|
||||
cp scripts/index-docs.ts /docs-runtime/scripts/ && \
|
||||
cp src/lib/marked.ts /docs-runtime/src/lib/ && \
|
||||
cp "src/routes/(docs)/docs.json" "/docs-runtime/src/routes/(docs)/" && \
|
||||
cp -r "src/routes/(docs)/docs/content" "/docs-runtime/src/routes/(docs)/docs/"; \
|
||||
fi
|
||||
|
||||
# 8. Remove devDependencies from node_modules
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
# =============================================================================
|
||||
# STAGE 2 — PRODUCTION (minimal runtime image)
|
||||
# =============================================================================
|
||||
|
||||
# ---------- Alpine runtime ----------
|
||||
FROM node:${NODE_VERSION}-alpine AS final-alpine
|
||||
RUN apk add --no-cache \
|
||||
sqlite \
|
||||
tzdata \
|
||||
iputils \
|
||||
curl \
|
||||
libcap && \
|
||||
# Grant ping the NET_RAW capability so non-root users can send ICMP packets
|
||||
setcap cap_net_raw+ep /bin/ping || true
|
||||
|
||||
# ---------- Debian runtime ----------
|
||||
FROM node:${NODE_VERSION}-slim AS final-debian
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
sqlite3 \
|
||||
tzdata \
|
||||
iputils-ping \
|
||||
curl \
|
||||
libcap2-bin && \
|
||||
setcap cap_net_raw+ep /usr/bin/ping || true && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ---------- Selected variant ----------
|
||||
FROM final-${VARIANT} AS final
|
||||
|
||||
# Argument for the port
|
||||
ARG PORT=3000
|
||||
# Set the environment variable for the port
|
||||
ENV PORT=$PORT
|
||||
ARG KENER_BASE_PATH=
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE $PORT
|
||||
ENV NODE_ENV=production \
|
||||
PORT=${PORT} \
|
||||
KENER_BASE_PATH=${KENER_BASE_PATH} \
|
||||
BODY_SIZE_LIMIT=3M \
|
||||
TZ=UTC \
|
||||
# Required so Node can import .ts migration/seed files at runtime
|
||||
NODE_OPTIONS="--experimental-strip-types"
|
||||
|
||||
# Set the command to run the application
|
||||
CMD ["node", "main"]
|
||||
WORKDIR /app
|
||||
|
||||
# Create writable directories owned by the non-root "node" user
|
||||
# (node:node is provided by the official Node.js images)
|
||||
RUN mkdir -p database && \
|
||||
chown -R node:node /app
|
||||
|
||||
# ---- Copy artifacts from builder (order: least → most likely to change) ----
|
||||
|
||||
# Production node_modules (largest layer, changes least often)
|
||||
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
|
||||
|
||||
# Package manifest (needed for ESM "type":"module" resolution)
|
||||
COPY --chown=node:node --from=builder /app/package.json ./package.json
|
||||
|
||||
# Knex migrations & seeds (run at startup by build/main.js)
|
||||
COPY --chown=node:node --from=builder /app/migrations ./migrations
|
||||
COPY --chown=node:node --from=builder /app/seeds ./seeds
|
||||
|
||||
# Seed data files imported by seeds at runtime (all are leaf modules)
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/db/seedSiteData.ts ./src/lib/server/db/seedSiteData.ts
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/db/seedMonitorData.ts ./src/lib/server/db/seedMonitorData.ts
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/db/seedPagesData.ts ./src/lib/server/db/seedPagesData.ts
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/templates/general ./src/lib/server/templates/general
|
||||
|
||||
# Locale JSON files (read at runtime by server-side i18n)
|
||||
COPY --chown=node:node --from=builder /app/src/lib/locales ./src/lib/locales
|
||||
|
||||
# Build output (SvelteKit client/server + esbuild main.js) — changes most often
|
||||
COPY --chown=node:node --from=builder /app/build ./build
|
||||
|
||||
# Docs runtime files (index-docs script + markdown sources; empty when WITH_DOCS=false)
|
||||
COPY --chown=node:node --from=builder /docs-runtime/ ./
|
||||
|
||||
# Entrypoint script (runs index-docs on startup when docs are bundled)
|
||||
COPY --chown=node:node docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
RUN chmod +x docker-entrypoint.sh
|
||||
|
||||
# ---- Runtime configuration ----
|
||||
|
||||
# Switch to non-root user
|
||||
USER node
|
||||
|
||||
EXPOSE ${PORT}
|
||||
|
||||
# Healthcheck: hit the /healthcheck endpoint exposed by Express in main.ts
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD sh -c 'curl -sf http://localhost:${PORT}${KENER_BASE_PATH}/healthcheck || exit 1'
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD ["node", "build/main.js"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Raj Nandan Sharma
|
||||
Copyright (c) 2026 Raj Nandan Sharma
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
# Kener - A Beautiful Status Page System
|
||||
# Kener - Stunning Status Pages
|
||||
|
||||
<p align="center">
|
||||
<img src="https://kener.ing/newbg.png?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
<img src="https://kener.ing/og.jpg?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&style=social">
|
||||
<a href="https://github.com/ivbeg/awesome-status-pages"><img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome status page" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener"><img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" /></a>
|
||||
<a href="https://awesome-selfhosted.net/tags/status--uptime-pages.html#kener"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome self hosted" /></a>
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener"><img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/latest?logo=docker&logoColor=white&label=debian" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/alpine?logo=docker&logoColor=white&label=alpine" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rajnandan1/kener/actions/workflows/publish-images.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publish-images.yml" /></a>
|
||||
<a href="https://github.com/rajnandan1/kener/commit/HEAD"><img src="https://img.shields.io/github/last-commit/rajnandan1/kener/main" alt="" /></a>
|
||||
<a href="https://github.com/rajnandan1/kener/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/rajnandan1/kener.svg" /></a>
|
||||
<a href="https://deepwiki.com/rajnandan1/kener"><img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.producthunt.com/posts/kener-2" target="_blank">
|
||||
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=kener-2&theme=light" alt="Kener on Product Hunt">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.webp" type="image/webp">
|
||||
@@ -25,83 +44,185 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
#### [👉 Visit a live server](https://kener.ing)
|
||||
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/v4/getting-started/quick-start) | [🗄 Documentation](https://kener.ing/docs/v4/getting-started/introduction) |
|
||||
| ----------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
|
||||
#### [👉 Quick Start](https://kener.ing/docs/quick-start)
|
||||
<p align="center">
|
||||
|
||||
#### [👉 Documentation](https://kener.ing/docs/home)
|
||||
[](https://railway.com/deploy/spSvic?referralCode=1Pn7vs&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||
[](https://zeabur.com/templates/1YRTMI?referralCode=rajnandan1)
|
||||
[](https://render.com/deploy?repo=https%3A%2F%2Fgithub.com%2Frajnandan1%2Fkener)
|
||||
|
||||
</p>
|
||||
|
||||
## What is Kener?
|
||||
|
||||
Kener is status page system built with Sveltekit and NodeJS. It does not try to replace the Datadogs and Atlassian of the world. It tries to help some who wants to come up with a status page that looks nice and minimum overhead, in a modern way.
|
||||
**Kener** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. It’s not here to replace heavyweights like Datadog or Atlassian but rather to offer a simple, modern, and hassle-free way to set up a great-looking status page with minimal effort.
|
||||
|
||||
It is carefully crafted to be easy to use and customize.
|
||||
Designed with **ease of use** and **customization in mind**, Kener provides all the essential features you’d expect from a status page—without unnecessary complexity.
|
||||
|
||||
It comes with all the basic asks for a status page. It is open-source and free to use.
|
||||
### Why Kener?
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a cheaply available domain.
|
||||
✅ Minimal overhead – Set up quickly with a clean, modern UI<br>
|
||||
✅ Customizable – Easily tailor it to match your brand<br>
|
||||
✅ Open-source & free – Because great tools should be accessible to everyone
|
||||
|
||||
### What's in a Name?
|
||||
|
||||
“Kener” is inspired by the Assamese word _“Kene”_, meaning _“how’s it going?”_. The _‘.ing’_ was added because, well… that domain was available. 😄
|
||||
|
||||
## Quick Start
|
||||
|
||||
Get Kener running in minutes.
|
||||
|
||||
### Docker (recommended)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
|
||||
# Uses docker-compose.yml (includes Redis + Kener)
|
||||
# Set a strong KENER_SECRET_KEY and ORIGIN in docker-compose.yml before first run
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Open `http://localhost:3000`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Set a strong `KENER_SECRET_KEY` and set `ORIGIN` to your public URL before starting for the first time.
|
||||
|
||||
Use `docker-compose.dev.yml` when you want to build from local source instead of pulling the published image:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
```
|
||||
|
||||
Or combine both files to keep base production config while overriding Kener with a local build:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||
```
|
||||
|
||||
### Run pre-built image
|
||||
|
||||
You can use either image:
|
||||
|
||||
- `docker.io/rajnandan1/kener:latest`
|
||||
- `ghcr.io/rajnandan1/kener:latest`
|
||||
|
||||
For subpath deployments (`/status`), use:
|
||||
|
||||
- `docker.io/rajnandan1/kener:latest-status`
|
||||
- `docker.io/rajnandan1/kener:latest-status-alpine`
|
||||
- `ghcr.io/rajnandan1/kener:latest-status`
|
||||
- `ghcr.io/rajnandan1/kener:latest-status-alpine`
|
||||
|
||||
```bash
|
||||
mkdir -p database
|
||||
docker run -d \
|
||||
--name kener \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/database:/app/database" \
|
||||
-e "KENER_SECRET_KEY=replace_with_a_random_string" \
|
||||
-e "ORIGIN=http://localhost:3000" \
|
||||
-e "REDIS_URL=redis://host.docker.internal:6379" \
|
||||
docker.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
### Run pre-built subpath image (`/status`)
|
||||
|
||||
```bash
|
||||
mkdir -p database
|
||||
docker run -d \
|
||||
--name kener-status \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/database:/app/database" \
|
||||
-e "KENER_SECRET_KEY=replace_with_a_random_string" \
|
||||
-e "ORIGIN=http://localhost:3000" \
|
||||
-e "KENER_BASE_PATH=/status" \
|
||||
-e "REDIS_URL=redis://host.docker.internal:6379" \
|
||||
docker.io/rajnandan1/kener:latest-status
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> For subpath mode, keep `ORIGIN` as the site origin (`http://localhost:3000`), not `http://localhost:3000/status`.
|
||||
|
||||
### Run without Docker
|
||||
|
||||
Requirements:
|
||||
|
||||
- Node.js `>= 20`
|
||||
- Redis
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
npm install
|
||||
|
||||
# Start Redis (example)
|
||||
docker run -d --name kener-redis -p 6379:6379 redis:7-alpine
|
||||
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
Create a `.env` with at least:
|
||||
|
||||
```dotenv
|
||||
KENER_SECRET_KEY=replace_with_a_random_string
|
||||
ORIGIN=http://localhost:3000
|
||||
REDIS_URL=redis://localhost:6379
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
For the full quick start (including local Docker builds and dev mode), see the docs:
|
||||
|
||||
- https://kener.ing/docs/v4/getting-started/quick-start
|
||||
|
||||
<div align="left">
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
Here are some of the features that you get out of the box. Please read the documentation to know how to use them.
|
||||
Kener combines public status page essentials with advanced admin workflows.
|
||||
|
||||
### Monitoring and Tracking
|
||||
### 📊 Monitoring, Reliability, and Communication
|
||||
|
||||
- Advanced application performance monitoring tools
|
||||
- Real-time network monitor software capabilities
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Adjusts Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
- Automatically adjusts timezones for visitors
|
||||
- Monitor **API, Ping, TCP, DNS, SSL, SQL, Heartbeat, and GameDig** checks
|
||||
- Manage incidents with clear timelines, updates, and acknowledgements
|
||||
- Schedule maintenance windows and keep users informed throughout
|
||||
- Send notifications via **Email, Webhook, Slack, and Discord**
|
||||
- Explore historical monitoring data and uptime trends
|
||||
|
||||
### Customization and Branding
|
||||
### 🎨 Status Page Experience and Branding
|
||||
|
||||
- Customizable status page
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
- Beautifully Crafted Status Page
|
||||
- Build branded, customizable status pages (logo, colors, CSS, themes)
|
||||
- Support **light/dark mode**, localization, and timezone-aware display
|
||||
- Embed status widgets and badges into external sites and portals
|
||||
- Provide SEO-friendly public pages for global audiences
|
||||
|
||||
### Incident Management
|
||||
### 🛠️ Operations, Collaboration, and Automation
|
||||
|
||||
- Incident Management
|
||||
- Incident Communication
|
||||
- Comprehensive APIs for Incident Management
|
||||
- Invite teams with role-based collaboration across workflows
|
||||
- Manage multiple status pages from one Kener instance
|
||||
- Use trigger-based workflows and template-driven messaging
|
||||
- Manage API keys for secure integrations and automations
|
||||
- Integrate analytics providers like GA, Plausible, Mixpanel, Umami, and Clarity
|
||||
- Access the full REST API for incidents, monitors, and reporting
|
||||
|
||||
### User Experience and Design
|
||||
## Technologies Used
|
||||
|
||||
- Good Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
- Server Side Rendering
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Support Me
|
||||
|
||||
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
|
||||
If you’re enjoying Kener and want to support its development, consider sponsoring me on GitHub or treating me to a coffee. Your support helps keep the project growing! 🚀
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
- [Sponsor Me Using GitHub](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
- [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
|
||||

|
||||
## Contributing
|
||||
|
||||
If you want to contribute to Kener, please read the [Contribution Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
+14
-11
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.postcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/routes/layout.css",
|
||||
"baseColor": "zinc"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# =============================================================================
|
||||
# Kener v4 — Development Docker Compose (local build testing)
|
||||
#
|
||||
# Builds the image from the local Dockerfile instead of pulling from a registry.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.dev.yml up -d --build
|
||||
#
|
||||
# Build a specific variant:
|
||||
# docker compose -f docker-compose.dev.yml build --build-arg VARIANT=debian
|
||||
# docker compose -f docker-compose.dev.yml up -d
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Redis
|
||||
# ---------------------------------------------------------------------------
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: kener-redis-dev
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kener — built from local Dockerfile
|
||||
# ---------------------------------------------------------------------------
|
||||
kener:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VARIANT: alpine # or "debian"
|
||||
NODE_VERSION: 24
|
||||
WITH_DOCS: "true"
|
||||
KENER_BASE_PATH: ""
|
||||
container_name: kener-dev
|
||||
environment:
|
||||
KENER_SECRET_KEY: dev-secret-key-for-local-testing-only
|
||||
ORIGIN: http://localhost:3000
|
||||
REDIS_URL: redis://redis:6379
|
||||
# DATABASE_URL: sqlite://./database/kener.sqlite.db
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- dev_data:/app/database
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
dev_data:
|
||||
@@ -0,0 +1,47 @@
|
||||
# =============================================================================
|
||||
# Kener v4 — Production Docker Compose for /status base path
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.status.yml up -d
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: kener-redis-status
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
kener:
|
||||
image: rajnandan1/kener:latest-status
|
||||
# For Alpine variant use: rajnandan1/kener:latest-status-alpine
|
||||
container_name: kener-status
|
||||
environment:
|
||||
KENER_SECRET_KEY: replace_me_with_a_random_string # generate: openssl rand -base64 32
|
||||
ORIGIN: http://localhost:3000/status
|
||||
REDIS_URL: redis://redis:6379
|
||||
KENER_BASE_PATH: /status
|
||||
|
||||
# DATABASE_URL: sqlite://./database/kener.sqlite.db
|
||||
# DATABASE_URL: postgresql://user:password@postgres:5432/kener
|
||||
# DATABASE_URL: mysql://user:password@mysql:3306/kener
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- data:/app/database
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: kener_db_status
|
||||
redis_data:
|
||||
name: kener_redis_status
|
||||
+113
-24
@@ -1,26 +1,115 @@
|
||||
version: '3.8'
|
||||
# =============================================================================
|
||||
# Kener v4 — Production Docker Compose
|
||||
#
|
||||
# Usage:
|
||||
# docker compose up -d
|
||||
#
|
||||
# Defaults to the published image. To use a local build instead:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
kener:
|
||||
image: rajnandan1/kener:latest
|
||||
container_name: kener
|
||||
#env_file: .env #uncomment this, if you are using .env file
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
- NODE_ENV=production
|
||||
#- PORT=3000
|
||||
#- KENER_BASE_PATH=
|
||||
#- RESEND_API_KEY=
|
||||
#- ORIGIN=
|
||||
#- KENER_SECRET_KEY=
|
||||
#- RESEND_SENDER_EMAIL=
|
||||
#- DATABASE_URL=
|
||||
|
||||
|
||||
### Most likely DO NOT need to change anything below this ###
|
||||
|
||||
#- PORT=3000 Port app listens on IN CONTAINER
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
# ---------------------------------------------------------------------------
|
||||
# Redis — required for BullMQ queues, caching, and scheduler
|
||||
# ---------------------------------------------------------------------------
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: kener-redis
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- '.:/app/database:rw'
|
||||
- '.:/app/uploads:rw'
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kener — Status Page Application
|
||||
# ---------------------------------------------------------------------------
|
||||
kener:
|
||||
image: rajnandan1/kener:latest
|
||||
# For Alpine variant use: rajnandan1/kener:alpine
|
||||
container_name: kener
|
||||
environment:
|
||||
# ── Required ──
|
||||
KENER_SECRET_KEY: replace_me_with_a_random_string # generate: openssl rand -base64 32
|
||||
ORIGIN: http://localhost:3000 # public URL of your Kener instance (required for CSRF protection)
|
||||
REDIS_URL: redis://redis:6379
|
||||
|
||||
# ── Database (default: SQLite) ──
|
||||
# DATABASE_URL: sqlite://./database/kener.sqlite.db
|
||||
# DATABASE_URL: postgresql://user:password@postgres:5432/kener
|
||||
# DATABASE_URL: mysql://user:password@mysql:3306/kener
|
||||
|
||||
# ── Email (optional) ──
|
||||
# RESEND_API_KEY:
|
||||
# RESEND_SENDER_EMAIL:
|
||||
# SMTP_HOST:
|
||||
# SMTP_PORT:
|
||||
# SMTP_USER:
|
||||
# SMTP_PASSWORD:
|
||||
# SMTP_SENDER:
|
||||
# SMTP_SECURE: 0
|
||||
|
||||
# ── Advanced (you likely don't need to change these) ──
|
||||
# PORT: 3000
|
||||
# KENER_BASE_PATH:
|
||||
# NODE_ENV: production # already set in the image
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- data:/app/database
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Optional: PostgreSQL (uncomment and set DATABASE_URL above)
|
||||
# ---------------------------------------------------------------------------
|
||||
# postgres:
|
||||
# image: postgres:16-alpine
|
||||
# container_name: kener-postgres
|
||||
# environment:
|
||||
# POSTGRES_USER: kener
|
||||
# POSTGRES_PASSWORD: change_me # use a strong password
|
||||
# POSTGRES_DB: kener
|
||||
# volumes:
|
||||
# - postgres_data:/var/lib/postgresql/data
|
||||
# restart: unless-stopped
|
||||
# healthcheck:
|
||||
# test: ["CMD-SHELL", "pg_isready -U kener"]
|
||||
# interval: 10s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Optional: MySQL / MariaDB (uncomment and set DATABASE_URL above)
|
||||
# ---------------------------------------------------------------------------
|
||||
# mysql:
|
||||
# image: mariadb:11
|
||||
# container_name: kener-mysql
|
||||
# environment:
|
||||
# MYSQL_USER: kener
|
||||
# MYSQL_PASSWORD: change_me # use a strong password
|
||||
# MYSQL_DATABASE: kener
|
||||
# MYSQL_RANDOM_ROOT_PASSWORD: "true"
|
||||
# volumes:
|
||||
# - mysql_data:/var/lib/mysql
|
||||
# restart: unless-stopped
|
||||
# healthcheck:
|
||||
# test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
# interval: 10s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: kener_db
|
||||
redis_data:
|
||||
name: kener_redis
|
||||
# postgres_data:
|
||||
# name: kener_postgres
|
||||
# mysql_data:
|
||||
# name: kener_mysql
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Default body size limit for SvelteKit adapter-node (512K default is too small for image uploads)
|
||||
export BODY_SIZE_LIMIT="${BODY_SIZE_LIMIT:-3M}"
|
||||
|
||||
# Index documentation into Redis when docs are bundled in the image
|
||||
if [ -f /app/scripts/index-docs.ts ]; then
|
||||
echo "[kener] Indexing documentation into Redis..."
|
||||
node --experimental-strip-types /app/scripts/index-docs.ts || \
|
||||
echo "[kener] Warning: docs indexing failed (is REDIS_URL set?). Continuing..."
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# used https://github.com/linuxserver/docker-plex as a template
|
||||
|
||||
POPULATE_EXAMPLES=false
|
||||
|
||||
echo "-------------------------------------"
|
||||
echo -e "Setting up app config directory based on CONFIG_DIR env: ${CONFIG_DIR}\n"
|
||||
|
||||
# make config folder if it does not exist
|
||||
if [ ! -d "${CONFIG_DIR}" ]; then
|
||||
echo "Directory does not exist! Creating..."
|
||||
POPULATE_EXAMPLES=true
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
else
|
||||
if [ "$(ls -A ${CONFIG_DIR})" ]; then
|
||||
echo "Directory is not empty, not populating with defaults."
|
||||
else
|
||||
POPULATE_EXAMPLES=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# add example configs
|
||||
if [ "$POPULATE_EXAMPLES" = true ]; then
|
||||
echo "Directory is empty, adding defaults..."
|
||||
mkdir -p "${CONFIG_DIR}"/static
|
||||
cp -r /app/static/. "${CONFIG_DIR}"/static
|
||||
cp /app/config/monitors.example.yaml "${CONFIG_DIR}"/monitors.yaml
|
||||
cp /app/config/site.example.yaml "${CONFIG_DIR}"/site.yaml
|
||||
fi
|
||||
|
||||
# permissions
|
||||
echo "chown'ing directory to ensure correct permissions."
|
||||
chown -R abc:abc "${CONFIG_DIR}"
|
||||
echo "Done!"
|
||||
echo -e "-------------------------------------\n"
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-app-config/run
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo -e "\nApp is starting!"
|
||||
export NODE_ENV=production
|
||||
cd /app || exit
|
||||
|
||||
# Run build first
|
||||
exec \
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/main.js
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
title: View Alerts | Kener
|
||||
description: Learn how to view alerts in kener.
|
||||
---
|
||||
|
||||
# View Alerts
|
||||
|
||||
Alerts are used to notify you when a monitor goes down or up. You can view alerts in the Kener dashboard.
|
||||
|
||||
## ID
|
||||
|
||||
The unique ID of the alert.
|
||||
|
||||
## Type
|
||||
|
||||
The type of status that the alert is for. Example DOWN, DEGRADED
|
||||
|
||||
## Monitor
|
||||
|
||||
The monitor for which the alert is triggered.
|
||||
|
||||
## Status
|
||||
|
||||
The status of the alert. TRIGGERED, RESOLVED
|
||||
|
||||
## Checks
|
||||
|
||||
The number of checks that have been done.
|
||||
|
||||
## Incident
|
||||
|
||||
The incident that the alert is part of.
|
||||
|
||||
## Created At
|
||||
|
||||
The time when the alert was created.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: Categorize Monitors Guide | Kener
|
||||
description: Categorize Monitors in Kener
|
||||
---
|
||||
|
||||
# Categorize Monitors
|
||||
|
||||
Let us add a category to our monitors.
|
||||
|
||||
## Sample monitors.yaml
|
||||
|
||||
```yaml
|
||||
- name: OkBookmarks
|
||||
description: A free bookmark manager that lets you save and search your bookmarks in the cloud.
|
||||
tag: "okbookmarks"
|
||||
image: "https://okbookmarks.com/assets/img/extension_icon128.png"
|
||||
api:
|
||||
method: GET
|
||||
url: https://okbookmarks.com
|
||||
- name: Earth
|
||||
description: Our blue planet
|
||||
tag: "earth"
|
||||
default_status: "UP"
|
||||
image: "/earth.png"
|
||||
category: "Hello"
|
||||
- name: Frogment
|
||||
description: A free openAPI spec editor and linter that breaks down your spec into fragments to make editing easier and more intuitive. Visit https://www.frogment.com
|
||||
tag: "frogment"
|
||||
image: "/frogment.png"
|
||||
api:
|
||||
method: GET
|
||||
url: https://www.frogment.com
|
||||
```
|
||||
|
||||
## Sample site.yaml
|
||||
|
||||
```yaml
|
||||
#...
|
||||
categories:
|
||||
- name: Hello
|
||||
description: Say Hello to the world
|
||||
#...
|
||||
```
|
||||
|
||||
The above will have OkBookmarks and Frogment under home. Earth will be under Hello category.
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
title: Changelogs | Kener
|
||||
description: Changelogs for Kener
|
||||
---
|
||||
|
||||
# Changelogs
|
||||
|
||||
## v3.0.1
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- Support for i18n in dates.
|
||||
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
|
||||
|
||||
## v3.0.0
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- New APIs for creating incidents and pushing updates. Read more [here](/docs/kener-apis)
|
||||
- Incident management is now part of the admin UI and removed from the config file.
|
||||
- The UI colors have been updated to be more muted.
|
||||
- Email Notifications for incidents using [resend](https://resend.com).
|
||||
- New Kener management portal. No monitors.yaml or site.yaml needed anymore
|
||||
- Login Page and Setup Page
|
||||
- Remove Github dependency
|
||||
- Options to disable square or dot pattern
|
||||
- Support for new languages
|
||||
- Multiple DB support (mysql, postgres, sqlite3)
|
||||
- New API reference
|
||||
- New documentation site
|
||||
|
||||
## v2.0.0
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for sqlite3 and removed dependency on file system
|
||||
- Added support for postgres database. Read more [here](/docs/database)
|
||||
- Added support for alerting. Read more [here](/docs/alerting)
|
||||
- Added color customization. Read more [here](/docs/customize-site#color)
|
||||
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
|
||||
- `barStyle`
|
||||
- `barRoundness`
|
||||
- `summaryStyle`
|
||||
|
||||
### Migration
|
||||
|
||||
Kener will automatically migrate your data from file system to sqlite3. If you are using a custom domain, you need to update the `site.yaml` file with the new `siteURL` field. Read more [here](/docs/customize-site#siteURL)
|
||||
|
||||
## v0.0.16
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for `hideURLForGet` in monitors. Read more [here](/docs/monitors)
|
||||
- New SVG badges for LIVE status. Read more [here](/docs/status-badges#live)
|
||||
- `[Breaking Change]` Removed dependency on Environment variable `PUBLIC_KENER_FOLDER`. Read more [here](#v0-0-16-migration)
|
||||
- Simplified build and deploy process
|
||||
- Added support for fonts. Read more [here](/docs/customize-site#font)
|
||||
- Added support for home page pattern. Read more [here](/docs/customize-site#pattern)
|
||||
- Added support for adding your analytics provider. Read more [here](/docs/site-analytics)
|
||||
- New Documentation Site
|
||||
- Addes support for `sqaures` pattern in home page. Read more [here](/docs/customize-site#pattern)
|
||||
- Redesigned the UI for better consistency
|
||||
- Embed now supports background color using a parameter `bgc`. Read more [here](/docs/embed#javascript-parameters)
|
||||
- Now title in `site.yaml` is `<title>` and `siteName` is actually the name of the site. Read more [here](/docs/customize-site#siteName)
|
||||
|
||||
### Migration
|
||||
|
||||
#### Source
|
||||
|
||||
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
|
||||
- Move `site.yaml` to `/config` folder
|
||||
- Move `monitors.yaml` to `/config` folder
|
||||
|
||||
#### Docker
|
||||
|
||||
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
title: Custom JS and CSS Guide | Kener
|
||||
description: Custom JS and CSS Guide for Kener
|
||||
---
|
||||
|
||||
Here is a guide to add custom JS and CSS to your Kener instance.
|
||||
|
||||
## Adding Custom JS
|
||||
|
||||
Add your custom JS to `static/` file. And in the `src/app.html` file, add the following line:
|
||||
|
||||
```html
|
||||
<script src="/your-custom-js-file.js"></script>
|
||||
```
|
||||
|
||||
## Adding Custom CSS
|
||||
|
||||
Three are two ways you can add custom CSS to your Kener instance.
|
||||
|
||||
### CSS file
|
||||
|
||||
Add your custom CSS to `static/` file. And in the `src/app.html` file, add the following line:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/your-custom-css-file.css" />
|
||||
```
|
||||
|
||||
Do not forget to add the base path if you are using a subpath. For example, if you are using a subpath `/kener`, then the path should be `/kener/your-custom-js-file.js`.
|
||||
|
||||
### Inline CSS
|
||||
|
||||
To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS there. Do not include `<style>` tags.
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
title: Embed Monitor | Kener
|
||||
description: Embed your monitor in your website
|
||||
---
|
||||
|
||||
# Embed Monitor
|
||||
|
||||
There are two ways to embed your monitor in your website
|
||||
|
||||
## Javascript
|
||||
|
||||
You can embed your monitor in your website using javascript. We recommend using this method as it takes care of the height of the embedded monitor.
|
||||
|
||||
```html
|
||||
<script
|
||||
async
|
||||
src="http://[hostname]/embed/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[tag]"
|
||||
></script>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<script
|
||||
async
|
||||
src="https://kener.ing/embed/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"
|
||||
></script>
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `monitor`: The monitor url
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
### Demo
|
||||
|
||||
<div class="border mx-auto rounded-sm w-585px">
|
||||
<script async src="/embed/monitor-earth/js?theme=dark&monitor=/embed/monitor-earth"></script>
|
||||
</div>
|
||||
|
||||
## Iframe
|
||||
|
||||
This is the simplest way to embed your monitor in your website. You can use the following code to embed your monitor in your website.
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="http://[hostname]/embed/monitor-[tag]?theme=light"
|
||||
width="100%"
|
||||
height="200"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://kener.ing/embed/monitor-earth?theme=light"
|
||||
width="100%"
|
||||
height="100"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
### Parameters
|
||||
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings
|
||||
|
||||
### Demo
|
||||
|
||||
<div class="border mx-auto rounded-sm w-585px">
|
||||
<iframe src="/embed/monitor-earth?theme=dark" width="100%" height="100" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
</div>
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
title: Github Setup | Kener
|
||||
description: Kener uses github for incident management. Issues created in github using certain tags go to kener as incidents.
|
||||
---
|
||||
|
||||
# Github Setup
|
||||
|
||||
Kener uses github for incident management. Issues created in github using certain tags go to kener as incidents.
|
||||
|
||||
## Step 1: Create Github Repository
|
||||
|
||||
Create a [Github Repository](https://github.com/new). It can be either public or private.
|
||||
|
||||
## Step 2: Create Github Token
|
||||
|
||||
You can create either a classic token or personal access token
|
||||
|
||||
### Creating Classic Token
|
||||
|
||||
- Go to [Tokens](https://github.com/settings/tokens/new)
|
||||
- Note: kener
|
||||
- Expiration: No Expiration
|
||||
- Scopes: write:packages
|
||||
- Click on generate Token
|
||||
|
||||
### Creating Personal Access Token
|
||||
|
||||
- Go to [Personal Access Token](https://github.com/settings/personal-access-tokens/new)
|
||||
- Token Name: kener
|
||||
- Expiration: Use custom to select a calendar date
|
||||
- Description: My Kener
|
||||
- Repository access: Check Only Selected Repositories. Select your github repository
|
||||
- Repository Permission: Select Issues Read Write
|
||||
- Click on generate token
|
||||
|
||||
## Step 3: Set environment
|
||||
|
||||
```bash
|
||||
export GH_TOKEN=github_pat_11AD3ZA3Y0
|
||||
```
|
||||
|
||||
## Step 4: Add to Kener
|
||||
|
||||
Add your repository details to kener.
|
||||
|
||||

|
||||
|
||||
### Github API URL
|
||||
|
||||
If you are on github enterprise you can set the github api url. For most users, it will be `https://api.github.com`
|
||||
|
||||
### Github Repo
|
||||
|
||||
The repository name you created in step 1
|
||||
|
||||
### Github Username
|
||||
|
||||
Your github username
|
||||
|
||||
### Incident History
|
||||
|
||||
It is in hours. It means if an issue is created before X hours then kener would not honor it. What it means, is that kener would not show it under active incidents nor it will update the uptime. Default is 30\*24 hours = 720 hours.
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
---
|
||||
title: Kener - A Sveltekit NodeJS Status Page System
|
||||
description: Kener is an open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents.
|
||||
---
|
||||
|
||||
# Kener - A Feature Rich & Modern Status Page
|
||||
|
||||
<p align="center">
|
||||
<img src="/newbg.png" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
</p>
|
||||
|
||||
<p class="flex space-x-2 justify-center">
|
||||
<a href="https://github.com/rajnandan1/kener/stargazers" >
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&
|
||||
style=social">
|
||||
</a>
|
||||
<a href="https://github.com/ivbeg/awesome-status-pages" >
|
||||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome status page" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener" >
|
||||
<img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" />
|
||||
</a>
|
||||
</p>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f38a/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f38a/512.gif" alt="🎊" width="32" height="32">
|
||||
</picture>
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.gif" alt="🔔" width="32" height="32">
|
||||
</picture>
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/2049_fe0f/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/2049_fe0f/512.gif" alt="⁉" width="32" height="32">
|
||||
</picture>
|
||||
</div>
|
||||
<div class="flex gap-2 kener-home-links">
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing">Live Demo</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/quick-start">Quick Start</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://github.com/rajnandan1/kener">Clone</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/deployment">Deploy</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/kener-apis">APIs</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## What is Kener?
|
||||
|
||||
Kener is status page system built with Sveltekit and NodeJS. It does not try to replace the Datadogs and Atlassian of the world. It tries to help some who wants to come up with a status page that looks nice and minimum overhead, in a modern way.
|
||||
|
||||
It comes with all the basic asks for a status page. It is open-source and free to use.
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a cheaply available domain.
|
||||
|
||||
## Features
|
||||
|
||||
### Monitoring and Tracking
|
||||
|
||||
- Advanced application performance monitoring tools
|
||||
- Real-time network monitor software capabilities
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Handles Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
|
||||
### Customization and Branding
|
||||
|
||||
- Customizable status page
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
- Beautifully Crafted Status Page
|
||||
|
||||
### Incident Management
|
||||
|
||||
- Incident Management
|
||||
- Incident Communication
|
||||
- Comprehensive APIs for Incident Management
|
||||
|
||||
### User Experience and Design
|
||||
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
- Server Side Rendering
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Support Me
|
||||
|
||||
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
---
|
||||
title: i18n | Kener
|
||||
description: Kener supports multiple languages. You can add translations to your site.
|
||||
---
|
||||
|
||||
# i18n
|
||||
|
||||
You can add translations to your site. By default it is set to `en`. Available translations are present in `src/lib/locales/` folders in the root directory. You can add more translations by adding a new file in the `src/lib/locales` folder.
|
||||
|
||||
## How to enable a translation
|
||||
|
||||
Kener has made it simple to add new translations.
|
||||
|
||||
### Step 1
|
||||
|
||||
Copy and update the translation file in the `locales` folder. The translation file should be a json file with the locale code as the name of the file. For example, `en.json`, `fr.json`, `de.json`.
|
||||
|
||||
<label for="exp2" class="accm">
|
||||
|
||||
<input type="checkbox" class="absolute opacity-0" id="exp2" />
|
||||
|
||||
<p class=" font-medium p-1 accmt rounded-md showaccm">
|
||||
<span>↓ Show File</span>
|
||||
<span>↑ Hide File</span>
|
||||
</p>
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||
```json
|
||||
{
|
||||
"14 Days": "14 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"Back": "Back",
|
||||
"Badge": "Badge",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Code Copied": "Code Copied",
|
||||
"Copy Code": "Copy Code",
|
||||
"Copy Link": "Copy Link",
|
||||
"Dark": "Dark",
|
||||
"Days": "Days",
|
||||
"DEGRADED": "DEGRADED",
|
||||
"DOWN": "DOWN",
|
||||
"Embed": "Embed",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
|
||||
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
|
||||
"Light": "Light",
|
||||
"Link Copied": "Link Copied",
|
||||
"LIVE Status": "LIVE Status",
|
||||
"Mode": "Mode",
|
||||
"MONITORING": "MONITORING",
|
||||
"No Data": "No Data",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Incident On %date": "No Incident On %date",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"Recent Incidents": "Recent Incidents",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"Share": "Share",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
|
||||
"Starts in %startedAt": "Starts in %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Theme",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime",
|
||||
"%status for %duration": "%status for %duration"
|
||||
}
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
### Step 2
|
||||
|
||||
Open `src/lib/locales/locales.json` and add the locale code and the name of the language.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"code": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"code": "hi",
|
||||
"name": "हिन्दी"
|
||||
},
|
||||
{
|
||||
"code": "zh-CN",
|
||||
"name": "中文"
|
||||
},
|
||||
{
|
||||
"code": "ja",
|
||||
"name": "日本語"
|
||||
},
|
||||
{
|
||||
"code": "vi",
|
||||
"name": "Tiếng Việt"
|
||||
},
|
||||
{
|
||||
"code": "fr",
|
||||
"name": "Français"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Step 3
|
||||
|
||||
Open `src/lib/i18n/client.js` and add the locale code to the `locales` array.
|
||||
|
||||
```js
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale";
|
||||
```
|
||||
|
||||
This should be valid for the date-fns library.
|
||||
|
||||
Next add a mapping in the `locales` object.
|
||||
|
||||
```js
|
||||
const locales = { ru, en: enUS, hi, dk: de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko };
|
||||
```
|
||||
|
||||
### Step 4
|
||||
|
||||
Restart the server and you will see a dropdown in the navbar to select the language.
|
||||
|
||||
<div class="note info">
|
||||
DO NOT forget to raise a PR with the new translation file so that others can use it too.
|
||||
</div>
|
||||
|
||||
## Variables
|
||||
|
||||
There are few variables that you you should not change,
|
||||
|
||||
- %status : This will be replaced by the status
|
||||
- %lastedFor : This will be replaced by the duration the incident lasted
|
||||
- %startedAt : This will be replaced by the time the incident started
|
||||
- %date : This will be replaced by the date
|
||||
- %duration : This will be replaced by the duration
|
||||
|
||||
## Dates
|
||||
|
||||
The localization for dates are still not there. We are working on it and will be available soon.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
title: Ping Monitors | Kener
|
||||
description: Learn how to set up and work with Ping monitors in kener.
|
||||
---
|
||||
|
||||
# Ping Monitors
|
||||
|
||||
Ping monitors are used to monitor livenees of your servers. You can use Ping monitors to monitor the uptime of your servers and get notified when they are down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Host V4
|
||||
|
||||
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv4 address. Example of IP4 address is `106.12.43.232`.
|
||||
|
||||
## Host V6
|
||||
|
||||
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv6 address. Example of IP6 address is `2001:0db8:85a3:0000:0000:8a2e:0370:7334`.
|
||||
|
||||
<p class="note danger">
|
||||
Please note that atleast one of the Host V4 or Host V6 is required.
|
||||
<p>
|
||||
@@ -1,22 +0,0 @@
|
||||
# Kener Showcase
|
||||
|
||||
This page is a showcase of how kener is getting used in the wild. If you want to add your site here, please raise a PR and modify this [file](https://github.com/rajnandan1/kener-docs/blob/main/docs/md/docs/showcase.md)
|
||||
|
||||
#### [Kener](https://kener.ing)
|
||||
#### [Cashfree Payments India](https://statuspage.cashfree.com/)
|
||||
#### [status.orhun.dev](https://status.orhun.dev/)
|
||||
#### [status.ordinalsbot.com](https://status.ordinalsbot.com/)
|
||||
#### [status.britsov.com](https://status.britsov.net/)
|
||||
#### [status.gosu.bar](https://status.gosu.bar/)
|
||||
#### [stat.imsun.org](https://stat.imsun.org/)
|
||||
#### [Goomer](https://status.goomer.com.br/)
|
||||
#### [kennek.io](https://status.kennek.io/)
|
||||
#### [evelan.io](https://status.evelan.io/)
|
||||
#### [evelan.io](https://status.evelan.io/)
|
||||
#### [sveir.xyz](https://status.sveir.xyz/)
|
||||
#### [cellcast.com](https://status.cellcast.com/)
|
||||
#### [flytbase.com](https://status.flytbase.com/)
|
||||
#### [scriptor-artis.fr](https://status.scriptor-artis.fr/)
|
||||
#### [jiance.f.ozizio.com](http://jiance.f.ozizio.com/)
|
||||
#### [sshaw.cn](https://s.sshaw.cn/)
|
||||
#### [donotes.app](https://status.donotes.app)
|
||||
@@ -1,156 +0,0 @@
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"sectionTitle": "Getting Started",
|
||||
"children": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"link": "/docs/home",
|
||||
"file": "/home.md"
|
||||
},
|
||||
{
|
||||
"title": "Get Started",
|
||||
"link": "/docs/quick-start",
|
||||
"file": "/quick-start.md"
|
||||
},
|
||||
{
|
||||
"title": "Concepts",
|
||||
"link": "/docs/concepts",
|
||||
"file": "/concepts.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Deployment",
|
||||
"link": "/docs/deployment",
|
||||
"file": "/deployment.md"
|
||||
},
|
||||
{
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"title": "Setup Environment",
|
||||
"link": "/docs/environment-vars",
|
||||
"file": "/environment-vars.md"
|
||||
},
|
||||
{
|
||||
"title": "Use Badges",
|
||||
"link": "/docs/status-badges",
|
||||
"file": "/status-badges.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Monitors",
|
||||
"link": "/docs/monitors",
|
||||
"file": "/monitors.md"
|
||||
},
|
||||
{
|
||||
"title": "API/Website Monitor",
|
||||
"link": "/docs/monitors-api",
|
||||
"file": "/monitors-api.md"
|
||||
},
|
||||
{
|
||||
"title": "Ping Monitor",
|
||||
"link": "/docs/monitors-ping",
|
||||
"file": "/monitors-ping.md"
|
||||
},
|
||||
{
|
||||
"title": "DNS Monitor",
|
||||
"link": "/docs/monitors-dns",
|
||||
"file": "/monitors-dns.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Triggers",
|
||||
"link": "/docs/triggers",
|
||||
"file": "/triggers.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Site",
|
||||
"link": "/docs/site",
|
||||
"file": "/site.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Github",
|
||||
"link": "/docs/gh-setup",
|
||||
"file": "/gh-setup.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup SEO",
|
||||
"link": "/docs/seo",
|
||||
"file": "/seo.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Home",
|
||||
"link": "/docs/home-page",
|
||||
"file": "/home-page.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Theme",
|
||||
"link": "/docs/theme",
|
||||
"file": "/theme.md"
|
||||
},
|
||||
{
|
||||
"title": "View Alerts",
|
||||
"link": "/docs/alerts",
|
||||
"file": "/alerts.md"
|
||||
},
|
||||
{
|
||||
"title": "API Keys",
|
||||
"link": "/docs/apikeys",
|
||||
"file": "/apikeys.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Incident Management",
|
||||
"link": "/docs/incident-management",
|
||||
"file": "/incident-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Embed",
|
||||
"link": "/docs/embed",
|
||||
"file": "/embed.md"
|
||||
},
|
||||
{
|
||||
"title": "Custom JS/CSS",
|
||||
"link": "/docs/custom-js-css-guide",
|
||||
"file": "/custom-js-css-guide.md"
|
||||
},
|
||||
{
|
||||
"title": "Internationalization",
|
||||
"link": "/docs/i18n",
|
||||
"file": "/i18n.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "API Reference",
|
||||
"children": [
|
||||
{
|
||||
"title": "Kener APIs",
|
||||
"link": "/docs/kener-apis",
|
||||
"file": "/kener-apis.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Help",
|
||||
"children": [
|
||||
{
|
||||
"title": "Changelogs",
|
||||
"link": "/docs/changelogs",
|
||||
"file": "/changelogs.md"
|
||||
},
|
||||
{
|
||||
"title": "Roadmap",
|
||||
"link": "/docs/roadmap",
|
||||
"file": "/roadmap.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
---
|
||||
title: Triggers | Kener
|
||||
description: Learn how to set up and work with triggers in kener.
|
||||
---
|
||||
|
||||
# Triggers
|
||||
|
||||
Triggers are used to trigger actions based on the status of your monitors. You can use triggers to send notifications, or call webhooks when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Name
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The name is used to define the name of the webhook. It is required and has to be a string.
|
||||
|
||||
### Description
|
||||
|
||||
The description is used to define the description of the webhook. It is optional and has to be a string.
|
||||
|
||||
Kener supports the following triggers:
|
||||
|
||||
- [Webhook](#webhook)
|
||||
- [Discord](#discord)
|
||||
- [Slack](#slack)
|
||||
- [Email](#email)
|
||||
|
||||
## Webhook
|
||||
|
||||
Webhook triggers are used to send a HTTP POST request to a URL when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### URL
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The URL is used to define the URL of the webhook. It is required and has to be a valid URL.
|
||||
|
||||
You can also pass secrets that are set in the environment variables.
|
||||
|
||||
Example: `https://example.com/webhook?secret=$SECRET_X`. Make sure `$SECRET_X` is set in the environment variables.
|
||||
|
||||
### Method
|
||||
|
||||
Method will always be `POST`
|
||||
|
||||
### Headers
|
||||
|
||||
The headers are used to define the headers that should be sent with the request. It is optional and has to be a valid JSON object. You can add secrets that are set in the environment variables.
|
||||
|
||||
Example: `Authorization: Bearer $SECRET_Y`. Make sure `$SECRET_Y` is set in the environment variables.
|
||||
|
||||
While sending webhook kener will add two more headers: `Content-Type: application/json` and `User-Agent: Kener/3.0.0`.
|
||||
|
||||
### Body
|
||||
|
||||
Body of the webhook will be sent as below:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Description |
|
||||
| --------------------- | ----------------------------------------------------------- |
|
||||
| id | Unique ID of the alert |
|
||||
| alert_name | Name of the alert |
|
||||
| severity | Severity of the alert. Can be `critical`, `warn` |
|
||||
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` |
|
||||
| source | Source of the alert. Can be `Kener` |
|
||||
| timestamp | Timestamp of the alert |
|
||||
| description | Description of the alert. This you can customize. See below |
|
||||
| details | Details of the alert. |
|
||||
| details.metric | Name of the monitor |
|
||||
| details.current_value | Current value of the monitor |
|
||||
| details.threshold | Alert trigger threshold of the monitor |
|
||||
| actions | Actions to be taken. Link to view the monitor. |
|
||||
|
||||
## Discord
|
||||
|
||||
Discord triggers are used to send a message to a discord channel when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Discord URL
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The Discord URL is used to define the URL of the discord webhook. It is required and has to be a valid URL.
|
||||
|
||||
#### How to get the Discord URL?
|
||||
|
||||
1. Go to your discord server
|
||||
2. Right-click on the channel you want to send the messages
|
||||
3. Click on `Edit Channel`
|
||||
4. Go to `Integrations`
|
||||
5. Click on `Create Webhook`
|
||||
6. Copy the URL
|
||||
|
||||
#### Discord Message
|
||||
|
||||
The discord message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||
|
||||
The discord message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
## Slack
|
||||
|
||||
Slack triggers are used to send a message to a slack channel when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Slack URL
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The Slack URL is used to define the URL of the slack webhook. It is required and has to be a valid URL.
|
||||
|
||||
#### How to get the Slack URL?
|
||||
|
||||
1. Go to your slack workspace
|
||||
2. Click on `Apps` on the left sidebar
|
||||
3. Search for `Incoming Webhooks`
|
||||
4. Click on `Add to Slack`
|
||||
5. Select the channel you want to send the messages
|
||||
6. Click on `Add Incoming Webhook Integration`
|
||||
7. Copy the URL
|
||||
|
||||
#### Slack Message
|
||||
|
||||
The slack message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||
|
||||
The slack message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
## Email
|
||||
|
||||
Email triggers are used to send an email when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div class="border px-2 rounded-md mt-4">
|
||||
|
||||
#### Note
|
||||
|
||||
Please make sure you have set the `RESEND_API_KEY` in the environment variables.
|
||||
|
||||
</div>
|
||||
|
||||
### To
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The email addresses to which the email should be sent. It is required and has to be a valid email addresses. You can pass multiple email addresses separated by a comma.
|
||||
|
||||
### Sender
|
||||
|
||||
The email address from which the email should be sent.
|
||||
|
||||
It should be in the format `Name <email@address.com>`
|
||||
|
||||
If you have not connected your domain with resend, then use `Some Name <onboarding@resend.dev>`
|
||||
|
||||
### Subject
|
||||
|
||||
Subject of the email when `TRIGGERED`
|
||||
|
||||
```text
|
||||
[TRIGGERED] Mockoon DOWN at 2024-12-27T04:42:01.430Z
|
||||
```
|
||||
|
||||
Subject of the email when `RESOLVED`
|
||||
|
||||
```text
|
||||
[RESOLVED] Mockoon DOWN at 2024-12-27T04:42:01.430Z
|
||||
```
|
||||
|
||||
### Body
|
||||
|
||||
The emaik message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||
|
||||
The emaik message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Edit Triggers
|
||||
|
||||
Click on the ⚙️ to edit the trigger.
|
||||
|
||||
### Deactivate Trigger
|
||||
|
||||
You can deactivate the trigger by switching the toggle to off. You cannot send message to a deactivated trigger. Any monitor with this trigger will not send any notifications.
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #111;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
Hello world
|
||||
<script
|
||||
async
|
||||
src="http://localhost:3000/embed/monitor-earth/js?theme=dark&bgc=111&locale=hi&monitor=http://localhost:3000/embed/monitor-earth"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const databaseURL = process.env.DATABASE_URL || "sqlite://./database/kener.sqlite.db";
|
||||
|
||||
const databaseURLParts = databaseURL.split("://");
|
||||
const databaseType = databaseURLParts[0];
|
||||
const databasePath = databaseURLParts[1];
|
||||
|
||||
const knexOb = {
|
||||
migrations: {
|
||||
directory: "./migrations"
|
||||
},
|
||||
seeds: {
|
||||
directory: "./seeds"
|
||||
}
|
||||
};
|
||||
if (databaseType === "sqlite") {
|
||||
knexOb.client = "better-sqlite3";
|
||||
knexOb.connection = {
|
||||
filename: databasePath
|
||||
};
|
||||
knexOb.useNullAsDefault = true;
|
||||
} else if (databaseType === "postgresql") {
|
||||
knexOb.client = "pg";
|
||||
knexOb.connection = databaseURL;
|
||||
} else if (databaseType === "mysql") {
|
||||
knexOb.client = "mysql2";
|
||||
knexOb.connection = databaseURL;
|
||||
} else {
|
||||
console.error("Invalid database type");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default knexOb;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const databaseURL = process.env.DATABASE_URL || "sqlite://./database/kener.sqlite.db";
|
||||
|
||||
const databaseURLParts = databaseURL.split("://");
|
||||
const databaseType = databaseURLParts[0];
|
||||
const databasePath = databaseURLParts[1];
|
||||
|
||||
interface KnexConfig {
|
||||
migrations: { directory: string };
|
||||
seeds: { directory: string };
|
||||
databaseType: string;
|
||||
client?: string;
|
||||
connection?: string | { filename: string };
|
||||
useNullAsDefault?: boolean;
|
||||
}
|
||||
|
||||
const knexOb: KnexConfig = {
|
||||
migrations: {
|
||||
directory: "./migrations",
|
||||
},
|
||||
seeds: {
|
||||
directory: "./seeds",
|
||||
},
|
||||
databaseType,
|
||||
};
|
||||
if (databaseType === "sqlite") {
|
||||
knexOb.client = "better-sqlite3";
|
||||
knexOb.connection = {
|
||||
filename: databasePath,
|
||||
};
|
||||
knexOb.useNullAsDefault = true;
|
||||
} else if (databaseType === "postgresql") {
|
||||
knexOb.client = "pg";
|
||||
knexOb.connection = databaseURL;
|
||||
} else if (databaseType === "mysql") {
|
||||
knexOb.client = "mysql2";
|
||||
knexOb.connection = databaseURL;
|
||||
} else {
|
||||
console.error("Invalid database type");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default knexOb;
|
||||
@@ -1,94 +0,0 @@
|
||||
import { handler } from "./build/handler.js";
|
||||
import { apiReference } from "@scalar/express-api-reference";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import express from "express";
|
||||
import Startup from "./src/lib/server/startup.js";
|
||||
import fs from "fs-extra";
|
||||
import knex from "knex";
|
||||
import knexOb from "./knexfile.js";
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const base = process.env.KENER_BASE_PATH || "";
|
||||
|
||||
const app = express();
|
||||
const db = knex(knexOb);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith("/embed")) {
|
||||
res.setHeader("Content-Security-Policy", "frame-ancestors *");
|
||||
}
|
||||
res.setHeader("X-Powered-By", "Kener");
|
||||
next();
|
||||
});
|
||||
app.get(base + "/healthcheck", (req, res) => {
|
||||
res.end("ok");
|
||||
});
|
||||
//part /uploads server static files from static/uploads
|
||||
|
||||
//set env variable for upload path
|
||||
process.env.UPLOAD_PATH = "./uploads";
|
||||
|
||||
app.use(base + "/uploads", express.static("uploads"));
|
||||
|
||||
try {
|
||||
const openapiJSON = fs.readFileSync("./openapi.json", "utf-8");
|
||||
app.use(
|
||||
"/api-reference",
|
||||
apiReference({
|
||||
spec: {
|
||||
content: openapiJSON
|
||||
},
|
||||
theme: "alternate",
|
||||
hideModels: true,
|
||||
hideTestRequestButton: true,
|
||||
darkMode: true,
|
||||
metaData: {
|
||||
title: "Kener API Reference",
|
||||
description: "Kener free open source status page API Reference",
|
||||
ogDescription: "Kener free open source status page API Reference",
|
||||
ogTitle: "Kener API Reference",
|
||||
ogImage: "https://kener.ing/newbg.png",
|
||||
twitterCard: "summary_large_image",
|
||||
twitterTitle: "Kener API Reference",
|
||||
twitterDescription: "Kener free open source status page API Reference",
|
||||
twitterImage: "https://kener.ing/newbg.png"
|
||||
},
|
||||
favicon: "https://kener.ing/logo96.png"
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Error loading openapi.json, but that is okay.");
|
||||
}
|
||||
|
||||
app.use(handler);
|
||||
|
||||
//migrations
|
||||
async function runMigrations() {
|
||||
try {
|
||||
console.log("Running migrations...");
|
||||
await db.migrate.latest(); // Runs migrations to the latest state
|
||||
console.log("Migrations completed successfully!");
|
||||
} catch (err) {
|
||||
console.error("Error running migrations:", err);
|
||||
}
|
||||
}
|
||||
|
||||
//seed
|
||||
async function runSeed() {
|
||||
try {
|
||||
console.log("Running seed...");
|
||||
await db.seed.run(); // Runs seed to the latest state
|
||||
console.log("Seed completed successfully!");
|
||||
} catch (err) {
|
||||
console.error("Error running seed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
await runMigrations();
|
||||
await runSeed();
|
||||
await db.destroy();
|
||||
Startup();
|
||||
console.log("Kener is running on port " + PORT + "!");
|
||||
});
|
||||
@@ -1,135 +0,0 @@
|
||||
// migrations/YYYYMMDDHHMMSS_create_monitoring_tables.js
|
||||
|
||||
export function up(knex) {
|
||||
return (
|
||||
knex.schema
|
||||
// Create monitoring_data table
|
||||
.createTable("monitoring_data", (table) => {
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.integer("timestamp").notNullable();
|
||||
table.text("status");
|
||||
table.float("latency", 8, 2);
|
||||
table.text("type");
|
||||
table.primary(["monitor_tag", "timestamp"]);
|
||||
})
|
||||
// Create monitor_alerts table
|
||||
.createTable("monitor_alerts", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_status", 255).notNullable();
|
||||
table.string("alert_status", 255).notNullable();
|
||||
table.integer("health_checks").notNullable();
|
||||
table.integer("incident_number").defaultTo(0);
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
// Add index to monitor_alerts table
|
||||
.raw(
|
||||
"CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)"
|
||||
)
|
||||
.createTable("site_data", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("key", 255).notNullable().unique();
|
||||
table.text("value").notNullable();
|
||||
table.string("data_type", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("tag", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.text("description");
|
||||
table.text("image");
|
||||
table.string("cron", 255);
|
||||
table.string("default_status", 255);
|
||||
table.string("status", 255);
|
||||
table.string("category_name", 255);
|
||||
table.string("monitor_type", 255);
|
||||
table.string("down_trigger", 255);
|
||||
table.string("degraded_trigger", 255);
|
||||
table.text("type_data");
|
||||
table.integer("day_degraded_minimum_count");
|
||||
table.integer("day_down_minimum_count");
|
||||
table.string("include_degraded_in_downtime", 255).defaultTo("NO");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("trigger_type", 255);
|
||||
table.text("trigger_desc");
|
||||
table.string("trigger_status", 255);
|
||||
table.text("trigger_meta");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("users", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("email", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable();
|
||||
table.string("password_hash", 255).notNullable();
|
||||
table.integer("is_active").defaultTo(1);
|
||||
table.integer("is_verified").defaultTo(0);
|
||||
table.string("role", 255).defaultTo("user");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("api_keys", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("hashed_key", 255).notNullable().unique();
|
||||
table.string("masked_key", 255).notNullable();
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("incidents", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("title", 255).notNullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
table.integer("end_date_time");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
})
|
||||
.createTable("incident_monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_impact", 255);
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.integer("incident_id").notNullable();
|
||||
table.unique(["monitor_tag", "incident_id"]);
|
||||
})
|
||||
.createTable("incident_comments", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("comment").notNullable();
|
||||
table.integer("incident_id").notNullable();
|
||||
table.integer("commented_at").notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function down(knex) {
|
||||
return (
|
||||
knex.schema
|
||||
// Drop tables in reverse order
|
||||
.dropTableIfExists("monitor_alerts")
|
||||
.dropTableIfExists("monitoring_data")
|
||||
.dropTableIfExists("site_data")
|
||||
.dropTableIfExists("monitors")
|
||||
.dropTableIfExists("triggers")
|
||||
.dropTableIfExists("users")
|
||||
.dropTableIfExists("api_keys")
|
||||
.dropTableIfExists("incidents")
|
||||
.dropTableIfExists("incident_monitors")
|
||||
.dropTableIfExists("incident_comments")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable("monitoring_data"))) {
|
||||
await knex.schema.createTable("monitoring_data", (table) => {
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.integer("timestamp").notNullable();
|
||||
table.text("status");
|
||||
table.float("latency", 8, 2);
|
||||
table.text("type");
|
||||
table.primary(["monitor_tag", "timestamp"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("monitor_alerts"))) {
|
||||
await knex.schema.createTable("monitor_alerts", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_status", 255).notNullable();
|
||||
table.string("alert_status", 255).notNullable();
|
||||
table.integer("health_checks").notNullable();
|
||||
table.integer("incident_number").defaultTo(0);
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// Add index (IF NOT EXISTS not supported by all DBs, so use try/catch)
|
||||
try {
|
||||
await knex.schema.raw("CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)");
|
||||
} catch (_e) {
|
||||
// Index already exists
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("site_data"))) {
|
||||
await knex.schema.createTable("site_data", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("key", 255).notNullable().unique();
|
||||
table.text("value").notNullable();
|
||||
table.string("data_type", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("monitors"))) {
|
||||
await knex.schema.createTable("monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("tag", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.text("description");
|
||||
table.text("image");
|
||||
table.string("cron", 255);
|
||||
table.string("default_status", 255);
|
||||
table.string("status", 255);
|
||||
table.string("category_name", 255);
|
||||
table.string("monitor_type", 255);
|
||||
table.string("down_trigger", 255);
|
||||
table.string("degraded_trigger", 255);
|
||||
table.text("type_data");
|
||||
table.integer("day_degraded_minimum_count");
|
||||
table.integer("day_down_minimum_count");
|
||||
table.string("include_degraded_in_downtime", 255).defaultTo("NO");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("triggers"))) {
|
||||
await knex.schema.createTable("triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("trigger_type", 255);
|
||||
table.text("trigger_desc");
|
||||
table.string("trigger_status", 255);
|
||||
table.text("trigger_meta");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("users"))) {
|
||||
await knex.schema.createTable("users", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("email", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable();
|
||||
table.string("password_hash", 255).notNullable();
|
||||
table.integer("is_active").defaultTo(1);
|
||||
table.integer("is_verified").defaultTo(0);
|
||||
table.string("role", 255).defaultTo("user");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("api_keys"))) {
|
||||
await knex.schema.createTable("api_keys", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("hashed_key", 255).notNullable().unique();
|
||||
table.string("masked_key", 255).notNullable();
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("incidents"))) {
|
||||
await knex.schema.createTable("incidents", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("title", 255).notNullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
table.integer("end_date_time");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("incident_monitors"))) {
|
||||
await knex.schema.createTable("incident_monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_impact", 255);
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.integer("incident_id").notNullable();
|
||||
table.unique(["monitor_tag", "incident_id"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("incident_comments"))) {
|
||||
await knex.schema.createTable("incident_comments", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("comment").notNullable();
|
||||
table.integer("incident_id").notNullable();
|
||||
table.integer("commented_at").notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema
|
||||
.dropTableIfExists("monitor_alerts")
|
||||
.dropTableIfExists("monitoring_data")
|
||||
.dropTableIfExists("site_data")
|
||||
.dropTableIfExists("monitors")
|
||||
.dropTableIfExists("triggers")
|
||||
.dropTableIfExists("users")
|
||||
.dropTableIfExists("api_keys")
|
||||
.dropTableIfExists("incidents")
|
||||
.dropTableIfExists("incident_monitors")
|
||||
.dropTableIfExists("incident_comments");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("incidents", "incident_type");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_type").defaultTo("INCIDENT");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.dropColumn("incident_type");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("incidents", "incident_source");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_source").defaultTo("DASHBOARD");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.dropColumn("incident_source");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable("invitations")) return;
|
||||
|
||||
await knex.schema.createTable("invitations", (table) => {
|
||||
// Primary key
|
||||
table.increments("id").primary();
|
||||
|
||||
// Core invitation fields
|
||||
table.string("invitation_token").unique().notNullable();
|
||||
table.string("invitation_type").notNullable();
|
||||
table.integer("invited_user_id").nullable();
|
||||
table.integer("invited_by_user_id").notNullable();
|
||||
|
||||
// Additional data fields
|
||||
table.text("invitation_meta").nullable(); // For storing JSON or other metadata
|
||||
table.timestamp("invitation_expiry").notNullable();
|
||||
table.string("invitation_status").notNullable().defaultTo("PENDING");
|
||||
|
||||
// Timestamps
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Indexes
|
||||
table.index("invitation_status");
|
||||
table.index("invitation_expiry");
|
||||
table.index(["invited_by_user_id", "invitation_status"]);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("invitations");
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable("subscribers"))) {
|
||||
await knex.schema.createTable("subscribers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscriber_send").notNullable();
|
||||
table.text("subscriber_meta").nullable();
|
||||
table.string("subscriber_type").notNullable();
|
||||
table.string("subscriber_status").notNullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add unique constraint on subscriber_send and subscriber_type
|
||||
table.unique(["subscriber_send", "subscriber_type"]);
|
||||
|
||||
// Add index on subscriber_send for better query performance
|
||||
table.index(["subscriber_send"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("subscriptions"))) {
|
||||
await knex.schema.createTable("subscriptions", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_id").unsigned().notNullable();
|
||||
table.string("subscriptions_status").notNullable();
|
||||
table.string("subscriptions_monitors").notNullable();
|
||||
table.text("subscriptions_meta").nullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add unique constraint on subscriber_id and subscriptions_monitors
|
||||
// This constraint also creates an index that will be used for queries
|
||||
table.unique(["subscriber_id", "subscriptions_monitors"]);
|
||||
|
||||
// Add index to optimize queries filtering by status and monitors
|
||||
table.index(["subscriptions_status", "subscriptions_monitors"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("subscription_triggers"))) {
|
||||
await knex.schema.createTable("subscription_triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscription_trigger_type").notNullable().unique();
|
||||
table.string("subscription_trigger_status").notNullable();
|
||||
table.text("config").nullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema
|
||||
.dropTableIfExists("subscription_triggers")
|
||||
.dropTableIfExists("subscriptions")
|
||||
.dropTableIfExists("subscribers");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable("images")) return;
|
||||
|
||||
await knex.schema.createTable("images", (table) => {
|
||||
table.string("id", 32).primary(); // nanoid generated ID with prefix
|
||||
table.text("data").notNullable(); // base64 encoded image data
|
||||
table.string("mime_type", 50).notNullable(); // image/png, image/jpeg, image/svg+xml
|
||||
table.string("original_name", 255); // original filename
|
||||
table.integer("width"); // image width after resize
|
||||
table.integer("height"); // image height after resize
|
||||
table.integer("size"); // size in bytes
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("images");
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Create pages table
|
||||
if (!(await knex.schema.hasTable("pages"))) {
|
||||
await knex.schema.createTable("pages", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("page_path", 255).notNullable().unique(); // e.g., "/", "/api", "/infrastructure"
|
||||
table.string("page_title", 255).notNullable();
|
||||
table.string("page_header", 255);
|
||||
table.string("page_subheader", 255);
|
||||
table.string("page_logo", 255);
|
||||
table.text("page_settings_json"); // JSON settings for the page
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// Create pages_monitors junction table
|
||||
if (!(await knex.schema.hasTable("pages_monitors"))) {
|
||||
await knex.schema.createTable("pages_monitors", (table) => {
|
||||
table.integer("page_id").unsigned().notNullable();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.text("monitor_settings_json"); // JSON settings for monitor on this page (e.g., order, visibility)
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Composite primary key
|
||||
table.primary(["page_id", "monitor_tag"]);
|
||||
|
||||
// Foreign key constraints
|
||||
table.foreign("page_id").references("id").inTable("pages").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Add indexes (safe to fail if they already exist)
|
||||
try {
|
||||
await knex.schema.raw("CREATE INDEX idx_pages_monitors_page_id ON pages_monitors (page_id)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.raw("CREATE INDEX idx_pages_monitors_monitor_tag ON pages_monitors (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("pages_monitors");
|
||||
await knex.schema.dropTableIfExists("pages");
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable("maintenances"))) {
|
||||
await knex.schema.createTable("maintenances", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("title", 255).notNullable();
|
||||
table.text("description").nullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
table.string("rrule", 500).notNullable();
|
||||
table.integer("duration_seconds").notNullable();
|
||||
table.string("status", 50).notNullable().defaultTo("ACTIVE");
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("maintenance_monitors"))) {
|
||||
await knex.schema.createTable("maintenance_monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("maintenance_id").unsigned().notNullable();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
|
||||
table.unique(["maintenance_id", "monitor_tag"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("maintenances_events"))) {
|
||||
await knex.schema.createTable("maintenances_events", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("maintenance_id").unsigned().notNullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
table.integer("end_date_time").notNullable();
|
||||
table.string("status", 50).notNullable().defaultTo("SCHEDULED");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Add indexes (safe to fail if they already exist)
|
||||
const indexes = [
|
||||
"CREATE INDEX idx_maintenances_status ON maintenances (status)",
|
||||
"CREATE INDEX idx_maintenances_start_time ON maintenances (start_date_time)",
|
||||
"CREATE INDEX idx_maintenance_monitors_maintenance_id ON maintenance_monitors (maintenance_id)",
|
||||
"CREATE INDEX idx_maintenance_monitors_monitor_tag ON maintenance_monitors (monitor_tag)",
|
||||
"CREATE INDEX idx_maintenances_events_maintenance_id ON maintenances_events (maintenance_id)",
|
||||
"CREATE INDEX idx_maintenances_events_status ON maintenances_events (status)",
|
||||
"CREATE INDEX idx_maintenances_events_start_time ON maintenances_events (start_date_time)",
|
||||
"CREATE INDEX idx_maintenances_events_end_time ON maintenances_events (end_date_time)",
|
||||
];
|
||||
for (const sql of indexes) {
|
||||
try {
|
||||
await knex.schema.raw(sql);
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("maintenances_events");
|
||||
await knex.schema.dropTableIfExists("maintenance_monitors");
|
||||
await knex.schema.dropTableIfExists("maintenances");
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Remove unique constraint from monitors.name (safe to fail if already dropped)
|
||||
try {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.dropUnique(["name"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
// Constraint already removed
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// Re-add unique constraint to monitors.name
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.unique(["name"]);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("monitors", "is_hidden");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.string("is_hidden").defaultTo("NO").notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.dropColumn("is_hidden");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("monitors", "monitor_settings_json");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.text("monitor_settings_json").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.dropColumn("monitor_settings_json");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable("maintenance_monitors"))) return;
|
||||
const hasCol = await knex.schema.hasColumn("maintenance_monitors", "monitor_impact");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("maintenance_monitors", (table) => {
|
||||
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable("maintenance_monitors"))) return;
|
||||
await knex.schema.alterTable("maintenance_monitors", (table) => {
|
||||
table.dropColumn("monitor_impact");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Create monitor_alerts_config table
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_config"))) {
|
||||
await knex.schema.createTable("monitor_alerts_config", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("alert_for", 50).notNullable(); // STATUS, LATENCY, UPTIME
|
||||
table.string("alert_value", 255).notNullable(); // DOWN, DEGRADED, or numeric value like "1000" or "99"
|
||||
table.integer("failure_threshold").notNullable().defaultTo(1);
|
||||
table.integer("success_threshold").notNullable().defaultTo(1);
|
||||
table.text("alert_description");
|
||||
table.string("create_incident", 10).notNullable().defaultTo("NO"); // YES or NO
|
||||
table.string("is_active", 10).notNullable().defaultTo("YES"); // YES or NO
|
||||
table.string("severity", 50).notNullable().defaultTo("WARNING"); // CRITICAL or WARNING
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Foreign key to monitors table
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Create indexes (safe to fail if they already exist)
|
||||
try {
|
||||
await knex.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
|
||||
// Create monitor_alerts_config_triggers junction table
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_config_triggers"))) {
|
||||
await knex.schema.createTable("monitor_alerts_config_triggers", (table) => {
|
||||
table.integer("monitor_alerts_id").unsigned().notNullable();
|
||||
table.integer("trigger_id").unsigned().notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Composite primary key
|
||||
table.primary(["monitor_alerts_id", "trigger_id"]);
|
||||
|
||||
// Foreign keys
|
||||
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
||||
table.foreign("trigger_id").references("id").inTable("triggers").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// Drop junction table first due to foreign key constraints
|
||||
await knex.schema.dropTableIfExists("monitor_alerts_config_triggers");
|
||||
await knex.schema.dropTableIfExists("monitor_alerts_config");
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_v2"))) {
|
||||
await knex.schema.createTable("monitor_alerts_v2", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("config_id").unsigned().notNullable();
|
||||
table.integer("incident_id").unsigned().nullable();
|
||||
table.string("alert_status", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add index for faster queries on config_id and alert_status
|
||||
table.index(["config_id", "alert_status"]);
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure config_id is unsigned (fix for MySQL users who had signed int from a prior failed run)
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.integer("config_id").unsigned().notNullable().alter();
|
||||
});
|
||||
} catch (_e) {
|
||||
/* column may already be correct */
|
||||
}
|
||||
|
||||
// Ensure incident_id is unsigned
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.integer("incident_id").unsigned().nullable().alter();
|
||||
});
|
||||
} catch (_e) {
|
||||
/* column may already be correct */
|
||||
}
|
||||
|
||||
// Add foreign key constraints (skip if they already exist)
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.foreign("config_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key may already exist */
|
||||
}
|
||||
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.foreign("incident_id").references("id").inTable("incidents").onDelete("SET NULL");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key may already exist */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("monitor_alerts_v2");
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// 1. Create subscriber_users table - the actual user identity
|
||||
if (!(await knex.schema.hasTable("subscriber_users"))) {
|
||||
await knex.schema.createTable("subscriber_users", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("email", 255).notNullable().unique();
|
||||
table.string("status", 20).notNullable().defaultTo("PENDING");
|
||||
table.string("verification_code", 10).nullable();
|
||||
table.timestamp("verification_expires_at").nullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.index(["status"]);
|
||||
table.index(["email"]);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Create subscriber_methods table
|
||||
if (!(await knex.schema.hasTable("subscriber_methods"))) {
|
||||
await knex.schema.createTable("subscriber_methods", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_user_id").unsigned().notNullable();
|
||||
table.string("method_type", 50).notNullable();
|
||||
table.string("method_value", 500).notNullable();
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
table.text("meta").nullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// Add indexes, unique constraints, and foreign keys for subscriber_methods (idempotent)
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.index(["subscriber_user_id"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.index(["method_type"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.index(["status"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.unique(["subscriber_user_id", "method_type", "method_value"], {
|
||||
indexName: "sub_methods_user_type_value_unique",
|
||||
});
|
||||
});
|
||||
} catch (_e) {
|
||||
/* unique constraint already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key already exists */
|
||||
}
|
||||
|
||||
// 3. Create user_subscriptions_v2 table
|
||||
if (!(await knex.schema.hasTable("user_subscriptions_v2"))) {
|
||||
await knex.schema.createTable("user_subscriptions_v2", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_user_id").unsigned().notNullable();
|
||||
table.integer("subscriber_method_id").unsigned().notNullable();
|
||||
table.string("event_type", 50).notNullable();
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// Add indexes, unique constraints, and foreign keys for user_subscriptions_v2 (idempotent)
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["subscriber_user_id"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["subscriber_method_id"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["event_type"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["status"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.unique(["subscriber_user_id", "subscriber_method_id", "event_type"], {
|
||||
indexName: "sub_v2_user_method_event_unique",
|
||||
});
|
||||
});
|
||||
} catch (_e) {
|
||||
/* unique constraint already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.foreign("subscriber_method_id").references("id").inTable("subscriber_methods").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("user_subscriptions_v2");
|
||||
await knex.schema.dropTableIfExists("subscriber_methods");
|
||||
await knex.schema.dropTableIfExists("subscriber_users");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable("general_email_templates")) return;
|
||||
|
||||
await knex.schema.createTable("general_email_templates", (table) => {
|
||||
table.string("template_id").primary();
|
||||
table.string("template_subject");
|
||||
table.text("template_html_body");
|
||||
table.text("template_text_body");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("general_email_templates");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn("monitors", "external_url"))) {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.text("external_url").nullable();
|
||||
});
|
||||
}
|
||||
if (!(await knex.schema.hasColumn("monitoring_data", "error_message"))) {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.text("error_message").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.dropColumn("external_url");
|
||||
});
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.dropColumn("error_message");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
try {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.index(["timestamp"], "idx_monitoring_data_timestamp");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.index(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.dropIndex(["timestamp"], "idx_monitoring_data_timestamp");
|
||||
table.dropIndex(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn("incidents", "is_global"))) {
|
||||
await knex.schema.table("incidents", (table) => {
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
});
|
||||
}
|
||||
if ((await knex.schema.hasTable("maintenances")) && !(await knex.schema.hasColumn("maintenances", "is_global"))) {
|
||||
await knex.schema.table("maintenances", (table) => {
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn("incidents", "is_global")) {
|
||||
await knex.schema.table("incidents", (table) => {
|
||||
table.dropColumn("is_global");
|
||||
});
|
||||
}
|
||||
if ((await knex.schema.hasTable("maintenances")) && (await knex.schema.hasColumn("maintenances", "is_global"))) {
|
||||
await knex.schema.table("maintenances", (table) => {
|
||||
table.dropColumn("is_global");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("users", "is_owner");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("users", (table) => {
|
||||
table.string("is_owner").defaultTo("NO").notNullable();
|
||||
});
|
||||
|
||||
// Set the first user (by id) as owner, if any users exist.
|
||||
// This only runs when the column has just been added to avoid
|
||||
// overwriting an existing owner on migration re-run.
|
||||
const firstUser = await knex("users").orderBy("id", "asc").first();
|
||||
if (firstUser) {
|
||||
await knex("users").where("id", firstUser.id).update({ is_owner: "YES" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("users", "is_owner");
|
||||
if (hasCol) {
|
||||
await knex.schema.alterTable("users", (table) => {
|
||||
table.dropColumn("is_owner");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("pages", (table) => {
|
||||
table.text("page_subheader").alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("pages", (table) => {
|
||||
table.string("page_subheader", 255).alter();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.alterTable("pages_monitors", (table) => {
|
||||
table.integer("position").unsigned().notNullable().defaultTo(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
|
||||
if (hasColumn) {
|
||||
await knex.schema.alterTable("pages_monitors", (table) => {
|
||||
table.dropColumn("position");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Migration: Multi-Monitor Alerts
|
||||
*
|
||||
* This migration refactors the monitor alerting system to support many-to-many
|
||||
* relationships between alert configurations and monitors. Previously, each
|
||||
* `monitor_alerts_config` row was tied to exactly one monitor via a `monitor_tag`
|
||||
* foreign key column. This migration:
|
||||
*
|
||||
* 1. Creates a new `monitor_alerts_config_monitors` junction table that links
|
||||
* `monitor_alerts_config` rows to one or more `monitors` rows, enabling a
|
||||
* single alert configuration to fire across multiple monitors.
|
||||
*
|
||||
* 2. Adds a `monitor_tag` column to `monitor_alerts_v2` so that each firing
|
||||
* alert record knows which specific monitor triggered it (important when one
|
||||
* config covers many monitors).
|
||||
*
|
||||
* 3. Migrates existing data: copies every `monitor_alerts_config.monitor_tag`
|
||||
* value into the new junction table and backfills `monitor_alerts_v2.monitor_tag`
|
||||
* from the same source, preserving all historical alert records.
|
||||
*
|
||||
* 4. Removes the one-to-one constraint on `monitor_alerts_config.monitor_tag` by
|
||||
* dropping its foreign key and setting the column nullable (SQLite workaround:
|
||||
* nulls the column directly since SQLite cannot drop foreign key constraints
|
||||
* inline).
|
||||
*
|
||||
* 5. Adds a composite index on `monitor_alerts_v2 (config_id, monitor_tag,
|
||||
* alert_status)` for fast per-monitor alert status lookups.
|
||||
*
|
||||
* The `down` migration reverses these steps: restores the first junction-table
|
||||
* entry back onto `monitor_alerts_config.monitor_tag`, re-adds the foreign key
|
||||
* (non-SQLite), removes the `monitor_tag` column from `monitor_alerts_v2`
|
||||
* (non-SQLite), and drops the junction table.
|
||||
*/
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Step 1: Create monitor_alerts_config_monitors junction table
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_config_monitors"))) {
|
||||
await knex.schema.createTable("monitor_alerts_config_monitors", (table) => {
|
||||
table.integer("monitor_alerts_id").unsigned().notNullable();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Composite primary key
|
||||
table.primary(["monitor_alerts_id", "monitor_tag"]);
|
||||
|
||||
// Foreign keys
|
||||
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Add monitor_tag column to monitor_alerts_v2 for per-monitor alert tracking
|
||||
const hasV2Column = await knex.schema.hasColumn("monitor_alerts_v2", "monitor_tag");
|
||||
if (!hasV2Column) {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.string("monitor_tag", 255).nullable();
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: Migrate existing data from monitor_alerts_config.monitor_tag to junction table
|
||||
// and backfill monitor_alerts_v2.monitor_tag from the same source
|
||||
const existingConfigs = await knex("monitor_alerts_config").whereNotNull("monitor_tag").select("id", "monitor_tag");
|
||||
|
||||
if (existingConfigs.length > 0) {
|
||||
// Build a config_id -> monitor_tag map for backfilling alerts
|
||||
const configTagMap = new Map<number, string>();
|
||||
const inserts = existingConfigs.map((config: { id: number; monitor_tag: string }) => {
|
||||
configTagMap.set(config.id, config.monitor_tag);
|
||||
return {
|
||||
monitor_alerts_id: config.id,
|
||||
monitor_tag: config.monitor_tag,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
};
|
||||
});
|
||||
|
||||
// Batch insert into junction table
|
||||
const chunkSize = 100;
|
||||
for (let i = 0; i < inserts.length; i += chunkSize) {
|
||||
await knex("monitor_alerts_config_monitors").insert(inserts.slice(i, i + chunkSize));
|
||||
}
|
||||
|
||||
// Backfill monitor_tag on existing monitor_alerts_v2 rows
|
||||
const existingAlerts = await knex("monitor_alerts_v2").whereNull("monitor_tag").select("id", "config_id");
|
||||
for (const alert of existingAlerts) {
|
||||
const tag = configTagMap.get(alert.config_id);
|
||||
if (tag) {
|
||||
await knex("monitor_alerts_v2").where({ id: alert.id }).update({ monitor_tag: tag });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Drop foreign key and make monitor_tag nullable on monitor_alerts_config
|
||||
const dbClient = knex.client.config.client;
|
||||
|
||||
if (dbClient === "sqlite3" || dbClient === "better-sqlite3") {
|
||||
// SQLite cannot ALTER COLUMN, so we rebuild the table with monitor_tag nullable
|
||||
await knex.transaction(async (trx) => {
|
||||
await trx.raw("PRAGMA foreign_keys = OFF");
|
||||
await trx.raw("DROP TABLE IF EXISTS monitor_alerts_config_new");
|
||||
await trx.raw(`
|
||||
CREATE TABLE monitor_alerts_config_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_tag VARCHAR(255),
|
||||
alert_for VARCHAR(50) NOT NULL,
|
||||
alert_value VARCHAR(255) NOT NULL,
|
||||
failure_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
success_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
alert_description TEXT,
|
||||
create_incident VARCHAR(10) NOT NULL DEFAULT 'NO',
|
||||
is_active VARCHAR(10) NOT NULL DEFAULT 'YES',
|
||||
severity VARCHAR(50) NOT NULL DEFAULT 'WARNING',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await trx.raw(`
|
||||
INSERT INTO monitor_alerts_config_new
|
||||
(id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at)
|
||||
SELECT
|
||||
id, NULL, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at
|
||||
FROM monitor_alerts_config
|
||||
`);
|
||||
await trx.raw("DROP TABLE monitor_alerts_config");
|
||||
await trx.raw("ALTER TABLE monitor_alerts_config_new RENAME TO monitor_alerts_config");
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
await trx.raw("PRAGMA foreign_keys = ON");
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
||||
table.dropForeign(["monitor_tag"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
// Foreign key may not exist or already dropped
|
||||
}
|
||||
|
||||
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
||||
table.string("monitor_tag", 255).nullable().alter();
|
||||
});
|
||||
|
||||
await knex("monitor_alerts_config").update({ monitor_tag: null });
|
||||
}
|
||||
|
||||
// Step 5: Add composite index on monitor_alerts_v2 for fast lookups
|
||||
try {
|
||||
await knex.raw(
|
||||
"CREATE INDEX idx_monitor_alerts_v2_config_tag_status ON monitor_alerts_v2 (config_id, monitor_tag, alert_status)",
|
||||
);
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// Step 1: Drop the composite index on monitor_alerts_v2
|
||||
try {
|
||||
await knex.raw("DROP INDEX IF EXISTS idx_monitor_alerts_v2_config_tag_status");
|
||||
} catch (_e) {
|
||||
/* index may not exist */
|
||||
}
|
||||
|
||||
// Step 2: Copy first monitor_tag from junction table back to monitor_alerts_config
|
||||
const configs = await knex("monitor_alerts_config").select("id");
|
||||
|
||||
for (const config of configs) {
|
||||
const firstMonitor = await knex("monitor_alerts_config_monitors").where({ monitor_alerts_id: config.id }).first();
|
||||
|
||||
if (firstMonitor) {
|
||||
await knex("monitor_alerts_config").where({ id: config.id }).update({ monitor_tag: firstMonitor.monitor_tag });
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Delete configs that have no monitors (can't satisfy NOT NULL)
|
||||
await knex("monitor_alerts_config").whereNull("monitor_tag").del();
|
||||
|
||||
// Step 4: Re-add foreign key constraint on monitor_alerts_config (non-SQLite only)
|
||||
const dbClient = knex.client.config.client;
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
||||
table.string("monitor_tag", 255).notNullable().alter();
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Step 5: Drop monitor_tag column from monitor_alerts_v2 (non-SQLite only)
|
||||
const hasV2Column = await knex.schema.hasColumn("monitor_alerts_v2", "monitor_tag");
|
||||
if (hasV2Column) {
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.dropForeign(["monitor_tag"]);
|
||||
table.dropColumn("monitor_tag");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Drop junction table
|
||||
await knex.schema.dropTableIfExists("monitor_alerts_config_monitors");
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Migration: Fix SQLite monitor_alerts_config.monitor_tag NOT NULL constraint
|
||||
*
|
||||
* The earlier migration 20260325120000_multi_monitor_alerts nulled out data in
|
||||
* monitor_alerts_config.monitor_tag for SQLite but could not alter the column
|
||||
* constraint (SQLite doesn't support ALTER COLUMN). This migration recreates
|
||||
* the table with monitor_tag as nullable, preserving all data and indexes.
|
||||
*
|
||||
* Only runs on SQLite/better-sqlite3; other databases already had the column
|
||||
* altered in the previous migration.
|
||||
*/
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const dbClient = knex.client.config.client;
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
return; // Already handled by 20260325120000_multi_monitor_alerts
|
||||
}
|
||||
|
||||
// Check if monitor_tag is already nullable (migration 1 already rebuilt the table)
|
||||
const tableInfo: Array<{ name: string; notnull: number }> = await knex.raw(
|
||||
"PRAGMA table_info(monitor_alerts_config)",
|
||||
);
|
||||
const monitorTagCol = tableInfo.find((col) => col.name === "monitor_tag");
|
||||
if (monitorTagCol && monitorTagCol.notnull === 0) {
|
||||
return; // Column is already nullable, nothing to do
|
||||
}
|
||||
|
||||
// Column is still NOT NULL — rebuild the table to make it nullable
|
||||
try {
|
||||
await knex.transaction(async (trx) => {
|
||||
await trx.raw("PRAGMA foreign_keys = OFF");
|
||||
await trx.raw("DROP TABLE IF EXISTS monitor_alerts_config_new");
|
||||
await trx.raw(`
|
||||
CREATE TABLE monitor_alerts_config_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_tag VARCHAR(255),
|
||||
alert_for VARCHAR(50) NOT NULL,
|
||||
alert_value VARCHAR(255) NOT NULL,
|
||||
failure_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
success_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
alert_description TEXT,
|
||||
create_incident VARCHAR(10) NOT NULL DEFAULT 'NO',
|
||||
is_active VARCHAR(10) NOT NULL DEFAULT 'YES',
|
||||
severity VARCHAR(50) NOT NULL DEFAULT 'WARNING',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await trx.raw(`
|
||||
INSERT INTO monitor_alerts_config_new
|
||||
(id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at)
|
||||
SELECT
|
||||
id, NULL, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at
|
||||
FROM monitor_alerts_config
|
||||
`);
|
||||
|
||||
await trx.raw("DROP TABLE monitor_alerts_config");
|
||||
await trx.raw("ALTER TABLE monitor_alerts_config_new RENAME TO monitor_alerts_config");
|
||||
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
await trx.raw("PRAGMA foreign_keys = ON");
|
||||
});
|
||||
} catch (e) {
|
||||
await knex.raw("PRAGMA foreign_keys = ON");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const dbClient = knex.client.config.client;
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Revert: make monitor_tag NOT NULL again via table rebuild
|
||||
try {
|
||||
await knex.transaction(async (trx) => {
|
||||
await trx.raw("PRAGMA foreign_keys = OFF");
|
||||
await trx.raw(`
|
||||
CREATE TABLE monitor_alerts_config_old (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_tag VARCHAR(255) NOT NULL,
|
||||
alert_for VARCHAR(50) NOT NULL,
|
||||
alert_value VARCHAR(255) NOT NULL,
|
||||
failure_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
success_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
alert_description TEXT,
|
||||
create_incident VARCHAR(10) NOT NULL DEFAULT 'NO',
|
||||
is_active VARCHAR(10) NOT NULL DEFAULT 'YES',
|
||||
severity VARCHAR(50) NOT NULL DEFAULT 'WARNING',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Only copy rows that have a non-null monitor_tag
|
||||
await trx.raw(`
|
||||
INSERT INTO monitor_alerts_config_old
|
||||
(id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at)
|
||||
SELECT
|
||||
id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at
|
||||
FROM monitor_alerts_config
|
||||
WHERE monitor_tag IS NOT NULL
|
||||
`);
|
||||
|
||||
await trx.raw("DROP TABLE monitor_alerts_config");
|
||||
await trx.raw("ALTER TABLE monitor_alerts_config_old RENAME TO monitor_alerts_config");
|
||||
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
await trx.raw("PRAGMA foreign_keys = ON");
|
||||
});
|
||||
} catch (e) {
|
||||
await knex.raw("PRAGMA foreign_keys = ON");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
-1102
File diff suppressed because it is too large
Load Diff
-804
@@ -1,804 +0,0 @@
|
||||
---
|
||||
info:
|
||||
title: Kener API
|
||||
version: 3.0.0
|
||||
description: |
|
||||
# Kener Selfhost node js status page
|
||||

|
||||
API specification for Kener status page and incident management system. This API spec was created using [Frogment](https://www.frogment.app)
|
||||
contact:
|
||||
name: Raj Nandan Sharma
|
||||
email: rajnandan1@gmail.com
|
||||
url: https://github.com/rajnandan1/kener/issues
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
openapi: 3.0.0
|
||||
servers:
|
||||
- url: https://{host}
|
||||
description: Kener host URL
|
||||
tags:
|
||||
- name: Monitors
|
||||
description: APIs to interact with monitors
|
||||
- name: Incidents
|
||||
description: APIs to integrate incidents
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: enter your api key here
|
||||
schemas:
|
||||
MonitorStatus:
|
||||
type: object
|
||||
description: Monitor Status
|
||||
required:
|
||||
- status
|
||||
- latency
|
||||
- tag
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: UP
|
||||
enum:
|
||||
- UP
|
||||
- DOWN
|
||||
- DEGRADED
|
||||
latency:
|
||||
type: number
|
||||
description: In seconds
|
||||
example: 100
|
||||
timestampInSeconds:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
example: 1731251760
|
||||
tag:
|
||||
type: string
|
||||
example: earth
|
||||
description: Tag of a monitor
|
||||
example:
|
||||
status: UP
|
||||
timestampInSeconds: 1731251760
|
||||
latency: 100
|
||||
tag: earth
|
||||
StatusResponse:
|
||||
type: object
|
||||
description: Status of a monitor given a tag
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: UP
|
||||
enum:
|
||||
- UP
|
||||
- DOWN
|
||||
- DEGRADED
|
||||
uptime:
|
||||
type: string
|
||||
example: '100'
|
||||
last_updated_at:
|
||||
type: integer
|
||||
example: 1731251760
|
||||
example:
|
||||
status: UP
|
||||
last_updated_at: 1731251760
|
||||
uptime: '100'
|
||||
Incident:
|
||||
type: object
|
||||
description: body of an incident
|
||||
properties:
|
||||
start_date_time:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
example: 1731901920
|
||||
end_date_time:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
nullable: true
|
||||
example: 1704123938
|
||||
title:
|
||||
type: string
|
||||
example: Outage in mumbai
|
||||
description: title of the incident
|
||||
created_at:
|
||||
type: string
|
||||
example: '2025-01-09 04:12:01'
|
||||
description: created at time
|
||||
updated_at:
|
||||
type: string
|
||||
example: '2025-01-09 06:10:00'
|
||||
description: updated at time
|
||||
status:
|
||||
type: string
|
||||
example: OPEN
|
||||
description: delete or not deleted incident
|
||||
enum:
|
||||
- OPEN
|
||||
- CLOSED
|
||||
state:
|
||||
type: string
|
||||
description: the current status of the incident
|
||||
example: INVESTIGATING
|
||||
enum:
|
||||
- INVESTIGATING
|
||||
- IDENTIFIED
|
||||
- MONITORING
|
||||
- RESOLVED
|
||||
example:
|
||||
start_date_time: 1731901920
|
||||
end_date_time: 1704123938
|
||||
title: title of the incident
|
||||
created_at: '2025-01-09 04:12:01'
|
||||
updated_at: '2025-01-09 04:12:01'
|
||||
state: INVESTIGATING
|
||||
status: OPEN
|
||||
id: 4
|
||||
IncidentResponse:
|
||||
description: Incident response schema
|
||||
allOf:
|
||||
- "$ref": "#/components/schemas/Incident"
|
||||
Comment:
|
||||
type: object
|
||||
description: Comment of an incident
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
example: comment 1
|
||||
commented_at:
|
||||
type: integer
|
||||
example: 1736398336
|
||||
state:
|
||||
type: string
|
||||
example: IDENTIFIED
|
||||
example:
|
||||
body: comment 2
|
||||
commented_at: 1736398336
|
||||
state: IDENTIFIED
|
||||
CommentResponse:
|
||||
type: object
|
||||
description: Comment Response
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: ID of the comment
|
||||
example: 60
|
||||
comment:
|
||||
type: string
|
||||
description: The content of the comment
|
||||
example: Sometimes, you want all the goodness of moment#from but you don't
|
||||
want to have to create two moments, you just want to display a length
|
||||
of time.
|
||||
incident_id:
|
||||
type: integer
|
||||
description: ID of the associated incident
|
||||
example: 24
|
||||
commented_at:
|
||||
type: integer
|
||||
description: Timestamp when the comment was added
|
||||
example: 1736398336
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp when the comment was created in ISO 8601 format
|
||||
example: '2025-01-09 04:52:16'
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp when the comment was last updated in ISO 8601 format
|
||||
example: '2025-01-09 04:52:16'
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the comment
|
||||
example: ACTIVE
|
||||
state:
|
||||
type: string
|
||||
description: the current status of the incident
|
||||
example: INVESTIGATING
|
||||
enum:
|
||||
- INVESTIGATING
|
||||
- IDENTIFIED
|
||||
- MONITORING
|
||||
- RESOLVED
|
||||
example:
|
||||
id: 60
|
||||
comment: Sometimes, you want all the goodness of moment
|
||||
incident_id: 24
|
||||
commented_at: 1736398336
|
||||
created_at: '2025-01-09 04:52:16'
|
||||
updated_at: '2025-01-09 04:52:16'
|
||||
status: ACTIVE
|
||||
state: MONITORING
|
||||
IncidentStatus:
|
||||
type: object
|
||||
description: Status of the incident
|
||||
properties:
|
||||
isIdentified:
|
||||
type: boolean
|
||||
description: Has the incident been indetified
|
||||
example: true
|
||||
isResolved:
|
||||
type: boolean
|
||||
description: has the incident been resolved
|
||||
example: true
|
||||
endDatetime:
|
||||
type: integer
|
||||
description: Incident end time
|
||||
example: 1731901920
|
||||
example:
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
endDatetime: 1731901920
|
||||
IncidentCreateRequest:
|
||||
title: Incident Create Request Body
|
||||
type: object
|
||||
description: body of an incident
|
||||
required:
|
||||
- title
|
||||
- start_date_time
|
||||
properties:
|
||||
start_date_time:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
example: 1731901920
|
||||
title:
|
||||
type: string
|
||||
example: Outage in mumbai
|
||||
description: title of the incident
|
||||
example:
|
||||
start_date_time: 1731901920
|
||||
title: title of the incident
|
||||
responses:
|
||||
Response401:
|
||||
description: Bad API keys response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Invalid token response
|
||||
example: invalid token
|
||||
example:
|
||||
error: invalid token
|
||||
Response400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: bad request while calling kener apis
|
||||
example: unknown tags
|
||||
example:
|
||||
error: unknown tags
|
||||
examples:
|
||||
GetMonitorStatusExample200:
|
||||
summary: response of get status of a monitor
|
||||
description: Some Description
|
||||
value:
|
||||
status: UP
|
||||
uptime: '100'
|
||||
lastUpdatedAt: 1731901920
|
||||
DegradedRequestBody:
|
||||
summary: update to degraded
|
||||
description: Some Description
|
||||
value:
|
||||
status: DEGRADED
|
||||
timestampInSeconds: 1731251760
|
||||
latency: 100
|
||||
tag: earth
|
||||
CreateIncidentResponse:
|
||||
summary: create incident response
|
||||
description: create incident response
|
||||
value:
|
||||
start_date_time: 1731901920
|
||||
end_date_time: 1731901920
|
||||
id: 4
|
||||
created_at: '2025-01-09 04:12:01'
|
||||
updated_at: '2025-01-10 04:12:01'
|
||||
title: title of the incident
|
||||
status: OPEN
|
||||
state: INVESTIGATING
|
||||
CreateIncidentRequest:
|
||||
summary: create incident request body
|
||||
description: create incident request body
|
||||
value:
|
||||
start_date_time: 1731901920
|
||||
title: title of the incident
|
||||
SearchIncidentsResponse:
|
||||
summary: array of incidents
|
||||
description: Some Description
|
||||
value:
|
||||
- id: 2
|
||||
title: future wala
|
||||
start_date_time: 1736774486
|
||||
end_date_time: 1737033787
|
||||
created_at: '2025-01-12 13:21:32'
|
||||
updated_at: '2025-01-12 13:23:15'
|
||||
status: OPEN
|
||||
state: RESOLVED
|
||||
- id: 1
|
||||
title: internatiinal asda
|
||||
start_date_time: 1736684329
|
||||
end_date_time:
|
||||
created_at: '2025-01-12 12:18:53'
|
||||
updated_at: '2025-01-12 13:21:32'
|
||||
status: OPEN
|
||||
state: INVESTIGATING
|
||||
CommentsResponse:
|
||||
summary: list of comments
|
||||
description: Some Description
|
||||
value:
|
||||
- id: 58
|
||||
comment: idensad
|
||||
incident_id: 24
|
||||
commented_at: 1736398295
|
||||
created_at: '2025-01-09 04:51:37'
|
||||
updated_at: '2025-01-09 04:51:37'
|
||||
status: ACTIVE
|
||||
state: IDENTIFIED
|
||||
- id: 57
|
||||
comment: Sometimes, you want all the goodness of moment#from but you don't
|
||||
want to have to create two moments, you just want to display a length of
|
||||
time.
|
||||
incident_id: 24
|
||||
commented_at: 1736398279
|
||||
created_at: '2025-01-09 04:51:19'
|
||||
updated_at: '2025-01-09 04:51:19'
|
||||
status: ACTIVE
|
||||
state: INVESTIGATING
|
||||
CommentRequestBody:
|
||||
summary: request body for a update
|
||||
description: request body for a update to add in an incident
|
||||
value:
|
||||
comment: This is a comment
|
||||
commented_at: 1736398336
|
||||
state: IDENTIFIED
|
||||
CreateCommentResponse:
|
||||
summary: create update response body sample
|
||||
description: create update response body sample
|
||||
value:
|
||||
id: 60
|
||||
comment: Sometimes, you want all the goodness of moment
|
||||
incident_id: 24
|
||||
commented_at: 1736398336
|
||||
created_at: '2025-01-09 04:52:16'
|
||||
updated_at: '2025-01-09 04:52:16'
|
||||
status: ACTIVE
|
||||
state: MONITORING
|
||||
paths:
|
||||
"/api/status":
|
||||
post:
|
||||
operationId: updateMonitorStatus
|
||||
summary: Update status of a monitor
|
||||
description: Update status of a monitor at a given timestamp UTC
|
||||
tags:
|
||||
- Monitors
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: request body to update status of a monitor
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/MonitorStatus"
|
||||
examples:
|
||||
degraded:
|
||||
"$ref": "#/components/examples/DegradedRequestBody"
|
||||
responses:
|
||||
'200':
|
||||
description: Status updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
example: 200
|
||||
message:
|
||||
type: string
|
||||
example: success at 1731251760
|
||||
example:
|
||||
status: 200
|
||||
message: success at 1731251760
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
get:
|
||||
operationId: getMonitorStatus
|
||||
summary: Get status of a monitor
|
||||
description: get status of a monitor at timestamp
|
||||
tags:
|
||||
- Monitors
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: tag
|
||||
in: query
|
||||
required: true
|
||||
description: monitor tag to get status of it
|
||||
schema:
|
||||
type: string
|
||||
example: earth
|
||||
responses:
|
||||
'200':
|
||||
description: Monitor status retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/StatusResponse"
|
||||
examples:
|
||||
successExample:
|
||||
"$ref": "#/components/examples/GetMonitorStatusExample200"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
"/api/incident":
|
||||
post:
|
||||
operationId: createIncident
|
||||
summary: Create a new incident
|
||||
description: API to create incidents
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: request body to manually create an incident
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/IncidentCreateRequest"
|
||||
examples:
|
||||
sample:
|
||||
"$ref": "#/components/examples/CreateIncidentRequest"
|
||||
responses:
|
||||
'200':
|
||||
description: Incident created successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/IncidentResponse"
|
||||
examples:
|
||||
success:
|
||||
"$ref": "#/components/examples/CreateIncidentResponse"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
get:
|
||||
operationId: searchIncidents
|
||||
summary: Search for incidents
|
||||
description: API to get incidents
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
description: status of the incident. Can be open or close. close means deleted
|
||||
schema:
|
||||
type: string
|
||||
description: status of the incident. Can be open or close. close means deleted
|
||||
enum:
|
||||
- OPEN
|
||||
- CLOSED
|
||||
default: OPEN
|
||||
example: OPEN
|
||||
- name: state
|
||||
in: query
|
||||
description: state of the incident. Can be open or close
|
||||
schema:
|
||||
type: string
|
||||
description: state of the incident. Can be open or close
|
||||
enum:
|
||||
- INVESTIGATING
|
||||
- IDENTIFIED
|
||||
- MONITORING
|
||||
- RESOLVED
|
||||
default: INVESTIGATING
|
||||
example: IDENTIFIED
|
||||
- name: page
|
||||
in: query
|
||||
description: page number
|
||||
schema:
|
||||
type: integer
|
||||
description: page number
|
||||
default: 1
|
||||
minimum: 1
|
||||
example: 1
|
||||
- name: limit
|
||||
in: query
|
||||
description: how many per page
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
description: how many per page
|
||||
maximum: 100
|
||||
example: 10
|
||||
- name: start_date_time
|
||||
description: start time
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: start time
|
||||
example: 1731866475
|
||||
- name: end_date_time
|
||||
description: end time
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: end time
|
||||
example: 1731866475
|
||||
responses:
|
||||
'200':
|
||||
description: Search results retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/IncidentResponse"
|
||||
examples:
|
||||
success:
|
||||
"$ref": "#/components/examples/SearchIncidentsResponse"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
"/api/incident/{incident_id}":
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
description: incident id as an integer to get incident by id
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
get:
|
||||
operationId: getIncident
|
||||
summary: Get an incident by id
|
||||
description: API to get an incident by incident id
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Incident retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/IncidentResponse"
|
||||
examples:
|
||||
success:
|
||||
"$ref": "#/components/examples/CreateIncidentResponse"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
patch:
|
||||
operationId: updateIncident
|
||||
summary: Update an incident
|
||||
description: API to update an incident by incident id
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: search for an incident
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Incident"
|
||||
examples:
|
||||
sample:
|
||||
"$ref": "#/components/examples/CreateIncidentRequest"
|
||||
responses:
|
||||
'200':
|
||||
description: Incident updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/IncidentResponse"
|
||||
examples:
|
||||
success:
|
||||
"$ref": "#/components/examples/CreateIncidentResponse"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
"/api/incident/{incident_id}/updates":
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
description: incident id to fetch comment for
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
post:
|
||||
operationId: addIncidentComment
|
||||
summary: Add an update to an incident
|
||||
description: API to create update for an incident
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: body to add an update
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Comment"
|
||||
examples:
|
||||
sample:
|
||||
"$ref": "#/components/examples/CommentRequestBody"
|
||||
responses:
|
||||
'200':
|
||||
description: Comment added successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/CommentResponse"
|
||||
examples:
|
||||
sample:
|
||||
"$ref": "#/components/examples/CreateCommentResponse"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
get:
|
||||
operationId: getIncidentComments
|
||||
summary: Get updates for an incident
|
||||
description: API to get updates for an incident
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: updates retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/CommentResponse"
|
||||
examples:
|
||||
success:
|
||||
"$ref": "#/components/examples/CommentsResponse"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
"/api/incident/{incident_id}/monitors":
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
description: incident id to fetch comment for
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
get:
|
||||
summary: Get monitors for an incident
|
||||
operationId: getIncidentMonitors
|
||||
tags:
|
||||
- Incidents
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the incident
|
||||
responses:
|
||||
'200':
|
||||
description: List of monitors for the incident
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
monitor_tag:
|
||||
type: string
|
||||
description: The tag of the monitor
|
||||
example: earth
|
||||
monitor_impact:
|
||||
type: string
|
||||
description: The impact status of the monitor
|
||||
example: DOWN
|
||||
security:
|
||||
- bearerAuth: []
|
||||
post:
|
||||
summary: Add a monitor to an incident
|
||||
operationId: addIncidentMonitor
|
||||
tags:
|
||||
- Incidents
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the incident
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
tag:
|
||||
type: string
|
||||
description: The tag for the monitor
|
||||
example: okbookmarks
|
||||
impact:
|
||||
type: string
|
||||
description: The impact status of the monitor
|
||||
example: DEGRADED
|
||||
responses:
|
||||
'200':
|
||||
description: Monitor added successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
monitor_tag:
|
||||
type: string
|
||||
description: The tag of the monitor
|
||||
example: okbookmarks
|
||||
monitor_impact:
|
||||
type: string
|
||||
description: The impact status of the monitor
|
||||
example: DEGRADED
|
||||
security:
|
||||
- bearerAuth: []
|
||||
delete:
|
||||
summary: Delete a monitor from an incident
|
||||
operationId: deleteIncidentMonitor
|
||||
tags:
|
||||
- Incidents
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the incident
|
||||
- name: tag
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The tag of the monitor to delete
|
||||
responses:
|
||||
'200':
|
||||
description: Monitor deleted successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: Confirmation message
|
||||
example: Monitor deleted successfully
|
||||
security:
|
||||
- bearerAuth: []
|
||||
Generated
+10972
-7865
File diff suppressed because it is too large
Load Diff
+184
-107
@@ -1,109 +1,186 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.0.1",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
|
||||
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
|
||||
"keywords": [
|
||||
"Node.js application",
|
||||
"Open-source status page",
|
||||
"Service monitoring tool",
|
||||
"Real-time incident management",
|
||||
"Customizable reporting",
|
||||
"Service outage tracker",
|
||||
"User-friendly dashboard",
|
||||
"Incident communication platform",
|
||||
"Scalable monitoring solution",
|
||||
"Community-driven software",
|
||||
"Website status tracker",
|
||||
"Incident response tool",
|
||||
"System status monitoring",
|
||||
"Service reliability management",
|
||||
"Incident alert system"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rajnandan1/kener.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"configure": "node build.js",
|
||||
"migrate": "npx knex migrate:latest",
|
||||
"preseed": "npx knex migrate:latest",
|
||||
"seed": "npx knex seed:run",
|
||||
"predev": "npm run seed",
|
||||
"devschedule": "node src/lib/server/startup.js",
|
||||
"schedule": "node src/lib/server/startup.js",
|
||||
"development": "vite dev",
|
||||
"dev": "npm-run-all --parallel devschedule development",
|
||||
"prettify": "prettier --write .",
|
||||
"start": "node main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.27.4",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@zerodevx/svelte-toast": "^0.9.6",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-picker-svelte": "^2.15.1",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-awesome-color-picker": "^3.1.4",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"bits-ui": "^0.9.9",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"date-fns": "^4.1.0",
|
||||
"dns2": "^2.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"figlet": "^1.8.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"marked": "^11.1.1",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mysql2": "^3.12.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pg": "^8.13.1",
|
||||
"pg-pool": "^3.7.0",
|
||||
"ping": "^0.4.4",
|
||||
"queue": "^7.0.0",
|
||||
"randomstring": "^1.3.0",
|
||||
"resend": "^4.0.1",
|
||||
"svelte-legos": "^0.2.5",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
"name": "kener",
|
||||
"version": "4.0.21",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener is a modern, open-source status page application built with Node.js. It provides real-time monitoring, uptime tracking, incident management, and beautiful dashboards. Perfect for DevOps teams, SaaS providers, and businesses needing reliable service status communication with minimal setup.",
|
||||
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
|
||||
"keywords": [
|
||||
"status page",
|
||||
"uptime monitoring",
|
||||
"incident management",
|
||||
"DevOps tools",
|
||||
"service reliability",
|
||||
"open source",
|
||||
"Node.js",
|
||||
"dashboard",
|
||||
"system monitoring",
|
||||
"status alerts",
|
||||
"outage communication",
|
||||
"API monitoring",
|
||||
"SaaS status",
|
||||
"performance metrics",
|
||||
"status reporting"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rajnandan1/kener.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build-sveltekit.js && node scripts/build-server.js",
|
||||
"build-with-docs": "node scripts/build-sveltekit.js --with-docs && node scripts/build-server.js",
|
||||
"build:sveltekit": "vite build",
|
||||
"build:server": "node scripts/build-server.js",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"configure": "node build.js",
|
||||
"dev": "npm-run-all --parallel devschedule development",
|
||||
"development": "vite dev",
|
||||
"devschedule": "vite-node src/lib/server/startup.ts",
|
||||
"generate-readme": "node scripts/generate-readme.js",
|
||||
"index-docs": "vite-node scripts/index-docs.ts",
|
||||
"migrate": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
|
||||
"predev": "npm run seed",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"preseed": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
|
||||
"prettify": "prettier --write .",
|
||||
"preview": "vite preview",
|
||||
"schedule": "vite-node src/lib/server/startup.ts",
|
||||
"seed": "npx knex seed:run",
|
||||
"start": "node build/main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/kit": "^2.53.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-shape": "^3.1.8",
|
||||
"@types/dns2": "^2.0.10",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/heic-convert": "^2.1.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"date-picker-svelte": "^2.17.0",
|
||||
"layerchart": "^2.0.0-next.43",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.53.5",
|
||||
"svelte-awesome-color-picker": "^4.1.0",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte-sonner": "^1.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^7.2.2",
|
||||
"vite-node": "^5.3.0",
|
||||
"vite-plugin-devtools-json": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "^6.39.11",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@humanspeak/svelte-purify": "^0.0.6",
|
||||
"@number-flow/svelte": "^0.3.9",
|
||||
"@scalar/express-api-reference": "^0.8.28",
|
||||
"@scalar/sveltekit": "^0.1.43",
|
||||
"@scaleway/random-name": "^5.1.4",
|
||||
"@uiw/codemirror-theme-github": "^4.25.3",
|
||||
"axios": "^1.13.2",
|
||||
"badge-maker": "^5.0.2",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"bits-ui": "^2.14.4",
|
||||
"bullmq": "^5.66.2",
|
||||
"cheerio": "^1.1.2",
|
||||
"croner": "^9.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dns2": "^2.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"esbuild": "^0.27.2",
|
||||
"express": "^5.2.1",
|
||||
"figlet": "^1.9.4",
|
||||
"flexsearch": "^0.8.212",
|
||||
"front-matter": "^4.0.2",
|
||||
"gamedig": "^5.3.2",
|
||||
"glob": "^13.0.6",
|
||||
"heic-convert": "^2.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ioredis": "^5.8.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.561.0",
|
||||
"marked": "^17.0.1",
|
||||
"marked-alert": "^2.1.2",
|
||||
"marked-custom-heading-id": "^2.0.16",
|
||||
"marked-gfm-heading-id": "^4.1.3",
|
||||
"marked-highlight": "^2.2.3",
|
||||
"marked-plaintify": "^1.1.1",
|
||||
"mobile-detect": "^1.4.5",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"mustache": "^4.2.0",
|
||||
"mysql2": "^3.15.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^7.0.11",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pg": "^8.16.3",
|
||||
"pg-pool": "^3.10.1",
|
||||
"ping": "^1.0.0",
|
||||
"randomstring": "^1.3.1",
|
||||
"resend": "^6.6.0",
|
||||
"rrule": "^2.8.1",
|
||||
"sharp": "^0.34.5",
|
||||
"striptags": "^3.2.0",
|
||||
"style-to-object": "^1.0.14",
|
||||
"svelte-codemirror-editor": "^2.1.0",
|
||||
"vite-plugin-package-version": "^1.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
"fast-xml-parser": "^5.5.6",
|
||||
"rollup": "^4.59.0",
|
||||
"undici": "^7.24.0",
|
||||
"minimatch": "^10.2.3",
|
||||
"devalue": "^5.6.4",
|
||||
"dompurify": "^3.3.2",
|
||||
"cookie": "^0.7.0",
|
||||
"mailparser": "^3.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
const tailwindcss = require("tailwindcss");
|
||||
const autoprefixer = require("autoprefixer");
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
services:
|
||||
- type: web
|
||||
name: kener
|
||||
runtime: image
|
||||
image:
|
||||
url: docker.io/rajnandan1/kener:latest
|
||||
envVars:
|
||||
- key: DATABASE_URL
|
||||
fromDatabase:
|
||||
name: kener-db
|
||||
property: connectionString
|
||||
- key: KENER_SECRET_KEY
|
||||
generateValue: true
|
||||
- key: ORIGIN
|
||||
fromService:
|
||||
type: web
|
||||
name: kener
|
||||
envVarKey: RENDER_EXTERNAL_URL
|
||||
- key: REDIS_URL
|
||||
fromService:
|
||||
name: kener-redis
|
||||
type: keyvalue
|
||||
property: connectionString
|
||||
- type: keyvalue
|
||||
name: kener-redis
|
||||
plan: starter
|
||||
ipAllowList:
|
||||
- source: 0.0.0.0/0
|
||||
description: everywhere
|
||||
maxmemoryPolicy: allkeys-lru
|
||||
|
||||
databases:
|
||||
- name: kener-db
|
||||
plan: basic-256mb
|
||||
databaseName: kener
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const pkg = JSON.parse(readFileSync("./package.json", "utf8"));
|
||||
|
||||
// Collect all dependency names to externalize, except CJS packages
|
||||
// that need to be bundled for ESM compatibility
|
||||
const CJS_PACKAGES_TO_BUNDLE = ["rrule"];
|
||||
|
||||
const externalDeps = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})].filter(
|
||||
(dep) => !CJS_PACKAGES_TO_BUNDLE.includes(dep),
|
||||
);
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: ["scripts/main.ts"],
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
format: "esm",
|
||||
outfile: "build/main.js",
|
||||
// Externalize all node_modules except CJS packages that break ESM named imports
|
||||
external: externalDeps,
|
||||
alias: {
|
||||
// Map SvelteKit's $lib alias so server code resolves correctly
|
||||
$lib: "./src/lib",
|
||||
},
|
||||
define: {
|
||||
// Inject version at build time so src/lib/version.ts resolves it
|
||||
// without relying on vite-plugin-package-version at runtime
|
||||
"import.meta.env.PACKAGE_VERSION": JSON.stringify(pkg.version),
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "rewrite-build-imports",
|
||||
setup(build) {
|
||||
// Since the output lives in build/, rewrite ../build/X → ./X
|
||||
// so that handler.js (SvelteKit output) resolves correctly
|
||||
build.onResolve({ filter: /^\.\.\/build\// }, (args) => {
|
||||
return {
|
||||
path: args.path.replace(/^\.\.\/build\//, "./"),
|
||||
external: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
banner: {
|
||||
js: "// Kener production server – built with esbuild",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Server build completed: build/main.js (v${pkg.version})`);
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* SvelteKit build script that optionally excludes docs routes.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/build-sveltekit.js # build WITHOUT docs
|
||||
* node scripts/build-sveltekit.js --with-docs # build WITH docs
|
||||
*/
|
||||
import { execSync } from "child_process";
|
||||
import { renameSync, existsSync, rmSync } from "fs";
|
||||
import { resolve, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = resolve(__dirname, "..");
|
||||
|
||||
const withDocs = process.argv.includes("--with-docs");
|
||||
|
||||
const docsDir = resolve(rootDir, "src/routes/(docs)");
|
||||
const docsHiddenDir = resolve(rootDir, ".docs-excluded");
|
||||
|
||||
function moveDocs(from, to) {
|
||||
if (existsSync(from)) {
|
||||
renameSync(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
function build() {
|
||||
if (!withDocs) {
|
||||
console.log("[build] Excluding docs routes from build...");
|
||||
moveDocs(docsDir, docsHiddenDir);
|
||||
// Clean generated route types so stale docs routes don't persist
|
||||
const svelteKitDir = resolve(rootDir, ".svelte-kit");
|
||||
if (existsSync(svelteKitDir)) {
|
||||
rmSync(svelteKitDir, { recursive: true, force: true });
|
||||
}
|
||||
} else {
|
||||
console.log("[build] Including docs routes in build...");
|
||||
}
|
||||
|
||||
try {
|
||||
execSync("npx vite build", { cwd: rootDir, stdio: "inherit" });
|
||||
} finally {
|
||||
// Always restore docs folder, even if build fails
|
||||
if (!withDocs) {
|
||||
moveDocs(docsHiddenDir, docsDir);
|
||||
console.log("[build] Restored docs routes.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
build();
|
||||
@@ -0,0 +1,210 @@
|
||||
//node scripts/check-translations.js
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { globSync } from "glob";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
|
||||
const srcDir = path.join(projectRoot, "src");
|
||||
const localesDir = path.join(srcDir, "lib", "locales");
|
||||
|
||||
const SUPPORTED_FORMATS = new Set(["json", "yaml"]);
|
||||
|
||||
const SOURCE_GLOBS = ["src/**/*.{svelte,ts,js,mts,cts}", "!src/lib/locales/**/*.json", "!src/**/*.d.ts"];
|
||||
|
||||
const TRANSLATION_CALL_REGEX = /\$t\s*\(\s*(["'`])([\s\S]*?)\1\s*(?:,|\))/g;
|
||||
|
||||
const COMMENT_PATTERNS = [/\/\*[\s\S]*?\*\//g, /\/\/[^\n\r]*/g, /<!--[\s\S]*?-->/g];
|
||||
|
||||
const WHITELISTED_DYNAMIC_KEYS = new Set([
|
||||
"All Systems Operational",
|
||||
"Degraded Performance",
|
||||
"Partial Degraded Performance",
|
||||
"Partial System Outage",
|
||||
"Major System Outage",
|
||||
"No Status Available",
|
||||
]);
|
||||
|
||||
function decodeQuotedContent(value) {
|
||||
return value
|
||||
.replace(/\\\\/g, "\\")
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\r/g, "\r")
|
||||
.replace(/\\t/g, "\t")
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\'/g, "'")
|
||||
.replace(/\\`/g, "`");
|
||||
}
|
||||
|
||||
function getLineNumber(text, index) {
|
||||
let line = 1;
|
||||
for (let i = 0; i < index; i += 1) {
|
||||
if (text[i] === "\n") {
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function maskComments(content) {
|
||||
let result = content;
|
||||
for (const pattern of COMMENT_PATTERNS) {
|
||||
result = result.replace(pattern, (match) => match.replace(/[^\n]/g, " "));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getUsedTranslationKeys() {
|
||||
const files = globSync(SOURCE_GLOBS, {
|
||||
cwd: projectRoot,
|
||||
nodir: true,
|
||||
ignore: ["**/node_modules/**", "**/.svelte-kit/**", "**/build/**", "**/dist/**"],
|
||||
});
|
||||
|
||||
const usedKeys = new Set();
|
||||
const skippedDynamicCalls = [];
|
||||
|
||||
for (const relativePath of files) {
|
||||
const absolutePath = path.join(projectRoot, relativePath);
|
||||
const content = fs.readFileSync(absolutePath, "utf8");
|
||||
const searchable = maskComments(content);
|
||||
TRANSLATION_CALL_REGEX.lastIndex = 0;
|
||||
|
||||
for (const match of searchable.matchAll(TRANSLATION_CALL_REGEX)) {
|
||||
const quote = match[1];
|
||||
const rawKey = match[2].trim();
|
||||
if (!rawKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (quote === "`" && rawKey.includes("${")) {
|
||||
skippedDynamicCalls.push({
|
||||
file: relativePath,
|
||||
line: getLineNumber(searchable, match.index ?? 0),
|
||||
reason: "template literal contains interpolation",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
usedKeys.add(decodeQuotedContent(rawKey));
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of WHITELISTED_DYNAMIC_KEYS) {
|
||||
usedKeys.add(key);
|
||||
}
|
||||
|
||||
return {
|
||||
usedKeys,
|
||||
skippedDynamicCalls,
|
||||
};
|
||||
}
|
||||
|
||||
function readLocaleMappings() {
|
||||
const localeFiles = globSync("*.json", {
|
||||
cwd: localesDir,
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
const localeMap = new Map();
|
||||
|
||||
for (const fileName of localeFiles) {
|
||||
const fullPath = path.join(localesDir, fileName);
|
||||
const raw = fs.readFileSync(fullPath, "utf8");
|
||||
const json = JSON.parse(raw);
|
||||
|
||||
if (!json || typeof json !== "object" || typeof json.mappings !== "object") {
|
||||
throw new Error(`Invalid locale file format in ${fileName}: expected { mappings: {...} }`);
|
||||
}
|
||||
|
||||
localeMap.set(fileName, new Set(Object.keys(json.mappings)));
|
||||
}
|
||||
|
||||
return localeMap;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
let format = "json";
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--format") {
|
||||
format = (argv[i + 1] || "").toLowerCase();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("--format=")) {
|
||||
format = arg.split("=")[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (!SUPPORTED_FORMATS.has(format)) {
|
||||
throw new Error(`Unsupported format: ${format}. Use --format json or --format yaml.`);
|
||||
}
|
||||
|
||||
return {
|
||||
format,
|
||||
outputFile: path.join(projectRoot, `translation-report.${format === "yaml" ? "yaml" : "json"}`),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { format, outputFile } = parseArgs(process.argv.slice(2));
|
||||
const { usedKeys, skippedDynamicCalls } = getUsedTranslationKeys();
|
||||
const locales = readLocaleMappings();
|
||||
|
||||
const sortedUsedKeys = [...usedKeys].sort((a, b) => a.localeCompare(b));
|
||||
const localeFiles = [...locales.keys()].sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const localesReport = {};
|
||||
|
||||
for (const localeFile of localeFiles) {
|
||||
const localeKeys = locales.get(localeFile) ?? new Set();
|
||||
|
||||
const missing = sortedUsedKeys.filter((key) => !localeKeys.has(key));
|
||||
const unused = [...localeKeys].filter((key) => !usedKeys.has(key)).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
localesReport[localeFile] = {
|
||||
missing,
|
||||
unused,
|
||||
missingCount: missing.length,
|
||||
unusedCount: unused.length,
|
||||
};
|
||||
}
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
format,
|
||||
scannedSourceDir: "src",
|
||||
usedLiteralKeysCount: sortedUsedKeys.length,
|
||||
usedLiteralKeys: sortedUsedKeys,
|
||||
whitelistedDynamicKeysCount: WHITELISTED_DYNAMIC_KEYS.size,
|
||||
whitelistedDynamicKeys: [...WHITELISTED_DYNAMIC_KEYS].sort((a, b) => a.localeCompare(b)),
|
||||
localeFilesCount: localeFiles.length,
|
||||
locales: localesReport,
|
||||
skippedDynamicCallsCount: skippedDynamicCalls.length,
|
||||
skippedDynamicCalls,
|
||||
};
|
||||
|
||||
if (format === "yaml") {
|
||||
fs.writeFileSync(outputFile, yaml.dump(report, { noRefs: true }) + "\n", "utf8");
|
||||
} else {
|
||||
fs.writeFileSync(outputFile, JSON.stringify(report, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
console.log(`Translation report written to: ${outputFile}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error("Failed to check translations.");
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user