Compare commits

..

399 Commits

Author SHA1 Message Date
Raj Nandan Sharma e581346f84 refactor: Enhance documentation for page ordering functionality in customizations 2026-02-26 10:45:44 +05:30
Raj Nandan Sharma 555fd3a8a2 Refactor: Implement monitor reordering functionality with up and down buttons 2026-02-26 10:42:59 +05:30
Raj Nandan Sharma 7a29d2f2ca refactor: Implement page ordering functionality and enhance page fetching logic 2026-02-26 09:45:34 +05:30
Raj Nandan Sharma 7a3fe20083 refactor: Simplify specification path determination in GET handler 2026-02-25 23:24:13 +05:30
Raj Nandan Sharma c16c119d65 Merge pull request #608 from rajnandan1/fix/mysql-indexname-long
refactor: Update site configuration documentation and enhance global …
2026-02-25 22:54:37 +05:30
Raj Nandan Sharma 048a12899d refactor: Enhance documentation guidelines for architecture knowledge preservation 2026-02-25 22:41:06 +05:30
Raj Nandan Sharma 5fd0e691b3 refactor: Improve down migration logic for global column removal 2026-02-25 22:37:37 +05:30
Raj Nandan Sharma 28932f8df5 refactor: Update event page navigation to use dynamic page path 2026-02-25 22:33:53 +05:30
Raj Nandan Sharma 4da29ac4e1 Merge branch 'main' into fix/mysql-indexname-long 2026-02-25 19:36:31 +05:30
Raj Nandan Sharma 396fc5e3c3 refactor: Update site configuration documentation and enhance global page visibility settings
- Revise site configuration documentation to clarify page visibility behavior.
- Introduce global page visibility settings with detailed descriptions and functionality.
- Modify API server to dynamically select the correct specification path based on environment.
- Streamline event fetching logic in event pages to improve performance and maintainability.
- Remove unused vault secret management code from the manage API.
- Enhance customizations page to support global page visibility settings.
- Create new guides for adding custom fonts and custom JavaScript/CSS.
- Implement server-side logic for handling events by month with improved date validation.
2026-02-25 19:33:34 +05:30
Raj Nandan Sharma af8daf98f6 Merge pull request #602 from theoludwig/docs/typo-github
docs: typo Github => GitHub
2026-02-25 15:47:56 +05:30
Raj Nandan Sharma 2e91f90057 Merge pull request #603 from theoludwig/docs/link-404-migration-to-v4
docs: fix link to migration guide v3 => v4
2026-02-25 15:47:17 +05:30
Théo LUDWIG 3213ab8efa docs: fix link to migration guide v3 => v4 2026-02-25 10:22:36 +01:00
Théo LUDWIG f2308d9bd1 docs: typo Github => GitHub 2026-02-25 10:21:01 +01:00
github-actions[bot] 623465ff50 chore(release): bump version to 4.0.8 2026-02-24 19:17:23 +00:00
Raj Nandan Sharma 7c224b3886 refactor: restructure monitor_alerts_v2 table creation and enforce foreign key constraints 2026-02-25 00:46:30 +05:30
github-actions[bot] 166073fbd4 chore(release): bump version to 4.0.7 2026-02-24 18:47:54 +00:00
Raj Nandan Sharma 373cbd5917 refactor: update notification and incident handling to include site URL in context 2026-02-25 00:17:12 +05:30
github-actions[bot] 2c27859197 chore(release): bump version to 4.0.6 2026-02-24 18:35:29 +00:00
Raj Nandan Sharma bb5d58b675 Merge pull request #601 from rajnandan1/fix/privilage-1
ensure inactive users cannot log in and restrict actions for non-admi…
2026-02-25 00:03:08 +05:30
Raj Nandan Sharma caca29d354 ensure inactive users cannot log in and restrict actions for non-admin roles. Fixes #600 2026-02-25 00:01:49 +05:30
github-actions[bot] 35f0adb235 chore(release): bump version to 4.0.5 2026-02-24 18:18:40 +00:00
Raj Nandan Sharma 8bcff45d87 fix: #599 migration db 2026-02-24 23:46:15 +05:30
github-actions[bot] 3ad256a626 chore(release): bump version to 4.0.4 2026-02-24 17:27:46 +00:00
Raj Nandan Sharma c4b9181a0a refactor: update migration scripts to rename .js entries to .ts in knex_migrations table 2026-02-24 22:55:19 +05:30
Raj Nandan Sharma 1ba40fadf6 refactor: remove upcoming version details and clean up README content 2026-02-24 21:23:15 +05:30
Raj Nandan Sharma 20f1481a01 refactor: include Mattermost Webhook Trigger guide in documentation 2026-02-24 20:27:13 +05:30
Raj Nandan Sharma 828747876b refactor: update v4.0.0 changelog to include categories and monitor ordering management through pages 2026-02-24 20:22:19 +05:30
Raj Nandan Sharma cbe0ea683f refactor: update SKILL.md for code-architecture and tailwindcss; enhance button component in docs page; implement API spec retrieval in v4 server 2026-02-24 19:54:03 +05:30
Raj Nandan Sharma 561864c625 Fix Railway deployment link in README
Updated the Railway deployment link in the README.
2026-02-24 18:51:35 +05:30
github-actions[bot] 3abdb5c17a chore(release): bump version to 4.0.3 2026-02-24 12:54:46 +00:00
Raj Nandan Sharma f2ef19e3d8 fix release workflow 2026-02-24 18:24:06 +05:30
github-actions[bot] 78b18a59ec chore(release): bump version to 4.0.2 2026-02-24 12:27:09 +00:00
Raj Nandan Sharma c28581b51d removed docs from main build 2026-02-24 17:56:10 +05:30
github-actions[bot] f9486927de chore(release): bump version to 4.0.1 2026-02-24 12:09:00 +00:00
Raj Nandan Sharma 86645d9ea3 Merge pull request #597 from rajnandan1/chore/docs-and-locales
added llms.txt and fixed locales
2026-02-24 17:38:19 +05:30
Raj Nandan Sharma feea1d76cd added llms.txt and fixed locales 2026-02-24 17:37:55 +05:30
Raj Nandan Sharma 2e95f31d93 Merge pull request #596 from rajnandan1/chore/docs-and-locales
added llms.txt and fixed locales
2026-02-24 17:35:49 +05:30
Raj Nandan Sharma 18cf8f51b7 added llms.txt and fixed locales 2026-02-24 17:33:22 +05:30
Raj Nandan Sharma 9af842ebc0 fix docker file 2026-02-24 13:30:30 +05:30
Raj Nandan Sharma 3d157f1c20 Merge pull request #595 from rajnandan1/chore/docs-4
docs: Include changelog for v4.0.0 release with breaking changes and …
2026-02-24 11:55:47 +05:30
Raj Nandan Sharma b970c5bba1 chore: Enhance Docker and documentation for subpath deployment support 2026-02-24 11:20:49 +05:30
Raj Nandan Sharma 7e7c0eb429 chore: Update documentation layout and enhance API reference with new metadata and server URL variable 2026-02-24 10:07:12 +05:30
Raj Nandan Sharma f0ab8f25e7 docs: Include changelog for v4.0.0 release with breaking changes and new features 2026-02-23 23:14:43 +05:30
Raj Nandan Sharma 87896ce2a9 docs: Update links in README for Quick Start and Documentation sections 2026-02-23 12:33:21 +05:30
Raj Nandan Sharma fae9f38d90 style: Adjust margin for notification button in NotificationsPopover component 2026-02-23 12:13:14 +05:30
Raj Nandan Sharma 1df62bc315 Merge pull request #588 from rajnandan1/next/4-pre-alpha
Next/4 pre alpha
2026-02-23 11:25:32 +05:30
Raj Nandan Sharma 9199b807d5 chore: Update environment variables and documentation for ORIGIN requirement; refactor Docker setup for documentation indexing 2026-02-23 11:15:19 +05:30
Raj Nandan Sharma 3363e90007 chore(locales): Update translations for multiple languages and add new status messages
feat(links): Correct URL formatting in invitation and verification email links
refactor(notification): Simplify notification utility imports
chore(docs): Update Discord link and API reference URLs in documentation
style(buttons): Change button variant for better UI consistency
chore(scripts): Implement script to sort translation keys in locale files
2026-02-23 09:57:22 +05:30
Raj Nandan Sharma 78bad84f0e refactor: update workflow to publish nightly Docker images with improved tagging and concurrency 2026-02-22 23:00:00 +05:30
Raj Nandan Sharma 50482aa21d changes 2026-02-22 22:55:54 +05:30
Raj Nandan Sharma 339d653bab delete static files 2026-02-22 20:24:01 +05:30
Raj Nandan Sharma 585db91263 refactor: enhance layout and site data handling with social preview image and custom CSS support 2026-02-22 19:53:19 +05:30
Raj Nandan Sharma cbc48d2ddc docs updates 2026-02-22 15:55:49 +05:30
Raj Nandan Sharma 1c6bed1297 changes 2026-02-21 20:11:44 +05:30
Raj Nandan Sharma bf126e7151 refactor: implement event display settings for incidents and maintenances in dashboard 2026-02-21 19:56:32 +05:30
Raj Nandan Sharma e5e244e2dc refactor: enhance MonitorBar component with layout options and improve dashboard data handling 2026-02-21 15:40:28 +05:30
Raj Nandan Sharma 4b23e729c1 changes 2026-02-20 22:57:34 +05:30
Raj Nandan Sharma c9c675da28 changes 2026-02-20 18:40:28 +05:30
Raj Nandan Sharma 1c485ac376 go live 2026-02-20 11:34:17 +05:30
Raj Nandan Sharma 9a8bf8431b changes 2026-02-17 18:19:20 +05:30
Raj Nandan Sharma 3aedb9be11 refactor: restructure authentication flow and remove obsolete API endpoints 2026-02-17 09:09:48 +05:30
Raj Nandan Sharma 58f0e9f98a refactor: remove copy current page link option and implement sharing options for monitors 2026-02-17 08:21:11 +05:30
Raj Nandan Sharma 88072b7de7 changes 2026-02-16 22:40:46 +05:30
Raj Nandan Sharma 9129e9606c changes 2026-02-16 18:26:42 +05:30
Raj Nandan Sharma b088a79143 refactor: update logging in analyticsEvent function and improve latency display in embed page 2026-02-16 11:51:32 +05:30
Raj Nandan Sharma 14d46cb3de Merge main into next/4-pre-alpha (keep overhaul changes) 2026-02-16 11:16:41 +05:30
Raj Nandan Sharma 5989e5b8cb refactor: improve error handling and logging in GamedigCall, SqlCall, and SSLCall classes; update documentation links and restructure introduction content 2026-02-16 10:22:26 +05:30
Raj Nandan Sharma 7b3afa4467 so many 2026-02-16 08:30:58 +05:30
Raj Nandan Sharma d758de0752 refactor: enhance image upload ID generation with file extension 2026-02-15 13:40:06 +05:30
Raj Nandan Sharma e6424b1d66 refactor: integrate @humanspeak/svelte-purify for enhanced HTML sanitization and update global constants for upload limits 2026-02-15 13:08:26 +05:30
Raj Nandan Sharma 5a95c81574 refactor: restrict role updates in user data modification 2026-02-14 23:11:30 +05:30
Raj Nandan Sharma d170ba69b7 refactor: enhance documentation and improve page titles with site name 2026-02-14 20:13:10 +05:30
Raj Nandan Sharma d04ee0a933 changes 2026-02-14 19:08:24 +05:30
Raj Nandan Sharma 444fe5890a ui revamp 2026-02-13 22:23:07 +05:30
Raj Nandan Sharma d1eb9266ea changes 2026-02-13 17:45:59 +05:30
Raj Nandan Sharma 275c9d7ff4 changes 2026-02-13 16:48:47 +05:30
Raj Nandan Sharma 552bdc09ad refactor: enhance ping and tcp monitoring logic, improve error handling, and introduce shared types 2026-02-13 12:02:13 +05:30
Raj Nandan Sharma 09afbbd17d refactor: optimize npm installation step by removing cache mount 2026-02-12 23:58:48 +05:30
Raj Nandan Sharma 6ea045d152 refactor: implement site announcement feature with management UI and display component 2026-02-12 21:59:41 +05:30
Raj Nandan Sharma 8fd8aa4bcf refactor: implement CloneMonitor functionality and enhance monitor loading with status filtering 2026-02-12 20:11:18 +05:30
Raj Nandan Sharma 366c2c0a22 refactor: update heartbeat handling and monitoring logic, enhance UI components, and improve documentation 2026-02-12 16:19:00 +05:30
Raj Nandan Sharma 21f06a4143 hb 2026-02-11 23:04:28 +05:30
Raj Crustdata d99da146bd sas 2026-02-11 19:33:42 +05:30
Raj Nandan Sharma 290113d553 refactor: update layout and styling for MinuteGrid and MonitorDayDetail components 2026-02-11 13:44:08 +05:30
Raj Nandan Sharma 1f370b3eea changes 2026-02-11 13:07:52 +05:30
Raj Nandan Sharma 17ed56c682 changes 2026-02-10 23:17:16 +05:30
Raj Nandan Sharma 2377a4faa6 changes 2026-02-10 18:17:52 +05:30
Raj Nandan Sharma 32081056c7 track: implement event tracking for user interactions across multiple components 2026-02-10 11:43:24 +05:30
Raj Nandan Sharma 5f86ca27e1 analystic 101 2026-02-10 11:09:29 +05:30
Raj Nandan Sharma c58530cda3 cleanup 1 2026-02-10 08:51:49 +05:30
Raj Nandan Sharma 9b99bec50b Create Dockerfile and Docker Compose configurations for Kener v4 application
- Implement multi-stage Dockerfile for building and running the Kener application with support for Alpine and Debian variants.
- Establish development and production Docker Compose files for local testing and deployment.
- Configure Redis service for caching and job scheduling.
- Set up environment variables for application configuration, including secret keys and database connections.
- Define health checks for Redis service to ensure reliability.
2026-02-10 08:36:28 +05:30
Raj Nandan Sharma 8b189b686e Merge pull request #556 from danynocz/main
Add Slovak language support
2026-02-10 08:17:49 +05:30
Raj Nandan Sharma 430733bd41 Merge pull request #576 from vyPal/feature/umami-custom-src
Umami custom script source
2026-02-10 08:17:25 +05:30
vyPal 6699887a75 Add support for custom umami script source URLs 2026-02-09 21:21:45 +01:00
Raj Nandan Sharma 8a498e902d refactor: update tooltip styles and improve component structure across multiple files 2026-02-09 22:30:11 +05:30
Raj Nandan Sharma eed71d3633 added group 2026-02-09 17:35:18 +05:30
Raj Nandan Sharma 228808a267 stable 2026-02-09 14:43:35 +05:30
Raj Nandan Sharma ee1adf8330 vv 2026-02-09 12:08:12 +05:30
Raj Nandan Sharma 9e8b0962a0 changes 2026-02-09 08:26:16 +05:30
Raj Nandan Sharma 72a28cac61 changes 2026-02-08 19:18:49 +05:30
Raj Nandan Sharma c759ade7b9 file clean up 2026-02-08 19:17:02 +05:30
Raj Nandan Sharma 7392b968ef cvcc 2026-02-08 14:34:53 +05:30
Raj Nandan Sharma 7f328023ea Merge pull request #575 from rajnandan1/claude/create-documentation-skill-file 2026-02-07 22:51:21 +05:30
anthropic-code-agent[bot] 4ff1604044 feat: Add documentation-writer Claude skill
Add comprehensive skill file for creating and editing high-quality Kener documentation with guidelines for:
- Documentation structure and organization
- Custom heading anchors for deep linking
- Markdown features and formatting
- Quality guidelines and best practices
- Avoiding content duplication with references
- Complete workflow and checklist

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Co-authored-by: rajnandan1 <16224367+rajnandan1@users.noreply.github.com>
2026-02-07 17:08:08 +00:00
anthropic-code-agent[bot] be0745f2f7 Initial plan 2026-02-07 17:06:12 +00:00
Raj Nandan Sharma cdc0de9405 refactor: Update SMTP configuration handling and improve email setup documentation 2026-02-07 22:26:52 +05:30
Raj Nandan Sharma 47c00c2837 refactor: Replace IsLoggedInSession with GetLoggedInSession and update related logic across components 2026-02-07 20:51:40 +05:30
Raj Nandan Sharma baa9449b8c refactor: Update components for improved styling and theme handling 2026-02-07 17:03:23 +05:30
Raj Nandan Sharma 1c05e2d7da style: Update LanguageSelector and ThemePlus components for improved styling
refactor: Modify alertToVariables function to streamline alert data handling

refactor: Change alert_id and alert_incident_id types in types.ts for consistency

refactor: Simplify alert notification templates to include incident URL and update variable types

docs: Update alerting templates documentation to reflect new variables and structure

refactor: Enhance webhook alert template to accommodate new incident URL handling

