mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 23:32:02 +00:00
Compare commits
877 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 | |||
| d54b346259 | |||
| a05de8269c | |||
| 130b70c6af | |||
| 7247a54c08 | |||
| 28de7ebff8 | |||
| e2c9d4dc2d | |||
| 4866382180 | |||
| 1b1507db1a | |||
| 0ea7d687a7 | |||
| 502e1539ca | |||
| f3910fea1b | |||
| ee9b48fb79 | |||
| 142686ef92 | |||
| 2aa187984e | |||
| c20d4d71c3 | |||
| 4f4d1e1b39 | |||
| f38b590f70 | |||
| c2caf42fd3 | |||
| 8c279c8c10 | |||
| 3480c78360 | |||
| 79ce708f60 | |||
| a4526eb4b5 | |||
| 866a133917 | |||
| adce978244 | |||
| dd3c26b29a | |||
| 25ad42ba9b | |||
| 3285b7e472 | |||
| c7da55480a | |||
| 35e81b5782 | |||
| 769a9526e7 | |||
| 5470153f4f | |||
| 9d6ad87275 | |||
| ac6261d9ac | |||
| 48effe077e | |||
| c656bf8612 | |||
| 9daf7c7156 | |||
| 4bd3bfae9d | |||
| 727964949c | |||
| 21e1fc1aca | |||
| eeba2ef2d4 | |||
| b3e738ce03 | |||
| 46848c7b19 | |||
| 5f3d48597f | |||
| 0efc3e5999 | |||
| 0c0ce89317 | |||
| 4a47061f76 | |||
| 9b3ff9b362 | |||
| 8315fede7d | |||
| d6d0568f67 | |||
| ff0119db9b | |||
| 37d776c315 | |||
| f53d4abd8b | |||
| f827bd4aeb | |||
| f28c4e96c5 | |||
| cf81b11c0b | |||
| 17dc752902 | |||
| bc9faf9456 | |||
| e5b615267c | |||
| 70e9086646 | |||
| 0cc30bc67e | |||
| 73792e7ce6 | |||
| 32f873d9c2 | |||
| 6566bc5f8f | |||
| cc93114eab | |||
| 6d5d949f5c | |||
| f1be4a4db0 | |||
| 8b9f576b30 | |||
| 0735f959ef |
@@ -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
|
||||
@@ -0,0 +1,51 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# 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
|
||||
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,7 +0,0 @@
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
GH_TOKEN=your_github_token
|
||||
API_TOKEN=your_api_token
|
||||
API_IP=""
|
||||
API_IP_REGEX=""
|
||||
KENER_BASE_PATH=""
|
||||
@@ -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
|
||||
+15
-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-*
|
||||
@@ -20,4 +23,15 @@ config/static/*
|
||||
db/*
|
||||
!db/.kener
|
||||
database/*
|
||||
!database/.kener
|
||||
!database/.kener
|
||||
uploads/*
|
||||
!uploads/upload.dir
|
||||
|
||||
static/uploads/*
|
||||
!static/uploads/upload.dir
|
||||
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
|
||||
+173
-58
@@ -1,78 +1,193 @@
|
||||
FROM lsiobase/alpine:3.18 as base
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ENV TZ=Etc/UTC
|
||||
# =============================================================================
|
||||
# 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
|
||||
# =============================================================================
|
||||
|
||||
RUN \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache \
|
||||
nodejs \
|
||||
npm \
|
||||
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 \
|
||||
make \
|
||||
gcc \
|
||||
g++ \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
libc6-compat && \
|
||||
echo "**** cleanup ****" && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/tmp/*
|
||||
tzdata
|
||||
|
||||
# set OS timezone specified by docker ENV
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
# ---------- Debian builder ----------
|
||||
FROM node:${NODE_VERSION}-slim AS builder-debian
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
python3 \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
tzdata && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ARG data_dir=/config
|
||||
# VOLUME $data_dir
|
||||
# ENV CONFIG_DIR=$data_dir
|
||||
# ---------- Selected variant ----------
|
||||
FROM builder-${VARIANT} AS builder
|
||||
|
||||
COPY docker/root/ /
|
||||
|
||||
# build requires devDependencies which are not used by production deploy
|
||||
# so build in a stage so we can copy results to clean "deploy" stage later
|
||||
FROM base as build
|
||||
ENV NPM_CONFIG_LOGLEVEL=error
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
ARG KENER_BASE_PATH
|
||||
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
|
||||
|
||||
RUN \
|
||||
npm install node-gyp -g && \
|
||||
npm_config_build_from_source=true npm install better-sqlite3 && \
|
||||
npm install \
|
||||
&& chown -R root:root node_modules \
|
||||
&& npm run build
|
||||
# 1. Copy package manifests first (maximises layer cache hits)
|
||||
COPY package*.json ./
|
||||
|
||||
FROM base as app
|
||||
# 2. Install ALL dependencies (devDependencies needed for the build step)
|
||||
RUN npm ci --no-fund && \
|
||||
npm cache clean --force
|
||||
|
||||
# copy package, required libs (npm,nodejs) results of build, prod entrypoint, and examples to be used to populate config dir
|
||||
# to clean, new stage
|
||||
COPY --chown=abc:abc package*.json ./
|
||||
COPY --from=base /usr/local/bin /usr/local/bin
|
||||
COPY --from=base /usr/local/lib /usr/local/lib
|
||||
# 3. Copy the rest of the source tree
|
||||
COPY . .
|
||||
|
||||
COPY --chown=abc:abc static /app/static
|
||||
COPY --chown=abc:abc database /app/database
|
||||
COPY --chown=abc:abc build.js /app/build.js
|
||||
COPY --chown=abc:abc sitemap.js /app/sitemap.js
|
||||
COPY --chown=abc:abc openapi.json /app/openapi.json
|
||||
COPY --chown=abc:abc src/lib/server /app/src/lib/server
|
||||
# 4. Create directories that the app expects
|
||||
RUN mkdir -p database
|
||||
|
||||
COPY --from=build --chown=abc:abc /app/build /app/build
|
||||
COPY --from=build --chown=abc:abc /app/main.js /app/main.js
|
||||
# 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
|
||||
|
||||
ENV NODE_ENV=production
|
||||
# 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
|
||||
|
||||
# install prod dependencies and clean cache
|
||||
RUN npm install --omit=dev \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules
|
||||
# 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
|
||||
|
||||
ARG webPort=3000
|
||||
ENV PORT=$webPort
|
||||
EXPOSE $PORT
|
||||
# 8. Remove devDependencies from node_modules
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
# leave entrypoint blank!
|
||||
# uses LSIO s6-init entrypoint with scripts
|
||||
# that populate CONFIG_DIR with static dir, monitor/site.yaml when dir is empty
|
||||
# and chown's all files so they are owned by proper user based on PUID/GUID env
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
ARG PORT=3000
|
||||
ARG KENER_BASE_PATH=
|
||||
|
||||
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"
|
||||
|
||||
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,16 +1,34 @@
|
||||
# Kener - A Sveltekit NodeJS Status Page System
|
||||
# Kener - Stunning Status Pages
|
||||
|
||||
<p align="center">
|
||||
<img src="https://kener.ing/newbg.png" 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>
|
||||
<img alt="GitHub Repo Updated" src="https://badges.pufler.dev/updated/rajnandan1/kener">
|
||||
<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">
|
||||
@@ -26,90 +44,186 @@
|
||||
</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: Open-source sveltekit status page system, crafted with lot of thought so that it looks modern.
|
||||
**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 does not aim to replace the Datadogs of the world. It simply tries to help someone come with a status page for the world.
|
||||
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.
|
||||
|
||||
### Why Kener?
|
||||
|
||||
✅ 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
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing is added to make cooler.
|
||||
<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
|
||||
|
||||
- Real-time monitoring
|
||||
- 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
|
||||
- Flexible monitor configuration using YAML. Define your own parsing for monitor being UP/DOWN/DEGRADED
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors. Example defaultStatus=DOWN if you don't hit API per minute with Status UP
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
- Supports webhooks/discord/slack for notifications
|
||||
- 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 using yaml or code
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
- 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
|
||||
|
||||
- Create Incidents using Github Issues - Rich Text
|
||||
- Or use APIs to create Incidents
|
||||
- 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
|
||||
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Inspired from
|
||||
|
||||
- [Upptime](https://upptime.js.org/)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
- [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
|
||||
|
||||
[](https://star-history.com/#rajnandan1/kener&Date)
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs-extra";
|
||||
import axios from "axios";
|
||||
import {
|
||||
IsValidURL,
|
||||
checkIfDuplicateExists,
|
||||
IsValidHTTPMethod,
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidRecordType,
|
||||
IsValidNameServer
|
||||
} from "./src/lib/server/tool.js";
|
||||
import { API_TIMEOUT, AnalyticsProviders } from "./src/lib/server/constants.js";
|
||||
import { GetAllGHLabels, CreateGHLabel } from "./src/lib/server/github.js";
|
||||
|
||||
const configPathFolder = "./config";
|
||||
const databaseFolder = process.argv[2] || "./database";
|
||||
const defaultEval = `(function (statusCode, responseTime, responseData) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
|
||||
function validateServerFile(server) {
|
||||
//if empty return true
|
||||
if (Object.keys(server).length === 0) {
|
||||
return true;
|
||||
}
|
||||
//server.triggers is present then it should be an array
|
||||
if (server.triggers !== undefined && !Array.isArray(server.triggers)) {
|
||||
console.log("triggers should be an array");
|
||||
return false;
|
||||
}
|
||||
///each trigger should have a name, type, and url
|
||||
if (server.triggers !== undefined) {
|
||||
for (let i = 0; i < server.triggers.length; i++) {
|
||||
const trigger = server.triggers[i];
|
||||
if (
|
||||
trigger.name === undefined ||
|
||||
trigger.type === undefined ||
|
||||
trigger.url === undefined
|
||||
) {
|
||||
console.log("trigger should have name, type, and url");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
//if database is present then it should be an object, and they key can be either postgres or sqlite
|
||||
if (server.database !== undefined && typeof server.database !== "object") {
|
||||
console.log("database should be an object");
|
||||
return false;
|
||||
}
|
||||
if (server.database !== undefined) {
|
||||
let dbtype = Object.keys(server.database);
|
||||
if (dbtype.length !== 1) {
|
||||
console.log("database should have only one key");
|
||||
return false;
|
||||
}
|
||||
if (dbtype[0] !== "postgres" && dbtype[0] !== "sqlite") {
|
||||
console.log("database should be either postgres or sqlite");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function Build() {
|
||||
console.log("ℹ️ Building Kener...");
|
||||
let site = {};
|
||||
let server = {};
|
||||
let monitors = [];
|
||||
try {
|
||||
site = yaml.load(fs.readFileSync(configPathFolder + "/site.yaml", "utf8"));
|
||||
monitors = yaml.load(fs.readFileSync(configPathFolder + "/monitors.yaml", "utf8"));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
server = yaml.load(fs.readFileSync(configPathFolder + "/server.yaml", "utf8"));
|
||||
if (!validateServerFile(server)) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("server.yaml not found");
|
||||
server = {};
|
||||
}
|
||||
|
||||
if (
|
||||
site.github === undefined ||
|
||||
site.github.owner === undefined ||
|
||||
site.github.repo === undefined
|
||||
) {
|
||||
console.log("github owner and repo are required");
|
||||
site.hasGithub = false;
|
||||
// process.exit(1);
|
||||
} else {
|
||||
site.hasGithub = true;
|
||||
}
|
||||
|
||||
if (site.hasGithub && !!!site.github.incidentSince) {
|
||||
site.github.incidentSince = 720;
|
||||
}
|
||||
if (site.hasGithub && !!!site.github.apiURL) {
|
||||
site.github.apiURL = "https://api.github.com";
|
||||
}
|
||||
|
||||
const FOLDER_DB = databaseFolder;
|
||||
const FOLDER_SITE = FOLDER_DB + "/site.json";
|
||||
const FOLDER_MONITOR = FOLDER_DB + "/monitors.json";
|
||||
const FOLDER_SERVER = FOLDER_DB + "/server.json";
|
||||
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
|
||||
let name = monitor.name;
|
||||
let tag = monitor.tag;
|
||||
let hasAPI = monitor.api !== undefined && monitor.api !== null;
|
||||
let hasPing = monitor.ping !== undefined && monitor.ping !== null;
|
||||
let hasDNS = monitor.dns !== undefined && monitor.dns !== null;
|
||||
let folderName = name.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
monitors[i].folderName = folderName;
|
||||
|
||||
if (!name || !tag) {
|
||||
console.log("name, tag are required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
monitor.dayDegradedMinimumCount &&
|
||||
(isNaN(monitor.dayDegradedMinimumCount) || monitor.dayDegradedMinimumCount < 1)
|
||||
) {
|
||||
console.log("dayDegradedMinimumCount is not a number or it is less than 1");
|
||||
process.exit(1);
|
||||
} else if (monitor.dayDegradedMinimumCount === undefined) {
|
||||
monitors[i].dayDegradedMinimumCount = 1;
|
||||
}
|
||||
|
||||
if (
|
||||
monitor.dayDownMinimumCount &&
|
||||
(isNaN(monitor.dayDownMinimumCount) || monitor.dayDownMinimumCount < 1)
|
||||
) {
|
||||
console.log("dayDownMinimumCount is not a number or it is less than 1");
|
||||
process.exit(1);
|
||||
} else if (monitor.dayDownMinimumCount === undefined) {
|
||||
monitors[i].dayDownMinimumCount = 1;
|
||||
}
|
||||
|
||||
if (
|
||||
monitor.includeDegradedInDowntime === undefined ||
|
||||
monitor.includeDegradedInDowntime !== true
|
||||
) {
|
||||
monitors[i].includeDegradedInDowntime = false;
|
||||
}
|
||||
if (hasPing) {
|
||||
let hostsV4 = monitor.ping.hostsV4;
|
||||
let hostsV6 = monitor.ping.hostsV6;
|
||||
let hasV4 = false;
|
||||
let hasV6 = false;
|
||||
if (hostsV4 && Array.isArray(hostsV4) && hostsV4.length > 0) {
|
||||
hostsV4.forEach((host) => {
|
||||
if (ValidateIpAddress(host) == "Invalid") {
|
||||
console.log(`hostsV4 ${host} is not valid`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
hasV4 = true;
|
||||
}
|
||||
if (hostsV6 && Array.isArray(hostsV6) && hostsV6.length > 0) {
|
||||
hostsV6.forEach((host) => {
|
||||
if (ValidateIpAddress(host) == "Invalid") {
|
||||
console.log(`hostsV6 ${host} is not valid`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
hasV6 = true;
|
||||
}
|
||||
|
||||
if (!hasV4 && !hasV6) {
|
||||
console.log("hostsV4 or hostsV6 is required");
|
||||
process.exit(1);
|
||||
}
|
||||
monitors[i].hasPing = true;
|
||||
}
|
||||
if (hasDNS) {
|
||||
let dnsData = monitor.dns;
|
||||
let domain = dnsData.host;
|
||||
//check if domain is valid
|
||||
if (!!!domain || !IsValidHost(domain)) {
|
||||
console.log("domain is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let recordType = dnsData.lookupRecord;
|
||||
//check if recordType is valid
|
||||
if (!!!recordType || !IsValidRecordType(recordType)) {
|
||||
console.log("recordType is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let nameServer = dnsData.nameServer;
|
||||
//check if nameserver is valid
|
||||
if (!!nameServer && !IsValidNameServer(nameServer)) {
|
||||
console.log("nameServer is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// matchType: "ANY" # ANY, ALL
|
||||
let matchType = dnsData.matchType;
|
||||
if (!!!matchType || (matchType !== "ANY" && matchType !== "ALL")) {
|
||||
console.log("matchType is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
//values array of string at least one
|
||||
let values = dnsData.values;
|
||||
if (!!!values || !Array.isArray(values) || values.length === 0) {
|
||||
console.log("values is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
monitors[i].hasDNS = true;
|
||||
}
|
||||
if (hasAPI) {
|
||||
let url = monitor.api.url;
|
||||
let method = monitor.api.method;
|
||||
let headers = monitor.api.headers;
|
||||
let evaluator = monitor.api.eval;
|
||||
let body = monitor.api.body;
|
||||
let timeout = monitor.api.timeout;
|
||||
let hideURLForGet = !!monitor.api.hideURLForGet;
|
||||
//url
|
||||
if (!!url) {
|
||||
if (!IsValidURL(url)) {
|
||||
console.log("url is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!!method) {
|
||||
if (!IsValidHTTPMethod(method)) {
|
||||
console.log("method is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
method = method.toUpperCase();
|
||||
} else {
|
||||
method = "GET";
|
||||
}
|
||||
monitors[i].api.method = method;
|
||||
//headers
|
||||
if (headers === undefined || headers === null) {
|
||||
monitors[i].api.headers = undefined;
|
||||
} else {
|
||||
//check if headers is a valid json
|
||||
try {
|
||||
JSON.parse(JSON.stringify(headers));
|
||||
} catch (error) {
|
||||
console.log("headers are not valid. Quitting");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//eval
|
||||
if (evaluator === undefined || evaluator === null) {
|
||||
monitors[i].api.eval = defaultEval;
|
||||
} else {
|
||||
let evalResp = eval(evaluator + `(200, 1000, "e30=")`);
|
||||
|
||||
if (
|
||||
evalResp === undefined ||
|
||||
evalResp === null ||
|
||||
evalResp.status === undefined ||
|
||||
evalResp.status === null ||
|
||||
evalResp.latency === undefined ||
|
||||
evalResp.latency === null
|
||||
) {
|
||||
console.log("eval is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
monitors[i].api.eval = evaluator;
|
||||
}
|
||||
//body
|
||||
if (body === undefined || body === null) {
|
||||
monitors[i].api.body = undefined;
|
||||
} else {
|
||||
//check if body is a valid string
|
||||
if (typeof body !== "string") {
|
||||
console.log("body is not valid should be a string");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//timeout
|
||||
if (timeout === undefined || timeout === null) {
|
||||
monitors[i].api.timeout = API_TIMEOUT;
|
||||
} else {
|
||||
//check if timeout is a valid number
|
||||
if (isNaN(timeout) || timeout < 0) {
|
||||
console.log("timeout is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
//add a description to the monitor if it is website using api.url and method = GET and headers == undefined
|
||||
//call the it to see if received content-type is text/html
|
||||
//if yes, append to description
|
||||
if (
|
||||
!hideURLForGet &&
|
||||
(headers === undefined || headers === null) &&
|
||||
url !== undefined &&
|
||||
method === "GET"
|
||||
) {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "GET",
|
||||
url: url,
|
||||
timeout: API_TIMEOUT
|
||||
});
|
||||
if (response.headers["content-type"].includes("text/html")) {
|
||||
let link = `<a href="${url}" class="font-medium underline underline-offset-4" target="_blank">${url}</a>`;
|
||||
if (monitors[i].description === undefined) {
|
||||
monitors[i].description = link;
|
||||
} else {
|
||||
monitors[i].description = monitors[i].description?.trim() + " " + link;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`error while fetching ${url}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
monitors[i].hasAPI = hasAPI;
|
||||
}
|
||||
|
||||
if (site.siteName === undefined) {
|
||||
site.siteName = site.title;
|
||||
}
|
||||
if (site.theme === undefined) {
|
||||
site.theme = "system";
|
||||
}
|
||||
if (site.themeToggle === undefined) {
|
||||
site.themeToggle = true;
|
||||
}
|
||||
if (site.barStyle === undefined) {
|
||||
site.barStyle = "FULL";
|
||||
}
|
||||
if (site.barRoundness === undefined) {
|
||||
site.barRoundness = "ROUNDED";
|
||||
} else {
|
||||
site.barRoundness = site.barRoundness.toLowerCase();
|
||||
}
|
||||
if (site.summaryStyle === undefined) {
|
||||
site.summaryStyle = "DAY";
|
||||
}
|
||||
site.colors = {
|
||||
UP: site.colors?.UP || "#4ead94",
|
||||
DOWN: site.colors?.DOWN || "#ca3038",
|
||||
DEGRADED: site.colors?.DEGRADED || "#e6ca61"
|
||||
};
|
||||
if (!!site.analytics) {
|
||||
const providers = {};
|
||||
|
||||
for (let i = 0; i < site.analytics.length; i++) {
|
||||
const element = site.analytics[i];
|
||||
if (!!AnalyticsProviders[element.type]) {
|
||||
if (providers[element.type] === undefined) {
|
||||
providers[element.type] = {};
|
||||
providers[element.type].measurementIds = [];
|
||||
providers[element.type].script = AnalyticsProviders[element.type];
|
||||
}
|
||||
providers[element.type].measurementIds.push(element.id);
|
||||
}
|
||||
}
|
||||
site.analytics = providers;
|
||||
}
|
||||
if (!!!site.font || !!!site.font.cssSrc || !!!site.font.family) {
|
||||
site.font = {
|
||||
cssSrc: "https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap",
|
||||
family: "Albert Sans"
|
||||
};
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.folderName)) === true) {
|
||||
console.log("duplicate monitor detected");
|
||||
process.exit(1);
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.tag)) === true) {
|
||||
console.log("duplicate tag detected");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.ensureFileSync(FOLDER_MONITOR);
|
||||
fs.ensureFileSync(FOLDER_SITE);
|
||||
fs.ensureFileSync(FOLDER_SERVER);
|
||||
try {
|
||||
fs.writeFileSync(FOLDER_MONITOR, JSON.stringify(monitors, null, 4));
|
||||
fs.writeFileSync(FOLDER_SITE, JSON.stringify(site, null, 4));
|
||||
fs.writeFileSync(FOLDER_SERVER, JSON.stringify(server, null, 4));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("✅ Kener built successfully");
|
||||
|
||||
if (site.hasGithub) {
|
||||
const ghLabels = await GetAllGHLabels(site);
|
||||
const tagsAndDescription = monitors.map((monitor) => {
|
||||
return { tag: monitor.tag, description: monitor.name };
|
||||
});
|
||||
//add incident label if does not exist
|
||||
|
||||
if (ghLabels.indexOf("incident") === -1) {
|
||||
await CreateGHLabel(site, "incident", "Status of the site");
|
||||
}
|
||||
if (ghLabels.indexOf("resolved") === -1) {
|
||||
await CreateGHLabel(site, "resolved", "Incident is resolved", "65dba6");
|
||||
}
|
||||
if (ghLabels.indexOf("identified") === -1) {
|
||||
await CreateGHLabel(site, "identified", "Incident is Identified", "EBE3D5");
|
||||
}
|
||||
if (ghLabels.indexOf("manual") === -1) {
|
||||
await CreateGHLabel(site, "manual", "Manually Created Incident", "6499E9");
|
||||
}
|
||||
if (ghLabels.indexOf("auto") === -1) {
|
||||
await CreateGHLabel(site, "auto", "Automatically Created Incident", "D6C0B3");
|
||||
}
|
||||
if (ghLabels.indexOf("investigating") === -1) {
|
||||
await CreateGHLabel(site, "investigating", "Incident is investigated", "D4E2D4");
|
||||
}
|
||||
if (ghLabels.indexOf("incident-degraded") === -1) {
|
||||
await CreateGHLabel(
|
||||
site,
|
||||
"incident-degraded",
|
||||
"Status is degraded of the site",
|
||||
"f5ba60"
|
||||
);
|
||||
}
|
||||
if (ghLabels.indexOf("incident-down") === -1) {
|
||||
await CreateGHLabel(site, "incident-down", "Status is down of the site", "ea3462");
|
||||
}
|
||||
//add tags if does not exist
|
||||
for (let i = 0; i < tagsAndDescription.length; i++) {
|
||||
const tag = tagsAndDescription[i].tag;
|
||||
const description = tagsAndDescription[i].description;
|
||||
if (ghLabels.indexOf(tag) === -1) {
|
||||
await CreateGHLabel(site, tag, description);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ Github labels created successfully");
|
||||
}
|
||||
}
|
||||
|
||||
Build();
|
||||
+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"
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
- 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"
|
||||
defaultStatus: "UP"
|
||||
image: "/earth.png"
|
||||
- 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
|
||||
alerts:
|
||||
DOWN:
|
||||
failureThreshold: 5
|
||||
successThreshold: 2
|
||||
createIncident: false
|
||||
description: "Write a description here please"
|
||||
triggers:
|
||||
- MyWebhook
|
||||
- Discord Test
|
||||
- name: CNAME Lookup
|
||||
description: Monitor example showing how to lookup CNAME record for a domain. The site www.rajnandan.com is hosted on GitHub Pages.
|
||||
tag: "cname-rajnandan"
|
||||
image: "https://www.rajnandan.com/assets/images/me.jpg"
|
||||
defaultStatus: "UP"
|
||||
dns:
|
||||
host: "www.rajnandan.com"
|
||||
lookupRecord: "CNAME"
|
||||
nameServer: "8.8.8.8"
|
||||
matchType: "ANY" # ANY, ALL
|
||||
values:
|
||||
- "rajnandan1.github.io"
|
||||
- name: "Frogment APP Ping"
|
||||
description: "Ping www.frogment.app"
|
||||
image: https://www.frogment.app/icons/Square107x107Logo.png
|
||||
tag: "pingFrogmentApp"
|
||||
defaultStatus: "UP"
|
||||
ping:
|
||||
hostsV4:
|
||||
- "www.frogment.app"
|
||||
@@ -1,13 +0,0 @@
|
||||
triggers:
|
||||
- name: MyWebhook
|
||||
type: webhook
|
||||
url: https://kener.requestcatcher.com/test
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: Bearer SomeToken
|
||||
- name: Discord Test
|
||||
type: discord
|
||||
url: https://discord.com/api/webhooks/1310641119767302164/XJvq4MO2lz5yp9XRCJgfc4dbUfcQdHsttFUKTFJx4y_Oo1jNkUXf-CS3RSnamnNNv4Lx
|
||||
database:
|
||||
sqlite:
|
||||
dbName: kener.db
|
||||
@@ -1,58 +0,0 @@
|
||||
title: "Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
siteName: "Kener.ing"
|
||||
home: "/"
|
||||
logo: "/logo.png"
|
||||
siteURL: "https://kener.ing"
|
||||
favicon: "/logo96.png"
|
||||
github:
|
||||
owner: "rajnandan1"
|
||||
repo: "kener"
|
||||
incidentSince: 720
|
||||
metaTags:
|
||||
description: "Kener: Open-source modern looking 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. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
keywords: "Node.js status page, Incident management tool, Service monitoring, Service outage tracking, Real-time status updates, GitHub integration for incidents, Open-source status page, Node.js monitoring application, Service reliability, User-friendly incident management, Collaborative incident resolution, Seamless outage communication, Service disruption tracker, Real-time incident alerts, Node.js status reporting"
|
||||
og:description: "Kener: 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. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
og:image: "https://kener.ing/ss.png"
|
||||
og:title: "Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
og:type: "website"
|
||||
og:site_name: "Kener"
|
||||
twitter:card: "summary_large_image"
|
||||
twitter:site: "@_rajnandan_"
|
||||
twitter:creator: "@_rajnandan_"
|
||||
twitter:image: "https://kener.ing/ss.png"
|
||||
twitter:title: "Kener: Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
twitter:description: "Kener: 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. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
nav:
|
||||
- name: "Documentation"
|
||||
url: "/docs/home"
|
||||
- name: "Github"
|
||||
iconURL: "/github.svg"
|
||||
url: "https://github.com/rajnandan1/kener"
|
||||
- name: "Buy me a coffee"
|
||||
iconURL: "/buymeacoffee.svg"
|
||||
url: "https://buymeacoffee.com/rajnandan1"
|
||||
hero:
|
||||
title: Kener is an Modern Open-Source Status Page System
|
||||
subtitle: Let your users know what's going on.
|
||||
footerHTML: |
|
||||
Made using
|
||||
<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">
|
||||
Kener
|
||||
</a>
|
||||
an open source status page system built with Svelte and TailwindCSS.
|
||||
i18n:
|
||||
defaultLocale: "en"
|
||||
locales:
|
||||
en: "English"
|
||||
hi: "हिन्दी"
|
||||
zh-CN: "中文"
|
||||
ja: "日本語"
|
||||
vi: "Tiếng Việt"
|
||||
pattern: "squares"
|
||||
analytics:
|
||||
- id: "G-Q3MLRXCBFT"
|
||||
type: "GA"
|
||||
barRoundness: SHARP
|
||||
summaryStyle: CURRENT
|
||||
barStyle: PARTIAL
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
let maxWait = 5000;
|
||||
let interval = 1000;
|
||||
let waitTime = 0;
|
||||
let serverDataPath = path.join(process.cwd(), "database", "server.json");
|
||||
let siteDataPath = path.join(process.cwd(), "database", "site.json");
|
||||
let monitorsDataPath = path.join(process.cwd(), "database", "monitors.json");
|
||||
|
||||
function allFilesExist() {
|
||||
return (
|
||||
fs.existsSync(serverDataPath) &&
|
||||
fs.existsSync(siteDataPath) &&
|
||||
fs.existsSync(monitorsDataPath)
|
||||
);
|
||||
}
|
||||
|
||||
//use setTimeout to create a delay promise
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
let requiredFilesExist = allFilesExist();
|
||||
|
||||
//create anonymous function to call the init function
|
||||
(async function init() {
|
||||
while (!requiredFilesExist && waitTime < maxWait) {
|
||||
await delay(1000);
|
||||
requiredFilesExist = allFilesExist();
|
||||
|
||||
waitTime += interval;
|
||||
}
|
||||
if (!requiredFilesExist) {
|
||||
console.error("Error loading site data");
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("✅ All files exist. Starting Frontend server...");
|
||||
}
|
||||
})();
|
||||
@@ -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
-25
@@ -1,27 +1,115 @@
|
||||
version: '3.7'
|
||||
# =============================================================================
|
||||
# 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
|
||||
#- GH_TOKEN=
|
||||
#- API_TOKEN=
|
||||
#- API_IP=
|
||||
#- API_IP_REGEX=
|
||||
#- KENER_BASE_PATH=
|
||||
|
||||
# If running on a LINUX HOST and not podman rootless these MUST BE SET
|
||||
# run "id $user" from command line and replace numbers below with output from command
|
||||
#- PUID=1000 # gid
|
||||
#- PGID=1000 # uid
|
||||
|
||||
### 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:
|
||||
- './database:/app/database:rw'
|
||||
- './config:/app/config'
|
||||
- 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,14 +0,0 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo -e "\nApp is starting!"
|
||||
export NODE_ENV=production
|
||||
cd /app || exit
|
||||
|
||||
# Run build first
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/build.js && (
|
||||
# Run startup and main in parallel
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/src/lib/server/startup.js &
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/main.js &
|
||||
# Wait for both processes
|
||||
wait
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
@@ -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"
|
||||
defaultStatus: "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,66 +0,0 @@
|
||||
---
|
||||
title: Changelogs | Kener
|
||||
description: Changelogs for Kener
|
||||
---
|
||||
|
||||
# Changelogs
|
||||
|
||||
## 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,24 +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
|
||||
|
||||
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`.
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
title: Database Config - Server.yaml - Kener
|
||||
description: Add database configuration to your kener server.yaml
|
||||
---
|
||||
|
||||
# Database Config
|
||||
|
||||
Use the `config/server.yaml` file to configure the database settings.
|
||||
|
||||
## Supported Database
|
||||
|
||||
- Sqlite (default)
|
||||
- Postgres
|
||||
|
||||
We are adding more database support in the future.
|
||||
|
||||
## Sqlite
|
||||
|
||||
Sqlite is the default database for Kener. You don't need to do anything to use it. The database file will be created in the `database` folder.
|
||||
|
||||
The name of the default database file is `kener.db`. The path will be `database/kener.db`.
|
||||
|
||||
You can change the database file name by changing the `database` key in the `server.yaml` file.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
sqlite:
|
||||
dbName: awesomeKener.db
|
||||
```
|
||||
|
||||
In this case, the database file will be created in the `database` folder with the name `awesomeKener.db`.
|
||||
|
||||
Make sure the `database` folder is writable by the Kener process.
|
||||
|
||||
## Postgres
|
||||
|
||||
To use Postgres, you need to provide the connection details in the `server.yaml` file.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
postgres:
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: kener
|
||||
password: kener
|
||||
database: kener
|
||||
```
|
||||
|
||||
Or if you want to use environment variables, you can do that as well. Make sure the environment variables are set before starting the Kener process. The environment variables should be `PG_HOST`, `PG_PORT`, `PG_USER`, `PG_PASSWORD`, and `PG_DB`.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
postgres:
|
||||
host: $PG_HOST
|
||||
port: $PG_PORT
|
||||
user: $PG_USER
|
||||
password: $PG_PASSWORD
|
||||
database: $PG_DB
|
||||
```
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
title: Kener Deployment - From Source or Docker
|
||||
description: Kener can be deployed in multiple ways. You can use the pre-built docker image or build from source.
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
Kener can be deployed in multiple ways. You can use the pre-built docker image or build from source.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have the following installed:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/)
|
||||
- [npm](https://www.npmjs.com/get-npm)
|
||||
- Make sure `./database` and `./config` directories are present in the root directory
|
||||
- [config/site.yaml](/docs/customize-site) Contains information about the site
|
||||
- [config/monitors.yaml](/docs/monitors) Contains your monitors and their related specifications
|
||||
- [Set up Environment Variables](/docs/environment-vars). You can use a `.env` file or pass them as arguments. Be sure to add `NODE_ENV=production` for production deployment
|
||||
|
||||
## NPM
|
||||
|
||||
```shell
|
||||
npm i
|
||||
npm run build
|
||||
npm run configure
|
||||
npm run prod
|
||||
```
|
||||
|
||||
## PM2
|
||||
|
||||
```shell
|
||||
npm i
|
||||
npm run build #build the frontend
|
||||
npm run configure #build the backend
|
||||
pm2 start src/lib/server/startup.js
|
||||
pm2 start main.js
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
[Dockerhub](https://hub.docker.com/r/rajnandan1/kener)
|
||||
|
||||
```shell
|
||||
docker.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
[Github Packages](https://github.com/rajnandan1/kener/pkgs/container/kener)
|
||||
|
||||
```shell
|
||||
ghcr.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
You should mount two host directories to persist your configuration and database. [Environmental variables](/docs/environment-vars) can be passed with `-e` An example `docker run` command:
|
||||
|
||||
Make sure `./database` and `./config` directories are present in the root directory
|
||||
|
||||
```shell
|
||||
mkdir database
|
||||
mkdir config
|
||||
curl -o config/site.yaml https://raw.githubusercontent.com/rajnandan1/kener/refs/heads/main/config/site.example.yaml
|
||||
curl -o config/monitors.yaml https://raw.githubusercontent.com/rajnandan1/kener/refs/heads/main/config/monitors.example.yaml
|
||||
curl -o config/server.yaml https://raw.githubusercontent.com/rajnandan1/kener/refs/heads/main/config/server.example.yaml
|
||||
docker run \
|
||||
-v $(pwd)/database:/app/database \
|
||||
-v $(pwd)/config:/app/config \
|
||||
-p 3000:3000 \
|
||||
-e "GH_TOKEN=1234" \
|
||||
rajnandan1/kener
|
||||
```
|
||||
|
||||
You can also use a .env file
|
||||
|
||||
```shell
|
||||
docker run \
|
||||
-v $(pwd)/database:/app/database \
|
||||
-v $(pwd)/config:/app/config \
|
||||
--env-file .env \
|
||||
-p 3000:3000 \
|
||||
rajnandan1/kener
|
||||
```
|
||||
|
||||
Or use **Docker Compose** with the example [docker-compose.yaml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
|
||||
|
||||
## Using PUID and PGID
|
||||
|
||||
If you are
|
||||
|
||||
- running on a **linux host** (ie unraid) and
|
||||
- **not** using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_)
|
||||
|
||||
then you must set the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid) in the container in order for it to generate files/folders your normal user can interact it.
|
||||
|
||||
Run these commands from your terminal
|
||||
|
||||
- `id -u` -- prints UID for **PUID**
|
||||
- `id -g` -- prints GID for **PGID**
|
||||
|
||||
Then add to your docker command like so:
|
||||
|
||||
```shell
|
||||
docker run -d ... -e "PUID=1000" -e "PGID=1000" ... rajnandan1/kener
|
||||
```
|
||||
|
||||
or substitute them in [docker-compose.yml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
|
||||
|
||||
## Base path
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path. Read more about it [here](/docs/environment-vars/#kener-base-path)
|
||||
@@ -1,87 +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-[tag]/js?theme=light&monitor=http://[hostname]/embed-[tag]"
|
||||
></script>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<script
|
||||
async
|
||||
src="https://kener.ing/embed-okbookmarks/js?theme=light&monitor=https://kener.ing/embed-okbookmarks"
|
||||
></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
|
||||
|
||||
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-okbookmarks/js?theme=dark&monitor=/embed-okbookmarks"></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-[tag]?theme=light"
|
||||
width="100%"
|
||||
height="200"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://kener.ing/embed-okbookmarks?theme=light"
|
||||
width="100%"
|
||||
height="200"
|
||||
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
|
||||
|
||||
### Demo
|
||||
|
||||
<div class="border mx-auto rounded-sm w-585px">
|
||||
<iframe src="/embed-okbookmarks?theme=dark" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
</div>
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
title: Environment Variables | Kener
|
||||
description: Kener needs some environment variables to be set to run properly. Here are the list of environment variables that you need to set.
|
||||
---
|
||||
|
||||
# Environment Variables
|
||||
|
||||
Kener needs some environment variables to be set to run properly. Here are the list of environment variables that you need to set.
|
||||
|
||||
All of these are optional but are required for specific features.
|
||||
|
||||
## PORT
|
||||
|
||||
Defaults to 3000 if not specified
|
||||
|
||||
```shell
|
||||
export PORT=4242
|
||||
```
|
||||
|
||||
## GH_TOKEN
|
||||
|
||||
A github token to read issues and create labels. This is required for **incident management**
|
||||
|
||||
```shell
|
||||
export GH_TOKEN=your-github-token
|
||||
```
|
||||
|
||||
## API_TOKEN
|
||||
|
||||
To talk to **kener apis** you will need to set up a token. It uses Bearer Authorization
|
||||
|
||||
```shell
|
||||
export API_TOKEN=sometoken
|
||||
```
|
||||
|
||||
## API_IP
|
||||
|
||||
While using API you can set this variable to accept request from a **specific IP**
|
||||
|
||||
```shell
|
||||
export API_IP=127.0.0.1
|
||||
```
|
||||
|
||||
## API_IP_REGEX
|
||||
|
||||
While using API you can set this variable to accept request from a specific IP that matches the regex. Below example shows an **IPv6 regex**
|
||||
|
||||
```shell
|
||||
export API_IP_REGEX=^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
|
||||
```
|
||||
|
||||
If you set both API_IP and API_IP_REGEX, API_IP will be given preference
|
||||
|
||||
## KENER_BASE_PATH
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path.
|
||||
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
- If you are using docker you will have to do your own build and set this env variable during `docker build`
|
||||
|
||||
```shell
|
||||
export KENER_BASE_PATH=/status
|
||||
```
|
||||
|
||||
## Using .env
|
||||
|
||||
You can also use a `.env` file to set these variables. Create a `.env` file in the root of the project and add the variables like below
|
||||
|
||||
```shell
|
||||
PORT=4242
|
||||
GH_TOKEN=your-github-token
|
||||
API_TOKEN=sometoken
|
||||
API_IP=
|
||||
API_IP_REGEX=
|
||||
KENER_BASE_PATH=/status
|
||||
```
|
||||
|
||||
## Secrets
|
||||
|
||||
Kener supports secrets in monitors. Let us say you have a monitor that is API based and you want to keep the API key secret. You can use the `secrets` key in the monitor to keep the API key secret.
|
||||
|
||||
```yaml
|
||||
- name: Example Secret Monitor
|
||||
description: Monitor to show how to use secrets
|
||||
tag: "secret"
|
||||
api:
|
||||
method: GET
|
||||
url: https://api.example.com/users
|
||||
headers:
|
||||
Authorization: Bearer $CLIENT_SECRET
|
||||
```
|
||||
|
||||
In the above example, the `CLIENT_SECRET` is a secret that you can set in the monitor. To properly make this work you will have to set up environment variables like below
|
||||
|
||||
```shell
|
||||
export CLIENT_SECRET=your-api-key
|
||||
```
|
||||
|
||||
Remember to set the `CLIENT_SECRET` in your `.env` file if you are using one.
|
||||
@@ -1,46 +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. It can be either public or private. After you have created a repository open `site.yaml` and add them like this
|
||||
|
||||
```yaml
|
||||
github:
|
||||
owner: "username"
|
||||
repo: "repository"
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
```shell
|
||||
export GH_TOKEN=github_pat_11AD3ZA3Y0
|
||||
```
|
||||
-149
@@ -1,149 +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 Sveltekit NodeJS Status Page System
|
||||
|
||||
<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://kener.ing/docs/quick-start">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
|
||||
- Flexible monitor configuration using YAML
|
||||
- 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 using yaml or code
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
|
||||
### Incident Management
|
||||
|
||||
- Create Incidents using Github Issues - Rich Text
|
||||
- Or use APIs to create Incidents
|
||||
|
||||
### 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
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Inspired from
|
||||
|
||||
Kener draws inspiration from a comprehensive ecosystem of monitoring and status page solutions, reflecting the diverse landscape of network and application performance tools:
|
||||
Uptime and Status Page Platforms
|
||||
|
||||
- Upptime - GitHub-powered uptime monitoring
|
||||
- Statuspage - Incident communication platform
|
||||
- Cachet - Open-source status page system
|
||||
- Upptrends - Global website monitoring
|
||||
- Hexometer - Website monitoring and performance tracking
|
||||
|
||||
Enterprise Monitoring Solutions
|
||||
|
||||
- Pingdom - Website monitoring service
|
||||
- New Relic - Application performance monitoring
|
||||
- Datadog - Observability and monitoring platform
|
||||
- AppDynamics - Application performance management
|
||||
- Dynatrace - AI-powered full stack monitoring
|
||||
|
||||
Network and Infrastructure Monitoring
|
||||
|
||||
- StatusCake - Uptime and performance monitoring
|
||||
- UptimeRobot - Free website monitoring
|
||||
- Better Uptime - Monitoring and incident management
|
||||
- Site24x7 - Comprehensive monitoring solution
|
||||
- Nagios - Infrastructure and network monitoring
|
||||
- Zabbix - Enterprise-class monitoring solution
|
||||
- Prometheus - Monitoring and alerting toolkit
|
||||
- Sentry - Error tracking and performance monitoring
|
||||
|
||||
Cloud and Hybrid Monitoring Platforms
|
||||
|
||||
- CloudWatch - AWS monitoring service
|
||||
- Azure Monitor - Microsoft's monitoring solution
|
||||
- Google Cloud Monitoring - Cloud-native monitoring
|
||||
|
||||
## 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)
|
||||
@@ -1,41 +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
|
||||
|
||||
Once you have added a new translation file in the `locales` folder, you can enable it by adding the locale code in the `site.yaml` file.
|
||||
|
||||
Let us say you have added a `hi.json` file in the `locales` folder. You can enable it by adding the following to the `site.yaml` file.
|
||||
|
||||
```yaml
|
||||
i18n:
|
||||
defaultLocale: en
|
||||
locales:
|
||||
en: English
|
||||
hi: हिन्दी
|
||||
```
|
||||
|
||||
> **_defaultLocale:_** The default locale to be used. This will be the language used when a user visits the site for the first time. It is important to note that the default locale json file should be present in the locales folder.
|
||||
|
||||
## Variables
|
||||
|
||||
There are few variables that you you should not change,
|
||||
|
||||
- %hours : This will be replaced by the hours
|
||||
- %minutes : This will be replaced by the minutes
|
||||
- %minute : This will be replaced by the minute
|
||||
- %status : This will be replaced by the status
|
||||
|
||||
> **locales:\_** A list of locales that you want to enable. The key is the locale code and the value is the name of the language. The locale code should be the same as the json file name in the locales folder. `en` means `en.json` should be present in the locales folder.
|
||||
|
||||
Adding more than one locales will enable a dropdown in the navbar to select the language.
|
||||
|
||||
Selected languages are stored in cookies and will be used when the user visits the site again.
|
||||
|
||||
There is no auto detection of the language. The user has to manually select the language.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
title: Incident Management | Kener
|
||||
description: Kener uses Github to power incident management using labels
|
||||
---
|
||||
|
||||
# Incident Management
|
||||
|
||||
Kener uses Github to power incident management using labels
|
||||
|
||||
## Labels
|
||||
|
||||
Kener auto creates labels for your monitors using the `tag` parameter
|
||||
|
||||
- `incident`: If an issue is marked as incident it will show up in kener home page
|
||||
- `incident-down`: If an issue is marked as incident-down and incident kener would make that monitor down
|
||||
- `incident-degraded`: If an issue is marked as incident-degraded and incident then kener would make the monitor degraded
|
||||
- `resolved`: Use this tag to mark the incident has RESOLVED
|
||||
- `identified`: Use this tag to show that the root cause of the incident has been identified
|
||||
|
||||
## Creating Incident
|
||||
|
||||
Kener uses Github issues to create incidents. Here is how you can create an incident
|
||||
|
||||
### Using Github
|
||||
|
||||
- Go to your github repo of kener
|
||||
- Go to issues
|
||||
- Create an issue. Give it a title
|
||||
- In the body add [start_datetime:1702651340] and [end_datetime:1702651140] and add some description. Time is UTC
|
||||
- Add `incident`, `incident-down` and the monitor tag. This will make the monitor down for 4 minutes
|
||||
|
||||
If you clone the repo it gives you an issue template to create incidents
|
||||
|
||||
Here is a [sample incident](https://github.com/rajnandan1/kener/issues/15) for your reference.
|
||||
|
||||
### Using API
|
||||
|
||||
You can also create incidents using the API. See the API [here](/docs/kener-apis#create-an-incident---api)
|
||||
@@ -1,427 +0,0 @@
|
||||
---
|
||||
title: Kener APIs
|
||||
description: Kener gives APIs to push data and create incident.
|
||||
---
|
||||
|
||||
# Kener APIs
|
||||
|
||||
Kener also gives APIs to push data and create incident. Before you use kener apis you will have to set an authorization token called `API_TOKEN`. This also has to be set as an environment variable.
|
||||
|
||||
```shell
|
||||
export API_TOKEN=some-token-set-by-you
|
||||
```
|
||||
|
||||
Additonally you can set IP whitelisting by setting another environment token called `API_IP` or `API_IP_REGEX`. If you set both `API_IP` and `API_IP_REGEX`, `API_IP` will be given preference. Read more [here](/docs/environment-vars#api_ip)
|
||||
|
||||
## Interactive API Reference
|
||||
|
||||
<p class="border p-4 rounded-md">
|
||||
<picture class="inline">
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f916/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f916/512.gif" alt="🤖" width="32" height="32">
|
||||
</picture>
|
||||
<a href="/api-reference">
|
||||
Click here to view the interactive API reference
|
||||
</a>
|
||||
|
||||
</p>
|
||||
|
||||
You can download the openapi spec
|
||||
|
||||
- [JSON](https://raw.githubusercontent.com/rajnandan1/kener/main/openapi.json)
|
||||
- [YAML](https://raw.githubusercontent.com/rajnandan1/kener/main/openapi.yaml)
|
||||
|
||||
---
|
||||
|
||||
## Update Status - API
|
||||
|
||||

|
||||
|
||||
The update status API can be used to manually update the state of a monitor from a remote server.
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------ |
|
||||
| status | `Required` Can be only UP/DOWN/DEGRADED |
|
||||
| latency | `Required` In Seconds. Leave 0 if not required |
|
||||
| timestampInSeconds | `Optional` Timestamp in UTC seconds. Defaults to now. Should between 90 Days and now |
|
||||
| tag | `Required` Monitor Tag set in monitors.yaml |
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/status \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"status": "DOWN",
|
||||
"latency": 1213,
|
||||
"timestampInSeconds": 1702405860,
|
||||
"tag": "google-search"
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "success at 1702405860"
|
||||
}
|
||||
```
|
||||
|
||||
This will update the status of the monitor with tag `google-search` to DOWN at UTC 1702405860
|
||||
|
||||
---
|
||||
|
||||
## Get Status - API
|
||||
|
||||

|
||||
|
||||
Use this API to get the status of a monitor.
|
||||
|
||||
### Request
|
||||
|
||||
Replace `google-search` with your monitor tag in query param
|
||||
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url 'http://your-kener.host/api/status?tag=google-search' \
|
||||
--header 'Authorization: Bearer some-token-set-by-you'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
"uptime": "9.0026",
|
||||
"lastUpdatedAt": 1706447160
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create an Incident - API
|
||||
|
||||

|
||||
|
||||
Can be use to create an incident from a remote server
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------- | -------------------------------------------------------- |
|
||||
| startDatetime | `Optional` When did the incident start in UTC second |
|
||||
| endDatetime | `Optional` When did the incident end in UTC seconds |
|
||||
| title | `Required` Title of the incident |
|
||||
| body | `Optional` Body of the incident |
|
||||
| tags | `Required` Array of String, Monitor Tags of the incident |
|
||||
| impact | `Optional` Can be only DOWN/DEGRADED |
|
||||
| isMaintenance | `Optional` Boolean if incident is a maintenance |
|
||||
| isIdentified | `Optional` Incident identified |
|
||||
| isResolved | `Optional` Incident resolved |
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"title": "Outage in Mumbai",
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"tags": ["google-search"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update an Incident - API
|
||||
|
||||

|
||||
|
||||
Can be use to update an incident from a remote server. It will clear values if not passed
|
||||
|
||||
### Request Param
|
||||
|
||||
- `incidentNumber`: Number of the incident
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------- | -------------------------------------------------------- |
|
||||
| startDatetime | `Optional` When did the incident start in UTC second |
|
||||
| endDatetime | `Optional` When did the incident end in UTC seconds |
|
||||
| title | `Required` Title of the incident |
|
||||
| body | `Optional` Body of the incident |
|
||||
| tags | `Required` Array of String, Monitor Tags of the incident |
|
||||
| impact | `Optional` Can be only DOWN/DEGRADED |
|
||||
| isMaintenance | `Optional` Boolean if incident is a maintenance |
|
||||
| isIdentified | `Optional` Incident identified |
|
||||
| isResolved | `Optional` Incident resolved |
|
||||
|
||||
```shell
|
||||
curl --request PATCH \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber} \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"title": "Outage in Mumbai",
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"tags": ["google-search"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get an Incident - API
|
||||
|
||||

|
||||
|
||||
Use `incidentNumber` to fetch an incident
|
||||
|
||||
### Request Body
|
||||
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber} \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add Comment - API
|
||||
|
||||

|
||||
|
||||
Add comments for incident using `incidentNumber`
|
||||
|
||||
### Request
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"body": "comment 1"
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"commentID": 1873376745,
|
||||
"body": "comment 1",
|
||||
"createdAt": 1704123938
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get Comments - API
|
||||
|
||||

|
||||
|
||||
Use this API to fetch all the comments for an incident
|
||||
|
||||
### Request
|
||||
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"commentID": 1873372042,
|
||||
"body": "comment 1",
|
||||
"createdAt": 1704123116
|
||||
},
|
||||
{
|
||||
"commentID": 1873372169,
|
||||
"body": "comment 2",
|
||||
"createdAt": 1704123139
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update Incident Status - API
|
||||
|
||||

|
||||
|
||||
Use this to API to update the status of an ongoing incident.
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| isIdentified | `Optional` Boolean, set it when incident has been identified |
|
||||
| isResolved | `Optional` Boolean, set it when incident has been resolved |
|
||||
| endDatetime | `Optional` When did the incident end in UTC seconds |
|
||||
|
||||
### Request
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber}/status \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
"endDatetime": 1702405920
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Search Incidents - API
|
||||
|
||||

|
||||
|
||||
Use this to API to search incidents.
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| state | `Optional` open or closed. Default is open |
|
||||
| tags | `Optional` Comma separated monitor tags, example: earth,google-seach |
|
||||
| page | `Optional` Page number, starts with 1, defaults to 1 |
|
||||
| per_page | `Optional` Page size, defaults to 10, max is 100 |
|
||||
| created_after_utc | `Optional` timestamp in UTC seconds when the incident was created after. Example: 1702405920 |
|
||||
| created_before_utc | `Optional` timestamp in UTC seconds when the incident was created before . Example: 1702405920 |
|
||||
| title_like | `Optional` search incidents with title |
|
||||
|
||||
### Request
|
||||
|
||||
Search incidents that are closed and title contains `hello incident`
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident?state=closed&title_like=Hello%20Incident \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
"endDatetime": 1702405920
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai - Hello Incident",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
title: Monitors | monitors.yaml | Kener
|
||||
description: Monitors are the heart of Kener. This is where you define the monitors you want to show on your site.
|
||||
---
|
||||
|
||||
# Monitors
|
||||
|
||||
Inside `config/` folder there is a file called `monitors.yaml`. We will be adding our monitors here. Please note that your yaml must be valid. It is an array.
|
||||
|
||||
## Understanding monitors
|
||||
|
||||
Each monitor runs at 1 minute interval by default. Monitor runs in below priority order.
|
||||
|
||||
- `defaultStatus` Data. Used to set the default status of the monitor
|
||||
- PING/API/DNS call Data overrides above data(if present)
|
||||
- Pushed Status Data overrides status Data using [Kener Update Statue API](/docs/kener-apis#update-status---api)
|
||||
- [Manual Incident](/docs/incident-management) Data overrides Pushed Status Data
|
||||
|
||||
## General Attributes
|
||||
|
||||
A list of attributes that can be used in all types of monitors.
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| name | Required + Unique | This will be shown in the UI to your users. Keep it short and unique |
|
||||
| description | Optional | This is a breif description for the monitor |
|
||||
| tag | Required + Unique | This is used to tag incidents created in Github using comments |
|
||||
| image | Optional | To show a logo before the name |
|
||||
| cron | Optional | Use a valid cron expression to specify the interval to run the monitors. Defaults to `* * * * *` i.e every minute |
|
||||
| defaultStatus | Optional | This will be the default status if no other way is specified to check the monitor. can be `UP`/`DOWN`/`DEGRADED` |
|
||||
| hidden | Optional | If set to `true` will not show the monitor in the UI |
|
||||
| category | Optional | Use this to group your monitors. Make sure you have defined category in `site.yaml` and use the `name` attribute. More about it [here](/docs/customize-site#categories). |
|
||||
| dayDegradedMinimumCount | Optional | Default is 1. It means, minimum this number of count for the day to be classified as DEGRADED(Yellow Bar) in 90 day view. Has to be `number` greater than 0 |
|
||||
| dayDownMinimumCount | Optional | Default is 1. It means, minimum this number of count for the day to be classified as DOWN(Red Bar) in 90 day view. Has to be `number` greater than 0 |
|
||||
| includeDegradedInDowntime | Optional | By deafault uptime percentage is calculated as (UP+DEGRADED/UP+DEGRADED+DOWN). Setting it as `true` will change the calculation to (UP/UP+DEGRADED+DOWN) |
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
- name: "Google"
|
||||
description: "Google Search Engine"
|
||||
tag: "google"
|
||||
image: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
|
||||
defaultStatus: "UP"
|
||||
hidden: false
|
||||
dayDegradedMinimumCount: 2
|
||||
dayDownMinimumCount: 3
|
||||
includeDegradedInDowntime: false
|
||||
```
|
||||
|
||||
`dayDegradedMinimumCount` and `dayDownMinimumCount` only works when `summaryStyle` is set as `DAY` in `site.yaml`. More about it [here](/docs/customize-site#summarystyle)
|
||||
|
||||
## API Monitor Attributes
|
||||
|
||||
A list of attributes that can be used in API monitors.
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| api.url | Required | HTTP URL |
|
||||
| api.method | Optional | HTTP Method. Default is `GET` |
|
||||
| api.headers | Optional | HTTP headers |
|
||||
| api.body | Optional | HTTP Body as string |
|
||||
| api.timeout | Optional | timeout for the api in milliseconds. Default is 10000(10 secs) |
|
||||
| api.eval | Optional | Evaluator written in JS, to parse HTTP response and calculate uptime and latency |
|
||||
| api.hideURLForGet | Optional | if the monitor is a GET URL and no headers are specified and the response body content-type is a text/html then kener shows a GET hyperlink in monitor description. To hide that set this as false. Default is `true` |
|
||||
|
||||
### Eval
|
||||
|
||||
This is a anonymous JS function, by default it looks like this.
|
||||
|
||||
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and lantecy(number)
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(function (statusCode, responseTime, responseDataBase64) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
status = 'UP',
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
|
||||
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64);
|
||||
//let jsonResp = JSON.parse(decodedResp)
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
- name: "Google"
|
||||
description: "Google Search Engine"
|
||||
tag: "google"
|
||||
image: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
|
||||
defaultStatus: "UP"
|
||||
hidden: false
|
||||
api:
|
||||
url: "https://www.google.com"
|
||||
method: "GET"
|
||||
headers:
|
||||
"Content-Type": "application/json"
|
||||
body: ""
|
||||
timeout: 10000
|
||||
eval: |
|
||||
(function (statusCode, responseTime, responseDataBase64) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
status = 'UP',
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
To view more examples of API monitors, please visit [here](/docs/monitors-examples)
|
||||
|
||||
## PING Monitor Attributes
|
||||
|
||||
A list of attributes that can be used in PING monitors.
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ------------ | --------- | ----------------------------------------------------------------------- |
|
||||
| ping.hostsV4 | Required | Array of hosts / IP to monitor ping response. Either domain name or IP4 |
|
||||
| ping.hostsV6 | Required | Array of hosts / IP to monitor ping response. Either domain name or IP6 |
|
||||
|
||||
Either one of `hostsV4` or `hostsV6` is required
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
- name: "Ping All"
|
||||
description: "Ping All is where I ping all the hosts"
|
||||
tag: "pingall"
|
||||
defaultStatus: "UP"
|
||||
ping:
|
||||
hostsV4:
|
||||
- "www.rajnandan.com"
|
||||
- "103.125.217.243"
|
||||
```
|
||||
|
||||
You can find more examples of PING monitors [here](/docs/monitor-examples#ping-monitor)
|
||||
|
||||
## DNS Monitor Attributes
|
||||
|
||||
A list of attributes that can be used in DNS monitors.
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ---------------- | --------- | ------------------------------------------------------------------------ |
|
||||
| dns.hosts | Required | Array of hosts to monitor DNS response. Either domain name or IP4 or IP6 |
|
||||
| dns.lookupRecord | Required | DNS record type. |
|
||||
| dns.nameServer | Required | DNS server to use. |
|
||||
| dns.matchType | Required | Match type for DNS response. Can be `ANY` or `ALL`. Default is `ALL` |
|
||||
| dns.values | Required | Expected values for the DNS response. Array of string |
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
- name: "DNS All"
|
||||
description: "DNS All is where I check all the DNS"
|
||||
tag: "dnsall"
|
||||
defaultStatus: "UP"
|
||||
dns:
|
||||
host: "www.rajnandan.com"
|
||||
lookupRecord: "CNAME"
|
||||
nameServer: "8.8.8.8"
|
||||
matchType: "ANY"
|
||||
values:
|
||||
- "rajnandan1.github.io"
|
||||
```
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
title: Quick Start | Kener
|
||||
description: Get started with Kener
|
||||
---
|
||||
|
||||
# Quick Start
|
||||
|
||||
Here is a demonstration of how to get started with Kener in seconds
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js Minimum version required is `v22.12.0`.
|
||||
- Git
|
||||
- sqlite3
|
||||
|
||||
## Clone the repository
|
||||
|
||||
```shell
|
||||
git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
```
|
||||
|
||||
## Install Dependencies
|
||||
|
||||
```shell
|
||||
npm install
|
||||
```
|
||||
|
||||
## Setup Configuration
|
||||
|
||||
- Rename `config/site.example.yaml` -> `config/site.yaml`
|
||||
- Rename `config/monitors.example.yaml` -> `config/monitors.yaml`
|
||||
- Rename `config/server.example.yaml` -> `config/server.yaml`
|
||||
|
||||
```shell
|
||||
cp config/site.example.yaml config/site.yaml
|
||||
cp config/monitors.example.yaml config/monitors.yaml
|
||||
cp config/server.example.yaml config/server.yaml
|
||||
```
|
||||
|
||||
## Start Kener
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Kener Development Server would be running at PORT 3000. Go to [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Configure Site](/docs/customize-site)
|
||||
- [Add Monitors](/docs/monitors)
|
||||
- [Alerting](/docs/alerting)
|
||||
@@ -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,129 +0,0 @@
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"sectionTitle": "Getting Started",
|
||||
"children": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"link": "/docs/home",
|
||||
"file": "/home.md"
|
||||
},
|
||||
{
|
||||
"title": "Quick Start",
|
||||
"link": "/docs/quick-start",
|
||||
"file": "/docs/quick-start.md"
|
||||
},
|
||||
{
|
||||
"title": "Changelogs",
|
||||
"link": "/docs/changelogs",
|
||||
"file": "/changelogs.md"
|
||||
},
|
||||
{
|
||||
"title": "Roadmap",
|
||||
"link": "/docs/roadmap",
|
||||
"file": "/roadmap.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Core Concepts",
|
||||
"children": [
|
||||
{
|
||||
"title": "Customization (site.yaml)",
|
||||
"link": "/docs/customize-site",
|
||||
"file": "/customize-site.md"
|
||||
},
|
||||
{
|
||||
"title": "Monitors (monitors.yaml)",
|
||||
"link": "/docs/monitors",
|
||||
"file": "/monitors.md"
|
||||
},
|
||||
{
|
||||
"title": "Alerting (server.yaml)",
|
||||
"link": "/docs/alerting",
|
||||
"file": "/alerting.md"
|
||||
},
|
||||
{
|
||||
"title": "Database (server.yaml)",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
},
|
||||
{
|
||||
"title": "Monitors Examples",
|
||||
"link": "/docs/monitor-examples",
|
||||
"file": "/monitor-examples.md"
|
||||
},
|
||||
{
|
||||
"title": "Incident Management",
|
||||
"link": "/docs/incident-management",
|
||||
"file": "/incident-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Localization",
|
||||
"link": "/docs/i18n",
|
||||
"file": "/i18n.md"
|
||||
},
|
||||
{
|
||||
"title": "Badges",
|
||||
"link": "/docs/status-badges",
|
||||
"file": "/status-badges.md"
|
||||
},
|
||||
{
|
||||
"title": "Embed",
|
||||
"link": "/docs/embed",
|
||||
"file": "/embed.md"
|
||||
},
|
||||
{
|
||||
"title": "Analytics",
|
||||
"link": "/docs/site-analytics",
|
||||
"file": "/site-analytics.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Deployment",
|
||||
"children": [
|
||||
{
|
||||
"title": "Environment Setup",
|
||||
"link": "/docs/environment-vars",
|
||||
"file": "/environment-vars.md"
|
||||
},
|
||||
{
|
||||
"title": "Deployment",
|
||||
"link": "/docs/deployment",
|
||||
"file": "/deployment.md"
|
||||
},
|
||||
{
|
||||
"title": "Github Setup",
|
||||
"link": "/docs/gh-setup",
|
||||
"file": "/gh-setup.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "API Reference",
|
||||
"children": [
|
||||
{
|
||||
"title": "Kener APIs",
|
||||
"link": "/docs/kener-apis",
|
||||
"file": "/kener-apis.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"title": "Categorize Monitors",
|
||||
"link": "/docs/categorize-guide",
|
||||
"file": "/categorize-guide.md"
|
||||
},
|
||||
{
|
||||
"title": "Custom JS/CSS",
|
||||
"link": "/docs/custom-js-css-guide",
|
||||
"file": "/custom-js-css-guide.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+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,60 +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 sitemap from "./sitemap.js";
|
||||
import fs from "fs-extra";
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
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("/healthcheck", (req, res) => {
|
||||
res.end("ok");
|
||||
});
|
||||
|
||||
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.get("/sitemap.xml", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/xml");
|
||||
res.end(sitemap);
|
||||
});
|
||||
|
||||
app.use(handler);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log("Kener is running on port " + PORT + "!");
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
-971
@@ -1,971 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"title": "Kener API",
|
||||
"version": "1.0.0",
|
||||
"description": "# Kener Self-hosted node js status page\n\nAPI specification for Kener status page and incident management system. This API spec was created using [Frogment](https://www.frogment.app)\n",
|
||||
"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://your-kener-host.com",
|
||||
"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"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"type": "integer",
|
||||
"example": 1731251760
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"status": "UP",
|
||||
"lastUpdatedAt": 1731251760,
|
||||
"uptime": "100"
|
||||
}
|
||||
},
|
||||
"Incident": {
|
||||
"type": "object",
|
||||
"description": "body of an incident",
|
||||
"required": ["title", "tags"],
|
||||
"properties": {
|
||||
"startDatetime": {
|
||||
"type": "integer",
|
||||
"description": "UTC timestamp in seconds",
|
||||
"example": 1731901920
|
||||
},
|
||||
"endDatetime": {
|
||||
"type": "integer",
|
||||
"description": "UTC timestamp in seconds",
|
||||
"example": 1704123938
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Outage in mumbai",
|
||||
"description": "title of the incident"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"example": "body of the incident",
|
||||
"description": "body of the incident"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "earth"
|
||||
},
|
||||
"description": "comma separated tags of monitors",
|
||||
"example": ["earth", "google"]
|
||||
},
|
||||
"impact": {
|
||||
"type": "string",
|
||||
"example": "DOWN",
|
||||
"description": "Impact of the incident",
|
||||
"enum": ["DOWN", "DEGRADED"]
|
||||
},
|
||||
"isMaintenance": {
|
||||
"type": "boolean",
|
||||
"description": "is this incident because of maintenance",
|
||||
"example": false
|
||||
},
|
||||
"isIdentified": {
|
||||
"type": "boolean",
|
||||
"description": "has the incident been indentified",
|
||||
"example": true
|
||||
},
|
||||
"isResolved": {
|
||||
"type": "boolean",
|
||||
"description": "has the incident been resovled",
|
||||
"example": true
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"startDatetime": 1731901920,
|
||||
"endDatetime": 1704123938,
|
||||
"title": "title of the incident",
|
||||
"body": "body of the incident",
|
||||
"tags": ["earth", "google"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": true
|
||||
}
|
||||
},
|
||||
"IncidentResponse": {
|
||||
"description": "Incident response schema",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Incident"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "integer",
|
||||
"description": "UTC timestamp in seconds, incident created at",
|
||||
"example": 1731901920
|
||||
},
|
||||
"closedAt": {
|
||||
"type": "integer",
|
||||
"nullable": true,
|
||||
"description": "UTC timestamp in seconds, incident closed at",
|
||||
"example": 1731901920
|
||||
},
|
||||
"incidentNumber": {
|
||||
"type": "integer",
|
||||
"description": "id of the incident",
|
||||
"example": 4
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"createdAt": 1731901920,
|
||||
"closedAt": 1731901920,
|
||||
"incidentNumber": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Comment": {
|
||||
"type": "object",
|
||||
"description": "Comment of an incident",
|
||||
"required": ["body"],
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "string",
|
||||
"example": "comment 1"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"body": "comment 2"
|
||||
}
|
||||
},
|
||||
"CommentResponse": {
|
||||
"type": "object",
|
||||
"description": "Comment Response",
|
||||
"properties": {
|
||||
"commentID": {
|
||||
"type": "integer",
|
||||
"description": "ID of the comment",
|
||||
"example": 1873376745
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "body of the comment",
|
||||
"example": "comment 3"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "integer",
|
||||
"description": "timestamp when comment was created",
|
||||
"example": 1704123938
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"commentID": 1873376745,
|
||||
"body": "comment 4",
|
||||
"createdAt": 1704123938
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"createdAt": 1731901920,
|
||||
"closedAt": 1731901920,
|
||||
"incidentNumber": 4,
|
||||
"startDatetime": 1731901920,
|
||||
"endDatetime": 1704123938,
|
||||
"title": "title of the incident",
|
||||
"body": "body of the incident",
|
||||
"tags": ["earth", "google"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": true
|
||||
}
|
||||
},
|
||||
"CreateIncidentRequest": {
|
||||
"summary": "create incident request body",
|
||||
"description": "Some Description",
|
||||
"value": {
|
||||
"startDatetime": 1731901920,
|
||||
"endDatetime": 1704123938,
|
||||
"title": "title of the incident",
|
||||
"body": "body of the incident",
|
||||
"tags": ["earth", "google"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": true
|
||||
}
|
||||
},
|
||||
"SearchIncidentsResponse": {
|
||||
"summary": "array of incidents",
|
||||
"description": "Some Description",
|
||||
"value": [
|
||||
{
|
||||
"createdAt": 1731901920,
|
||||
"closedAt": 1731901920,
|
||||
"incidentNumber": 4,
|
||||
"startDatetime": 1731901920,
|
||||
"endDatetime": 1704123938,
|
||||
"title": "title of the incident",
|
||||
"body": "body of the incident",
|
||||
"tags": ["earth", "google"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": true
|
||||
},
|
||||
{
|
||||
"createdAt": 1731901920,
|
||||
"closedAt": 1731901920,
|
||||
"incidentNumber": 4,
|
||||
"startDatetime": 1731901920,
|
||||
"endDatetime": 1704123938,
|
||||
"title": "title of the incident",
|
||||
"body": "body of the incident",
|
||||
"tags": ["earth", "google"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommentsResponse": {
|
||||
"summary": "list of comments",
|
||||
"description": "Some Description",
|
||||
"value": [
|
||||
{
|
||||
"commentID": 1873376745,
|
||||
"body": "comment 4",
|
||||
"createdAt": 1704123938
|
||||
},
|
||||
{
|
||||
"commentID": 1873376745,
|
||||
"body": "comment 4",
|
||||
"createdAt": 1704123938
|
||||
}
|
||||
]
|
||||
},
|
||||
"CommentRequestBody": {
|
||||
"summary": "request body for a comment",
|
||||
"description": "request body for a comment to add in an incident",
|
||||
"value": {
|
||||
"body": "This is a comment"
|
||||
}
|
||||
},
|
||||
"CreateCommentResponse": {
|
||||
"summary": "create comment response body sample",
|
||||
"description": "create comment response body sample",
|
||||
"value": {
|
||||
"commentID": 1873376745,
|
||||
"body": "comment 4",
|
||||
"createdAt": 1704123938
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/api/status": {
|
||||
"post": {
|
||||
"operationId": "updateMonitorStatus",
|
||||
"summary": "Update status of a monitor",
|
||||
"description": "Update status of an incident at a given timestamp",
|
||||
"tags": ["Monitors"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "request body to update an incident",
|
||||
"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 an incident",
|
||||
"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/Incident"
|
||||
},
|
||||
"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": "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": ["open", "closed"],
|
||||
"default": "open",
|
||||
"example": "open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"in": "query",
|
||||
"description": "Comma separated monitor tags",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Comma separated monitor tags",
|
||||
"example": "earth,google"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "page number",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"description": "page number",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
"example": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "how many per page",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "how many per page",
|
||||
"maximum": 100,
|
||||
"example": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "created_after_utc",
|
||||
"description": "start time",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"description": "start time",
|
||||
"example": 1731866475
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "created_before_utc",
|
||||
"description": "end time",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"description": "end time",
|
||||
"example": 1731866475
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "title_like",
|
||||
"description": "title of the incident",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "title of the incident",
|
||||
"example": "outage"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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/{incidentNumber}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "incidentNumber",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "incident number as an integer to get incident by id",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"example": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"operationId": "getIncident",
|
||||
"summary": "Get an incident by number",
|
||||
"description": "API to get an incident by incident number",
|
||||
"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 number",
|
||||
"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/{incidentNumber}/comment": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "incidentNumber",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "incident number to fetch comment for",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"example": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"operationId": "addIncidentComment",
|
||||
"summary": "Add a comment to an incident",
|
||||
"description": "API to create comment for an incident",
|
||||
"tags": ["Incidents"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "body to add a comment",
|
||||
"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 comments for an incident",
|
||||
"description": "API to get comments for an incident",
|
||||
"tags": ["Incidents"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Comments 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/{incidentNumber}/status": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "incidentNumber",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "incident number to fetch status for",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"example": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"operationId": "updateIncidentStatus",
|
||||
"summary": "Update the status of an incident",
|
||||
"description": "API to update status of an incident",
|
||||
"tags": ["Incidents"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "request body to update status of an incident",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IncidentStatus"
|
||||
},
|
||||
"example": {
|
||||
"isIdentified": true,
|
||||
"isResolved": true,
|
||||
"endDatetime": 1731901920
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Incident status 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-708
@@ -1,708 +0,0 @@
|
||||
---
|
||||
info:
|
||||
title: Kener API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
# Kener Self-hosted 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://your-kener-host.com
|
||||
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'
|
||||
lastUpdatedAt:
|
||||
type: integer
|
||||
example: 1731251760
|
||||
example:
|
||||
status: UP
|
||||
lastUpdatedAt: 1731251760
|
||||
uptime: '100'
|
||||
Incident:
|
||||
type: object
|
||||
description: body of an incident
|
||||
required:
|
||||
- title
|
||||
- tags
|
||||
properties:
|
||||
startDatetime:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
example: 1731901920
|
||||
endDatetime:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
example: 1704123938
|
||||
title:
|
||||
type: string
|
||||
example: Outage in mumbai
|
||||
description: title of the incident
|
||||
body:
|
||||
type: string
|
||||
example: body of the incident
|
||||
description: body of the incident
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: earth
|
||||
description: comma separated tags of monitors
|
||||
example:
|
||||
- earth
|
||||
- google
|
||||
impact:
|
||||
type: string
|
||||
example: DOWN
|
||||
description: Impact of the incident
|
||||
enum:
|
||||
- DOWN
|
||||
- DEGRADED
|
||||
isMaintenance:
|
||||
type: boolean
|
||||
description: is this incident because of maintenance
|
||||
example: false
|
||||
isIdentified:
|
||||
type: boolean
|
||||
description: has the incident been indentified
|
||||
example: true
|
||||
isResolved:
|
||||
type: boolean
|
||||
description: has the incident been resovled
|
||||
example: true
|
||||
example:
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
IncidentResponse:
|
||||
description: Incident response schema
|
||||
allOf:
|
||||
- "$ref": "#/components/schemas/Incident"
|
||||
- type: object
|
||||
properties:
|
||||
createdAt:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds, incident created at
|
||||
example: 1731901920
|
||||
closedAt:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: UTC timestamp in seconds, incident closed at
|
||||
example: 1731901920
|
||||
incidentNumber:
|
||||
type: integer
|
||||
description: id of the incident
|
||||
example: 4
|
||||
example:
|
||||
createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
Comment:
|
||||
type: object
|
||||
description: Comment of an incident
|
||||
required:
|
||||
- body
|
||||
properties:
|
||||
body:
|
||||
type: string
|
||||
example: comment 1
|
||||
example:
|
||||
body: comment 2
|
||||
CommentResponse:
|
||||
type: object
|
||||
description: Comment Response
|
||||
properties:
|
||||
commentID:
|
||||
type: integer
|
||||
description: ID of the comment
|
||||
example: 1873376745
|
||||
body:
|
||||
type: string
|
||||
description: body of the comment
|
||||
example: comment 3
|
||||
createdAt:
|
||||
type: integer
|
||||
description: timestamp when comment was created
|
||||
example: 1704123938
|
||||
example:
|
||||
commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
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
|
||||
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:
|
||||
createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
CreateIncidentRequest:
|
||||
summary: create incident request body
|
||||
description: Some Description
|
||||
value:
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
SearchIncidentsResponse:
|
||||
summary: array of incidents
|
||||
description: Some Description
|
||||
value:
|
||||
- createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
- createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
CommentsResponse:
|
||||
summary: list of comments
|
||||
description: Some Description
|
||||
value:
|
||||
- commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
- commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
CommentRequestBody:
|
||||
summary: request body for a comment
|
||||
description: request body for a comment to add in an incident
|
||||
value:
|
||||
body: This is a comment
|
||||
CreateCommentResponse:
|
||||
summary: create comment response body sample
|
||||
description: create comment response body sample
|
||||
value:
|
||||
commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
paths:
|
||||
"/api/status":
|
||||
post:
|
||||
operationId: updateMonitorStatus
|
||||
summary: Update status of a monitor
|
||||
description: Update status of an incident at a given timestamp
|
||||
tags:
|
||||
- Monitors
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: request body to update an incident
|
||||
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 an incident
|
||||
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/Incident"
|
||||
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: 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:
|
||||
- open
|
||||
- closed
|
||||
default: open
|
||||
example: open
|
||||
- name: tags
|
||||
in: query
|
||||
description: Comma separated monitor tags
|
||||
schema:
|
||||
type: string
|
||||
description: Comma separated monitor tags
|
||||
example: earth,google
|
||||
- name: page
|
||||
in: query
|
||||
description: page number
|
||||
schema:
|
||||
type: integer
|
||||
description: page number
|
||||
default: 1
|
||||
minimum: 1
|
||||
example: 1
|
||||
- name: per_page
|
||||
in: query
|
||||
description: how many per page
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
description: how many per page
|
||||
maximum: 100
|
||||
example: 10
|
||||
- name: created_after_utc
|
||||
description: start time
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: start time
|
||||
example: 1731866475
|
||||
- name: created_before_utc
|
||||
description: end time
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: end time
|
||||
example: 1731866475
|
||||
- name: title_like
|
||||
description: title of the incident
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: title of the incident
|
||||
example: outage
|
||||
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/{incidentNumber}":
|
||||
parameters:
|
||||
- name: incidentNumber
|
||||
in: path
|
||||
required: true
|
||||
description: incident number as an integer to get incident by id
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
get:
|
||||
operationId: getIncident
|
||||
summary: Get an incident by number
|
||||
description: API to get an incident by incident number
|
||||
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 number
|
||||
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/{incidentNumber}/comment":
|
||||
parameters:
|
||||
- name: incidentNumber
|
||||
in: path
|
||||
required: true
|
||||
description: incident number to fetch comment for
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
post:
|
||||
operationId: addIncidentComment
|
||||
summary: Add a comment to an incident
|
||||
description: API to create comment for an incident
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: body to add a comment
|
||||
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 comments for an incident
|
||||
description: API to get comments for an incident
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Comments 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/{incidentNumber}/status":
|
||||
parameters:
|
||||
- name: incidentNumber
|
||||
in: path
|
||||
required: true
|
||||
description: incident number to fetch status for
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
post:
|
||||
operationId: updateIncidentStatus
|
||||
summary: Update the status of an incident
|
||||
description: API to update status of an incident
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: request body to update status of an incident
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/IncidentStatus"
|
||||
example:
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
endDatetime: 1731901920
|
||||
responses:
|
||||
'200':
|
||||
description: Incident status 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"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user