refactor: Adjust server API to utilize the latest monitor tag for testing alerts
2026-02-07 12:43:40 +05:30
Raj Nandan Sharma a20125a6ed changes 2026-02-06 23:15:34 +05:30
Raj Nandan Sharma 845132fc58 base path adding 2026-02-06 18:43:11 +05:30
Raj Nandan Sharma 038c52ad7d base path adding 2026-02-06 17:43:33 +05:30
Raj Nandan Sharma 6aa43fa261 Implement URL resolver functions for client and server, update navigation links to use resolved URLs, and remove deprecated nav component 2026-02-06 11:41:02 +05:30
Raj Nandan Sharma 6ed95ed8dc Implement user invitation system with password setup and validation
- Create UserRecordDashboard interface to include password status.
- Update password reset API to validate password strength and set user as verified.
- Refactor invitation handling in the manage route to streamline user management.
- Introduce invitation acceptance flow with password creation and validation.
- Create email template for user invitations.
- Implement invitation verification logic to ensure token validity and user existence.
- Enhance user management UI to support invitation resending and account activation.
2026-02-06 10:24:43 +05:30
Raj Nandan Sharma ba41c6f2a7 Implement password reset functionality with email template and API endpoints 2026-02-05 20:43:14 +05:30
Raj Nandan Sharma 96fd8d0874 Create maintenance documentation for impact on monitoring, overview, and RRULE patterns 2026-02-05 18:22:53 +05:30
Raj Nandan Sharma 9a0ca6cde4 Create alert configuration and logs pages with functionality for managing alerts 2026-02-05 17:07:28 +05:30
Raj Nandan Sharma 2d7d6a3d77 fixes 2026-02-05 15:00:22 +05:30
Raj Nandan Sharma 08f72e5aeb Enhance monitoring functionality with new uptime calculations and alert conditions 2026-02-05 13:27:36 +05:30
Raj Nandan Sharma 52c76fc664 clean up 2026-02-05 10:39:52 +05:30
Raj Nandan Sharma 4d0e1357a6 changes 2026-02-04 18:47:17 +05:30
Raj Nandan Sharma e92c8a0975 added docs might get modified for api monitors 2026-02-04 14:19:11 +05:30
Raj Nandan Sharma 331cd5b59a Refactor documentation structure and enhance monitor features with nested pages and custom evaluations 2026-02-04 11:58:27 +05:30
Raj Nandan Sharma 601186dcc0 good docs 2026-02-03 18:12:18 +05:30
Raj Nandan Sharma 3295b7b081 docs 2026-02-03 12:47:28 +05:30
Raj Nandan Sharma 34605fe3b0 Remove email customization feature and related API endpoints from the management interface. 2026-02-03 09:09:42 +05:30
Raj Nandan Sharma 045ce12c8b Implement monitor management components including DangerZoneCard, GeneralSettingsCard, ModifyDataCard, MonitorTypeCard, PageVisibilityCard, UptimeSettingsCard for enhanced monitoring capabilities. 2026-02-02 23:09:33 +05:30
Raj Nandan Sharma 98c723cfa5 changes 2026-02-02 19:10:14 +05:30
Raj Nandan Sharma 24872d2dc5 changes 2026-02-02 19:02:12 +05:30
Raj Nandan Sharma 00511da24c changes 2026-02-02 19:01:44 +05:30
Raj Nandan Sharma d851610432 Implement theme configuration and user preferences for status page 2026-02-02 11:32:39 +05:30
Raj Nandan Sharma 6c15d48b58 new api spec 2026-02-02 10:28:55 +05:30
Raj Nandan Sharma 8bd26cca92 delete trigger 2026-02-02 09:20:23 +05:30
Raj Nandan Sharma 31655679e6 added few apis 2026-02-02 08:56:34 +05:30
Raj Nandan Sharma accdd7698c changes 2026-01-31 22:20:16 +05:30
Raj Nandan Sharma 8f187aa917 Implement sub-menu options configuration and localization updates 2026-01-31 14:35:48 +05:30
Raj Nandan Sharma c77985fa92 i18n 2026-01-30 23:24:35 +05:30
_DANYNO_ 2749b8017c Add Slovak language support 2026-01-30 17:01:41 +01:00
_DANYNO_ c4aff9f81b Add Slovak language support 2026-01-30 17:00:12 +01:00
_DANYNO_ c4739e0257 Add Slovak language support 2026-01-30 16:59:08 +01:00
Raj Nandan Sharma 93f91ab332 changes 2026-01-30 19:38:28 +05:30
Raj Nandan Sharma 8a0e1e7a61 changes 2026-01-30 11:18:39 +05:30
Raj Nandan Sharma 463d1c8ddf changes 2026-01-30 10:57:07 +05:30
Raj Nandan Sharma 9c8d4bcb89 Merge pull request #551 from danynocz/main
Update Czech translation
2026-01-30 10:56:06 +05:30
_DANYNO_ d300f3cdae Update Czech translation 2026-01-29 15:19:48 +01:00
_DANYNO_ b14a5c9f5f Update Czech translation 2026-01-29 15:18:05 +01:00
Raj Nandan Sharma 2abb266e3b changes 2026-01-29 18:15:53 +05:30
Raj Nandan Sharma e714fe8fe7 changes 2026-01-29 11:36:26 +05:30
Raj Nandan Sharma b7023b7b1f changes 2026-01-29 10:29:49 +05:30
Raj Nandan Sharma 3936edb017 Merge pull request #550 from DANYNOCZ/main
Add Czech language support
2026-01-29 09:07:03 +05:30
_DANYNO_ 71a9157abf Add Czech language support 2026-01-28 22:43:40 +01:00
_DANYNO_ 3b571f404c Add Czech language support 2026-01-28 22:41:04 +01:00
_DANYNO_ 6ff6e595a6 Add Czech language support 2026-01-28 22:40:04 +01:00
Raj Nandan Sharma 28bfd8cd17 refactor: simplify SubscribeMenu props and integrate subscription settings in layout 2026-01-28 22:54:01 +05:30
Raj Nandan Sharma af829fa73e changes 2026-01-28 22:46:11 +05:30
Raj Nandan Sharma d0d8e60a8f changes 2026-01-27 11:40:57 +05:30
Raj Nandan Sharma c4f094572d subs v1 2026-01-26 20:14:28 +05:30
Raj Nandan Sharma b4aeb5134b Update seed function to insert all alert templates when no templates exist 2026-01-26 13:41:01 +05:30
Raj Nandan Sharma 4608b03659 alerting new set up done 2026-01-26 12:46:25 +05:30
Raj Nandan Sharma b51fc19671 changes 2026-01-24 22:50:17 +05:30
Raj Nandan Sharma 2dce9814ac changes 2026-01-24 10:52:19 +05:30
Raj Nandan Sharma 9f2ab70ded changes 2026-01-23 16:22:24 +05:30
Raj Nandan Sharma bc7c23c1f0 Update issue templates 2026-01-23 14:44:41 +05:30
Raj Nandan Sharma d6b68e3638 Refactor monitoring components and enhance latency handling; update UI labels and improve data retrieval logic 2026-01-23 10:07:17 +05:30
Raj Nandan Sharma 07091110f5 Refactor monitoring data retrieval and caching mechanisms; enhance monitor filtering options 2026-01-22 22:23:49 +05:30
Raj Nandan Sharma 6b88cf11a1 changes 2026-01-22 18:29:42 +05:30
Raj Nandan Sharma 0149b3e61a first commit for version 4, very unstable 2026-01-22 11:19:13 +05:30
Raj Nandan Sharma 0d31187ab4 clean repo for overhaul 2026-01-22 09:52:23 +05:30
Raj Nandan Sharma 9187fdd399 Merge pull request #535 from Aj7Ay/main
fix: API Reference page not loading (#532)
2026-01-18 18:20:28 +05:30
Aj7Ay d5dba78c7c Merge pull request #2 from Aj7Ay/fix/api-reference-page
fix: API Reference page not loading (#532)
2026-01-09 12:23:39 +05:30
aj7ay bb8e6229d3 fix: API Reference page not loading (#532)
The API reference page was blank because spec.content expected a parsed
JSON object but received a JSON string. Added JSON.parse() to properly
parse the OpenAPI spec before passing it to @scalar/express-api-reference.

Fixes #532
2026-01-09 12:20:59 +05:30
Raj Nandan Sharma 5dea324626 Merge pull request #531 from Aj7Ay/main
fix: improve API call connection handling to prevent downtime
2026-01-09 08:58:08 +05:30
Raj Nandan Sharma 0a616c1c94 Merge pull request #534 from ramsaysewell/feature/graph-hover-flashes
feat: changes timestamp box to use border
2026-01-09 08:57:19 +05:30
Ramsay Sewell a188696a8e feat: changes timestamp box to use border
grid of boxes currently uses margin but this interrupts hover experience. using border ensures hover stays active without flashing
2026-01-07 22:42:36 +00:00
Aj7Ay c69d9fdf4e Merge pull request #1 from Aj7Ay/fix/improve-api-call-connection-handling
fix: improve API call connection handling to prevent downtime
2026-01-05 18:11:06 +05:30
ajay 83e807ac24 fix: improve API call connection handling to prevent downtime
- Add HTTPS agent with connection pooling (keepAlive, maxSockets)
- Improve timeout detection to catch ECONNABORTED errors
- Add better axios configuration (maxRedirects, validateStatus)
- Enhance error handling for more reliable monitoring

This prevents false downtime reports by:
1. Reusing connections to reduce overhead
2. Better timeout detection triggering retry mechanism
3. Connection pooling for improved resilience during traffic spikes
2026-01-05 18:09:42 +05:30
Raj Nandan Sharma 5dea28e6a6 Update README with upcoming version details
Added details about the upcoming version 4.0.0 and its updates.
2025-12-23 09:07:04 +05:30
Raj Nandan Sharma a1bba1e69c Merge pull request #503 from henb/main
Feat: improve date handling and add navigation controls
2025-10-17 20:31:06 +05:30
Vadim Henb b056266adf feat: improve date handling and add navigation controls
- Set Russian as default locale in seedSiteData and i18n server configuration
- Replace MMMM with LLLL format for proper Russian month declension (nominative case)
- Update all date format strings across incident pages and controllers
- Add date range validation (2023 to current+12 months) with 404 error handling
- Implement conditional navigation buttons that hide at date boundaries
- Fix Russian month names display (октябрь instead of октября in titles)
- Add MIN_YEAR constant to prevent navigation before system launch
- Improve TypeScript compatibility and fix linter errors

This resolves Russian grammatical issues and prevents access to invalid date ranges
while maintaining proper localization and user experience.
2025-10-15 20:11:36 +03:00
github-actions 249c9e8e4f Auto-generate README.md with release versions 2025-08-06 02:26:47 +00:00
Raj Nandan Sharma ebc30b674f Merge pull request #476 from rajnandan1/fix/475
refactor(users): change plain password input type to password for sec…
2025-08-06 07:31:22 +05:30
Raj Nandan Sharma bf079798d3 refactor(users): change plain password input type to password for security #475 2025-08-06 07:30:49 +05:30
Raj Nandan Sharma 480cf64738 Merge pull request #474 from rajnandan1/fix/429
refactor(docker): enhance build process and organize template files f…
2025-08-05 22:37:47 +05:30
Raj Nandan Sharma 65f039b7bd refactor(docker): enhance build process and organize template files fixes #429 2025-08-05 22:36:31 +05:30
Raj Nandan Sharma 5c7b62e9f7 Merge pull request #451 from moaazbhnas228/fix/heartbeat-grace-period
Fix: Heartbeat monitor respects cron-based grace periods
2025-08-05 22:21:37 +05:30
Raj Nandan Sharma 5e6e891ea6 Merge pull request #473 from rajnandan1/fix/441
Fix/441
2025-08-05 22:00:06 +05:30
Raj Nandan Sharma 82c904608b refactor(subscriptions): simplify condition for subscription trigger ID check 2025-08-05 21:59:22 +05:30
Raj Nandan Sharma f5260ceb86 chore(package): update version to 3.2.19 2025-08-05 21:18:32 +05:30
Raj Nandan Sharma abb32fdd6f fix(subscriptions): improve status toggle handling
Refactors the subscription status toggle to use a dedicated handler, ensuring correct behavior when toggling status for both new and existing triggers. Enhances user experience by automatically saving and reloading data when needed.
2025-08-05 21:17:09 +05:30
Raj Nandan Sharma 97aef5c5f5 Merge pull request #461 from antonio-sessa/fix/issue_456
fix: enhance monitor rearrangement with scrollable container
2025-08-05 11:06:47 +05:30
Raj Nandan Sharma c3eb8ae7f4 Merge pull request #467 from Baw-Appie/patch-1
Update Korean translations for clarity and consistency
2025-08-05 11:06:15 +05:30
Raj Nandan Sharma 72f74f9164 Merge pull request #469 from faridvatani/feature/persian-support
feat(i18n): add Persian language support
2025-08-05 11:05:50 +05:30
Farid Vatani 958676e5a7 feat(i18n): add Persian language support 2025-07-21 14:27:42 +03:30
JiHun Oh 51134670e5 Update Korean translations for clarity and consistency 2025-07-19 11:02:39 +00:00
JiHun Oh ecea2b220d Update ko.json 2025-07-19 19:51:27 +09:00
antonio-sessa 212f4d5e7a fix: enhance monitor rearrangement with scrollable container 2025-07-14 09:30:41 +02:00
Raj Nandan Sharma 2edb09bd13 Merge pull request #455 from xgenos/fix-issue-332
fixed the issue#332 with more optimized ORIGIN env calls
2025-07-11 10:10:07 +05:30
Biswa Kalyan Bhuyan 6b9f073cb7 fix: fixed the issue#332 with more optimized ORIGIN env calls 2025-07-02 19:15:52 +05:30
Moaaz Bhnas 9f933a0f1c Fixed returned status/type on heartbeat error 2025-07-02 09:21:58 +03:00
Moaaz Bhnas 8a6f721bb4 previousRun() 2025-07-01 09:03:50 +03:00
Moaaz Bhnas eeadcca6ed Used croner instead of cron-parser 2025-07-01 08:59:04 +03:00
Moaaz Bhnas 8e89c10906 Small change 2025-07-01 08:53:46 +03:00
Moaaz Bhnas 833d16351c FIX: heartbeat latency 2025-07-01 08:49:32 +03:00
Raj Nandan Sharma 0c10552fa7 Merge pull request #448 from federico-it/main
feat(i18n): add Italian language support
2025-06-30 21:30:05 +05:30
Raj Nandan Sharma 443afe2f6e Merge pull request #447 from sendmiche/patch-1
The translation has been made more suitable for the Russian language.
2025-06-30 21:29:33 +05:30
Federico db48d98da7 feat(i18n): add Italian language support 2025-06-20 17:44:28 +02:00
sendmiche bf3150434a The translation has been made more suitable for the Russian language. 2025-06-20 12:30:59 +03:00
github-actions 71c4bd26d2 Auto-generate README.md with release versions 2025-06-12 04:47:55 +00:00
Raj Nandan Sharma ca4f7040e8 refactor(themeInfo): remove maintenance status migration and update default value handling 2025-06-12 09:54:03 +05:30
Raj Nandan Sharma 1cfa861d7e Merge pull request #431 from rajnandan1/release/18
Release/18
2025-06-12 09:21:40 +05:30
Raj Nandan Sharma 007baefe80 Merge pull request #426 from ToxykAuBleu/feat/scheduled-downtime
Add maintenance visibility to monitor' status
2025-06-02 09:31:36 +05:30
Raj Nandan Sharma 7d77f4d996 chore: update version to 3.2.18 2025-05-28 10:01:15 +05:30
Raj Nandan Sharma 3d5a62c085 feat(subscription): add status toggle and restrict config editing #418
Adds support for toggling the subscription trigger's active/inactive status from the UI and updates backend/controller logic to allow dynamic status changes. Restricts editing of subscription event configuration and user subscriptions to when the trigger is active, improving admin control and preventing unintended changes when subscriptions are inactive.
2025-05-28 10:00:32 +05:30
Raj Nandan Sharma 24ffef997b feat(docker): copy server templates to build output #429 2025-05-28 08:52:37 +05:30
ToxykAuBleu 7d5275152c Fixing responsiveness of "Availability per Component", above monitors. 2025-05-26 12:08:03 +00:00
ToxykAuBleu 93167c2358 Reverting commit about recurring maintenance (forgotten). 2025-05-26 11:22:30 +00:00
ToxykAuBleu 734d3ad280 Adding translations for all translations. 2025-05-26 09:57:59 +00:00
ToxykAuBleu 753668dfa5 Adding documentation related to new maintenance status. 2025-05-26 06:58:03 +00:00
ToxykAuBleu 351817338d Reverting commits about recurring maintenance (moved to new branch). 2025-05-26 06:47:17 +00:00
ToxykAuBleu 7c12218489 Changing default color of maintenance' status. 2025-05-22 06:50:24 +00:00
ToxykAuBleu 7c71298227 Adding maintenance calculation & message to system data. 2025-05-22 06:41:10 +00:00
ToxykAuBleu 167db6d7e6 Adding visibility of this new status to Home page.
Adding MAINTENANCE to affected monitors (Events page).
2025-05-21 07:08:35 +00:00
Raj Nandan Sharma 74834f3559 Merge pull request #417 from chunchet-ng/main
fix: remove VOLUME declarations to prevent unintended anonymous volumes
2025-05-21 09:24:23 +05:30
ToxykAuBleu e13ca5c639 Adding visibility of this new type of maintenance into Events page. 2025-05-15 12:08:29 +00:00
Chun Chet a51806d62b fix: remove VOLUME declarations to prevent unintended anonymous volumes 2025-05-15 11:18:23 +08:00
ToxykAuBleu 57238dfb72 Adding input validation for maintenance duration.
Fixing some labels + flex layout.
2025-05-13 07:02:42 +00:00
ToxykAuBleu a2be642af0 Updating controller to accept new input data from Incident creation page.
Updating last migration to create new columns.
2025-05-13 07:01:14 +00:00
Raj Nandan Sharma aec2d23c2f Merge pull request #416 from OzkrOssa/feature/spanish-support
feat(i18n): add Spanish language support
2025-05-13 09:56:31 +05:30
Oscar Ossa 11a11162da feat(i18n): add Spanish language support 2025-05-12 09:48:06 -05:00
ToxykAuBleu 12a7be497e Editing incident creation page to include maintenance strategy option.
Removing unused properties inside newIncident.
2025-05-12 08:25:26 +00:00
github-actions 9b69da66a5 Auto-generate README.md with release versions 2025-05-06 17:27:39 +00:00
Raj Nandan Sharma ee831f96bb Merge pull request #413 from rajnandan1/css-fix-1
chore: update version to 3.2.17 and adjust layout properties in monit…
2025-05-06 22:40:06 +05:30
Raj Nandan Sharma 5542790145 chore: update version to 3.2.17 and adjust layout properties in monitor component 2025-05-06 22:39:09 +05:30
github-actions 3b1091a34f Auto-generate README.md with release versions 2025-05-06 01:54:32 +00:00
Raj Nandan Sharma eebe6541c3 Merge pull request #408 from ToxykAuBleu/fix/gamedig-call
Fixing missing call of any gamedig monitors.
2025-05-06 07:05:59 +05:30
Raj Nandan Sharma e308089cfa refactor(triggerInfo): remove timeout for test loaders and improve image tag formatting 2025-05-06 07:05:04 +05:30
Raj Nandan Sharma 5132d6894e Merge pull request #407 from ToxykAuBleu/fix/compiler-warnings
Fixing compiler warnings.
2025-05-06 07:02:19 +05:30
Raj Nandan Sharma 006d1e1fa0 Merge pull request #411 from rajnandan1/fix/ui-cycle
fix(notifications): improve error handling and UI feedback
2025-05-06 07:01:05 +05:30
Raj Nandan Sharma 4c6dc24355 fix(notifications): improve error handling and UI feedback
Returns structured error messages from notification providers, updates API logic to surface errors to users, and enhances UI to display error or success states during trigger tests. Also adjusts monitor API request payload and corrects data range calculation.

Relates to improved reliability and user experience.
2025-05-06 06:57:12 +05:30
ToxykAuBleu acf227a947 Fixing missing call of any gamedig monitors. 2025-05-05 09:57:39 +00:00
ToxykAuBleu 3b05ca73e5 Adding validation to this new status.
Adding migration + seed for this new status.
2025-05-05 09:05:16 +00:00
ToxykAuBleu 236d7ea585 Adding new maintenance status to theme page. 2025-05-05 09:04:37 +00:00
ToxykAuBleu 87408d22c4 Merge branch 'main' into fix/compiler-warnings 2025-05-05 06:54:14 +00:00
Raj Nandan Sharma ea6754775d feat: update localization files with new duration options for multiple languages 2025-05-05 11:16:38 +05:30
Raj Nandan Sharma 3210f4d406 Merge pull request #406 from rajnandan1/cycle-changer
feat: add configurable home data range per device
2025-05-05 10:50:15 +05:30
Raj Nandan Sharma 16ac97a9c9 feat: document hidden categories and configurable data range in changelogs 2025-05-05 10:48:08 +05:30
Raj Nandan Sharma 8857a51dde chore: update version to 3.2.16 in package.json 2025-05-05 10:44:23 +05:30
Raj Nandan Sharma fc2abe7f16 refactor: remove commented code and clean up dropdown labels in homePage and monitor components 2025-05-05 10:31:15 +05:30
Raj Nandan Sharma f04a930e13 feat: add configurable home data range per device
Introduces per-device configuration for maximum data range and selectable days on the homepage, allowing separate settings for desktop and mobile. Updates UI, server logic, and data model to support these options, and uses user agent detection to apply the correct configuration. Improves flexibility for data display across devices.

Relates to #105
2025-05-05 10:28:02 +05:30
Raj Nandan Sharma 1de1e2d6ba Merge pull request #405 from rajnandan1/hide-cate
refactor: rename visibility property to isHidden and update related l…
2025-05-04 12:47:32 +05:30
Raj Nandan Sharma 0208e4baf9 refactor: rename visibility property to isHidden and update related logic #321 2025-05-04 12:46:11 +05:30
Raj Nandan Sharma 21af2a6d02 Merge pull request #399 from rohith1222004/feature/monitorVisibility
feat: implement visibility control for monitors
2025-05-04 12:11:37 +05:30
github-actions 2fbe301a51 Auto-generate README.md with release versions 2025-04-30 17:53:33 +00:00
Raj Nandan Sharma 6d5382982c Merge pull request #402 from rajnandan1/fix0embed
feat(embed): add timezone support to embed parameters
2025-04-30 23:03:05 +05:30
Raj Nandan Sharma a20b4ee570 feat(embed): add timezone support to embed parameters
Introduces a 'tz' parameter for specifying timezone in embed scripts and iframe URLs, improving localization for embedded monitors.
Updates documentation and refactors parameter handling to prioritize explicit timezone over browser-detected values.

Relates to improved internationalization and user experience.
2025-04-30 23:02:10 +05:30
Raj Nandan Sharma 909ff9e070 Merge pull request #330 from Aj7ay7/main
docker/fix-compose-configuration
2025-04-30 19:58:05 +05:30
github-actions 7d3e265ece Auto-generate README.md with release versions 2025-04-30 14:18:34 +00:00
Raj Nandan Sharma 78d8f311c0 Merge pull request #400 from ToxykAuBleu/fix/398-docker-not-starting 2025-04-30 17:50:14 +05:30
ToxykAuBleu c52ba95fd0 Removing bad import of "@sveltejs/kit" in gamedigCall.js. 2025-04-30 11:37:43 +00:00
Rohith Kumar 9462a0331a feat: implement visibility control for monitors
- Private monitors are visible only to logged-in users
- Public monitors are accessible to all
2025-04-30 15:46:08 +05:30
ToxykAuBleu 4e3d1838c0 Fixing compiler warnings. 2025-04-30 06:43:21 +00:00
Raj Nandan Sharma aad7b5c6ea chore: update Dockerfile to use latest package versions for alpine build 2025-04-30 07:37:47 +05:30
Raj Nandan Sharma abec24d65b Merge pull request #397 from ToxykAuBleu/fix/396-gamedig-install
Updating Gamedig to 5.3.0 + fix build app.
2025-04-30 07:32:50 +05:30
Raj Nandan Sharma e0c3d1864c chore: remove specific version for iputils in Dockerfile 2025-04-30 07:14:54 +05:30
ToxykAuBleu bd25c6fe05 Updating Gamedig to 5.3.0.
Moving Gamedig to dependencies (not devDependencies).
2025-04-29 20:34:13 +00:00
Raj Nandan Sharma 4ec5e14431 chore: update changelog for version 3.2.14 with new features and bug fixes 2025-04-29 22:58:09 +05:30
Raj Nandan Sharma c9b8c5bec3 chore: update changelogs for version 3.2.14 with new features and bug fixes 2025-04-29 22:50:28 +05:30
Raj Nandan Sharma 223883adf7 Merge pull request #385 from rajnandan1/event-subscription
Event subscription
2025-04-29 22:21:22 +05:30
Raj Nandan Sharma 83c2d7ff79 fix: improve subscription UX and email code handling
Enhances subscription menu by showing a default icon when no image is available.
Replaces hardcoded verification code in email template with a variable.
Prevents background scrolling when subscription menu is open.
Removes redundant new subscriber response in API.
Removes unused documentation button from subscriptions page.
Adjusts checkbox styling for better UI consistency.
2025-04-29 22:07:39 +05:30
Raj Nandan Sharma cf33f5a2f6 refactor: improve downtime duration calculation in closure comment #387 2025-04-29 21:20:04 +05:30
Raj Nandan Sharma 92b4596924 refactor: centralize monitoring data insertion logic
Introduces a unified function for monitoring data insertion,
adds support for group monitor status updates, and ensures
consistency by routing all monitoring data writes through the
new logic. Removes duplicated validation and streamlines
group monitor handling for improved reliability.

Relates to #123
2025-04-29 20:45:14 +05:30
Raj Nandan Sharma ce9b748570 style: adjust height of checkbox and refine status color banner with hover effects 2025-04-28 22:57:51 +05:30
Raj Nandan Sharma 87a9069ab4 feat: add configurable site status banner with i18n
Introduces an option to enable a site status banner summarizing the operational state of monitored systems. Calculates and displays aggregated status with a progress bar. Localizes banner messages in all supported languages.

Helps users quickly assess overall system health from the homepage.
2025-04-28 22:18:15 +05:30
Raj Nandan Sharma 3e6a76c8c3 Refactor monitoring data update logic to include type parameter and optimize database insertion with batching 2025-04-28 10:48:33 +05:30
Raj Nandan Sharma 3509955374 Implement date input for monitoring data modification and enhance validation logic 2025-04-28 10:08:58 +05:30
Raj Nandan Sharma 225711f95d Merge branch 'main' into event-subscription 2025-04-27 16:10:35 +05:30
Raj Nandan Sharma 5436647cd3 Merge pull request #380 from mcorbin/feature/allow-modifying-monitors-data
Allow users to modify monitors data
2025-04-27 16:10:22 +05:30
Raj Nandan Sharma fbd9da3b33 Merge branch 'main' into feature/allow-modifying-monitors-data 2025-04-27 16:07:31 +05:30
Raj Nandan Sharma edb6705ac8 Merge branch 'main' into event-subscription 2025-04-27 15:18:47 +05:30
Raj Nandan Sharma 382a2a5bcc Merge pull request #384 from jensvandenreyt/feature/extend-monitor-api
feat: add CRUD api for monitors
2025-04-27 15:18:29 +05:30
Raj Nandan Sharma b5967607d2 Refactor game data handling and improve metadata for event pages
- Updated monitorSheet.svelte to use raw JSON data for AllGamesList, replacing the previous import method.
- Refactored game retrieval logic in monitorSheet.svelte to utilize GetGameFromId function with the new AllGamesList structure.
- Modified monitorsAdd.svelte to parse AllGamesList from raw JSON, ensuring consistent data usage across components.
- Enhanced event pages (+page.svelte) with appropriate meta tags for better SEO, including titles and descriptions.
- Added a new all-games-list.json file containing comprehensive game data for improved functionality and maintainability.
- Updated subscriptions page to clarify SMTP setup instructions for users.
2025-04-27 15:05:35 +05:30
Raj Nandan Sharma 03bbb6c3b6 Merge branch 'main' into event-subscription 2025-04-27 12:57:17 +05:30
Raj Nandan Sharma 6054be0ff1 feat: enhance subscription table with index for optimized queries 2025-04-27 12:56:31 +05:30
Raj Nandan Sharma ff7355bcc2 Merge pull request #389 from ToxykAuBleu/feat/gamedig
Adding Gamedig monitor functionality and related documentation
2025-04-27 12:55:52 +05:30
Raj Nandan Sharma 23a9124e03 feat(i18n): localizes subscription and maintenance UI
Integrates translation function into subscription and maintenance components,
localizing all user-facing strings. Expands translation resources for multiple languages
to cover newly localized phrases, improving accessibility and user experience for non-English users.
2025-04-27 12:12:10 +05:30
Raj Nandan Sharma ac41807cdd Merge pull request #392 from myned/fix-smtp-secure
Fix SMTP_SECURE environment variable always being truthy when set
2025-04-27 11:08:36 +05:30
Raj Nandan Sharma d95bb13e6e refactor: unify incident URL paths and add incident pagination
Unifies URLs for incident events by switching from singular to plural path segments and removes obsolete routes to streamline navigation. Introduces paginated incident fetching with filtering and sorting to improve performance and scalability. Updates UI components to reflect new paths and behaviors.

Relates to improved incident management and navigation.
2025-04-27 10:13:00 +05:30
Raj Nandan Sharma b9d58ba3c1 remove debug console log from +page.svelte 2025-04-26 14:01:06 +05:30
Raj Nandan Sharma a648b29f31 feat: add incident link copy button and chevron hide option
Introduces a copy-to-clipboard button for incident links with animated feedback. Adds ability to hide accordion chevrons and refactors chevron and copy button styles. Updates incident URL generation and allows querying monitors by tag.

Relates to improved UX for incident management.
2025-04-26 14:00:23 +05:30
Myned e702feb04f fix: coerce SMTP_SECURE to Number before boolean 2025-04-26 00:18:51 -05:00
Raj Nandan Sharma ce83355e58 feat: add incident event email notifications via queue
Introduces a queue-based system to send email notifications for incident events, including creation, updates, monitor changes, and comments. Fetches eligible subscribers dynamically and uses configurable templates for email delivery. Also updates the UI to improve the Preview button styling for better user experience.
2025-04-25 20:38:59 +05:30
ToxykAuBleu 683beed3b7 Refactoring port validation into a function: IsValidPort(port). 2025-04-24 07:01:37 +00:00
ToxykAuBleu c9095036eb Fixing auto formatting done in #e63992a. 2025-04-24 06:36:36 +00:00
Jens Vandenreyt 29ec935d67 feat: update json OpenAPI spec
Also improve 400 and 404 error messages
2025-04-23 16:17:53 +02:00
ToxykAuBleu 339015a093 Adding documentation related to Gamedig monitor. 2025-04-23 11:49:48 +00:00
Jens Vandenreyt b8b5b4170f Merge branch 'rajnandan1:main' into feature/extend-monitor-api 2025-04-23 11:59:21 +02:00
ToxykAuBleu 8a104e4480 Adding timeout feature for Gamedig monitor. 2025-04-23 06:31:41 +00:00
Raj Nandan Sharma 89e1a5e88a Merge branch 'main' into event-subscription 2025-04-23 11:31:58 +05:30
Raj Nandan Sharma 8e5746d068 style: Update logout button design and improve subscription info display 2025-04-23 11:31:40 +05:30
Raj Nandan Sharma 1df7c6560a Merge pull request #386 from lolen/main
feat: Polish translation
2025-04-23 11:24:38 +05:30
Piotr Lasota 0ff4546379 feat: Polish translation 2025-04-22 22:56:13 +02:00
Raj Nandan Sharma ab036c968d Refactor subscription trigger configuration and update database schema for subscription management 2025-04-22 22:58:45 +05:30
Raj Nandan Sharma 625ece2e7b feat: Add token generation with expiry and enhance subscription management
- Implemented GenerateTokenWithExpiry function for JWT token generation with a specified expiry time.
- Added GetSubscriberByID function to retrieve subscriber details by ID.
- Updated CreateNewSubscription to accept subscriber ID directly.
- Introduced GetSubscribersPaginated for paginated retrieval of subscribers.
- Enhanced subscription trigger management with CreateSubscriptionTrigger and GetSubscriptionTriggerByEmail functions.
- Updated email_code.html to change expiration notice from 1 day to 5 minutes.
- Modified layout.server.js to include canSendEmail check for conditional rendering.
- Updated subscription page to display subscribers with pagination and subscription status management.
- Refactored subscription-related API endpoints for improved clarity and functionality.
- Added server-side logic for managing subscription triggers and email settings.
2025-04-22 21:30:17 +05:30
Jens Vandenreyt 41842cc1b9 feat: add CRUD api for monitors
Added API endpoints for search, get, create, update and delete of monitors
2025-04-22 16:07:54 +02:00
ToxykAuBleu e63992a7a5 Adding eval feature for Gamedig monitor.
Sorting by default games by name.
2025-04-22 11:54:07 +00:00
ToxykAuBleu 7ae5d8f1b1 Adding Gamedig monitor functionality. 2025-04-19 19:13:40 +00:00
Raj Nandan Sharma 0a34f2138a feat: Implement subscriptions page and update navigation 2025-04-19 13:56:31 +05:30
Raj Nandan Sharma 38b326fe72 feat: Implement subscription management features
- Added functionality to create, update, and delete subscribers and their subscriptions in the controller and database implementation.
- Introduced new API endpoints for subscribing and unsubscribing users, including email verification and management of subscription preferences.
- Created a new Svelte component for managing subscriptions, allowing users to subscribe to updates and manage their preferences.
- Added email template for sending verification codes to subscribers.
- Implemented utility functions for generating random numbers and validating email addresses.
2025-04-19 13:09:51 +05:30
Mathieu Corbin 8785c609a9 Allow users to modify monitors data
Sometimes, it's useful to modify data in the monitoring_data table, for example, in case of a false positive.

I'm adding a new button in the monitor list that opens a popup
allowing users to change the status of monitor data for a given time
range.
2025-04-17 15:13:44 +02:00
github-actions 1a67560137 Auto-generate README.md with release versions 2025-04-08 05:33:49 +00:00
Raj Nandan Sharma 6c7534b9b6 Merge pull request #374 from rajnandan1/release/3.2.13
Release/3.2.13
2025-04-08 10:46:07 +05:30
Raj Nandan Sharma ae6c2c985f feat: enhance incident details formatting and add active status filter for triggers #370 2025-04-08 10:44:17 +05:30
Raj Nandan Sharma 940331b87f feat: update package description and enhance incident date formatting #371 2025-04-08 09:23:12 +05:30
Raj Nandan Sharma e6b5600a47 feat: add Umami analytics support and enhance capture functionality 2025-04-02 21:16:34 +05:30
Raj Nandan Sharma ce2a6ab756 feat: update version to 3.2.13 and refactor monitoring data calculations 2025-04-01 22:51:55 +05:30
Raj Nandan Sharma 6af91af639 chore: bump version to 3.2.12 in package.json 2025-04-01 21:22:43 +05:30
github-actions e1ff156b91 Auto-generate README.md with release versions 2025-03-31 12:16:53 +00:00
Raj Nandan Sharma b56d20ac35 Merge pull request #367 from rajnandan1/release/3.2.12
feat: add kenerTheme support and enhance footer HTML structure
2025-03-31 17:17:30 +05:30
Raj Nandan Sharma 9ee7a7b861 feat: add kenerTheme support and enhance footer HTML structure 2025-03-31 17:12:40 +05:30
Raj Nandan Sharma f73ffce3f0 feat: enhance documentation and styling for custom JS/CSS guide and incident comments 2025-03-30 12:58:19 +05:30
Raj Nandan Sharma 0997967787 docs: update environment variables documentation for CORS and reverse proxy setup
feat: modify analytics event naming convention for consistency

fix: adjust delete monitor confirmation form layout for better UX

style: refine positioning of monitor action buttons for improved alignment

refactor: streamline password validation imports in setup and forgot password routes

fix: remove unnecessary line breaks in layout server file
2025-03-29 22:33:17 +05:30
github-actions 3476ec2b29 Auto-generate README.md with release versions 2025-03-28 16:39:18 +00:00
Raj Nandan Sharma e0cf568aa9 feat: implement custom handle function to modify response headers 2025-03-28 21:50:07 +05:30
Raj Nandan Sharma c12d7f8345 feat: add null check for document before dispatching analytics event 2025-03-28 21:18:26 +05:30
Raj Nandan Sharma 9ba115d5ab Merge pull request #366 from rajnandan1/release/3.2.11
release 3.2.11
2025-03-28 20:56:59 +05:30
Raj Nandan Sharma 3756bbfd06 chore: bump version to 3.2.11 in package.json 2025-03-28 20:54:50 +05:30
Raj Nandan Sharma 9096bcc641 feat: import analytics event module for enhanced tracking in incident page 2025-03-28 20:53:44 +05:30
Raj Nandan Sharma c4d04854e5 feat: enhance monitor deletion confirmation UI and add analytics event for incident back button click 2025-03-28 20:52:59 +05:30
Raj Nandan Sharma d95fc0005b feat: indicate Markdown support in incident summary and comment sections 2025-03-28 20:40:24 +05:30
Raj Nandan Sharma 2b9093184a feat: add support for Markdown language in events and enhance SEO documentation as requested in #359 2025-03-28 20:34:19 +05:30
Raj Nandan Sharma 831376054b feat: add analytics capture snippets and enhance event tracking also added plausible and ms clarity #355 2025-03-28 15:07:45 +05:30
Raj Nandan Sharma 885ee0b6d6 feat: implement monitor deletion functionality with confirmation 2025-03-26 11:47:53 +05:30
Raj Nandan Sharma 511d0f26dd feat: add email validation in CreateUpdateTrigger function 2025-03-26 10:16:36 +05:30
github-actions 5263698625 Auto-generate README.md with release versions 2025-03-25 17:45:05 +00:00
Raj Nandan Sharma 7859f9564e fix: bump version to 3.2.10 in package.json 2025-03-25 22:58:04 +05:30
github-actions 149456682d Auto-generate README.md with release versions 2025-03-25 17:20:55 +00:00
Raj Nandan Sharma 8c040997da fix: refactor monitorTriggers initialization into a separate function 2025-03-25 22:47:05 +05:30
Raj Nandan Sharma 868fed0176 fix: update tzdata version in Dockerfile to use latest available 2025-03-25 22:38:13 +05:30
Raj Nandan Sharma c7f44a48da fix: bump version to 3.2.9 in package.json 2025-03-25 22:31:57 +05:30
Raj Nandan Sharma 6300bff4e7 Merge pull request #363 from rajnandan1/fix/357
fix: update RBAC documentation and improve monitor data retrieval log…
2025-03-25 22:27:04 +05:30
Raj Nandan Sharma 3e6b1ffdbb fix: streamline query execution in DbImpl by removing redundant return statement 2025-03-25 22:24:10 +05:30
Raj Nandan Sharma 408fd5318b fix: update RBAC documentation and improve monitor data retrieval logic, fixes #357 and #361 2025-03-25 22:20:45 +05:30
Raj Nandan Sharma cfe419f6df Merge pull request #362 from rajnandan1/fix/360
Fix/360
2025-03-25 20:39:07 +05:30
Raj Nandan Sharma 5403726099 fix: replace parseInt with Number for timeout conversion in monitorSheet.svelte and ping.js 2025-03-25 20:38:23 +05:30
Raj Nandan Sharma 057c14909f fix #360 2025-03-25 20:29:30 +05:30
Raj Nandan Sharma 37b18c06bb fix: validate timeout input and ensure it's a number in monitorSheet.svelte 2025-03-25 20:28:39 +05:30
github-actions 3ad1eb9a03 Auto-generate README.md with release versions 2025-03-24 15:18:34 +00:00
Raj Nandan Sharma a1064771f8 fix: increment package version from 3.2.7 to 3.2.8 in package.json 2025-03-24 20:34:24 +05:30
Raj Nandan Sharma 463ee286bf fix: increment package version from 3.2.6 to 3.2.7 in package.json 2025-03-24 20:30:05 +05:30
Raj Nandan Sharma a7ae6e72ef fix: downgrade package version from 3.2.8 to 3.2.6 in package.json 2025-03-24 20:28:01 +05:30
github-actions 36c67f60dd Auto-generate README.md with release versions 2025-03-24 14:56:20 +00:00
Raj Nandan Sharma bb718b04a6 fix: update Dockerfile to use ARG variables for Node.js version tags 2025-03-24 20:10:13 +05:30
Raj Nandan Sharma b6ea060054 fix: update publish-images workflow and increment package version to 3.2.8 2025-03-24 20:01:53 +05:30
Raj Nandan Sharma 5dba54c048 fix: update publish-images workflow to set GH_TOKEN environment variable correctly 2025-03-24 19:53:29 +05:30
Raj Nandan Sharma 415276163f Merge pull request #356 from rajnandan1/release/3.2.8-x
fix: update Node.js version to 23 and increment package version to 3.2.7
2025-03-24 19:45:44 +05:30
Raj Nandan Sharma 017b9e2e84 fix: update Node.js version to 23 and increment package version to 3.2.7 2025-03-24 19:45:15 +05:30
Raj Nandan Sharma 5740991ee1 fix: update GitHub Actions workflow to use GH_PAT instead of GITHUB_TOKEN for authentication 2025-03-24 15:12:40 +05:30
Raj Nandan Sharma 06008bd719 fix: streamline Dockerfile by removing unnecessary paths and using variable references for base images 2025-03-24 12:11:03 +05:30
Raj Nandan Sharma b53cd485e5 Merge pull request #263 from kaffolder7/feature/dependabot-version-updates
feat: automate dependency updates 🤖
2025-03-24 11:59:41 +05:30
Raj Nandan Sharma 7c1fd39158 Merge branch 'main' into feature/dependabot-version-updates 2025-03-24 11:58:58 +05:30
Raj Nandan Sharma f7888f1ba4 fix: update package versions in Dockerfile for consistency and stability 2025-03-24 11:32:05 +05:30
Raj Nandan Sharma ece5ac37ad feat: update embed monitor URLs to use siteURL and base variables for improved flexibility 2025-03-24 11:19:51 +05:30
Raj Nandan Sharma a51183fb23 Merge pull request #348 from rajnandan1/rbac
Features role-based access control and user management
2025-03-24 10:45:39 +05:30
Raj Nandan Sharma 7875192fb0 feat: update package dependencies and improve invitation expiry date formatting 2025-03-24 10:17:21 +05:30
Raj Nandan Sharma eb128ad431 feat: add "View in detail" button for group monitors and update localization files 2025-03-23 22:44:11 +05:30
Raj Nandan Sharma ba61ed87ab feat: enhance incident month handling with improved date calculations and formatting, fixing #349 2025-03-23 21:51:51 +05:30
Raj Nandan Sharma 8b5348081d docs: update changelogs to include improvements in build time and component migration 2025-03-23 14:29:15 +05:30
Raj Nandan Sharma bb7e69ace5 fix: update icon imports to use specific paths from lucide-svelte 2025-03-23 14:28:04 +05:30
Raj Nandan Sharma 3b8bb60423 fix: update print width in configuration files and improve documentation links 2025-03-23 13:36:22 +05:30
Raj Nandan Sharma 93339ce9df feat: add support for self-signed certificates and update changelog, fixes #351 2025-03-23 12:32:36 +05:30
Raj Nandan Sharma d30d99d8c2 docs: update changelogs to include resolved GitHub issues and improve formatting 2025-03-22 22:57:26 +05:30
Raj Nandan Sharma 3468df16e1 fix: improve error handling and code structure in Minuter function #345 2025-03-22 22:50:17 +05:30
Raj Nandan Sharma ccdeff98dc feat: implement dynamic versioning in headers and documentation as reported in #346 2025-03-22 21:54:24 +05:30
Raj Nandan Sharma 7e182b1df2 fix: replace showModal with closeModal for better modal handling #350 2025-03-22 21:06:37 +05:30
Raj Nandan Sharma ff652ad435 fix: #238 #210 #309 #334 2025-03-22 16:42:21 +05:30
Raj Nandan Sharma 1fdafa7966 Adds badges and embed features
Implements badges for monitors, including status, uptime, and liveness, along with a dedicated management page.

Adds embed options for various platforms with customizable styles.
2025-03-22 16:36:19 +05:30
Raj Nandan Sharma 1f5e683af5 feat: integrate CodeMirror editor for enhanced JavaScript and JSON editing experience 2025-03-20 23:23:54 +05:30
Raj Nandan Sharma db8c881571 refactor: remove performance logging and clean up code in various files 2025-03-19 19:45:12 +05:30
Raj Nandan Sharma 14f49c5b3b Features role-based access control and user management
Implements role-based access control with admin, editor, and member roles.

Introduces a user management system with profiles, activation/deactivation, and password reset.

Adds an email authentication system with verification and password reset via email.

Includes an invitation system with token-based invitations and admin controls.

Improves performance, security, UI, and developer experience.

Updates package version to 3.2.5 and adds vite-plugin-package-version.

Removes libcap related code from Dockerfile.
2025-03-18 23:08:51 +05:30
Raj Nandan Sharma 52286f26a5 fix: update version to 3.2.5 and reflect changes in User-Agent and documentation 2025-03-13 09:55:04 +05:30
github-actions 45e7567cb6 Auto-generate README.md with release versions 2025-03-12 17:48:41 +00:00
Aj7ay7 1288b463f1 Aj7ay7/fix/docker-compose-config
hotfix: updated container_name in compose
2025-02-28 23:21:02 +05:30
Aj7ay7 1aed6d0703 hotfix: updated container_name in compose 2025-02-28 23:20:38 +05:30
Aj7ay7 69f1e69140 Merge pull request #2 from Aj7ay7/revert-1-fix/docker-compose-config-validation
Revert "hotfix: Docker Compose Configuration Issues"
2025-02-28 23:17:52 +05:30
Aj7ay7 ab4ecd29c6 Revert "hotfix: Docker Compose Configuration Issues" 2025-02-28 23:17:40 +05:30
Aj7ay7 1d76c8a3d3 fix: docker-compose-config-validation
hotfix: Docker Compose Configuration Issues
2025-02-28 23:13:49 +05:30
Aj7ay7 b1c2cedbac hotfix: Docker Compose Configuration Issues 2025-02-28 23:11:13 +05:30
Kyle Affolder 8c95c94472 update: combine to avoid multiple processes
Combined to reduce spawning multiple `rm` processes.
2025-02-17 13:29:34 -05:00
Kyle Affolder 7dafb2eddc add: GHA job to confirm if Dependabot PRs exist
Adds job to check if any Dependabot PRs are open and if so, fail the Docker build (since we need to ensure OS packages exist and are in their correct versions when using pinned versions for security purposes).
2025-02-17 13:24:22 -05:00
Kyle Affolder 7c9f3eb87f add: add back pinned tzdata version
Necessary for Dependabot to track
2025-02-17 13:22:15 -05:00
Kyle ab34dd81f8 Merge branch 'rajnandan1:main' into feature/dependabot-version-updates 2025-02-17 13:04:23 -05:00
Kyle Affolder a8be878a87 update: commented out node.js deps. monitoring
Not sure if we are wanting Dependabot to track Node.js packages, so for the time being, commenting this block out, but leaving for now w/ “TODO” to come back to at a later point.
2025-02-11 17:54:37 -05:00
Kyle Affolder 2b1849f1a3 update: grammar updates 2025-02-11 17:47:45 -05:00
Kyle Affolder 25a6590324 Merge branch 'feature/dependabot-version-updates' of https://github.com/kaffolder7/kener into feature/dependabot-version-updates 2025-02-11 17:31:48 -05:00
Kyle Affolder fa251dc07f update: README generation workflow 2025-02-11 17:31:33 -05:00
Kyle 4de93e7e95 Merge branch 'main' into feature/dependabot-version-updates 2025-02-11 17:25:28 -05:00
Kyle Affolder a5e5f33dc8 Merge branch 'main' into feature/dependabot-version-updates
# Conflicts:
#	.github/workflows/publish-images.yml
#	Dockerfile
2025-02-11 17:17:45 -05:00
Kyle Affolder 4060094404 add(dependabot): to automate dependency updates
Integrating Dependabot into the workflow ensures automatic dependency updates, improving security, reducing technical debt, and keeping packages up to date with minimal manual effort. This helps prevent vulnerabilities and maintain code stability over time.

Dependabot will automatically monitor the project’s dependencies and open pull requests (PRs) to update them when new versions are released. Here’s how it works:

1. Scans for Outdated Dependencies – It checks project dependency files (e.g., package.json, Dockerfile, .env.build, etc.) for outdated versions.
2. Fetches Latest Versions – When a newer version of a dependency is available, Dependabot retrieves it and updates the dependency files accordingly.
3. Opens a Pull Request – It then creates a PR with the updated dependency, detailing the changes and linking to release notes, changelogs, or security advisories.
4. Runs CI/CD Tests – If we end up setting up continuous integration (CI) tests, the PR will trigger the tests to check for breaking changes.
5. Security Updates – Dependabot also detects vulnerable dependencies and creates PRs to update them to a secure version.
6. Auto-Merging (Optional) – We might consider this at a later point, but if configured, it can automatically merge PRs when updates pass all tests and meet the requirements.
2025-02-09 22:08:06 -05:00
1056 changed files with 90440 additions and 27063 deletions
+195
View File
@@ -0,0 +1,195 @@
---
name: code-context
description: Persistent code architecture documentation via a `.codecontext/` folder.
user-invokable: false
metadata:
category: architecture
---
# Code Architecture Documentation Skill
Use this skill to **read architecture docs before work** and **document architecture after work** using the `.codecontext/` folder.
`.codecontext/` is a living architecture reference — it helps new developers onboard and coding agents pick up where previous sessions left off. It is **NOT** a session log, changelog, or task diary.
---
## What Belongs in `.codecontext/`
Only document **architecture-level knowledge** that would take significant effort to rediscover by reading code alone.
### Include
- **Code architecture** — how modules/components are structured, layered, and why
- **Code flow** — request lifecycle, data flow between layers, event/cron pipelines
- **Component relationships** — which modules depend on each other, call chains, shared state
- **Edge cases and gotchas** — non-obvious behaviors, race conditions, ordering constraints
- **Design decisions and rationale** — why a pattern was chosen over alternatives
- **Integration points** — how external services, databases, queues connect
- **Invariants and constraints** — rules that must hold (e.g., "timestamps are always UTC seconds", "all DB access goes through db singleton")
- **Error handling patterns** — how errors propagate, retry logic, fallback behavior
- **Key file map** — which files own which responsibilities (only when non-obvious)
### Exclude
- Session logs, changelogs, or diary-style entries
- What files were changed in a specific task
- Raw terminal output or build logs
- Code snippets (reference file paths + line ranges instead)
- Obvious facts that can be inferred from reading one file
- Task status, TODO lists, or progress tracking
- Anything already covered in README, AGENTS.md, or inline comments
---
## Trigger Conditions
Run this skill at the **start and end** of any coding task that touches architecture:
- Feature implementations spanning multiple files/modules
- Refactors that change module boundaries or data flow
- Bug fixes that reveal non-obvious system behavior
- New integrations or service connections
- Discovery of undocumented edge cases or invariants
**Skip** for trivial changes (typo fixes, single-line edits, style-only changes).
---
## Phase A — Read Architecture Docs (Before Acting)
### A1) Discover docs
```bash
ls .codecontext/
```
If `.codecontext/` does not exist, continue the task and create it in Phase B.
### A2) Find relevant docs
```bash
grep -ril "<domain keyword>" .codecontext/
```
Use keywords from the feature area you are working on (e.g., "alerting", "auth", "monitors", "cron").
### A3) Read and apply
Read only relevant files. Extract:
- Architecture constraints that affect your implementation
- Code flow you need to hook into or extend
- Edge cases to preserve or handle
- Integration points to respect
If existing docs conflict with current code, trust the code — update docs in Phase B.
---
## Phase B — Document Architecture (Before Ending)
Only write/update docs if the task revealed architecture knowledge worth preserving.
### B1) Decide what to document
Ask: _"Would a new developer or future agent need to re-discover this to work in this area?"_
If yes, proceed. If no, skip Phase B entirely.
Then apply this filter to **every sentence** before writing:
> "Does this sentence describe how the code is structured, a design decision, or a constraint that would change how someone writes future code in this area?"
If no → cut it. This is the line between architecture documentation and a session diary.
### B2) Write architecture documentation
Structure each doc as a **reference document**, not a session diary.
Template (use only the sections that apply):
```markdown
# <Domain/Feature Area>
## Overview
Brief description of what this area does and its role in the system.
## Architecture
How the components are structured, key abstractions, layers.
## Code Flow
Step-by-step flow for the primary operations (e.g., "How a monitor check executes").
## Key Files
| File | Responsibility |
| -------------------- | -------------- |
| `src/lib/server/...` | Does X |
## Edge Cases and Gotchas
- Non-obvious behavior 1
- Constraint that must be preserved
## Design Decisions
- Why X was chosen over Y (if non-obvious)
```
Not all sections are required — include only what is relevant. Keep each doc under **300 lines**.
### B3) Pick target file
```bash
ls .codecontext/
grep -ril "<topic keyword>" .codecontext/
```
| Condition | Action |
| ------------------------------- | -------------------------------- |
| Existing doc covers this domain | Update/rewrite relevant sections |
| Different domain | Create new file |
| No match | Create new file |
When updating, **replace outdated sections** rather than appending session entries. The doc should always read as a clean, current architecture reference.
### B4) Persist
```bash
mkdir -p .codecontext
```
Create or overwrite the file so it reads as a standalone reference:
```bash
cat > .codecontext/<domain>.md
```
---
## Naming Rules
- Name by domain/feature area: `alerting.md`, `auth.md`, `monitor-execution.md`, `incident-lifecycle.md`
- Use kebab-case for multi-word topics
- Never use generic names: `notes.md`, `misc.md`, `context.md`, `session-1.md`
- One file per bounded domain — split if a file exceeds ~300 lines
---
## Fast Checklist
Before coding:
- [ ] Checked `.codecontext/` for relevant architecture docs
- [ ] Applied constraints and patterns from existing docs
Before finishing:
- [ ] Every sentence passed the B1 architecture filter
- [ ] Documented any new architecture knowledge discovered
- [ ] Updated outdated docs if current code contradicts them
- [ ] Doc reads as a clean architecture reference, not a session log
@@ -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 (12 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
@@ -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
+361
View File
@@ -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
+44 -2
View File
@@ -1,9 +1,51 @@
# Dependencies
node_modules
# Version control
.git
.github
# IDE and editor
.vscode
dist
.idea
*.swp
*.swo
# Build outputs (rebuilt inside Docker)
build
dist
.svelte-kit
.docs-excluded
# Environment and secrets
.env
.env.*
!.env.example
# OS files
.DS_Store
*.log
Thumbs.db
# Logs
*.log
npm-debug.log*
# Docker files (prevent recursive context)
Dockerfile
docker-compose*.yml
.dockerignore
# Documentation and meta
README.md
README.template.md
AGENTS.md
CHANGELOG.md
LICENSE
check-output.txt
# AI / tooling config
.claude
# Test artifacts
*.test.*
*.spec.*
+1
View File
@@ -43,6 +43,7 @@ indent_size = 2
indent_style = space
indent_size = 4
trim_trailing_whitespace = false
print_width = 180
# Dockerfile
[Dockerfile*]
+3
View File
@@ -0,0 +1,3 @@
KENER_SECRET_KEY=some_secret_key_for_kener
REDIS_URL=redis://localhost:6379
ORIGIN=http://localhost:3000
-26
View File
@@ -1,26 +0,0 @@
TZ=Etc/UTC
KENER_SECRET_KEY=please_change_me_to_something_secure
# For SQLite database...
DATABASE_URL=sqlite://./database/kener.sqlite.db
# For PostgreSQL database...
# DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db
# POSTGRES_PASSWORD=some_super_random_secure_password
# For MySQL database...
# DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/kener_db
# MYSQL_PASSWORD=some_super_random_secure_password
KENER_BASE_PATH=""
ORIGIN=http://localhost:3000
RESEND_API_KEY=""
RESEND_SENDER_EMAIL=Accounts <accounts@resend.dev>
# Likely no need to change...
# NODE_ENV=production # already defined in container
# PORT=3000 # default port Kener service is exposed upon
# Add the below variable if you would like to white-label the product (aka. remove some of the attributions scattered throughout the app)
# WHITE_LABEL=true
@@ -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]
+10
View File
@@ -0,0 +1,10 @@
---
name: Kener v4 items
about: Kener v4 items
title: ''
labels: kener_v4
assignees: rajnandan1
---
Kener v4 items
+417
View File
@@ -0,0 +1,417 @@
# 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
## 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
+140
View File
@@ -0,0 +1,140 @@
# Kener - AI Coding Instructions
## Project Overview
Kener is an open-source status page application built with **SvelteKit 2.x** (**Svelte 5**) and **Node.js**, and is migrating to a **TypeScript-first** codebase. It provides real-time monitoring, uptime tracking, incident management, and customizable dashboards.
## Architecture
### Entry Points
- **`main.js`** - Production server entry: Express + SvelteKit handler + cron scheduler
- **`src/lib/server/startup.js`** - Cron job scheduler for monitors (runs every minute)
### Route Groups (SvelteKit)
- **`(kener)/`** - Public status page routes
- **`(manage)/`** - Admin dashboard (requires authentication)
- **`(embed)/`** - Embeddable widgets
- **`(docs)/`** - Documentation pages
### Core Server Components
- **`src/lib/server/controllers/controller.js`** - Main business logic (~1700 lines), handles monitors, incidents, auth, email
- **`src/lib/server/db/dbimpl.js`** - Database abstraction layer using Knex.js
- **`src/lib/server/services/`** - Monitor type implementations: API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group
- **`src/lib/server/cron-minute.js`** - 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`
## Development Commands
```bash
npm run dev # Start dev server with hot reload + cron scheduler
npm run build # Production build
npm run preview # Preview production build
npm run check # Typecheck + Svelte checks (uses tsconfig)
```
## Key Patterns
### Svelte 5 + TypeScript conventions
- Prefer **TypeScript** for new/modified code (`.ts`, and `.svelte` with `lang="ts"`).
- Prefer **Svelte 5 runes** for component state/effects in new code (e.g. `$state`, `$derived`, `$effect`).
- Prefer Svelte 5 props via `$props()` in new components. Keep existing `export let` props where already used to avoid churn.
- For SvelteKit route typing, prefer generated `$types` (e.g. `import type { PageServerLoad } from './$types'`).
- Avoid packages that hard-require Svelte 4 (they can break or force `--legacy-peer-deps`).
### Monitor Types
Defined in `src/lib/server/services/service.js`. Each type has its own implementation file:
```javascript
// Supported: API, PING, TCP, DNS, GROUP, SSL, SQL, HEARTBEAT, GAMEDIG
```
### Status Constants
Use constants from `src/lib/server/constants`:
```javascript
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "./constants";
```
### API Authentication
APIs use Bearer token auth verified via `VerifyAPIKey()`:
```javascript
import { VerifyAPIKey } from "$lib/server/controllers/controller.js";
```
### Database Queries
Always use the db singleton, never instantiate Knex directly:
```javascript
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.js`:
```javascript
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC } from "./tool";
```
### i18n
Locales are in `src/lib/locales/`. Add new translations by creating `{code}.json` and updating `locales.json`.
## UI Components
Uses **shadcn-svelte** components in `src/lib/components/ui/`. Import pattern:
```javascript
import { Button } from "$lib/components/ui/button";
```
Styling: **TailwindCSS** with HSL CSS variables for theming (see `tailwind.config.js`).
## Environment Variables
Required in `.env`:
- `KENER_SECRET_KEY` - JWT secret for auth
- `ORIGIN` - Site URL (e.g., `http://localhost:3000`)
- `DATABASE_URL` - Database connection string
Optional:
- `KENER_BASE_PATH` - Base path for reverse proxy
- `RESEND_API_KEY` / `RESEND_SENDER_EMAIL` - Email notifications
## File Conventions
- Server-only code: `src/lib/server/`
- Shared utilities: `src/lib/` (except `server/`)
- Route data loading: `+page.server.ts` / `+layout.server.ts` (and client-side `+page.ts` / `+layout.ts` when needed)
- 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. Use for DB models, internal service types, auth/session types, and anything that uses `$env/static/private` or Node-only APIs.
- **`src/lib/client/types/`** - Client-only types. Use for UI-specific types, component prop types, and anything that relies on browser/DOM APIs.
Always use `import type { ... }` when importing types to avoid accidental runtime imports.
# Other skills
Read files in .claude/skills for more instructions on specific tasks or file types.
## Code Architecture Documentation (MUST)
For every coding task that touches architecture (multi-file features, refactors, new integrations):
1. **Before edits**
- Read and apply `.claude/skills/code-context/SKILL.md`.
- Load relevant architecture docs from `.codecontext/` when present.
2. **Before finishing the response**
- If the task revealed new architecture knowledge (code flow, edge cases, component relationships, design decisions), write/update a `.codecontext/*.md` entry as a clean reference doc.
- Skip if the task was trivial (typo fixes, single-line edits).
3. **Final response contract**
- Include a short line: `Context loaded: ...`
- Include a short line: `Context updated: ...`
`.codecontext/` documents **code architecture only** — not session logs, changelogs, or task summaries.
+47
View File
@@ -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"
+98
View File
@@ -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 }}
-83
View File
@@ -1,83 +0,0 @@
name: Generate README
on:
push:
branches:
- main
paths:
- 'README.template.md'
pull_request:
branches:
- main
paths:
- 'README.template.md'
workflow_dispatch: # Allows for manual execution
workflow_run: # Triggers this workflow to run when it recognizes 'publish-images' workflow has ran and successfully completed
workflows: ["Publish Docker Image to Registries"]
types:
- completed
permissions:
contents: write # Explicitly allow pushing changes
jobs:
generate-readme:
name: Generate README from template
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # Fetch full history, including tags
persist-credentials: false # We'll manually authenticate
- name: Configure Git
run: |
git config --global user.name "github-actions"
git config --global user.email "github-actions@github.com"
- name: Setup Node.js
uses: actions/setup-node@v4.2.0
with:
node-version: "20"
- name: Install Dependencies
run: npm install mustache dotenv
- name: Extract Major and Major-Minor Versions
run: |
VERSION="${{ vars.BUILD_VERSION }}"
# Check if VERSION is empty and set a fallback value
if [ -z "$VERSION" ]; then
# Fetch the latest release using Git
VERSION=$(git tag -l --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
fi
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
echo "Full version: $VERSION"
echo "Major version: $MAJOR"
echo "Major-Minor version: $MAJOR_MINOR"
# Export all as environment variables
echo "LATEST_VERSION=$VERSION" >> $GITHUB_ENV
echo "LATEST_MAJOR_VERSION=$MAJOR" >> $GITHUB_ENV
echo "LATEST_MAJOR_MINOR_VERSION=$MAJOR_MINOR" >> $GITHUB_ENV
- name: Generate README.md
env:
BUILD_FULL_VERSION: ${{ env.LATEST_VERSION }} # e.g., 1.2.3
BUILD_MAJOR_VERSION: ${{ env.LATEST_MAJOR_VERSION}} # e.g., 1
BUILD_MAJOR_MINOR_VERSION: ${{ env.LATEST_MAJOR_MINOR_VERSION }} # e.g., 1.2
run: node scripts/generate-readme.js
- name: Commit and Push Changes
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git add README.md
git commit -m "Auto-generate README.md with release versions" || echo "No changes to commit"
git push https://x-access-token:${{ secrets.GH_PAT }}@github.com/${{ github.repository }}.git HEAD:main
-21
View File
@@ -1,21 +0,0 @@
name: Prevent Direct README Changes
on:
pull_request:
paths:
- "README.md"
jobs:
check-readme:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4.2.2
- name: Detect direct README changes
run: |
if git diff --name-only origin/main | grep -q "README.md"; then
echo "❌ Direct modifications to README.md are not allowed!"
echo "Please update README.template.md instead."
exit 1
fi
+19 -84
View File
@@ -1,93 +1,58 @@
name: Publish Docker Image to Registries
name: Publish Nightly Docker Image
on:
release:
types:
- published # Runs only when a GitHub Release is published
workflow_dispatch: # Allows for manual execution
push:
branches:
- next/**
workflow_dispatch:
env:
ALPINE_VERSION: "23-alpine"
DEBIAN_VERSION: "23-slim"
# Registry URLs
DOCKERHUB_REGISTRY: docker.io
GITHUB_REGISTRY: ghcr.io
# Docker Hub image name (using Docker Hub username)
DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
# GitHub image name (formatted as `account/repo`)
GITHUB_IMAGE_NAME: ${{ github.repository }}
concurrency:
group: nightly-docker-${{ github.ref }}
cancel-in-progress: true
jobs:
check-lockfile:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Adjust as needed
- name: Install dependencies
run: npm ci
- name: Check for lock-file changes
run: |
git diff --exit-code package-lock.json || (
echo "🫥 package-lock.json is outdated or missing. Please run 'npm install' and commit the updated lockfile."
exit 1
)
build-and-push-to-registries:
needs: check-lockfile # Runs only after `check-lockfile` completes successfully
name: Push Docker images to Docker Hub and GitHub Container Registry
build-and-push-nightly:
name: Build and push nightly Docker images
strategy:
matrix:
variant: [alpine, debian]
variant: [debian, alpine]
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
contents: read
packages: write
# This is used to complete the identity challenge with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4.2.2
# Install the cosign tool (except on PR)
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.8.0
with:
cosign-release: 'v2.2.4'
cosign-release: v2.2.4
# Set up BuildKit Docker container builder to be able to build multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0
# Log in to Docker Hub (except on PR)
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Log in to GitHub Container Registry (except on PR)
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Combined metadata extraction for both registries
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.6.1
@@ -96,61 +61,31 @@ jobs:
${{ env.DOCKERHUB_IMAGE_NAME }}
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
tags: |
# Debian Variant Tags
type=semver,pattern={{version}},enable=${{ matrix.variant == 'debian' }}
type=semver,pattern={{major}}.{{minor}},enable=${{ matrix.variant == 'debian' }}
type=semver,pattern={{major}},enable=${{ matrix.variant == 'debian' }}
type=raw,value=latest,enable=${{ matrix.variant == 'debian' }}
# Alpine Variant Tags
type=semver,pattern={{version}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
type=semver,pattern={{major}}.{{minor}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
type=semver,pattern={{major}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
type=raw,value=alpine,enable=${{ matrix.variant == 'alpine' }}
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
# Build and push Docker image with Buildx to both registries (don't push on PR)
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6.13.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VARIANT=${{ matrix.variant }}
ALPINE_VERSION=${{ env.ALPINE_VERSION }}
DEBIAN_VERSION=${{ env.DEBIAN_VERSION }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digests
- name: Sign the published Docker images
if: ${{ github.event_name != 'pull_request' }}
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: |
echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
# For use in other workflows (e.g. 'generate-readme', etc.)
- name: Save Build Version to Repository Variable
if: matrix.variant == 'debian' && github.run_attempt == 1
run: |
# VERSION="${{ steps.meta.outputs.version }}"
VERSION=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "")
# Check if VERSION is empty and set a fallback value
if [ -z "$VERSION" ]; then
VERSION=$(git tag -l --sort=-version:refname | grep -E '^(v)?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
fi
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Setting BUILD_VERSION to $VERSION"
gh variable set BUILD_VERSION --body "$VERSION"
env:
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
+92
View File
@@ -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}
+146
View File
@@ -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}
+8 -1
View File
@@ -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-*
@@ -27,4 +30,8 @@ uploads/*
static/uploads/*
!static/uploads/upload.dir
temp.txt
temp.js
temp.js
.DS_Store
knip-output.txt
check-output.txt
translation-report.json
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+1 -1
View File
@@ -54,7 +54,7 @@
"semi": false,
"tabWidth": 4,
"trailingComma": "none",
"printWidth": 100
"printWidth": 180
}
},
{
+51
View File
@@ -0,0 +1,51 @@
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.
## 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.
## Code architecture docs skill - Important for all tasks
Always try to use the code-context skill at the start and end of coding sessions:
- `.claude/skills/code-context/SKILL.md`
## Code architecture enforcement (mandatory)
The code-context skill is not optional. Agents MUST do both:
1. **Before coding**: load relevant architecture docs from `.codecontext/`.
2. **Before final response**: if the task revealed new architecture knowledge (code flow, edge cases, component relationships), update or create a `.codecontext/*.md` entry. Skip if the task was trivial.
Required output evidence in the final response:
- `Context loaded:` list of `.codecontext` files read (or `none found`).
- `Context updated:` exact `.codecontext` file path written (or `skipped — no architecture changes`).
The `.codecontext/` folder documents **code architecture only** — not session logs, changelogs, or task summaries.
+150 -125
View File
@@ -1,164 +1,189 @@
# syntax=docker/dockerfile:1
# Global build arguments
ARG ALPINE_VERSION=23.7.0-alpine3.21
ARG DEBIAN_VERSION=23.7.0-bookworm-slim
ARG VARIANT=debian
# =============================================================================
# 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
# =============================================================================
#==========================================================#
# STAGE 1: BUILD STAGE #
#==========================================================#
ARG NODE_VERSION=24
ARG VARIANT=alpine
ARG WITH_DOCS=false
ARG KENER_BASE_PATH=
FROM node:${DEBIAN_VERSION} AS builder-debian
RUN apt-get update && apt-get install -y \
build-essential=12.9 \
python3=3.11.2-1+b1 \
sqlite3=3.40.1-2+deb12u1 \
libsqlite3-dev=3.40.1-2+deb12u1 \
make=4.3-4.1 \
node-gyp=9.3.0-2 \
g++=4:12.2.0-3 \
tzdata=2024b-0+deb12u1 \
iputils-ping=3:20221126-1+deb12u1 && \
# =============================================================================
# STAGE 1 — BUILDER (installs deps, compiles native modules, builds app)
# =============================================================================
# ---------- Alpine builder ----------
FROM node:${NODE_VERSION}-alpine AS builder-alpine
RUN apk add --no-cache \
build-base \
python3 \
sqlite \
sqlite-dev \
tzdata
# ---------- Debian builder ----------
FROM node:${NODE_VERSION}-slim AS builder-debian
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
python3 \
sqlite3 \
libsqlite3-dev \
tzdata && \
rm -rf /var/lib/apt/lists/*
FROM node:${ALPINE_VERSION} AS builder-alpine
RUN apk add --no-cache --update \
build-base=0.5-r3 \
python3=3.12.9-r0 \
py3-pip=24.3.1-r0 \
make=4.4.1-r2 \
g++=14.2.0-r4 \
sqlite=3.48.0-r0 \
sqlite-dev=3.48.0-r0 \
tzdata \
iputils=20240905-r0
# ---------- Selected variant ----------
FROM builder-${VARIANT} AS builder
# Set environment variables
ENV NPM_CONFIG_LOGLEVEL=error \
VITE_BUILD_ENV=production
ENV NPM_CONFIG_LOGLEVEL=error
# Set the working directory
WORKDIR /app
# Copy package files for dependency installation
ARG KENER_BASE_PATH
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
# 1. Copy package manifests first (maximises layer cache hits)
COPY package*.json ./
# Install all dependencies, including `devDependencies` (cache enabled for faster builds)
# TODO: Possibly add `--no-audit` flag to `npm ci` to prevent `npm` from running a security audit on installed packages. (By default, `npm install` performs an audit to check for vulnerabilities in dependencies, which can slow down installation. Adding this flag would skip the audit, thus making `npm install` significantly faster for the CI/CD pipeline.)
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-fund && \
# 2. Install ALL dependencies (devDependencies needed for the build step)
RUN npm ci --no-fund && \
npm cache clean --force
# Copy application source code
# 3. Copy the rest of the source tree
COPY . .
# TODO: Reevaluate permissions (possibly reduce?)...
# Remove docs directory and ensure required directories exist
RUN rm -rf src/routes/\(docs\) \
static/documentation \
static/fonts/lato/full && \
mkdir -p uploads database && \
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
chmod -R 750 uploads database
# 4. Create directories that the app expects
RUN mkdir -p database
# Build the application and remove `devDependencies`
RUN npm run build && \
npm prune --omit=dev
# 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
#==========================================================#
# STAGE 2: PRODUCTION/FINAL STAGE #
#==========================================================#
# 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
FROM node:${DEBIAN_VERSION} AS final-debian
# TODO: Consider adding `--no-install-recommends`, but will need testing (may further help reduce final build size)
RUN apt-get update && apt-get install -y \
iputils-ping=3:20221126-1+deb12u1 \
sqlite3=3.40.1-2+deb12u1 \
tzdata=2024b-0+deb12u1 \
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
curl=7.88.1-10+deb12u8 && \
# 7. Stage docs runtime files for index-docs (empty dir when docs disabled)
RUN mkdir -p /docs-runtime && \
if [ "$WITH_DOCS" = "true" ]; then \
mkdir -p /docs-runtime/scripts && \
mkdir -p /docs-runtime/src/lib && \
mkdir -p "/docs-runtime/src/routes/(docs)/docs" && \
cp scripts/index-docs.ts /docs-runtime/scripts/ && \
cp src/lib/marked.ts /docs-runtime/src/lib/ && \
cp "src/routes/(docs)/docs.json" "/docs-runtime/src/routes/(docs)/" && \
cp -r "src/routes/(docs)/docs/content" "/docs-runtime/src/routes/(docs)/docs/"; \
fi
# 8. Remove devDependencies from node_modules
RUN npm prune --omit=dev
# =============================================================================
# STAGE 2 — PRODUCTION (minimal runtime image)
# =============================================================================
# ---------- Alpine runtime ----------
FROM node:${NODE_VERSION}-alpine AS final-alpine
RUN apk add --no-cache \
sqlite \
tzdata \
iputils \
curl \
libcap && \
# Grant ping the NET_RAW capability so non-root users can send ICMP packets
setcap cap_net_raw+ep /bin/ping || true
# ---------- Debian runtime ----------
FROM node:${NODE_VERSION}-slim AS final-debian
RUN apt-get update && apt-get install -y --no-install-recommends \
sqlite3 \
tzdata \
iputils-ping \
curl \
libcap2-bin && \
setcap cap_net_raw+ep /usr/bin/ping || true && \
rm -rf /var/lib/apt/lists/*
FROM node:${ALPINE_VERSION} AS final-alpine
RUN apk add --no-cache --update \
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata
# ---------- Selected variant ----------
FROM final-${VARIANT} AS final
ARG PORT=3000 \
USERNAME=node
ARG PORT=3000
ARG KENER_BASE_PATH=
# Set environment variables
ENV HEALTHCHECK_PORT=$PORT \
HEALTHCHECK_PATH= \
NODE_ENV=production \
NPM_CONFIG_LOGLEVEL=error \
PORT=$PORT \
TZ=Etc/UTC
ENV NODE_ENV=production \
PORT=${PORT} \
KENER_BASE_PATH=${KENER_BASE_PATH} \
TZ=UTC \
# Required so Node can import .ts migration/seed files at runtime
NODE_OPTIONS="--experimental-strip-types"
# Set the working directory
WORKDIR /app
# Copy package files build artifacts, and necessary files from builder stage
COPY --chown=node:node --from=builder /app/src/lib/ ./src/lib/
COPY --chown=node:node --from=builder /app/build ./build
COPY --chown=node:node --from=builder /app/uploads ./uploads
COPY --chown=node:node --from=builder /app/database ./database
# TODO: Consider changing from copying `node_modules` to instead letting `npm ci --omit=dev` handle production dependencies. Right now, copying `node_modules` is leading to a smaller image, whereas letting `npm ci` handle the install in final image is slightly faster, but leads to larger image size. IMO, having a slightly longer build time (e.g. ~10 sec.) is better in the end to have a smaller image.
# 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
COPY --chown=node:node --from=builder /app/static ./static
COPY --chown=node:node --from=builder /app/entrypoint.sh ./entrypoint.sh
COPY --chown=node:node --from=builder /app/knexfile.js ./knexfile.js
COPY --chown=node:node --from=builder /app/main.js ./main.js
COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
# Install libcap tools before setting capabilities
RUN if [ -f "/etc/alpine-release" ]; then \
apk add --no-cache libcap; \
else \
apt-get update && apt-get install -y libcap2-bin && rm -rf /var/lib/apt/lists/*; \
fi
# 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
# Set capabilities for ping (before changing to non-root user)
RUN if [ -f "/etc/alpine-release" ]; then \
# Alpine path
setcap cap_net_raw+ep /bin/ping; \
else \
# Debian path
setcap cap_net_raw+ep /usr/bin/ping; \
fi
# Build output (SvelteKit client/server + esbuild main.js) — changes most often
COPY --chown=node:node --from=builder /app/build ./build
# Ensure necessary directories are writable
VOLUME ["/uploads", "/database"]
# Docs runtime files (index-docs script + markdown sources; empty when WITH_DOCS=false)
COPY --chown=node:node --from=builder /docs-runtime/ ./
# Set container timezone and make entrypoint script executable
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
chmod +x ./entrypoint.sh
# TODO: To improve security, consider dropping unnecessary capabilities instead of granting image all network capabilities of host. (Maybe `setcap cap_net_raw+p /usr/bin/ping`, etc.) Could also drop all and then grant only the capabilities that are explicitly needed. Some examples are commented out below...
# setcap cap_net_bind_service=+ep /usr/local/bin/node
# setcap cap_net_bind_service=+ep /usr/bin/ping
# setcap cap_net_bind_service=+ep /usr/bin/ping6
# setcap cap_net_bind_service=+ep /usr/bin/tracepath
# setcap cap_net_bind_service=+ep /usr/bin/clockdiff
# 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
# Expose the application port
EXPOSE $PORT
# ---- Runtime configuration ----
# Add a healthcheck to the container; `wget` vs. `curl` depending on base image. Using this approach because `wget` does not actually maintain versioning across architectures, so we cannot pin a `wget` version (in above `final-debian` base, `apt-get install`) between differing architectures (e.g. arm64, amd64)
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD sh -c 'if [ -f "/etc/alpine-release" ]; then wget --quiet --spider http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; else curl --silent --head --fail http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; fi'
# Switch to non-root user
USER node
# TODO: Revisit letting user define $PUID & $PGID overrides (e.g. `addgroup -g $PGID newgroup && adduser -D -G newgroup -u $PUID node`) as well as potentially ensure no root user exists. (Make sure no processes are running as root, first!)
# Use a non-root user (recommended for security)
USER $USERNAME
EXPOSE ${PORT}
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["node", "main"]
# 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"]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+124 -117
View File
@@ -1,7 +1,7 @@
# Kener - Stunning Status Pages
<p align="center">
<img src="https://kener.ing/newbg.png?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
<img src="https://kener.ing/og.jpg?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
</p>
<p align="center">
@@ -43,8 +43,8 @@
</picture>
</p>
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/quick-start) | [🗄 Documentation](https://kener.ing/docs/home) |
| ----------------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
| [🌍 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) |
| ----------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- |
## What is Kener?
@@ -62,151 +62,158 @@ Designed with **ease of use** and **customization in mind**, Kener provides all
“Kener” is inspired by the Assamese word _“Kene”_, meaning _“hows it going?”_. The _.ing_ was added because, well… that domain was available. 😄
## Installation
## Quick Start
### Manual
Get Kener running in minutes.
```shell
# Clone the repository
### 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
cp .env.example .env
npm run dev
# Start Redis (example)
docker run -d --name kener-redis -p 6379:6379 redis:7-alpine
npm run build
npm run start
```
### Docker
Create a `.env` with at least:
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.2-blue" alt="Kener latest stable version: 3.2.2" /></a>
#### Available Tags
<table>
<tr>
<th>Image Tag</th>
<th>Description</th>
</tr>
<tr>
<td align="left" colspan="2" style="color:#A81D33;text-align:left;">Debian 12 <small>(Bookwork Slim)</small> w/ Node.js v23.7.0 &nbsp;<strong><em>(default)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka 3.2.2)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2" target="_blank"><code>3.2.2</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.2) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.2) release within that major version (3.x.x)</td>
</tr>
<tr>
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka 3.2.2)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2-alpine" target="_blank"><code>3.2.2-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.2) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.2) release within that major version (3.x.x)</td>
</tr>
</table>
#### Usage
Pull the latest stable version:
```sh
docker pull rajnandan1/kener:latest
```dotenv
KENER_SECRET_KEY=replace_with_a_random_string
ORIGIN=http://localhost:3000
REDIS_URL=redis://localhost:6379
PORT=3000
```
Or use the smaller, Alpine-based variant:
For the full quick start (including local Docker builds and dev mode), see the docs:
```sh
docker pull rajnandan1/kener:alpine
```
- https://kener.ing/docs/v4/getting-started/quick-start
For a production setup, refer to the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
This keeps things clean, structured, and easy to read while preserving all the details.
## One Click Deployment
### One Click
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/spSvic?referralCode=1Pn7vs&utm_medium=integration&utm_source=template&utm_campaign=generic)
## 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.
### 📊 &nbsp;Monitoring and Tracking
### 📊 &nbsp;Monitoring, Reliability, and Communication
- Advanced **application performance monitoring** tools
- **Real-time network monitoring** capabilities
- Supports **polling HTTP endpoints** or **pushing data** via REST APIs
- **Timezone auto-adjustment** for visitors
- Organize monitors into **custom sections**
- **Cron-based scheduling** (minimum: **every minute**)
- **Create complex API polls** (chaining, secrets, etc.)
- Set a **default status** for monitors
- Supports **base path hosting in Kubernetes (k8s)**
- **Pre-built Docker images** for easy deployment
- 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
### 🎨 &nbsp;Customization and Branding
### 🎨 &nbsp;Status Page Experience and Branding
- Fully **customizable status page**
- **Badge generation** for status and uptime tracking
- Support for **custom domains**
- Embed monitors as **iframes or widgets**
- **Light & Dark Mode**
- **Internationalization (i18n) support**
- **Sleek, beautifully crafted UI**
- 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
### 🚨 &nbsp;Incident Management
### 🛠️ &nbsp;Operations, Collaboration, and Automation
- **Incident tracking & communication** tools
- **Comprehensive APIs** for incident management
### 🧑‍💻 &nbsp;User Experience and Design
- **Accessible & user-friendly interface**
- **Quick & easy installation**
- **Responsive design** for all devices
- **Auto SEO & Social Media ready**
- **Server-Side Rendering (SSR) for better performance**
<div align="left">
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
</div>
- 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
## Technologies Used
- [SvelteKit](https://kit.svelte.dev/)
- [shadcn-svelte](https://www.shadcn-svelte.com/)
- [SvelteKit](https://kit.svelte.dev/)
- [shadcn-svelte](https://www.shadcn-svelte.com/)
## Support Me
If youre 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)
☕ &nbsp;[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
![image](https://badges.pufler.dev/visits/rajnandan1/kener)
- [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
## Contributing
-217
View File
@@ -1,217 +0,0 @@
# Kener - Stunning Status Pages
<p align="center">
<img src="https://kener.ing/newbg.png?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
</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://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>
</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">
<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/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>
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a7/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a7/512.gif" alt="🚧" width="32" height="32">
</picture>
</p>
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/quick-start) | [🗄 Documentation](https://kener.ing/docs/home) |
| ----------------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
## What is Kener?
**Kener** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. Its 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.
Designed with **ease of use** and **customization in mind**, Kener provides all the essential features youd expect from a status page—without unnecessary complexity.
### Why Kener?
✅ &nbsp;Minimal overhead &ndash; Set up quickly with a clean, modern UI<br>
✅ &nbsp;Customizable &ndash; Easily tailor it to match your brand<br>
✅ &nbsp;Open-source & free &ndash; Because great tools should be accessible to everyone
### What's in a Name?
“Kener” is inspired by the Assamese word _“Kene”_, meaning _“hows it going?”_. The _.ing_ was added because, well… that domain was available. 😄
## Installation
### Manual
```shell
# Clone the repository
git clone https://github.com/rajnandan1/kener.git
cd kener
npm install
cp .env.example .env
npm run dev
```
### Docker
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}"><img src="https://img.shields.io/badge/Latest_Stable_Release-{{kener_full_version}}-blue" alt="Kener latest stable version: {{kener_full_version}}" /></a>
#### Available Tags
<table>
<tr>
<th>Image Tag</th>
<th>Description</th>
</tr>
<tr>
<td align="left" colspan="2" style="color:#A81D33;text-align:left;">Debian 12 <small>(Bookwork Slim)</small> w/ Node.js v23.7.0 &nbsp;<strong><em>(default)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka {{kener_full_version}})</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}" target="_blank"><code>{{kener_full_version}}</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_minor_version}}" target="_blank"><code>{{kener_major_minor_version}}</code></a></td>
<td>Major-minor version tag pointing to the latest patch ({{kener_full_version}}) release within that minor version ({{kener_major_minor_version}}.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_version}}" target="_blank"><code>{{kener_major_version}}</code></a></td>
<td>Major version tag pointing to the latest stable ({{kener_full_version}}) release within that major version ({{kener_major_version}}.x.x)</td>
</tr>
<tr>
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka {{kener_full_version}})</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}-alpine" target="_blank"><code>{{kener_full_version}}-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_minor_version}}-alpine" target="_blank"><code>{{kener_major_minor_version}}-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch ({{kener_full_version}}) release within that minor version ({{kener_major_minor_version}}.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_version}}-alpine" target="_blank"><code>{{kener_major_version}}-alpine</code></a></td>
<td>Major version tag pointing to the latest stable ({{kener_full_version}}) release within that major version ({{kener_major_version}}.x.x)</td>
</tr>
</table>
#### Usage
Pull the latest stable version:
```sh
docker pull rajnandan1/kener:latest
```
Or use the smaller, Alpine-based variant:
```sh
docker pull rajnandan1/kener:alpine
```
For a production setup, refer to the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
This keeps things clean, structured, and easy to read while preserving all the details.
### One Click
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
## Features
Here are some of the features that you get out of the box. Please read the documentation to know how to use them.
### 📊 &nbsp;Monitoring and Tracking
- Advanced **application performance monitoring** tools
- **Real-time network monitoring** capabilities
- Supports **polling HTTP endpoints** or **pushing data** via REST APIs
- **Timezone auto-adjustment** for visitors
- Organize monitors into **custom sections**
- **Cron-based scheduling** (minimum: **every minute**)
- **Create complex API polls** (chaining, secrets, etc.)
- Set a **default status** for monitors
- Supports **base path hosting in Kubernetes (k8s)**
- **Pre-built Docker images** for easy deployment
### 🎨 &nbsp;Customization and Branding
- Fully **customizable status page**
- **Badge generation** for status and uptime tracking
- Support for **custom domains**
- Embed monitors as **iframes or widgets**
- **Light & Dark Mode**
- **Internationalization (i18n) support**
- **Sleek, beautifully crafted UI**
### 🚨 &nbsp;Incident Management
- **Incident tracking & communication** tools
- **Comprehensive APIs** for incident management
### 🧑‍💻 &nbsp;User Experience and Design
- **Accessible & user-friendly interface**
- **Quick & easy installation**
- **Responsive design** for all devices
- **Auto SEO & Social Media ready**
- **Server-Side Rendering (SSR) for better performance**
<div align="left">
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
</div>
## Technologies Used
- [SvelteKit](https://kit.svelte.dev/)
- [shadcn-svelte](https://www.shadcn-svelte.com/)
## Support Me
If youre 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)
☕ &nbsp;[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
![image](https://badges.pufler.dev/visits/rajnandan1/kener)
## 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
[![Star History Chart](https://api.star-history.com/svg?repos=rajnandan1/kener&type=Date)](https://star-history.com/#rajnandan1/kener&Date)
+14 -11
View File
@@ -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"
}
+56
View File
@@ -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:
+47
View File
@@ -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
+105 -51
View File
@@ -1,61 +1,115 @@
# Docker Compose Configuration
# Description: This file sets up a multi-container environment for Kener (https://github.com/rajnandan1/kener).
# Last Updated: 2025-02-08
# Docker Compose Version: 3.8
# Notes: Ensure that you specify a random value for the `KENER_SECRET_KEY` environment variable before running `docker-compose up -d`.
version: '3.8'
# =============================================================================
# Kener v4 — Production Docker Compose
#
# Usage:
# docker compose up -d
#
# Defaults to the published image. To use a local build instead:
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# =============================================================================
services:
kener:
image: rajnandan1/kener:latest # Change to 'rajnandan1/kener:alpine' for an even smaller image! 😁🚀
container_name: kener
# env_file: custom.env # Uncomment this if you are needing to export environment variables from a custom environment file. By default, Docker will import any variables that exist in `.env`
environment:
TZ: Etc/UTC
KENER_SECRET_KEY: replace_me_with_a_random_string # Keep private!! - best to define in `.env` file or through Docker Secret
# DATABASE_URL: custom_db_url # By default, a SQLite database is used - you may override the database url/type here
# RESEND_API_KEY:
# RESEND_SENDER_EMAIL:
### You most likely will NOT need to change anything below this line. Be sure you know what you're doing!! (https://kener.ing/docs/deployment/#docker-environment-variables)
# PORT: 3000 # Port that app listens on in the container
# KENER_BASE_PATH: # By default, Kener runs at `/`. You may change this to be, e.g. `/status`, etc. Do NOT add a trailing slash!! (more info here: https://kener.ing/docs/deployment/#docker-environment-variables)
# ORIGIN: http://localhost:3000
# NODE_ENV: production # This is already set to "production" by default within the 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:
- data:/app/database # We suggest using a Docker named volume, which is more performant for databases
- $(pwd)/uploads:/app/uploads
# read_only: true # Uncommenting this fortifies security by marking the container's filesystem as read-only (aka no data can be written to the container's filesystem except for explicitly defined writable volumes and bind mounts, an exception has already been defined for `/database` and `/uploads`)
restart: unless-stopped
# depends_on: # <-- Uncomment if you would like to use PostgreSQL or MySQL
# - postgres # ...instead of SQLite
# - mysql #
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Only use below section if you would like to utilize PostgreSQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db`)
postgres:
image: postgres:alpine
name: kener_db
# ---------------------------------------------------------------------------
# Kener — Status Page Application
# ---------------------------------------------------------------------------
kener:
image: rajnandan1/kener:latest
# For Alpine variant use: rajnandan1/kener:alpine
container_name: kener
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
POSTGRES_DB: kener_db
# ── 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
# Only use below section if you would like to utilize MySQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=mysql://db_user:db_password@localhost:3306/kener_db`)
mysql:
image: mariadb:11
name: kener_db
environment:
MYSQL_USER: user
MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
MYSQL_DATABASE: kener_db
MYSQL_RANDOM_ROOT_PASSWORD: true
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
name: kener_db
redis_data:
name: kener_redis
# postgres_data:
# name: kener_postgres
# mysql_data:
# name: kener_mysql
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
# Index documentation into Redis when docs are bundled in the image
if [ -f /app/scripts/index-docs.ts ]; then
echo "[kener] Indexing documentation into Redis..."
node --experimental-strip-types /app/scripts/index-docs.ts || \
echo "[kener] Warning: docs indexing failed (is REDIS_URL set?). Continuing..."
fi
exec "$@"
@@ -1,36 +0,0 @@
#!/usr/bin/with-contenv bash
# used https://github.com/linuxserver/docker-plex as a template
POPULATE_EXAMPLES=false
echo "-------------------------------------"
echo -e "Setting up app config directory based on CONFIG_DIR env: ${CONFIG_DIR}\n"
# make config folder if it does not exist
if [ ! -d "${CONFIG_DIR}" ]; then
echo "Directory does not exist! Creating..."
POPULATE_EXAMPLES=true
mkdir -p "${CONFIG_DIR}"
else
if [ "$(ls -A ${CONFIG_DIR})" ]; then
echo "Directory is not empty, not populating with defaults."
else
POPULATE_EXAMPLES=true
fi
fi
# add example configs
if [ "$POPULATE_EXAMPLES" = true ]; then
echo "Directory is empty, adding defaults..."
mkdir -p "${CONFIG_DIR}"/static
cp -r /app/static/. "${CONFIG_DIR}"/static
cp /app/config/monitors.example.yaml "${CONFIG_DIR}"/monitors.yaml
cp /app/config/site.example.yaml "${CONFIG_DIR}"/site.yaml
fi
# permissions
echo "chown'ing directory to ensure correct permissions."
chown -R abc:abc "${CONFIG_DIR}"
echo "Done!"
echo -e "-------------------------------------\n"
@@ -1 +0,0 @@
oneshot
@@ -1 +0,0 @@
/etc/s6-overlay/s6-rc.d/init-app-config/run
@@ -1,9 +0,0 @@
#!/usr/bin/with-contenv bash
echo -e "\nApp is starting!"
export NODE_ENV=production
cd /app || exit
# Run build first
exec \
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/main.js
@@ -1 +0,0 @@
longrun
-36
View File
@@ -1,36 +0,0 @@
---
title: View Alerts | Kener
description: Learn how to view alerts in kener.
---
# View Alerts
Alerts are used to notify you when a monitor goes down or up. You can view alerts in the Kener dashboard.
## ID
The unique ID of the alert.
## Type
The type of status that the alert is for. Example DOWN, DEGRADED
## Monitor
The monitor for which the alert is triggered.
## Status
The status of the alert. TRIGGERED, RESOLVED
## Checks
The number of checks that have been done.
## Incident
The incident that the alert is part of.
## Created At
The time when the alert was created.
-45
View File
@@ -1,45 +0,0 @@
---
title: Categorize Monitors Guide | Kener
description: Categorize Monitors in Kener
---
# Categorize Monitors
Let us add a category to our monitors.
## Sample monitors.yaml
```yaml
- name: OkBookmarks
description: A free bookmark manager that lets you save and search your bookmarks in the cloud.
tag: "okbookmarks"
image: "https://okbookmarks.com/assets/img/extension_icon128.png"
api:
method: GET
url: https://okbookmarks.com
- name: Earth
description: Our blue planet
tag: "earth"
default_status: "UP"
image: "/earth.png"
category: "Hello"
- name: Frogment
description: A free openAPI spec editor and linter that breaks down your spec into fragments to make editing easier and more intuitive. Visit https://www.frogment.com
tag: "frogment"
image: "/frogment.png"
api:
method: GET
url: https://www.frogment.com
```
## Sample site.yaml
```yaml
#...
categories:
- name: Hello
description: Say Hello to the world
#...
```
The above will have OkBookmarks and Frogment under home. Earth will be under Hello category.
-304
View File
@@ -1,304 +0,0 @@
---
title: Changelogs | Kener
description: Changelogs for Kener
---
# Changelogs
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.2.1
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **New Heartbeat Monitors**
- Added support for push-based monitoring via heartbeats
- Monitors systems that send periodic signals to confirm they're running
- Configurable thresholds for degraded and down states
- Secured with secret token authentication
- Accessible via simple HTTP API endpoints
- **Data Interpolation Improvements**
- Fixed data interpolation issues for more accurate uptime calculations
- Improved handling of timestamp boundaries
### Fixes & Improvements
- **UI Enhancements**
- Better mobile responsiveness for theme toggles and controls
- Fixed layout issues with bottom navigation buttons
- Improved timezone selector display
- **Data Processing**
- Fixed edge case issues with uptime calculations
- Improved handling of "No Data" status in summaries
- More accurate time range display across timezones
- **Documentation**
- Added comprehensive documentation for Heartbeat Monitors
- Updated API documentation with new endpoints
## v3.2.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>
### Features
- **Improved Monitor Evaluation Functions**
- Replaced unsafe `eval()` with secure `Function()` constructors across all monitor types
- Added support for using modules (like cheerio) directly in evaluation functions
- **Enhanced API Monitors**
- Raw response data is now passed directly to eval functions instead of base64 encoded
- Added `modules` parameter with access to cheerio for HTML parsing
- Updated default evaluation function to use the new parameter structure
- **Improved TCP & Ping Monitors**
- Simplified evaluation functions with direct access to ping/TCP data
- Removed unnecessary base64 encoding/decoding steps
- Better error handling for invalid evaluation functions
- **Documentation Updates**
- Updated all examples and documentation for the new evaluation function signatures
- Added more detailed explanations of input parameters
- Improved examples showing usage with the new parameter structure
### Breaking Changes
- **Monitor Evaluation Functions**
- Custom evaluation functions will need to be updated to the new parameter structure
- API monitors: `(statusCode, responseTime, responseRaw, modules)` instead of `(statusCode, responseTime, responseDataBase64)`
- TCP/Ping monitors: `(arrayOfPings)` instead of `(responseDataBase64)`
### Fixes
- Fixed "cheerio is undefined" errors in API monitor evaluations
- Improved error handling and logging for monitor evaluation failures
- Security enhancements by removing `eval()` usage
### Migration
If you're using custom evaluation functions in your monitors, you'll need to update them to the new format:
#### API Monitors
```javascript
// Old format
;(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64)
// Your logic here
})
```
```javascript
// New format
;(async function (statusCode, responseTime, responseRaw, modules) {
// responseRaw is the direct response - no need to decode
// Access cheerio with modules.cheerio
// Your logic here
})
```
#### TCP/Ping Monitors
```javascript
// Old format
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
// Your logic here
})
```
```javascript
// New format
;(async function (arrayOfPings) {
// arrayOfPings is directly available - no need to decode
// Your logic here
})
```
## v3.1.8
<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>
### Features
- **Timezone Support & UI Toggle**
- Added timezone support using `date-fns-tz` for improved date formatting.
- Introduced a UI toggle in settings to switch between different timezones.
- **Enhanced Incident Management**
- Improved incident filtering to prevent duplicate auto incidents when creating manual incidents.
- Added support for incident sources to refine incident handling.
- **Dynamic Cron Job Scheduling**
- Cron jobs are now dynamically added and removed based on active monitors.
- Ensures jobs are triggered in the correct order and prevents duplicate incidents.
- **Monitor Component Improvements**
- Refactored monitor component for better data display and interaction.
- Improved uptime calculations.
- Added a dropdown to select time ranges for better visibility.
### Fixes & Improvements
- **Refactored Incident Handling & Scheduling** for better reliability and performance.
- **UI Responsiveness Fixes** to improve the experience on smaller screens.
- **Dependency Updates** to support new timezone functionality and ensure stability.
## v3.0.10
<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>
### Features
- Added TCP monitors
### Breaking Changes
- Ping monitors will break.
### Fixes
- Bug fixes in the UI
## v3.0.9
<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>
### Features
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
- Introduction of event type `MAINTENANCE` for incidents.
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
- Added category filter for monitor management.
### Fixes
- Support longer TLD in `siteURL` example `https://example.network`
- Remove googleapis preconnect and preload
- Fixed wrong action url in webhook.
## v3.0.1
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Support for i18n in dates.
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
## v3.0.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- New APIs for creating incidents and pushing updates. Read more [here](/docs/kener-apis)
- Incident management is now part of the admin UI and removed from the config file.
- The UI colors have been updated to be more muted.
- Email Notifications for incidents using [resend](https://resend.com).
- New Kener management portal. No monitors.yaml or site.yaml needed anymore
- Login Page and Setup Page
- Remove Github dependency
- Options to disable square or dot pattern
- Support for new languages
- Multiple DB support (mysql, postgres, sqlite3)
- New API reference
- New documentation site
## v2.0.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Added support for sqlite3 and removed dependency on file system
- Added support for postgres database. Read more [here](/docs/database)
- Added support for alerting. Read more [here](/docs/alerting)
- Added color customization. Read more [here](/docs/customize-site#color)
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
- `barStyle`
- `barRoundness`
- `summaryStyle`
### Migration
Kener will automatically migrate your data from file system to sqlite3. If you are using a custom domain, you need to update the `site.yaml` file with the new `siteURL` field. Read more [here](/docs/customize-site#siteURL)
## v0.0.16
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Added support for `hideURLForGet` in monitors. Read more [here](/docs/monitors)
- New SVG badges for LIVE status. Read more [here](/docs/status-badges#live)
- `[Breaking Change]` Removed dependency on Environment variable `PUBLIC_KENER_FOLDER`. Read more [here](#v0-0-16-migration)
- Simplified build and deploy process
- Added support for fonts. Read more [here](/docs/customize-site#font)
- Added support for home page pattern. Read more [here](/docs/customize-site#pattern)
- Added support for adding your analytics provider. Read more [here](/docs/site-analytics)
- New Documentation Site
- Addes support for `sqaures` pattern in home page. Read more [here](/docs/customize-site#pattern)
- Redesigned the UI for better consistency
- Embed now supports background color using a parameter `bgc`. Read more [here](/docs/embed#javascript-parameters)
- Now title in `site.yaml` is `<title>` and `siteName` is actually the name of the site. Read more [here](/docs/customize-site#siteName)
### Migration
#### Source
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
- Move `site.yaml` to `/config` folder
- Move `monitors.yaml` to `/config` folder
#### Docker
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
-42
View File
@@ -1,42 +0,0 @@
---
title: Custom JS and CSS Guide | Kener
description: Custom JS and CSS Guide for Kener
---
Here is a guide to add custom JS and CSS to your Kener instance.
## Adding Custom JS
Add your custom JS to `static/` file. And in the `src/app.html` file, add the following line:
```html
<script src="/your-custom-js-file.js"></script>
```
## Adding Custom CSS
Three are two ways you can add custom CSS to your Kener instance.
### CSS file
Add your custom CSS to `static/` file. And in the `src/app.html` file, add the following line:
```html
<link rel="stylesheet" href="/your-custom-css-file.css" />
```
Do not forget to add the base path if you are using a subpath. For example, if you are using a subpath `/kener`, then the path should be `/kener/your-custom-js-file.js`.
### Inline CSS
To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS there.
```css
.my-class {
color: red;
}
```
<div class="note danger">
Do not include &#x3C;style&#x3E; tags.
</div>
-89
View File
@@ -1,89 +0,0 @@
---
title: Embed Monitor | Kener
description: Embed your monitor in your website
---
# Embed Monitor
There are two ways to embed your monitor in your website
## Javascript
You can embed your monitor in your website using javascript. We recommend using this method as it takes care of the height of the embedded monitor.
```html
<script
async
src="http://[hostname]/embed/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[tag]"
></script>
```
Here is an example
```html
<script
async
src="https://kener.ing/embed/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"
></script>
```
### Parameters
You can pass the following parameters to the embed code
- `theme`: You can pass `light` or `dark` theme
- `monitor`: The monitor url
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
### Demo
<div class="border mx-auto rounded-sm w-585px">
<script async src="/embed/monitor-earth/js?theme=dark&monitor=/embed/monitor-earth"></script>
</div>
## Iframe
This is the simplest way to embed your monitor in your website. You can use the following code to embed your monitor in your website.
```html
<iframe
src="http://[hostname]/embed/monitor-[tag]?theme=light"
width="100%"
height="200"
allowfullscreen="allowfullscreen"
allowpaymentrequest
frameborder="0"
></iframe>
```
Here is an example
```html
<iframe
src="https://kener.ing/embed/monitor-earth?theme=light"
width="100%"
height="100"
allowfullscreen="allowfullscreen"
allowpaymentrequest
frameborder="0"
></iframe>
```
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
### Parameters
You can pass the following parameters to the embed code
- `theme`: You can pass `light` or `dark` theme
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
### Demo
<div class="border mx-auto rounded-sm w-585px">
<iframe src="/embed/monitor-earth?theme=dark" width="100%" height="100" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
</div>
-62
View File
@@ -1,62 +0,0 @@
---
title: Github Setup | Kener
description: Kener uses github for incident management. Issues created in github using certain tags go to kener as incidents.
---
# Github Setup
Kener uses github for incident management. Issues created in github using certain tags go to kener as incidents.
## Step 1: Create Github Repository
Create a [Github Repository](https://github.com/new). It can be either public or private.
## Step 2: Create Github Token
You can create either a classic token or personal access token
### Creating Classic Token
- Go to [Tokens](https://github.com/settings/tokens/new)
- Note: kener
- Expiration: No Expiration
- Scopes: write:packages
- Click on generate Token
### Creating Personal Access Token
- Go to [Personal Access Token](https://github.com/settings/personal-access-tokens/new)
- Token Name: kener
- Expiration: Use custom to select a calendar date
- Description: My Kener
- Repository access: Check Only Selected Repositories. Select your github repository
- Repository Permission: Select Issues Read Write
- Click on generate token
## Step 3: Set environment
```bash
export GH_TOKEN=github_pat_11AD3ZA3Y0
```
## Step 4: Add to Kener
Add your repository details to kener.
![Monitors API](/gh_s.png)
### Github API URL
If you are on github enterprise you can set the github api url. For most users, it will be `https://api.github.com`
### Github Repo
The repository name you created in step 1
### Github Username
Your github username
### Incident History
It is in hours. It means if an issue is created before X hours then kener would not honor it. What it means, is that kener would not show it under active incidents nor it will update the uptime. Default is 30\*24 hours = 720 hours.
View File
-22
View File
@@ -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)
-187
View File
@@ -1,187 +0,0 @@
{
"sidebar": [
{
"sectionTitle": "Getting Started",
"children": [
{
"title": "Introduction",
"link": "/docs/home",
"file": "/home.md"
},
{
"title": "Get Started",
"link": "/docs/quick-start",
"file": "/quick-start.md"
},
{
"title": "Concepts",
"link": "/docs/concepts",
"file": "/concepts.md"
},
{
"title": "Deployment",
"link": "/docs/deployment",
"file": "/deployment.md"
},
{
"title": "Databases",
"link": "/docs/database",
"file": "/database.md"
}
]
},
{
"sectionTitle": "Guides",
"children": [
{
"title": "Setup Environment",
"link": "/docs/environment-vars",
"file": "/environment-vars.md"
},
{
"title": "Use Badges",
"link": "/docs/status-badges",
"file": "/status-badges.md"
},
{
"title": "Setup Monitors",
"link": "/docs/monitors",
"file": "/monitors.md"
},
{
"title": "Setup Triggers",
"link": "/docs/triggers",
"file": "/triggers.md"
},
{
"title": "Setup Site",
"link": "/docs/site",
"file": "/site.md"
},
{
"title": "Setup SEO",
"link": "/docs/seo",
"file": "/seo.md"
},
{
"title": "Setup Home",
"link": "/docs/home-page",
"file": "/home-page.md"
},
{
"title": "Setup Theme",
"link": "/docs/theme",
"file": "/theme.md"
},
{
"title": "View Alerts",
"link": "/docs/alerts",
"file": "/alerts.md"
},
{
"title": "API Keys",
"link": "/docs/apikeys",
"file": "/apikeys.md"
},
{
"title": "Incident Management",
"link": "/docs/incident-management",
"file": "/incident-management.md"
},
{
"title": "Embed",
"link": "/docs/embed",
"file": "/embed.md"
},
{
"title": "Custom JS/CSS",
"link": "/docs/custom-js-css-guide",
"file": "/custom-js-css-guide.md"
},
{
"title": "Internationalization",
"link": "/docs/i18n",
"file": "/i18n.md"
}
]
},
{
"sectionTitle": "Monitors",
"children": [
{
"title": "API/Website Monitor",
"link": "/docs/monitors-api",
"file": "/monitors-api.md"
},
{
"title": "Ping Monitor",
"link": "/docs/monitors-ping",
"file": "/monitors-ping.md"
},
{
"title": "TCP Monitor",
"link": "/docs/monitors-tcp",
"file": "/monitors-tcp.md"
},
{
"title": "DNS Monitor",
"link": "/docs/monitors-dns",
"file": "/monitors-dns.md"
},
{
"title": "Group Monitor",
"link": "/docs/monitors-group",
"file": "/monitors-group.md"
},
{
"title": "SSL Monitor",
"link": "/docs/monitors-ssl",
"file": "/monitors-ssl.md"
},
{
"title": "SQL Monitor",
"link": "/docs/monitors-sql",
"file": "/monitors-sql.md"
},
{
"title": "Heartbeat Monitor",
"link": "/docs/monitors-heartbeat",
"file": "/monitors-heartbeat.md"
}
]
},
{
"sectionTitle": "API Reference",
"children": [
{
"title": "Kener APIs",
"link": "/docs/kener-apis",
"file": "/kener-apis.md"
}
]
},
{
"sectionTitle": "Help",
"children": [
{
"title": "Fonts",
"link": "/docs/custom-fonts",
"file": "/custom-fonts.md"
},
{
"title": "Changelogs",
"link": "/docs/changelogs",
"file": "/changelogs.md"
},
{
"title": "Roadmap",
"link": "/docs/roadmap",
"file": "/roadmap.md"
}
]
}
]
}
-295
View File
@@ -1,295 +0,0 @@
---
title: Triggers | Kener
description: Learn how to set up and work with triggers in kener.
---
# Triggers
Triggers are used to trigger actions based on the status of your monitors. You can use triggers to send notifications, or call webhooks when a monitor goes down or up.
<div class="border rounded-md">
![Trigger API](/documentation/trig_1.png)
</div>
### Name
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The name is used to define the name of the webhook. It is required and has to be a string.
### Description
The description is used to define the description of the webhook. It is optional and has to be a string.
Kener supports the following triggers:
- [Webhook](#webhook)
- [Discord](#discord)
- [Slack](#slack)
- [Email](#email)
## Webhook
Webhook triggers are used to send a HTTP POST request to a URL when a monitor goes down or up.
<div class="border rounded-md">
![Trigger API](/documentation/trig_web.png)
</div>
### URL
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The URL is used to define the URL of the webhook. It is required and has to be a valid URL.
You can also pass secrets that are set in the environment variables.
Example: `https://example.com/webhook?secret=$SECRET_X`. Make sure `$SECRET_X` is set in the environment variables.
### Method
Method will always be `POST`
### Headers
The headers are used to define the headers that should be sent with the request. It is optional and has to be a valid JSON object. You can add secrets that are set in the environment variables.
Example: `Authorization: Bearer $SECRET_Y`. Make sure `$SECRET_Y` is set in the environment variables.
While sending webhook kener will add two more headers: `Content-Type: application/json` and `User-Agent: Kener/3.0.0`.
### Body
Body of the webhook will be sent as below:
```json
{
"id": "mockoon-9",
"alert_name": "Mockoon DOWN",
"severity": "critical",
"status": "TRIGGERED",
"source": "Kener",
"timestamp": "2024-11-27T04:55:00.369Z",
"description": "🚨 **Service Alert**: Check the details below",
"details": {
"metric": "Mockoon",
"current_value": 1,
"threshold": 1
},
"actions": [
{
"text": "View Monitor",
"url": "https://kener.ing/monitor-mockoon"
}
]
}
```
| Key | Description | Variable |
| --------------------- | ----------------------------------------------------------- | ----------------------------- |
| id | Unique ID of the alert | ${id} |
| alert_name | Name of the alert | ${alert_name} |
| severity | Severity of the alert. Can be `critical`, `warn` | ${severity} |
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` | ${status} |
| source | Source of the alert. Can be `Kener` | ${source} |
| timestamp | Timestamp of the alert | ${timestamp} |
| description | Description of the alert. This you can customize. See below | ${description} |
| details | Details of the alert. | - |
| details.metric | Name of the monitor | ${metric} |
| details.current_value | Current value of the monitor | ${current_value} |
| details.threshold | Alert trigger threshold of the monitor | ${threshold} |
| actions | Actions to be taken. Link to view the monitor. | ${action_text}, ${action_url} |
### Custom Body
You can customize the body of the webhook. You can use the variables mentioned above. If you are not using a json body then please make sure you are using the right content-type by setting custom headers. See examples below.
## Discord
Discord triggers are used to send a message to a discord channel when a monitor goes down or up.
<div class="border rounded-md">
![Trigger API](/documentation/trig_2.png)
</div>
### Discord URL
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The Discord URL is used to define the URL of the discord webhook. It is required and has to be a valid URL.
#### How to get the Discord URL?
1. Go to your discord server
2. Right-click on the channel you want to send the messages
3. Click on `Edit Channel`
4. Go to `Integrations`
5. Click on `Create Webhook`
6. Copy the URL
#### Discord Message
The discord message when alert is `TRIGGERED` will look like this
![Discord](/discord.png)
The discord message when alert is `RESOLVED` will look like this
![Discord](/documentation/discord_resolved.png)
## Slack
Slack triggers are used to send a message to a slack channel when a monitor goes down or up.
<div class="border rounded-md">
![Trigger API](/documentation/trig_3.png)
</div>
### Slack URL
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The Slack URL is used to define the URL of the slack webhook. It is required and has to be a valid URL.
#### How to get the Slack URL?
1. Go to your slack workspace
2. Click on `Apps` on the left sidebar
3. Search for `Incoming Webhooks`
4. Click on `Add to Slack`
5. Select the channel you want to send the messages
6. Click on `Add Incoming Webhook Integration`
7. Copy the URL
#### Slack Message
The slack message when alert is `TRIGGERED` will look like this
![Slack](/slack.png)
The slack message when alert is `RESOLVED` will look like this
![Slack](/documentation/slack_resolved.png)
## Email
Email triggers are used to send an email when a monitor goes down or up. Kener supports sending emails via [resend](https://resend.com) or over SMTP.
<div class="border rounded-md">
![Trigger API](/documentation/trig_4.png)
</div>
### Resend
To send emails using Resend you just need to set `RESEND_API_KEY` in the environment variables.
### SMTP
To send emails using SMTP, please enter
- Host: SMTP server host
- Port: SMTP server port
- User: SMTP server username
- Password: SMTP server password
<div class="note danger">
Since the password will be stored as plain text we encourage to use environment variables for the password. Let us say if you have an environment variable `SMTP_PASSWORD` then you can use it as `$SMTP_PASSWORD`.
</div>
<div class=" note info ">
If your SMTP provider does require username and password, you can set `SMTP_USER` and `SMTP_PASS` to `-`.
</div>
### To
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The email addresses to which the email should be sent. It is required and has to be a valid email addresses. You can pass multiple email addresses separated by a comma.
### Sender
The email address from which the email should be sent.
It should be in the format `Name <email@address.com>`
If you have not connected your domain with resend, then use `Some Name <onboarding@resend.dev>`
### Subject
Subject of the email when `TRIGGERED`
```text
[TRIGGERED] Mockoon DOWN at 2024-12-27T04:42:01.430Z
```
Subject of the email when `RESOLVED`
```text
[RESOLVED] Mockoon DOWN at 2024-12-27T04:42:01.430Z
```
### Body
The emaik message when alert is `TRIGGERED` will look like this
![Slack](/documentation/em_t.png)
The emaik message when alert is `RESOLVED` will look like this
![Slack](/documentation/em_r.png)
---
## Edit Triggers
Click on the ⚙️ to edit the trigger.
### Deactivate Trigger
You can deactivate the trigger by switching the toggle to off. You cannot send message to a deactivated trigger. Any monitor with this trigger will not send any notifications.
---
## Examples
### Telegram
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
```json
{
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
"text": "<b>${alert_name}</b>\n\n<b>Severity:</b> <code>${severity}</code>\n<b>Status:</b> ${status}\n<b>Source:</b> Kener\n<b>Time:</b> ${timestamp}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>${metric}\n- <b>Current Value:</b> <code>${current_value}</code>\n- <b>Threshold:</b> <code>${threshold}</code>\n\n🔍 <a href=\"${action_url}\">${action_text}</a>",
"parse_mode": "HTML"
}
```
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
-20
View File
@@ -1,20 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
background-color: #111;
}
</style>
</head>
<body>
Hello world
<script
async
src="http://localhost:3000/embed/monitor-earth/js?theme=dark&bgc=111&locale=hi&monitor=http://localhost:3000/embed/monitor-earth"
></script>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
set -e
# Automatically set PUBLIC_WHITE_LABEL based on WHITE_LABEL
export PUBLIC_WHITE_LABEL="${WHITE_LABEL}"
# Replace shell with the given command (from CMD or runtime args)
exec "$@"
-18
View File
@@ -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
}
+10 -2
View File
@@ -1,4 +1,3 @@
// @ts-nocheck
import dotenv from "dotenv";
dotenv.config();
@@ -8,7 +7,16 @@ const databaseURLParts = databaseURL.split("://");
const databaseType = databaseURLParts[0];
const databasePath = databaseURLParts[1];
const knexOb = {
interface KnexConfig {
migrations: { directory: string };
seeds: { directory: string };
databaseType: string;
client?: string;
connection?: string | { filename: string };
useNullAsDefault?: boolean;
}
const knexOb: KnexConfig = {
migrations: {
directory: "./migrations",
},
+27
View File
@@ -0,0 +1,27 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["src/app.html", "build/main.js", "scripts/**/*.{js,ts}", "migrations/**/*.{js,ts}", "seeds/**/*.{js,ts}"],
"project": ["src/**/*.{js,ts,svelte}", "scripts/**/*.{js,ts}", "migrations/**/*.{js,ts}", "seeds/**/*.{js,ts}"],
"ignore": ["src/lib/components/ui/**"],
"ignoreDependencies": [
"@babel/runtime",
"js-yaml",
"mysql2",
"node-cache",
"pg",
"pg-pool",
"randomstring",
"style-to-object",
"lucide-svelte",
"marked-gfm-heading-id"
],
"ignoreExportsUsedInFile": {
"interface": true,
"type": true
},
"ignoreBinaries": [],
"compilers": {
"css": ["postcss"],
"svelte": ["svelte"]
}
}
-99
View File
@@ -1,99 +0,0 @@
import { handler } from "./build/handler.js";
import { apiReference } from "@scalar/express-api-reference";
import dotenv from "dotenv";
dotenv.config();
import express from "express";
import Startup from "./src/lib/server/startup.js";
import { GetSiteMap } from "./src/lib/server/controllers/controller.js";
import fs from "fs-extra";
import knex from "knex";
import knexOb from "./knexfile.js";
const PORT = process.env.PORT || 3000;
const base = process.env.KENER_BASE_PATH || "";
const app = express();
const db = knex(knexOb);
app.use((req, res, next) => {
if (req.path.startsWith("/embed")) {
res.setHeader("Content-Security-Policy", "frame-ancestors *");
}
res.setHeader("X-Powered-By", "Kener");
next();
});
app.get(base + "/healthcheck", (req, res) => {
res.end("ok");
});
app.get(base + "/sitemap.xml", async (req, res) => {
res.header("Content-Type", "application/xml");
res.send(await GetSiteMap());
});
//part /uploads server static files from static/uploads
//set env variable for upload path
process.env.UPLOAD_PATH = "./uploads";
app.use(base + "/uploads", express.static("uploads"));
try {
const openapiJSON = fs.readFileSync("./openapi.json", "utf-8");
app.use(
"/api-reference",
apiReference({
spec: {
content: openapiJSON
},
theme: "alternate",
hideModels: true,
hideTestRequestButton: true,
darkMode: true,
metaData: {
title: "Kener API Reference",
description: "Kener free open source status page API Reference",
ogDescription: "Kener free open source status page API Reference",
ogTitle: "Kener API Reference",
ogImage: "https://kener.ing/newbg.png",
twitterCard: "summary_large_image",
twitterTitle: "Kener API Reference",
twitterDescription: "Kener free open source status page API Reference",
twitterImage: "https://kener.ing/newbg.png"
},
favicon: "https://kener.ing/logo96.png"
})
);
} catch (e) {
console.warn("Error loading openapi.json, but that is okay.");
}
app.use(handler);
//migrations
async function runMigrations() {
try {
console.log("Running migrations...");
await db.migrate.latest(); // Runs migrations to the latest state
console.log("Migrations completed successfully!");
} catch (err) {
console.error("Error running migrations:", err);
}
}
//seed
async function runSeed() {
try {
console.log("Running seed...");
await db.seed.run(); // Runs seed to the latest state
console.log("Seed completed successfully!");
} catch (err) {
console.error("Error running seed:", err);
}
}
app.listen(PORT, async () => {
await runMigrations();
await runSeed();
await db.destroy();
Startup();
console.log("Kener is running on port " + PORT + "!");
});
-135
View File
@@ -1,135 +0,0 @@
// migrations/YYYYMMDDHHMMSS_create_monitoring_tables.js
export function up(knex) {
return (
knex.schema
// Create monitoring_data table
.createTable("monitoring_data", (table) => {
table.string("monitor_tag", 255).notNullable();
table.integer("timestamp").notNullable();
table.text("status");
table.float("latency", 8, 2);
table.text("type");
table.primary(["monitor_tag", "timestamp"]);
})
// Create monitor_alerts table
.createTable("monitor_alerts", (table) => {
table.increments("id").primary();
table.string("monitor_tag", 255).notNullable();
table.string("monitor_status", 255).notNullable();
table.string("alert_status", 255).notNullable();
table.integer("health_checks").notNullable();
table.integer("incident_number").defaultTo(0);
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
// Add index to monitor_alerts table
.raw(
"CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)"
)
.createTable("site_data", (table) => {
table.increments("id").primary();
table.string("key", 255).notNullable().unique();
table.text("value").notNullable();
table.string("data_type", 255).notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("monitors", (table) => {
table.increments("id").primary();
table.string("tag", 255).notNullable().unique();
table.string("name", 255).notNullable().unique();
table.text("description");
table.text("image");
table.string("cron", 255);
table.string("default_status", 255);
table.string("status", 255);
table.string("category_name", 255);
table.string("monitor_type", 255);
table.string("down_trigger", 255);
table.string("degraded_trigger", 255);
table.text("type_data");
table.integer("day_degraded_minimum_count");
table.integer("day_down_minimum_count");
table.string("include_degraded_in_downtime", 255).defaultTo("NO");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("triggers", (table) => {
table.increments("id").primary();
table.string("name", 255).notNullable().unique();
table.string("trigger_type", 255);
table.text("trigger_desc");
table.string("trigger_status", 255);
table.text("trigger_meta");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("users", (table) => {
table.increments("id").primary();
table.string("email", 255).notNullable().unique();
table.string("name", 255).notNullable();
table.string("password_hash", 255).notNullable();
table.integer("is_active").defaultTo(1);
table.integer("is_verified").defaultTo(0);
table.string("role", 255).defaultTo("user");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("api_keys", (table) => {
table.increments("id").primary();
table.string("name", 255).notNullable().unique();
table.string("hashed_key", 255).notNullable().unique();
table.string("masked_key", 255).notNullable();
table.string("status", 255).defaultTo("ACTIVE");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("incidents", (table) => {
table.increments("id").primary();
table.string("title", 255).notNullable();
table.integer("start_date_time").notNullable();
table.integer("end_date_time");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.string("status", 255).defaultTo("ACTIVE");
table.string("state", 255).defaultTo("INVESTIGATING");
})
.createTable("incident_monitors", (table) => {
table.increments("id").primary();
table.string("monitor_tag", 255).notNullable();
table.string("monitor_impact", 255);
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.integer("incident_id").notNullable();
table.unique(["monitor_tag", "incident_id"]);
})
.createTable("incident_comments", (table) => {
table.increments("id").primary();
table.text("comment").notNullable();
table.integer("incident_id").notNullable();
table.integer("commented_at").notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.string("status", 255).defaultTo("ACTIVE");
table.string("state", 255).defaultTo("INVESTIGATING");
})
);
}
export function down(knex) {
return (
knex.schema
// Drop tables in reverse order
.dropTableIfExists("monitor_alerts")
.dropTableIfExists("monitoring_data")
.dropTableIfExists("site_data")
.dropTableIfExists("monitors")
.dropTableIfExists("triggers")
.dropTableIfExists("users")
.dropTableIfExists("api_keys")
.dropTableIfExists("incidents")
.dropTableIfExists("incident_monitors")
.dropTableIfExists("incident_comments")
);
}
+159
View File
@@ -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");
}
@@ -1,11 +0,0 @@
export function up(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.text("incident_type").defaultTo("INCIDENT");
});
}
export function down(knex) {
return 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_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");
});
}
@@ -1,11 +0,0 @@
export function up(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.text("incident_source").defaultTo("DASHBOARD");
});
}
export function down(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.dropColumn("incident_source");
});
}
@@ -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");
});
}
}
-1102
View File
File diff suppressed because it is too large Load Diff
-804
View File
@@ -1,804 +0,0 @@
---
info:
title: Kener API
version: 3.0.0
description: |
# Kener Selfhost node js status page
![Kener](https://kener.ing/newbg.png "kener")
API specification for Kener status page and incident management system. This API spec was created using [Frogment](https://www.frogment.app)
contact:
name: Raj Nandan Sharma
email: rajnandan1@gmail.com
url: https://github.com/rajnandan1/kener/issues
license:
name: MIT
url: https://opensource.org/licenses/MIT
openapi: 3.0.0
servers:
- url: https://{host}
description: Kener host URL
tags:
- name: Monitors
description: APIs to interact with monitors
- name: Incidents
description: APIs to integrate incidents
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: enter your api key here
schemas:
MonitorStatus:
type: object
description: Monitor Status
required:
- status
- latency
- tag
properties:
status:
type: string
example: UP
enum:
- UP
- DOWN
- DEGRADED
latency:
type: number
description: In seconds
example: 100
timestampInSeconds:
type: integer
description: UTC timestamp in seconds
example: 1731251760
tag:
type: string
example: earth
description: Tag of a monitor
example:
status: UP
timestampInSeconds: 1731251760
latency: 100
tag: earth
StatusResponse:
type: object
description: Status of a monitor given a tag
properties:
status:
type: string
example: UP
enum:
- UP
- DOWN
- DEGRADED
uptime:
type: string
example: '100'
last_updated_at:
type: integer
example: 1731251760
example:
status: UP
last_updated_at: 1731251760
uptime: '100'
Incident:
type: object
description: body of an incident
properties:
start_date_time:
type: integer
description: UTC timestamp in seconds
example: 1731901920
end_date_time:
type: integer
description: UTC timestamp in seconds
nullable: true
example: 1704123938
title:
type: string
example: Outage in mumbai
description: title of the incident
created_at:
type: string
example: '2025-01-09 04:12:01'
description: created at time
updated_at:
type: string
example: '2025-01-09 06:10:00'
description: updated at time
status:
type: string
example: OPEN
description: delete or not deleted incident
enum:
- OPEN
- CLOSED
state:
type: string
description: the current status of the incident
example: INVESTIGATING
enum:
- INVESTIGATING
- IDENTIFIED
- MONITORING
- RESOLVED
example:
start_date_time: 1731901920
end_date_time: 1704123938
title: title of the incident
created_at: '2025-01-09 04:12:01'
updated_at: '2025-01-09 04:12:01'
state: INVESTIGATING
status: OPEN
id: 4
IncidentResponse:
description: Incident response schema
allOf:
- "$ref": "#/components/schemas/Incident"
Comment:
type: object
description: Comment of an incident
properties:
comment:
type: string
example: comment 1
commented_at:
type: integer
example: 1736398336
state:
type: string
example: IDENTIFIED
example:
body: comment 2
commented_at: 1736398336
state: IDENTIFIED
CommentResponse:
type: object
description: Comment Response
properties:
id:
type: integer
description: ID of the comment
example: 60
comment:
type: string
description: The content of the comment
example: Sometimes, you want all the goodness of moment#from but you don't
want to have to create two moments, you just want to display a length
of time.
incident_id:
type: integer
description: ID of the associated incident
example: 24
commented_at:
type: integer
description: Timestamp when the comment was added
example: 1736398336
created_at:
type: string
format: date-time
description: Timestamp when the comment was created in ISO 8601 format
example: '2025-01-09 04:52:16'
updated_at:
type: string
format: date-time
description: Timestamp when the comment was last updated in ISO 8601 format
example: '2025-01-09 04:52:16'
status:
type: string
description: Current status of the comment
example: ACTIVE
state:
type: string
description: the current status of the incident
example: INVESTIGATING
enum:
- INVESTIGATING
- IDENTIFIED
- MONITORING
- RESOLVED
example:
id: 60
comment: Sometimes, you want all the goodness of moment
incident_id: 24
commented_at: 1736398336
created_at: '2025-01-09 04:52:16'
updated_at: '2025-01-09 04:52:16'
status: ACTIVE
state: MONITORING
IncidentStatus:
type: object
description: Status of the incident
properties:
isIdentified:
type: boolean
description: Has the incident been indetified
example: true
isResolved:
type: boolean
description: has the incident been resolved
example: true
endDatetime:
type: integer
description: Incident end time
example: 1731901920
example:
isIdentified: true
isResolved: true
endDatetime: 1731901920
IncidentCreateRequest:
title: Incident Create Request Body
type: object
description: body of an incident
required:
- title
- start_date_time
properties:
start_date_time:
type: integer
description: UTC timestamp in seconds
example: 1731901920
title:
type: string
example: Outage in mumbai
description: title of the incident
example:
start_date_time: 1731901920
title: title of the incident
responses:
Response401:
description: Bad API keys response
content:
application/json:
schema:
type: object
properties:
error:
type: string
description: Invalid token response
example: invalid token
example:
error: invalid token
Response400:
description: bad request
content:
application/json:
schema:
type: object
properties:
error:
type: string
description: bad request while calling kener apis
example: unknown tags
example:
error: unknown tags
examples:
GetMonitorStatusExample200:
summary: response of get status of a monitor
description: Some Description
value:
status: UP
uptime: '100'
lastUpdatedAt: 1731901920
DegradedRequestBody:
summary: update to degraded
description: Some Description
value:
status: DEGRADED
timestampInSeconds: 1731251760
latency: 100
tag: earth
CreateIncidentResponse:
summary: create incident response
description: create incident response
value:
start_date_time: 1731901920
end_date_time: 1731901920
id: 4
created_at: '2025-01-09 04:12:01'
updated_at: '2025-01-10 04:12:01'
title: title of the incident
status: OPEN
state: INVESTIGATING
CreateIncidentRequest:
summary: create incident request body
description: create incident request body
value:
start_date_time: 1731901920
title: title of the incident
SearchIncidentsResponse:
summary: array of incidents
description: Some Description
value:
- id: 2
title: future wala
start_date_time: 1736774486
end_date_time: 1737033787
created_at: '2025-01-12 13:21:32'
updated_at: '2025-01-12 13:23:15'
status: OPEN
state: RESOLVED
- id: 1
title: internatiinal asda
start_date_time: 1736684329
end_date_time:
created_at: '2025-01-12 12:18:53'
updated_at: '2025-01-12 13:21:32'
status: OPEN
state: INVESTIGATING
CommentsResponse:
summary: list of comments
description: Some Description
value:
- id: 58
comment: idensad
incident_id: 24
commented_at: 1736398295
created_at: '2025-01-09 04:51:37'
updated_at: '2025-01-09 04:51:37'
status: ACTIVE
state: IDENTIFIED
- id: 57
comment: Sometimes, you want all the goodness of moment#from but you don't
want to have to create two moments, you just want to display a length of
time.
incident_id: 24
commented_at: 1736398279
created_at: '2025-01-09 04:51:19'
updated_at: '2025-01-09 04:51:19'
status: ACTIVE
state: INVESTIGATING
CommentRequestBody:
summary: request body for a update
description: request body for a update to add in an incident
value:
comment: This is a comment
commented_at: 1736398336
state: IDENTIFIED
CreateCommentResponse:
summary: create update response body sample
description: create update response body sample
value:
id: 60
comment: Sometimes, you want all the goodness of moment
incident_id: 24
commented_at: 1736398336
created_at: '2025-01-09 04:52:16'
updated_at: '2025-01-09 04:52:16'
status: ACTIVE
state: MONITORING
paths:
"/api/status":
post:
operationId: updateMonitorStatus
summary: Update status of a monitor
description: Update status of a monitor at a given timestamp UTC
tags:
- Monitors
security:
- bearerAuth: []
requestBody:
required: true
description: request body to update status of a monitor
content:
application/json:
schema:
"$ref": "#/components/schemas/MonitorStatus"
examples:
degraded:
"$ref": "#/components/examples/DegradedRequestBody"
responses:
'200':
description: Status updated successfully
content:
application/json:
schema:
type: object
properties:
status:
type: integer
example: 200
message:
type: string
example: success at 1731251760
example:
status: 200
message: success at 1731251760
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
get:
operationId: getMonitorStatus
summary: Get status of a monitor
description: get status of a monitor at timestamp
tags:
- Monitors
security:
- bearerAuth: []
parameters:
- name: tag
in: query
required: true
description: monitor tag to get status of it
schema:
type: string
example: earth
responses:
'200':
description: Monitor status retrieved successfully
content:
application/json:
schema:
"$ref": "#/components/schemas/StatusResponse"
examples:
successExample:
"$ref": "#/components/examples/GetMonitorStatusExample200"
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
"/api/incident":
post:
operationId: createIncident
summary: Create a new incident
description: API to create incidents
tags:
- Incidents
security:
- bearerAuth: []
requestBody:
required: true
description: request body to manually create an incident
content:
application/json:
schema:
"$ref": "#/components/schemas/IncidentCreateRequest"
examples:
sample:
"$ref": "#/components/examples/CreateIncidentRequest"
responses:
'200':
description: Incident created successfully
content:
application/json:
schema:
"$ref": "#/components/schemas/IncidentResponse"
examples:
success:
"$ref": "#/components/examples/CreateIncidentResponse"
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
get:
operationId: searchIncidents
summary: Search for incidents
description: API to get incidents
tags:
- Incidents
security:
- bearerAuth: []
parameters:
- name: status
in: query
description: status of the incident. Can be open or close. close means deleted
schema:
type: string
description: status of the incident. Can be open or close. close means deleted
enum:
- OPEN
- CLOSED
default: OPEN
example: OPEN
- name: state
in: query
description: state of the incident. Can be open or close
schema:
type: string
description: state of the incident. Can be open or close
enum:
- INVESTIGATING
- IDENTIFIED
- MONITORING
- RESOLVED
default: INVESTIGATING
example: IDENTIFIED
- name: page
in: query
description: page number
schema:
type: integer
description: page number
default: 1
minimum: 1
example: 1
- name: limit
in: query
description: how many per page
schema:
type: integer
default: 10
description: how many per page
maximum: 100
example: 10
- name: start_date_time
description: start time
in: query
schema:
type: integer
description: start time
example: 1731866475
- name: end_date_time
description: end time
in: query
schema:
type: integer
description: end time
example: 1731866475
responses:
'200':
description: Search results retrieved successfully
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/IncidentResponse"
examples:
success:
"$ref": "#/components/examples/SearchIncidentsResponse"
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
"/api/incident/{incident_id}":
parameters:
- name: incident_id
in: path
required: true
description: incident id as an integer to get incident by id
schema:
type: integer
example: 4
get:
operationId: getIncident
summary: Get an incident by id
description: API to get an incident by incident id
tags:
- Incidents
security:
- bearerAuth: []
responses:
'200':
description: Incident retrieved successfully
content:
application/json:
schema:
"$ref": "#/components/schemas/IncidentResponse"
examples:
success:
"$ref": "#/components/examples/CreateIncidentResponse"
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
patch:
operationId: updateIncident
summary: Update an incident
description: API to update an incident by incident id
tags:
- Incidents
security:
- bearerAuth: []
requestBody:
required: true
description: search for an incident
content:
application/json:
schema:
"$ref": "#/components/schemas/Incident"
examples:
sample:
"$ref": "#/components/examples/CreateIncidentRequest"
responses:
'200':
description: Incident updated successfully
content:
application/json:
schema:
"$ref": "#/components/schemas/IncidentResponse"
examples:
success:
"$ref": "#/components/examples/CreateIncidentResponse"
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
"/api/incident/{incident_id}/updates":
parameters:
- name: incident_id
in: path
required: true
description: incident id to fetch comment for
schema:
type: integer
example: 4
post:
operationId: addIncidentComment
summary: Add an update to an incident
description: API to create update for an incident
tags:
- Incidents
security:
- bearerAuth: []
requestBody:
required: true
description: body to add an update
content:
application/json:
schema:
"$ref": "#/components/schemas/Comment"
examples:
sample:
"$ref": "#/components/examples/CommentRequestBody"
responses:
'200':
description: Comment added successfully
content:
application/json:
schema:
"$ref": "#/components/schemas/CommentResponse"
examples:
sample:
"$ref": "#/components/examples/CreateCommentResponse"
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
get:
operationId: getIncidentComments
summary: Get updates for an incident
description: API to get updates for an incident
tags:
- Incidents
security:
- bearerAuth: []
responses:
'200':
description: updates retrieved successfully
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/CommentResponse"
examples:
success:
"$ref": "#/components/examples/CommentsResponse"
'400':
"$ref": "#/components/responses/Response400"
'401':
"$ref": "#/components/responses/Response401"
"/api/incident/{incident_id}/monitors":
parameters:
- name: incident_id
in: path
required: true
description: incident id to fetch comment for
schema:
type: integer
example: 4
get:
summary: Get monitors for an incident
operationId: getIncidentMonitors
tags:
- Incidents
parameters:
- name: incident_id
in: path
required: true
schema:
type: integer
description: The ID of the incident
responses:
'200':
description: List of monitors for the incident
content:
application/json:
schema:
type: array
items:
type: object
properties:
monitor_tag:
type: string
description: The tag of the monitor
example: earth
monitor_impact:
type: string
description: The impact status of the monitor
example: DOWN
security:
- bearerAuth: []
post:
summary: Add a monitor to an incident
operationId: addIncidentMonitor
tags:
- Incidents
parameters:
- name: incident_id
in: path
required: true
schema:
type: integer
description: The ID of the incident
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
tag:
type: string
description: The tag for the monitor
example: okbookmarks
impact:
type: string
description: The impact status of the monitor
example: DEGRADED
responses:
'200':
description: Monitor added successfully
content:
application/json:
schema:
type: array
items:
type: object
properties:
monitor_tag:
type: string
description: The tag of the monitor
example: okbookmarks
monitor_impact:
type: string
description: The impact status of the monitor
example: DEGRADED
security:
- bearerAuth: []
delete:
summary: Delete a monitor from an incident
operationId: deleteIncidentMonitor
tags:
- Incidents
parameters:
- name: incident_id
in: path
required: true
schema:
type: integer
description: The ID of the incident
- name: tag
in: query
required: true
schema:
type: string
description: The tag of the monitor to delete
responses:
'200':
description: Monitor deleted successfully
content:
application/json:
schema:
type: object
properties:
message:
type: string
description: Confirmation message
example: Monitor deleted successfully
security:
- bearerAuth: []
+5548 -3450
View File
File diff suppressed because it is too large Load Diff
+152 -93
View File
@@ -1,115 +1,174 @@
{
"name": "kener",
"version": "3.2.4",
"version": "4.0.8",
"type": "module",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
"description": "Kener is a modern, open-source status page application built with Node.js. It provides real-time monitoring, uptime tracking, incident management, and beautiful dashboards. Perfect for DevOps teams, SaaS providers, and businesses needing reliable service status communication with minimal setup.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
"status page",
"uptime monitoring",
"incident management",
"DevOps tools",
"service reliability",
"open source",
"Node.js",
"dashboard",
"system monitoring",
"status alerts",
"outage communication",
"API monitoring",
"SaaS status",
"performance metrics",
"status reporting"
],
"repository": {
"type": "git",
"url": "https://github.com/rajnandan1/kener.git"
},
"scripts": {
"build": "vite build",
"preview": "vite preview",
"build": "node scripts/build-sveltekit.js && node scripts/build-server.js",
"build-with-docs": "node scripts/build-sveltekit.js --with-docs && node scripts/build-server.js",
"build:sveltekit": "vite build",
"build:server": "node scripts/build-server.js",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"configure": "node build.js",
"migrate": "npx knex migrate:latest",
"preseed": "npx knex migrate:latest",
"seed": "npx knex seed:run",
"predev": "npm run seed",
"devschedule": "node src/lib/server/startup.js",
"schedule": "node src/lib/server/startup.js",
"development": "vite dev",
"generate-readme": "node scripts/generate-readme.js",
"dev": "npm-run-all --parallel devschedule development",
"development": "vite dev",
"devschedule": "vite-node src/lib/server/startup.ts",
"generate-readme": "node scripts/generate-readme.js",
"index-docs": "vite-node scripts/index-docs.ts",
"migrate": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
"predev": "npm run seed",
"prepare": "svelte-kit sync || echo ''",
"preseed": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
"prettify": "prettier --write .",
"start": "node main.js"
"preview": "vite preview",
"schedule": "vite-node src/lib/server/startup.ts",
"seed": "npx knex seed:run",
"start": "node build/main.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.27.4",
"@tailwindcss/typography": "^0.5.10",
"@zerodevx/svelte-toast": "^0.9.6",
"autoprefixer": "^10.4.14",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"date-picker-svelte": "^2.15.1",
"mustache": "^4.2.0",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"svelte": "^4.0.5",
"svelte-awesome-color-picker": "^3.1.4",
"svelte-check": "^3.6.0",
"svelte-dnd-action": "^0.9.55",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
"analytics": "^0.8.14",
"axios": "^1.6.2",
"badge-maker": "^3.3.1",
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"cheerio": "^1.0.0",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dns2": "^2.1.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.292.0",
"marked": "^11.1.1",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"mysql2": "^3.12.0",
"node-cache": "^5.1.2",
"nodemailer": "^6.10.0",
"npm-run-all": "^4.1.5",
"pg": "^8.13.1",
"pg-pool": "^3.7.0",
"ping": "^0.4.4",
"queue": "^7.0.0",
"randomstring": "^1.3.0",
"resend": "^4.0.1",
"svelte-legos": "^0.2.5",
"tailwind-merge": "^2.0.0",
"tailwind-variants": "^0.1.18"
"@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-node": "^5.4.0",
"@sveltejs/kit": "^2.48.5",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"@types/bcrypt": "^6.0.0",
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.8",
"@types/dns2": "^2.0.10",
"@types/express": "^5.0.6",
"@types/fs-extra": "^11.0.4",
"@types/jsonwebtoken": "^9.0.10",
"@types/mustache": "^4.2.6",
"@types/node": "^25.0.3",
"@types/nodemailer": "^7.0.4",
"autoprefixer": "^10.4.22",
"clsx": "^2.1.1",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"date-picker-svelte": "^2.17.0",
"layerchart": "^2.0.0-next.43",
"postcss": "^8.5.6",
"postcss-load-config": "^6.0.1",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.43.8",
"svelte-awesome-color-picker": "^4.1.0",
"svelte-check": "^4.3.4",
"svelte-sonner": "^1.0.7",
"tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vaul-svelte": "^1.0.0-next.7",
"vite": "^7.2.2",
"vite-node": "^5.3.0",
"vite-plugin-devtools-json": "^1.0.0"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@babel/runtime": "^7.28.4",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/language": "^6.12.1",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.11",
"@formkit/auto-animate": "^0.9.0",
"@humanspeak/svelte-purify": "^0.0.6",
"@number-flow/svelte": "^0.3.9",
"@scalar/express-api-reference": "^0.8.28",
"@scalar/sveltekit": "^0.1.43",
"@scaleway/random-name": "^5.1.4",
"@uiw/codemirror-theme-github": "^4.25.3",
"axios": "^1.13.2",
"badge-maker": "^5.0.2",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0",
"bits-ui": "^2.14.4",
"bullmq": "^5.66.2",
"cheerio": "^1.1.2",
"croner": "^9.1.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dns2": "^2.1.0",
"dotenv": "^17.2.3",
"esbuild": "^0.27.2",
"express": "^5.2.1",
"figlet": "^1.9.4",
"flexsearch": "^0.8.212",
"front-matter": "^4.0.2",
"fs-extra": "^11.3.2",
"gamedig": "^5.3.2",
"glob": "^13.0.6",
"highlight.js": "^11.11.1",
"ioredis": "^5.8.2",
"js-yaml": "^4.1.1",
"jsonwebtoken": "^9.0.3",
"knex": "^3.1.0",
"lucide-svelte": "^0.561.0",
"marked": "^17.0.1",
"marked-alert": "^2.1.2",
"marked-custom-heading-id": "^2.0.16",
"marked-gfm-heading-id": "^4.1.3",
"marked-highlight": "^2.2.3",
"marked-plaintify": "^1.1.1",
"mobile-detect": "^1.4.5",
"mode-watcher": "^1.1.0",
"mustache": "^4.2.0",
"mysql2": "^3.15.3",
"nanoid": "^5.1.6",
"node-cache": "^5.1.2",
"nodemailer": "^7.0.11",
"npm-run-all": "^4.1.5",
"pg": "^8.16.3",
"pg-pool": "^3.10.1",
"ping": "^1.0.0",
"randomstring": "^1.3.1",
"resend": "^6.6.0",
"rrule": "^2.8.1",
"sharp": "^0.34.5",
"striptags": "^3.2.0",
"style-to-object": "^1.0.14",
"svelte-codemirror-editor": "^2.1.0",
"vite-plugin-package-version": "^1.1.0"
}
}
-13
View File
@@ -1,13 +0,0 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
};
module.exports = config;
+52
View File
@@ -0,0 +1,52 @@
import * as esbuild from "esbuild";
import { readFileSync } from "fs";
const pkg = JSON.parse(readFileSync("./package.json", "utf8"));
// Collect all dependency names to externalize, except CJS packages
// that need to be bundled for ESM compatibility
const CJS_PACKAGES_TO_BUNDLE = ["rrule"];
const externalDeps = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})].filter(
(dep) => !CJS_PACKAGES_TO_BUNDLE.includes(dep),
);
await esbuild.build({
entryPoints: ["scripts/main.ts"],
bundle: true,
platform: "node",
target: "node20",
format: "esm",
outfile: "build/main.js",
// Externalize all node_modules except CJS packages that break ESM named imports
external: externalDeps,
alias: {
// Map SvelteKit's $lib alias so server code resolves correctly
$lib: "./src/lib",
},
define: {
// Inject version at build time so src/lib/version.ts resolves it
// without relying on vite-plugin-package-version at runtime
"import.meta.env.PACKAGE_VERSION": JSON.stringify(pkg.version),
},
plugins: [
{
name: "rewrite-build-imports",
setup(build) {
// Since the output lives in build/, rewrite ../build/X → ./X
// so that handler.js (SvelteKit output) resolves correctly
build.onResolve({ filter: /^\.\.\/build\// }, (args) => {
return {
path: args.path.replace(/^\.\.\/build\//, "./"),
external: true,
};
});
},
},
],
banner: {
js: "// Kener production server built with esbuild",
},
});
console.log(`Server build completed: build/main.js (v${pkg.version})`);
+51
View File
@@ -0,0 +1,51 @@
/**
* SvelteKit build script that optionally excludes docs routes.
*
* Usage:
* node scripts/build-sveltekit.js # build WITHOUT docs
* node scripts/build-sveltekit.js --with-docs # build WITH docs
*/
import { execSync } from "child_process";
import { renameSync, existsSync, rmSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = resolve(__dirname, "..");
const withDocs = process.argv.includes("--with-docs");
const docsDir = resolve(rootDir, "src/routes/(docs)");
const docsHiddenDir = resolve(rootDir, ".docs-excluded");
function moveDocs(from, to) {
if (existsSync(from)) {
renameSync(from, to);
}
}
function build() {
if (!withDocs) {
console.log("[build] Excluding docs routes from build...");
moveDocs(docsDir, docsHiddenDir);
// Clean generated route types so stale docs routes don't persist
const svelteKitDir = resolve(rootDir, ".svelte-kit");
if (existsSync(svelteKitDir)) {
rmSync(svelteKitDir, { recursive: true, force: true });
}
} else {
console.log("[build] Including docs routes in build...");
}
try {
execSync("npx vite build", { cwd: rootDir, stdio: "inherit" });
} finally {
// Always restore docs folder, even if build fails
if (!withDocs) {
moveDocs(docsHiddenDir, docsDir);
console.log("[build] Restored docs routes.");
}
}
}
build();
+210
View File
@@ -0,0 +1,210 @@
//node scripts/check-translations.js
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "glob";
import yaml from "js-yaml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
const srcDir = path.join(projectRoot, "src");
const localesDir = path.join(srcDir, "lib", "locales");
const SUPPORTED_FORMATS = new Set(["json", "yaml"]);
const SOURCE_GLOBS = ["src/**/*.{svelte,ts,js,mts,cts}", "!src/lib/locales/**/*.json", "!src/**/*.d.ts"];
const TRANSLATION_CALL_REGEX = /\$t\s*\(\s*(["'`])([\s\S]*?)\1\s*(?:,|\))/g;
const COMMENT_PATTERNS = [/\/\*[\s\S]*?\*\//g, /\/\/[^\n\r]*/g, /<!--[\s\S]*?-->/g];
const WHITELISTED_DYNAMIC_KEYS = new Set([
"All Systems Operational",
"Degraded Performance",
"Partial Degraded Performance",
"Partial System Outage",
"Major System Outage",
"No Status Available",
]);
function decodeQuotedContent(value) {
return value
.replace(/\\\\/g, "\\")
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
.replace(/\\"/g, '"')
.replace(/\\'/g, "'")
.replace(/\\`/g, "`");
}
function getLineNumber(text, index) {
let line = 1;
for (let i = 0; i < index; i += 1) {
if (text[i] === "\n") {
line += 1;
}
}
return line;
}
function maskComments(content) {
let result = content;
for (const pattern of COMMENT_PATTERNS) {
result = result.replace(pattern, (match) => match.replace(/[^\n]/g, " "));
}
return result;
}
function getUsedTranslationKeys() {
const files = globSync(SOURCE_GLOBS, {
cwd: projectRoot,
nodir: true,
ignore: ["**/node_modules/**", "**/.svelte-kit/**", "**/build/**", "**/dist/**"],
});
const usedKeys = new Set();
const skippedDynamicCalls = [];
for (const relativePath of files) {
const absolutePath = path.join(projectRoot, relativePath);
const content = fs.readFileSync(absolutePath, "utf8");
const searchable = maskComments(content);
TRANSLATION_CALL_REGEX.lastIndex = 0;
for (const match of searchable.matchAll(TRANSLATION_CALL_REGEX)) {
const quote = match[1];
const rawKey = match[2].trim();
if (!rawKey) {
continue;
}
if (quote === "`" && rawKey.includes("${")) {
skippedDynamicCalls.push({
file: relativePath,
line: getLineNumber(searchable, match.index ?? 0),
reason: "template literal contains interpolation",
});
continue;
}
usedKeys.add(decodeQuotedContent(rawKey));
}
}
for (const key of WHITELISTED_DYNAMIC_KEYS) {
usedKeys.add(key);
}
return {
usedKeys,
skippedDynamicCalls,
};
}
function readLocaleMappings() {
const localeFiles = globSync("*.json", {
cwd: localesDir,
nodir: true,
});
const localeMap = new Map();
for (const fileName of localeFiles) {
const fullPath = path.join(localesDir, fileName);
const raw = fs.readFileSync(fullPath, "utf8");
const json = JSON.parse(raw);
if (!json || typeof json !== "object" || typeof json.mappings !== "object") {
throw new Error(`Invalid locale file format in ${fileName}: expected { mappings: {...} }`);
}
localeMap.set(fileName, new Set(Object.keys(json.mappings)));
}
return localeMap;
}
function parseArgs(argv) {
let format = "json";
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--format") {
format = (argv[i + 1] || "").toLowerCase();
i += 1;
continue;
}
if (arg.startsWith("--format=")) {
format = arg.split("=")[1].toLowerCase();
}
}
if (!SUPPORTED_FORMATS.has(format)) {
throw new Error(`Unsupported format: ${format}. Use --format json or --format yaml.`);
}
return {
format,
outputFile: path.join(projectRoot, `translation-report.${format === "yaml" ? "yaml" : "json"}`),
};
}
function main() {
const { format, outputFile } = parseArgs(process.argv.slice(2));
const { usedKeys, skippedDynamicCalls } = getUsedTranslationKeys();
const locales = readLocaleMappings();
const sortedUsedKeys = [...usedKeys].sort((a, b) => a.localeCompare(b));
const localeFiles = [...locales.keys()].sort((a, b) => a.localeCompare(b));
const localesReport = {};
for (const localeFile of localeFiles) {
const localeKeys = locales.get(localeFile) ?? new Set();
const missing = sortedUsedKeys.filter((key) => !localeKeys.has(key));
const unused = [...localeKeys].filter((key) => !usedKeys.has(key)).sort((a, b) => a.localeCompare(b));
localesReport[localeFile] = {
missing,
unused,
missingCount: missing.length,
unusedCount: unused.length,
};
}
const report = {
generatedAt: new Date().toISOString(),
format,
scannedSourceDir: "src",
usedLiteralKeysCount: sortedUsedKeys.length,
usedLiteralKeys: sortedUsedKeys,
whitelistedDynamicKeysCount: WHITELISTED_DYNAMIC_KEYS.size,
whitelistedDynamicKeys: [...WHITELISTED_DYNAMIC_KEYS].sort((a, b) => a.localeCompare(b)),
localeFilesCount: localeFiles.length,
locales: localesReport,
skippedDynamicCallsCount: skippedDynamicCalls.length,
skippedDynamicCalls,
};
if (format === "yaml") {
fs.writeFileSync(outputFile, yaml.dump(report, { noRefs: true }) + "\n", "utf8");
} else {
fs.writeFileSync(outputFile, JSON.stringify(report, null, 2) + "\n", "utf8");
}
console.log(`Translation report written to: ${outputFile}`);
}
try {
main();
} catch (error) {
console.error("Failed to check translations.");
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}
+247
View File
@@ -0,0 +1,247 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "glob";
import yaml from "js-yaml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
const localesDir = path.join(projectRoot, "src", "lib", "locales");
const WHITELISTED_DYNAMIC_KEYS = new Set([
"All Systems Operational",
"Degraded Performance",
"Partial Degraded Performance",
"Partial System Outage",
"Major System Outage",
"No Status Available",
]);
function fail(message) {
throw new Error(message);
}
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function parseArgs(argv) {
let reportPath;
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--report") {
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
fail("Missing value for --report. Usage: --report <path>");
}
reportPath = next;
i += 1;
continue;
}
if (arg.startsWith("--report=")) {
reportPath = arg.slice("--report=".length);
if (!reportPath) {
fail("Missing value for --report. Usage: --report <path>");
}
continue;
}
fail(`Unknown argument: ${arg}`);
}
return { reportPath };
}
function detectReportPath(overridePath) {
if (overridePath) {
const resolved = path.resolve(projectRoot, overridePath);
if (!fs.existsSync(resolved)) {
fail(`Report file not found: ${resolved}`);
}
return resolved;
}
const jsonPath = path.join(projectRoot, "translation-report.json");
const yamlPath = path.join(projectRoot, "translation-report.yaml");
if (fs.existsSync(jsonPath)) return jsonPath;
if (fs.existsSync(yamlPath)) return yamlPath;
fail(
"Could not find translation report. Expected translation-report.json or translation-report.yaml in project root, or use --report <path>.",
);
}
function loadReport(reportPath) {
const ext = path.extname(reportPath).toLowerCase();
const raw = fs.readFileSync(reportPath, "utf8");
let parsed;
try {
if (ext === ".json") {
parsed = JSON.parse(raw);
} else if (ext === ".yaml" || ext === ".yml") {
parsed = yaml.load(raw);
} else {
fail(`Unsupported report file extension: ${ext}. Use .json, .yaml, or .yml.`);
}
} catch (error) {
fail(`Failed to parse report at ${reportPath}: ${error instanceof Error ? error.message : String(error)}`);
}
if (!isPlainObject(parsed)) {
fail("Invalid report format: expected a top-level object.");
}
if (!isPlainObject(parsed.locales)) {
fail("Invalid report format: expected report.locales to be an object.");
}
return parsed;
}
function getUnusedKeysForLocale(report, localeFileName) {
const localeReport = report.locales[localeFileName];
if (localeReport === undefined) return [];
if (!isPlainObject(localeReport)) {
fail(`Invalid report format for locales.${localeFileName}: expected an object.`);
}
const { unused } = localeReport;
if (unused === undefined) return [];
if (!Array.isArray(unused)) {
fail(`Invalid report format for locales.${localeFileName}.unused: expected an array.`);
}
const nonStrings = unused.filter((key) => typeof key !== "string");
if (nonStrings.length > 0) {
fail(`Invalid report format for locales.${localeFileName}.unused: all entries must be strings.`);
}
return unused;
}
function loadLocaleJson(localePath, localeFileName) {
const raw = fs.readFileSync(localePath, "utf8");
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
fail(`Invalid JSON in ${localeFileName}: ${error instanceof Error ? error.message : String(error)}`);
}
if (!isPlainObject(parsed)) {
fail(`Invalid locale file ${localeFileName}: expected a top-level object.`);
}
if (!isPlainObject(parsed.mappings)) {
fail(`Invalid locale file ${localeFileName}: expected \"mappings\" to be an object.`);
}
return parsed;
}
function sortObjectKeysAscending(input) {
const keys = Object.keys(input).sort((a, b) => a.localeCompare(b));
const result = {};
for (const key of keys) {
result[key] = input[key];
}
return result;
}
function replaceMappingsPreserveTopLevelOrder(localeData, sortedMappings) {
const next = {};
let sawMappings = false;
for (const key of Object.keys(localeData)) {
if (key === "mappings") {
next[key] = sortedMappings;
sawMappings = true;
} else {
next[key] = localeData[key];
}
}
if (!sawMappings) {
next.mappings = sortedMappings;
}
return next;
}
function cleanTranslations(report) {
if (!fs.existsSync(localesDir)) {
fail(`Locales directory not found: ${localesDir}`);
}
const localeFiles = globSync("*.json", {
cwd: localesDir,
nodir: true,
}).sort((a, b) => a.localeCompare(b));
if (localeFiles.length === 0) {
fail(`No locale files found in ${localesDir}`);
}
let totalRemoved = 0;
const perFile = [];
for (const localeFileName of localeFiles) {
const localePath = path.join(localesDir, localeFileName);
const localeData = loadLocaleJson(localePath, localeFileName);
const unusedKeys = getUnusedKeysForLocale(report, localeFileName);
const unusedSet = new Set(unusedKeys.filter((key) => !WHITELISTED_DYNAMIC_KEYS.has(key)));
const currentMappings = localeData.mappings;
const cleanedMappings = {};
let removedCount = 0;
for (const [key, value] of Object.entries(currentMappings)) {
if (unusedSet.has(key)) {
removedCount += 1;
} else {
cleanedMappings[key] = value;
}
}
const sortedMappings = sortObjectKeysAscending(cleanedMappings);
const nextLocaleData = replaceMappingsPreserveTopLevelOrder(localeData, sortedMappings);
fs.writeFileSync(localePath, `${JSON.stringify(nextLocaleData, null, 2)}\n`, "utf8");
totalRemoved += removedCount;
perFile.push({ file: localeFileName, removed: removedCount });
}
for (const item of perFile) {
console.log(`${item.file}: removed ${item.removed} key${item.removed === 1 ? "" : "s"}`);
}
console.log(`Total removed: ${totalRemoved}`);
}
function main() {
const { reportPath: reportArg } = parseArgs(process.argv.slice(2));
const reportPath = detectReportPath(reportArg);
const report = loadReport(reportPath);
console.log(`Using report: ${path.relative(projectRoot, reportPath)}`);
cleanTranslations(report);
}
try {
main();
} catch (error) {
console.error("Failed to clean translations.");
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Renames .js migration entries to .ts in the knex_migrations table.
* This is needed because migration files were renamed from .js to .ts,
* but existing databases still reference the old .js filenames.
*
* Idempotent — safe to run multiple times.
*/
import knex from "knex";
import knexOb from "../knexfile.js";
const db = knex(knexOb);
async function fixMigrationExtensions() {
try {
const hasTable = await db.schema.hasTable("knex_migrations");
if (!hasTable) {
console.log("No knex_migrations table found, skipping.");
return;
}
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
if (oldJsMigrations.length === 0) {
console.log("No .js migration entries found, nothing to rename.");
return;
}
for (const row of oldJsMigrations) {
const newName = row.name.replace(/\.js$/, ".ts");
await db("knex_migrations").where("id", row.id).update({ name: newName });
console.log(`Renamed: ${row.name} -> ${newName}`);
}
console.log(`Fixed ${oldJsMigrations.length} migration record(s).`);
} catch (err) {
console.error("Error fixing migration extensions:", err);
process.exit(1);
} finally {
await db.destroy();
}
}
fixMigrationExtensions();
-31
View File
@@ -1,31 +0,0 @@
import fs from "fs";
import Mustache from "mustache";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
import path from "path";
// Load environment variables from .env file
dotenv.config();
// Resolve paths correctly in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load the template
const templatePath = path.resolve(__dirname, "../README.template.md");
const template = fs.readFileSync(templatePath, "utf-8");
// Load environment variables and provide default values
const data = {
kener_full_version: process.env.BUILD_FULL_VERSION || "N/A",
kener_major_version: process.env.BUILD_MAJOR_VERSION || "N/A",
kener_major_minor_version: process.env.BUILD_MAJOR_MINOR_VERSION || "N/A",
};
// Render README.md
const output = Mustache.render(template, data);
// Write to README.md
fs.writeFileSync(path.resolve(__dirname, "../README.md"), output);
console.log("✅ README.md generated successfully!");
+220
View File
@@ -0,0 +1,220 @@
/**
* Standalone script to index documentation content into Redis for full-text search.
*
* This script reads docs.json and all markdown files from disk, converts them
* to plain text, and stores the search documents in Redis. It should be run
* manually whenever documentation content changes.
*
* Usage: npm run index-docs
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import fm from "front-matter";
import IORedis from "ioredis";
import dotenv from "dotenv";
import { marked } from "marked";
import plaintify from "marked-plaintify";
import { mdToText } from "../src/lib/marked.ts";
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DOCS_JSON_PATH = path.join(__dirname, "../src/routes/(docs)/docs.json");
const CONTENT_DIR = path.join(__dirname, "../src/routes/(docs)/docs/content");
const REDIS_DOCS_KEY = "kener-docs:search:documents";
interface DocsPage {
title: string;
content?: string;
slug?: string;
pages?: DocsPageSource[];
}
interface DocsPageSource {
title: string;
slug: string;
pages?: DocsPageSource[];
}
interface DocsSidebarGroup {
group: string;
pages: DocsPage[];
}
interface DocsNavTab {
name: string;
url?: string;
sidebar?: DocsSidebarGroup[];
}
interface DocsVersion {
name: string;
slug: string;
latest?: boolean;
content: {
navigation?: {
tabs?: DocsNavTab[];
};
};
}
interface DocsRootConfig {
versions: DocsVersion[];
}
interface DocsSearchDocument {
id: string;
title: string;
slug: string;
group: string;
content: string;
rawContent: string;
}
interface DocsSearchIndexData {
documents: DocsSearchDocument[];
lastUpdated: number;
}
/**
* Read markdown content for a slug (body only, without frontmatter)
*/
function getMarkdownContent(slug: string): string | null {
const directPath = path.join(CONTENT_DIR, `${slug}.md`);
try {
let rawContent: string | null = null;
if (fs.existsSync(directPath)) {
rawContent = fs.readFileSync(directPath, "utf-8");
} else {
const indexPath = path.join(CONTENT_DIR, slug, "index.md");
if (fs.existsSync(indexPath)) {
rawContent = fs.readFileSync(indexPath, "utf-8");
}
}
if (!rawContent) return null;
const parsed = fm(rawContent);
return parsed.body;
} catch {
return null;
}
}
/**
* Recursively collect all pages from sidebar groups (including nested pages)
*/
function collectPages(
pages: DocsPageSource[],
group: string,
result: Array<{ page: DocsPageSource; group: string }>,
): void {
for (const page of pages) {
result.push({ page, group });
if (page.pages && page.pages.length > 0) {
collectPages(page.pages, group, result);
}
}
}
function normalizePage(page: DocsPage): DocsPageSource {
const resolvedPath = page.content ?? page.slug;
if (!resolvedPath) {
throw new Error(`[index-docs] Page \"${page.title}\" must define content or slug`);
}
return {
title: page.title,
slug: resolvedPath,
pages: page.pages?.map(normalizePage),
};
}
function normalizeSidebar(sidebar: DocsSidebarGroup[]): Array<{ group: string; pages: DocsPageSource[] }> {
return sidebar.map((group) => ({
group: group.group,
pages: group.pages.map(normalizePage),
}));
}
async function main(): Promise<void> {
// Validate Redis URL
if (!process.env.REDIS_URL) {
console.error("Error: REDIS_URL environment variable is not set.");
process.exit(1);
}
// Read docs.json
if (!fs.existsSync(DOCS_JSON_PATH)) {
console.error(`Error: docs.json not found at ${DOCS_JSON_PATH}`);
process.exit(1);
}
const config: DocsRootConfig = JSON.parse(fs.readFileSync(DOCS_JSON_PATH, "utf-8"));
const latestVersion = config.versions.find((version) => version.latest) ?? config.versions[0];
if (!latestVersion) {
console.error("[index-docs] No versions found in docs.json");
process.exit(1);
}
const tabs = latestVersion.content.navigation?.tabs ?? [];
const documents: DocsSearchDocument[] = [];
// Collect all pages from all tabs' sidebars
const allPages: Array<{ page: DocsPageSource; group: string }> = [];
for (const tab of tabs) {
const sidebar = normalizeSidebar(tab.sidebar ?? []);
for (const sidebarGroup of sidebar) {
collectPages(sidebarGroup.pages, sidebarGroup.group, allPages);
}
}
console.log(`[index-docs] Indexing version ${latestVersion.slug}`);
console.log(`[index-docs] Found ${allPages.length} pages to index`);
for (const { page, group } of allPages) {
const markdownContent = getMarkdownContent(page.slug);
if (markdownContent) {
const plainContent = mdToText(markdownContent);
documents.push({
id: page.slug,
title: page.title,
slug: page.slug,
group,
content: plainContent,
rawContent: markdownContent,
});
} else {
console.warn(`[index-docs] No content found for slug: ${page.slug}`);
}
}
console.log(`[index-docs] Indexed ${documents.length} documents`);
// Store in Redis
const redis = new IORedis(process.env.REDIS_URL, { maxRetriesPerRequest: null });
const indexData: DocsSearchIndexData = {
documents,
lastUpdated: Date.now(),
};
await redis.set(REDIS_DOCS_KEY, JSON.stringify(indexData));
console.log(`[index-docs] Stored ${documents.length} documents in Redis (key: ${REDIS_DOCS_KEY})`);
await redis.quit();
console.log("[index-docs] Done.");
}
main().catch((err) => {
console.error("[index-docs] Fatal error:", err);
process.exit(1);
});
+93
View File
@@ -0,0 +1,93 @@
import { handler } from "../build/handler.js";
import dotenv from "dotenv";
dotenv.config();
import express from "express";
import Startup from "../src/lib/server/startup.ts";
import shutdownSchedulers from "../src/lib/server/schedulers/shutdown.ts";
import shutdownQueues from "../src/lib/server/queues/shutdown.ts";
import dbInstance from "../src/lib/server/db/db.ts";
import knex from "knex";
import knexOb from "../knexfile.js";
const PORT = process.env.PORT || 3000;
const base = process.env.KENER_BASE_PATH || "";
const app: any = express();
const db = knex(knexOb);
app.get(base + "/healthcheck", (req: any, res: any) => {
res.end("ok");
});
app.use(handler);
//migrations
async function runMigrations() {
try {
// Rename old .js migration entries to .ts in the knex_migrations table
// so Knex can find the renamed files on disk
const hasTable = await db.schema.hasTable("knex_migrations");
if (hasTable) {
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
for (const row of oldJsMigrations) {
const newName = row.name.replace(/\.js$/, ".ts");
await db("knex_migrations").where("id", row.id).update({ name: newName });
console.log(`Renamed migration record: ${row.name} -> ${newName}`);
}
}
console.log("Running migrations...");
await db.migrate.latest(); // Runs migrations to the latest state
console.log("Migrations completed successfully!");
} catch (err) {
console.error("Error running migrations:", err);
}
}
//seed
async function runSeed() {
try {
console.log("Running seed...");
await db.seed.run(); // Runs seed to the latest state
console.log("Seed completed successfully!");
} catch (err) {
console.error("Error running seed:", err);
}
}
app.listen(PORT, async () => {
await runMigrations();
await runSeed();
await db.destroy();
Startup();
console.log("Kener is running on port " + PORT + "!");
});
// Graceful shutdown handler
async function gracefulShutdown(signal: string) {
console.log(`\nReceived ${signal}. Starting graceful shutdown...`);
try {
console.log("Shutting down schedulers...");
await shutdownSchedulers();
console.log("Schedulers shut down successfully.");
console.log("Shutting down queues...");
await shutdownQueues();
console.log("Queues shut down successfully.");
console.log("Closing database connection...");
await dbInstance.close();
console.log("Database connection closed successfully.");
console.log("Graceful shutdown completed.");
process.exit(0);
} catch (err) {
console.error("Error during graceful shutdown:", err);
process.exit(1);
}
}
// Handle termination signals
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
+113
View File
@@ -0,0 +1,113 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "glob";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
const localesDir = path.join(projectRoot, "src", "lib", "locales");
function fail(message) {
throw new Error(message);
}
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function sortObjectKeysAscending(input) {
const keys = Object.keys(input).sort((a, b) => a.localeCompare(b));
const result = {};
for (const key of keys) {
result[key] = input[key];
}
return result;
}
function replaceMappingsPreserveTopLevelOrder(localeData, sortedMappings) {
const next = {};
let sawMappings = false;
for (const key of Object.keys(localeData)) {
if (key === "mappings") {
next[key] = sortedMappings;
sawMappings = true;
} else {
next[key] = localeData[key];
}
}
if (!sawMappings) {
next.mappings = sortedMappings;
}
return next;
}
function loadLocaleJson(localePath, localeFileName) {
const raw = fs.readFileSync(localePath, "utf8");
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
fail(`Invalid JSON in ${localeFileName}: ${error instanceof Error ? error.message : String(error)}`);
}
if (!isPlainObject(parsed)) {
fail(`Invalid locale file ${localeFileName}: expected a top-level object.`);
}
if (!isPlainObject(parsed.mappings)) {
fail(`Invalid locale file ${localeFileName}: expected "mappings" to be an object.`);
}
return parsed;
}
function sortTranslations() {
if (!fs.existsSync(localesDir)) {
fail(`Locales directory not found: ${localesDir}`);
}
const localeFiles = globSync("*.json", {
cwd: localesDir,
nodir: true,
}).sort((a, b) => a.localeCompare(b));
if (localeFiles.length === 0) {
fail(`No locale files found in ${localesDir}`);
}
let changedFilesCount = 0;
for (const localeFileName of localeFiles) {
const localePath = path.join(localesDir, localeFileName);
const localeData = loadLocaleJson(localePath, localeFileName);
const sortedMappings = sortObjectKeysAscending(localeData.mappings);
const nextLocaleData = replaceMappingsPreserveTopLevelOrder(localeData, sortedMappings);
const nextContent = `${JSON.stringify(nextLocaleData, null, 2)}\n`;
const currentContent = fs.readFileSync(localePath, "utf8");
if (nextContent !== currentContent) {
fs.writeFileSync(localePath, nextContent, "utf8");
changedFilesCount += 1;
console.log(`${localeFileName}: sorted mappings in ascending key order`);
} else {
console.log(`${localeFileName}: already sorted`);
}
}
console.log(`Done. Updated ${changedFilesCount} file${changedFilesCount === 1 ? "" : "s"}.`);
}
try {
sortTranslations();
} catch (error) {
console.error("Failed to sort translations.");
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
+76
View File
@@ -0,0 +1,76 @@
import subscriptionAccountCodeTemplate from "../src/lib/server/templates/general/subscription_account_code_template.ts";
import subscriptionUpdateTemplate from "../src/lib/server/templates/general/subscription_update_template.ts";
import forgotPasswordTemplate from "../src/lib/server/templates/general/forgot_password_template.ts";
import inviteUserTemplate from "../src/lib/server/templates/general/invite_user_template.ts";
import verifyEmailTemplate from "../src/lib/server/templates/general/verify_email_template.ts";
import type { Knex } from "knex";
export async function seed(knex: Knex): Promise<void> {
// Check if the table has template_id 'subscription_account_code'
let count = await knex("general_email_templates")
.where({ template_id: subscriptionAccountCodeTemplate.template_id })
.count("template_id as CNT")
.first();
if (count && count.CNT == 0) {
await knex("general_email_templates").insert({
template_id: subscriptionAccountCodeTemplate.template_id,
template_subject: subscriptionAccountCodeTemplate.template_subject,
template_html_body: subscriptionAccountCodeTemplate.template_html_body,
template_text_body: subscriptionAccountCodeTemplate.template_text_body,
});
}
count = await knex("general_email_templates")
.where({ template_id: subscriptionUpdateTemplate.template_id })
.count("template_id as CNT")
.first();
if (count && count.CNT == 0) {
await knex("general_email_templates").insert({
template_id: subscriptionUpdateTemplate.template_id,
template_subject: subscriptionUpdateTemplate.template_subject,
template_html_body: subscriptionUpdateTemplate.template_html_body,
template_text_body: subscriptionUpdateTemplate.template_text_body,
});
}
count = await knex("general_email_templates")
.where({ template_id: forgotPasswordTemplate.template_id })
.count("template_id as CNT")
.first();
if (count && count.CNT == 0) {
await knex("general_email_templates").insert({
template_id: forgotPasswordTemplate.template_id,
template_subject: forgotPasswordTemplate.template_subject,
template_html_body: forgotPasswordTemplate.template_html_body,
template_text_body: forgotPasswordTemplate.template_text_body,
});
}
count = await knex("general_email_templates")
.where({ template_id: inviteUserTemplate.template_id })
.count("template_id as CNT")
.first();
if (count && count.CNT == 0) {
await knex("general_email_templates").insert({
template_id: inviteUserTemplate.template_id,
template_subject: inviteUserTemplate.template_subject,
template_html_body: inviteUserTemplate.template_html_body,
template_text_body: inviteUserTemplate.template_text_body,
});
}
count = await knex("general_email_templates")
.where({ template_id: verifyEmailTemplate.template_id })
.count("template_id as CNT")
.first();
if (count && count.CNT == 0) {
await knex("general_email_templates").insert({
template_id: verifyEmailTemplate.template_id,
template_subject: verifyEmailTemplate.template_subject,
template_html_body: verifyEmailTemplate.template_html_body,
template_text_body: verifyEmailTemplate.template_text_body,
});
}
}
-32
View File
@@ -1,32 +0,0 @@
import monitorSeed from "../src/lib/server/db/seedMonitorData.js";
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function seed(knex) {
// Check if the table is empty
const count = await knex("monitors").count("id as CNT").first();
if (count.CNT == 0) {
// Deletes ALL existing entries
for (const monitor of monitorSeed) {
await knex("monitors").insert({
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
cron: monitor.cron,
default_status: monitor.default_status,
status: monitor.status,
category_name: monitor.category_name,
monitor_type: monitor.monitor_type,
type_data: monitor.type_data,
day_degraded_minimum_count: monitor.day_degraded_minimum_count,
day_down_minimum_count: monitor.day_down_minimum_count,
include_degraded_in_downtime: monitor.include_degraded_in_downtime,
created_at: knex.fn.now(),
updated_at: knex.fn.now()
});
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import monitorSeed from "../src/lib/server/db/seedMonitorData.ts";
import type { Knex } from "knex";
export async function seed(knex: Knex): Promise<void> {
// Check if the table is empty
const count = await knex("monitors").count("id as CNT").first();
if (count && count.CNT == 0) {
// Deletes ALL existing entries
for (const monitor of monitorSeed) {
await knex("monitors").insert({
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
cron: monitor.cron,
default_status: monitor.default_status,
status: monitor.status,
category_name: monitor.category_name,
monitor_type: monitor.monitor_type,
type_data: monitor.type_data,
day_degraded_minimum_count: monitor.day_degraded_minimum_count,
day_down_minimum_count: monitor.day_down_minimum_count,
include_degraded_in_downtime: monitor.include_degraded_in_downtime,
is_hidden: monitor.is_hidden || "NO",
monitor_settings_json: monitor.monitor_settings_json || null,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import seedPagesData from "../src/lib/server/db/seedPagesData.ts";
import type { Knex } from "knex";
export async function seed(knex: Knex): Promise<void> {
// Check if the pages table is empty
const pageCount = await knex("pages").count("id as CNT").first();
if (pageCount && pageCount.CNT == 0) {
// Insert seed pages
for (const page of seedPagesData) {
const [insertedPage] = await knex("pages")
.insert({
page_path: page.page_path,
page_title: page.page_title,
page_header: page.page_header,
page_subheader: page.page_subheader,
page_logo: page.page_logo,
page_settings_json: page.page_settings_json,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
})
.returning("id");
// For the home page, add the default monitor (earth) if it exists
if (page.page_path === "") {
const earthMonitor = await knex("monitors").where({ tag: "earth" }).first();
if (earthMonitor) {
const pageId = typeof insertedPage === "object" ? insertedPage.id : insertedPage;
await knex("pages_monitors").insert({
page_id: pageId,
monitor_tag: "earth",
monitor_settings_json: "",
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
}
const kenerMonitor = await knex("monitors").where({ tag: "kener" }).first();
if (kenerMonitor) {
const pageId = typeof insertedPage === "object" ? insertedPage.id : insertedPage;
await knex("pages_monitors").insert({
page_id: pageId,
monitor_tag: "kener",
monitor_settings_json: "",
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
}
}
}
}
}
-24
View File
@@ -1,24 +0,0 @@
import seedSiteData from "../src/lib/server/db/seedSiteData.js";
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function seed(knex) {
// Check if the table is empty
const count = await knex("site_data").count("id as CNT").first();
for (const key in seedSiteData) {
if (Object.prototype.hasOwnProperty.call(seedSiteData, key)) {
let value = seedSiteData[key];
let data_type = typeof value;
if (data_type === "object") {
value = JSON.stringify(value);
}
const existingEntry = await knex("site_data").where({ key: key }).first();
if (!existingEntry) {
await knex("site_data").insert([{ key: key, value: value, data_type: data_type }]);
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import seedSiteData from "../src/lib/server/db/seedSiteData.ts";
import type { Knex } from "knex";
export async function seed(knex: Knex): Promise<void> {
// Check if the table is empty
const count = await knex("site_data").count("id as CNT").first();
const seedDataRecord = seedSiteData as Record<string, unknown>;
for (const key in seedDataRecord) {
if (Object.prototype.hasOwnProperty.call(seedDataRecord, key)) {
let value = seedDataRecord[key];
let data_type = typeof value;
if (data_type === "object") {
value = JSON.stringify(value);
}
const existingEntry = await knex("site_data").where({ key: key }).first();
if (!existingEntry) {
await knex("site_data").insert([{ key: key, value: value, data_type: data_type }]);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More