Compare commits

...
275 Commits
Author SHA1 Message Date
github-actions[bot] 180ea21041 chore: release 1.9.3 2026-03-29 09:23:24 +00:00
Charles GTEandGitHub e1c0fc6917 fix: refactoring logs and adding pino logger for production and dev. (#236) 2026-03-29 11:22:28 +02:00
github-actions[bot] 057c192314 chore: release 1.9.2 2026-03-29 07:25:20 +00:00
Charles GTEandGitHub aff782aa28 fix: mariadb and mysql separation (#234) 2026-03-29 09:24:31 +02:00
github-actions[bot] 2e729cb79c chore: release 1.9.1 2026-03-28 18:01:28 +00:00
Charles GTEandGitHub d8677d74a7 fix: default notification (#233) 2026-03-28 19:00:50 +01:00
github-actions[bot] 2f77714994 chore: release 1.9.0 2026-03-28 15:37:58 +00:00
Charles GTEandGitHub bc8050c4df feat: add-healthcheck (#231)
* fix: refactoring

* fix: refactoring

* fix: refactoring
2026-03-28 16:37:03 +01:00
Charles GTEandGitHub a9c69b112d Merge pull request #232 from Portabase/revert-230-revert-229-feat/add-healthcheck
Revert "Revert "feat: add-healthcheck""
2026-03-28 16:35:18 +01:00
Charles GTEandGitHub 04785c323b Revert "Revert "feat: add-healthcheck"" 2026-03-28 16:35:02 +01:00
Charles GTEandGitHub 1d209ceb28 Merge pull request #230 from Portabase/revert-229-feat/add-healthcheck
Revert "feat: add-healthcheck"
2026-03-28 16:28:25 +01:00
Charles GTEandGitHub 941a9256f0 Revert "feat: add-healthcheck" 2026-03-28 16:27:57 +01:00
Charles GTEandGitHub 9098012426 feat: add-healthcheck (#229 )
feat: add-healthcheck
2026-03-28 16:23:11 +01:00
Charles GTE 8e12ff59a4 fix: refactoring 2026-03-28 16:22:04 +01:00
Charles GTE 0f8a267861 chore: update security.yml 2026-03-28 16:17:24 +01:00
Charles GTE da53a00fd8 chore: update security.yml 2026-03-28 16:13:36 +01:00
Charles GTE 47e3a93e42 fix: healthcheck system 2026-03-28 16:06:55 +01:00
Charles GTE 0262a1c176 fix: database health cron 2026-03-28 15:10:46 +01:00
Charles GTE 7aecd911c0 feat: database health logs 2026-03-27 23:06:19 +01:00
Charles GTE 595ccadfb3 feat: added heath_error_count and the cron for health check agent errors 2026-03-27 22:13:32 +01:00
charles-gauthereau bded273f94 fix: set default notification channel 2026-03-27 19:22:44 +01:00
charles-gauthereau c095d393cf fix: set default notification channel 2026-03-26 22:45:42 +01:00
charles-gauthereau c3ae82fde7 feat: set default notification channel 2026-03-26 20:17:23 +01:00
charles-gauthereau a8e11ebef7 feat: set default notification channel 2026-03-25 22:19:41 +01:00
charles-gauthereau 61b1a2e724 feat: set default notification channel 2026-03-25 22:07:20 +01:00
charles-gauthereau 7a4af4e1e0 feat: added cron job to remove healthcheck logs older than 12h 2026-03-25 21:11:09 +01:00
charles-gauthereau a9d6481193 fix: health chart labels and get logs for 12h instead of 24h 2026-03-25 20:45:20 +01:00
charles-gauthereau fb97963cc3 feat: front form for default notification settings page choice 2026-03-25 09:06:53 +01:00
charles-gauthereau 0cc0ba989c feat: add agent healthcheck 2026-03-24 20:06:43 +01:00
github-actions[bot] 26eff02943 chore: release 1.8.0 2026-03-18 18:48:30 +00:00
67246efafc feat: adding basic end-to-end testing and ci workflow (#225)
* feat: add end-to-end testing

* chore: fix bug in github action.

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: killianlarcher <killian.larcher@soluce-technologies.com>
2026-03-18 19:47:41 +01:00
github-actions[bot] 260b3ba1f9 chore: release 1.7.1 2026-03-17 14:03:35 +00:00
Charles GTEandGitHub 90f8885e73 fix: removed version number on auth layout (#223)
fix: removed version number on auth layout
2026-03-17 15:02:55 +01:00
charles-gauthereau 0bacd6a65c fix: removed version number on auth layout 2026-03-17 15:01:34 +01:00
github-actions[bot] 2846deb4d0 chore: release 1.7.0 2026-03-16 13:39:18 +00:00
ae6dbcab25 feat: add-valkey engine (#222)
* feat: add-valkey engine

* fix: README.md with Valkey content

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-16 14:38:43 +01:00
charles-gauthereau d823ec2e82 fix: README.md with Valkey content 2026-03-16 14:26:25 +01:00
charles-gauthereau b0ce5273ce feat: add-valkey engine 2026-03-16 12:16:50 +01:00
github-actions[bot] b576862024 chore: release 1.6.1 2026-03-16 07:56:56 +00:00
dc2496ee18 fix: type mismatch for s3 (#221)
* fix: release.yml

* fix: S3ChannelConfigSchema type port mismatch

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-16 08:56:19 +01:00
87db207833 fix: add redis badge and Supported databases section in README.md (#219)
Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-15 13:56:01 +01:00
github-actions[bot] b1ccf6ec5f chore: release 1.6.0 2026-03-15 11:10:32 +00:00
Charles GTEandGitHub ca3f63af81 feat: add-redis #217
feat: add-redis
2026-03-15 12:08:44 +01:00
Charles GTEGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
29b0ca5bf5 fix: 'Semicolon insertion'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-03-15 12:05:48 +01:00
charlesgauthereau da50193e8c fix: detabase details page in order to support backup only databases. 2026-03-15 12:02:00 +01:00
charlesgauthereau 1829c1fc1c feat: add redis database support for backup 2026-03-14 14:41:51 +01:00
github-actions[bot] 79a17407bd chore: release 1.5.4 2026-03-08 20:59:04 +00:00
dd1e3283d5 fix: release.yml (#214)
Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-08 21:58:25 +01:00
d1baedd092 fix: pipeline [skip-release] (#213)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 14:11:28 +01:00
9e504cefc1 fix: pipeline [skip-release] (#212)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 14:09:39 +01:00
3cc5ab70fa fix: release.yml [skip-release] (#211)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 14:06:28 +01:00
0441af78a1 fix: readme [skip-release] (#210)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 13:59:18 +01:00
3c615a4fd7 fix: workflow [skip-release]
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 13:52:42 +01:00
8681e16fde fix: updated the README.md with helm and some bug fixes [skip-release]
* fix: README.md

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 13:46:47 +01:00
github-actions[bot] 918c6c448c chore: release 1.5.3 2026-03-07 11:57:16 +00:00
Charles GTEandGitHub 1e2b4f16b3 fix: helm-directory
fix: move helm directory to root
2026-03-07 12:56:48 +01:00
charlesgauthereau f0d02eb8b3 fix: Chart.yaml 2026-03-07 12:55:54 +01:00
charlesgauthereau 96274206c5 fix: move helm directory to root 2026-03-07 12:43:18 +01:00
github-actions[bot] 7ae7ea6f79 chore: release 1.5.2 2026-03-07 11:29:44 +00:00
Charles GTEandGitHub 487e7ad83d fix: helm.yml
fix: helm.yml
2026-03-07 12:29:07 +01:00
charlesgauthereau c9dabd39fe fix: helm.yml 2026-03-07 12:28:33 +01:00
github-actions[bot] 30ab689c6e chore: release 1.5.1 2026-03-07 11:06:41 +00:00
Charles GTEandGitHub 8143af18af fix: workflow for helm chart
feat/helm-chart
2026-03-07 12:06:12 +01:00
charlesgauthereau 471c65cc48 fix: release.yml 2026-03-07 12:05:21 +01:00
charlesgauthereau 9162d5afcc fix: release.yml 2026-03-07 12:04:22 +01:00
charlesgauthereau 9236c664c2 Merge branch 'main' into feat/helm-chart 2026-03-07 12:02:56 +01:00
charlesgauthereau 767ed69020 fix: release.yml 2026-03-07 12:02:25 +01:00
github-actions[bot] 93c706719f chore: release 1.6.0 2026-03-07 10:56:16 +00:00
Charles GTEandGitHub ed1ac1926b feat: helm-chart
feat/helm-chart
2026-03-07 11:55:50 +01:00
charlesgauthereau 3adb6d01c0 fix: release.yml 2026-03-07 11:54:43 +01:00
Charles GTEandGitHub 9c195c1f30 feat: helm-chart
feat/helm-chart
2026-03-07 11:52:24 +01:00
charlesgauthereau e27093404b feat: update the release pipeline with helm publish 2026-03-07 11:49:16 +01:00
charlesgauthereau 9602427ca0 feat: add config files for helm chart 2026-03-07 11:11:48 +01:00
charlesgauthereau 46812cb5ff Merge branch 'main' into dev 2026-03-07 10:36:29 +01:00
github-actions[bot] 6258d92464 chore: release 1.5.0 2026-03-06 21:02:09 +00:00
c57d913c46 feat: new pipeline for release process
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix: workflow

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 22:01:40 +01:00
charlesgauthereau 5d285b7ac6 Merge branch 'main' into dev 2026-03-06 22:01:05 +01:00
charlesgauthereau c96219f851 fix: workflow 2026-03-06 22:00:46 +01:00
github-actions[bot] c7d949b1d1 chore: release 1.4.56 2026-03-06 20:47:32 +00:00
6593a8e324 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 21:47:00 +01:00
charlesgauthereau 0988fa0115 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/docker.yml
2026-03-06 21:46:22 +01:00
charlesgauthereau 9f0b1a172c fix 2026-03-06 21:45:43 +01:00
github-actions[bot] 0126a829dd chore: release 1.4.55 2026-03-06 20:33:33 +00:00
b04a2750a3 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 21:33:05 +01:00
charlesgauthereau 9e22160b16 Merge branch 'main' into dev 2026-03-06 21:32:28 +01:00
charlesgauthereau e8e4b093d2 fix 2026-03-06 21:32:10 +01:00
github-actions[bot] 0cb27f2637 chore: release 1.4.54 2026-03-06 18:25:30 +00:00
1497c7385b fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 19:24:55 +01:00
charlesgauthereau 327b06d2a1 Merge branch 'main' into dev 2026-03-06 19:24:18 +01:00
charlesgauthereau 2c2cfc0b73 fix 2026-03-06 19:22:52 +01:00
github-actions[bot] 93327b0a6a chore: release 1.4.53 2026-03-06 18:00:01 +00:00
11c74d4117 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:59:29 +01:00
charlesgauthereau f93bb6e33e fix 2026-03-06 18:59:01 +01:00
charlesgauthereau 73699bbda9 Merge branch 'main' into dev 2026-03-06 18:55:35 +01:00
charlesgauthereau 5f24ec2d86 fix 2026-03-06 18:55:19 +01:00
c08bb8f8e0 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:38:53 +01:00
charlesgauthereau d01e705866 fix 2026-03-06 18:37:12 +01:00
ecc80c3c01 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:36:48 +01:00
charlesgauthereau 784a907384 fix 2026-03-06 18:36:19 +01:00
b68a5f32ca fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:35:19 +01:00
charlesgauthereau 467f9e2ffd fix 2026-03-06 18:33:36 +01:00
charlesgauthereau 1d25acde61 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 18:33:21 +01:00
charlesgauthereau cb6ac86bb0 fix 2026-03-06 18:32:10 +01:00
cd2da4441d fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:30:11 +01:00
charlesgauthereau d7515b172c Merge branch 'main' into dev 2026-03-06 18:28:21 +01:00
charlesgauthereau e4de903cc9 fix 2026-03-06 18:27:54 +01:00
github-actions[bot] 45166a3b9c chore: release 1.4.52 2026-03-06 17:15:05 +00:00
687b59e397 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:14:35 +01:00
charlesgauthereau 9a3be8d5a5 fix 2026-03-06 18:13:49 +01:00
0c23c24651 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:12:57 +01:00
charlesgauthereau e20e7f016c fix 2026-03-06 18:12:01 +01:00
charlesgauthereau a0c1a24ba9 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 18:09:27 +01:00
charlesgauthereau 0475b3d01a fix 2026-03-06 18:08:35 +01:00
39cb9b99f6 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:07:51 +01:00
charlesgauthereau efd123ddfe fix 2026-03-06 18:07:06 +01:00
charlesgauthereau 413318886c Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 18:06:51 +01:00
charlesgauthereau 187f9d2386 fix 2026-03-06 18:06:06 +01:00
github-actions[bot] 5f61c7baa6 chore: release 1.4.51 2026-03-06 17:03:58 +00:00
e3c5ec9a58 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:03:32 +01:00
charlesgauthereau fde52415cb fix 2026-03-06 18:02:51 +01:00
charlesgauthereau d92225d093 Merge branch 'main' into dev 2026-03-06 18:02:31 +01:00
charlesgauthereau e2a0932229 fix 2026-03-06 18:02:19 +01:00
github-actions[bot] 6acdf73557 chore: release 1.4.50 2026-03-06 16:43:28 +00:00
4405b61e03 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:43:01 +01:00
charlesgauthereau fba714d679 fix 2026-03-06 17:42:32 +01:00
charlesgauthereau 8e15282405 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 17:42:24 +01:00
charlesgauthereau 72b5816451 fix 2026-03-06 17:42:09 +01:00
github-actions[bot] 4016191497 chore: release 1.4.49 2026-03-06 16:37:57 +00:00
94b36c1622 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:37:14 +01:00
charlesgauthereau aa736b8b4a fix 2026-03-06 17:36:42 +01:00
charlesgauthereau 94cd937c56 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 17:36:14 +01:00
charlesgauthereau 9ccf3b7242 fix 2026-03-06 17:35:56 +01:00
github-actions[bot] 01b7a58c75 chore: release 1.4.48 2026-03-06 16:30:16 +00:00
2b6c661fea fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:29:41 +01:00
charlesgauthereau fbd61cdf2f fix 2026-03-06 17:28:49 +01:00
charlesgauthereau e9534181da Merge branch 'main' into dev 2026-03-06 17:28:38 +01:00
fdc613fe8e fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:26:45 +01:00
charlesgauthereau fca09a84cb fix 2026-03-06 17:26:17 +01:00
charlesgauthereau 6ec6fb1f11 Merge branch 'main' into dev 2026-03-06 17:25:58 +01:00
charlesgauthereau 3d51d3548f fix 2026-03-06 17:25:33 +01:00
github-actions[bot] 8479b6c822 chore: release 1.4.47 2026-03-06 16:07:42 +00:00
21bb165103 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:07:08 +01:00
charlesgauthereau c7ca01c0fb Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 17:06:47 +01:00
charlesgauthereau 5d2172f2bf fix 2026-03-06 17:06:00 +01:00
1decd9d7a5 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:05:09 +01:00
d0091cfed3 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 16:59:42 +01:00
charlesgauthereau 0ebcf1729c Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 16:59:04 +01:00
charlesgauthereau 51d101e8f0 fix 2026-03-06 16:58:35 +01:00
charlesgauthereau 2d9bf225a0 fix 2026-03-06 16:57:53 +01:00
github-actions[bot] f85d12448c chore: release 1.4.46 2026-03-06 15:07:34 +00:00
82c8ad98b4 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 16:07:03 +01:00
charlesgauthereau 2499c7c426 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 16:06:18 +01:00
charlesgauthereau da530217ab fix 2026-03-06 16:05:44 +01:00
github-actions[bot] 4c70d8aac7 chore: release 1.4.45 2026-03-06 15:01:50 +00:00
ba1dbcc9f3 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 16:01:15 +01:00
charlesgauthereau 0ad380cc81 fix 2026-03-06 16:00:40 +01:00
charlesgauthereau b77a2e374e Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 16:00:00 +01:00
charlesgauthereau 647108e8ee fix 2026-03-06 15:59:33 +01:00
github-actions[bot] 8e4e41ca07 chore: release 1.4.44 2026-03-06 12:31:40 +00:00
a8eb3f5f1a fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:31:03 +01:00
charlesgauthereau eb5ac5c8ea Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 13:30:35 +01:00
charlesgauthereau 29344e0c48 fix 2026-03-06 13:29:27 +01:00
github-actions[bot] 73f7ac057c chore: release 1.4.43 2026-03-06 12:25:50 +00:00
d359d53fb7 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:25:14 +01:00
charlesgauthereau ff83ee9c62 fix 2026-03-06 13:24:18 +01:00
charlesgauthereau da3a8d7fbe Merge branch 'main' into dev 2026-03-06 13:23:18 +01:00
github-actions[bot] ba2fc55188 chore: release v1.4.42 2026-03-06 12:16:19 +00:00
a6896c9b50 dev (#176)
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:15:49 +01:00
charlesgauthereau f2563c5e8d Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 13:14:53 +01:00
charlesgauthereau 9326291780 fix 2026-03-06 13:12:37 +01:00
github-actions[bot] 8dd74f310d chore: release v1.4.41 2026-03-06 12:08:08 +00:00
44ed9e9ca6 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:07:39 +01:00
charlesgauthereau ab22680625 fix 2026-03-06 13:07:03 +01:00
charlesgauthereau a73e29efc9 Merge branch 'main' into dev 2026-03-06 13:05:51 +01:00
charlesgauthereau 26273b7240 fix 2026-03-06 13:04:58 +01:00
github-actions[bot] c925407f05 chore: release v1.4.40 2026-03-06 10:51:11 +00:00
622fc369d0 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:50:44 +01:00
charlesgauthereau 0c4b7573cc Merge branch 'main' into dev 2026-03-06 11:49:58 +01:00
charlesgauthereau 401ce5c290 fix 2026-03-06 11:49:35 +01:00
github-actions[bot] 9580ad3934 chore: release v1.4.39 2026-03-06 10:48:15 +00:00
b33c0810d5 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:47:44 +01:00
charlesgauthereau e3e84e0d18 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:47:26 +01:00
charlesgauthereau 55cb4a7144 fix 2026-03-06 11:46:33 +01:00
8fbe639aa1 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:45:11 +01:00
charlesgauthereau 9bc042e2cd Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:44:17 +01:00
charlesgauthereau 67302c6a17 fix 2026-03-06 11:43:48 +01:00
github-actions[bot] d7e2ec3b08 chore: release v1.4.38 2026-03-06 10:25:41 +00:00
fdaca0e8a7 dev (#171)
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:25:13 +01:00
charlesgauthereau 3af5f89606 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:24:39 +01:00
charlesgauthereau 3ccccfc5e5 fix 2026-03-06 11:24:12 +01:00
github-actions[bot] 7959f903bb chore: release v1.4.37 2026-03-06 10:22:43 +00:00
59eb91f28f fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:22:16 +01:00
charlesgauthereau 9aecfb22a5 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:21:31 +01:00
charlesgauthereau ba4476a3a7 fix 2026-03-06 11:21:11 +01:00
64724d2a92 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:53:49 +01:00
charlesgauthereau be58ebdb7d Merge branch 'main' into dev 2026-03-06 10:53:05 +01:00
charlesgauthereau 746a9f5eb6 fix 2026-03-06 10:49:04 +01:00
github-actions[bot] 91a6d672fa chore: release v1.4.36 2026-03-06 09:42:31 +00:00
50809476fa fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:40:27 +01:00
charlesgauthereau 483f9cf192 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 10:39:45 +01:00
charlesgauthereau f1c7dcda45 fix 2026-03-06 10:39:28 +01:00
282ffb543c fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:34:13 +01:00
charlesgauthereau c840196a14 fix 2026-03-06 10:33:38 +01:00
charlesgauthereau 7d76037a43 Merge branch 'main' into dev 2026-03-06 10:33:18 +01:00
68fea480ae fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:30:09 +01:00
charlesgauthereau b063e5c276 fix 2026-03-06 10:29:27 +01:00
charlesgauthereau f2d98505f1 Merge branch 'main' into dev 2026-03-06 10:29:14 +01:00
ec64ea8229 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:25:44 +01:00
charlesgauthereau 59dc181625 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 10:25:16 +01:00
charlesgauthereau 79387b83f6 fix 2026-03-06 10:24:58 +01:00
github-actions[bot] b13433e554 chore: release v1.4.37 2026-03-06 09:03:53 +00:00
d9fd382cb2 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:03:23 +01:00
charlesgauthereau baa5e300c8 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 10:02:53 +01:00
charlesgauthereau 51747773ba fix 2026-03-06 10:02:32 +01:00
github-actions[bot] 4d324d885d chore: release v1.4.36 2026-03-06 08:59:45 +00:00
af1a437523 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:59:15 +01:00
charlesgauthereau dd8663591e Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:58:42 +01:00
charlesgauthereau de4f31f255 fix 2026-03-06 09:58:11 +01:00
charlesgauthereau 53c1bc9e59 fix 2026-03-06 09:56:13 +01:00
github-actions[bot] 77901ae1bc chore: release v1.4.35 2026-03-06 08:55:03 +00:00
1aeff542e5 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:54:33 +01:00
charlesgauthereau c05469d704 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:53:43 +01:00
charlesgauthereau 1e6bb542f9 fix 2026-03-06 09:53:23 +01:00
github-actions[bot] b81ad079ee chore: release v1.4.34 2026-03-06 08:49:01 +00:00
044edcda07 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:48:32 +01:00
charlesgauthereau 7eb85d2a99 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:47:23 +01:00
charlesgauthereau 7d4d6cd9df fix 2026-03-06 09:47:02 +01:00
github-actions[bot] 3f7eb4528e chore: release v1.4.33 2026-03-06 08:37:28 +00:00
0008f70552 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:36:59 +01:00
charlesgauthereau ff48517b43 fix 2026-03-06 09:36:31 +01:00
github-actions[bot] 2695715180 chore: release v1.4.32 2026-03-06 08:34:59 +00:00
b3be1a561c fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:34:31 +01:00
charlesgauthereau cab7a7f8bb Merge branch 'main' into dev 2026-03-06 09:33:51 +01:00
charlesgauthereau f6da5961c3 fix 2026-03-06 09:33:23 +01:00
github-actions[bot] d297f1a27b chore: release v1.4.31 2026-03-06 08:31:07 +00:00
b513509b01 fix: workflow
* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:30:39 +01:00
charlesgauthereau bfa3f7e04e Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:30:01 +01:00
charlesgauthereau 539b516e45 fix 2026-03-06 09:29:35 +01:00
github-actions[bot] 25744efc66 chore: release v1.4.30 2026-03-06 08:25:40 +00:00
21e37366fb fix: workflow
* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:25:07 +01:00
charlesgauthereau 6002ad44e1 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:24:36 +01:00
charlesgauthereau a431270718 fix 2026-03-06 09:24:09 +01:00
github-actions[bot] 3f6a47c39d chore: release v1.4.29 2026-03-06 08:22:18 +00:00
4dace0a32b fix: workflow
* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:21:47 +01:00
charlesgauthereau efb5318fc5 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:21:11 +01:00
charlesgauthereau a5e83f3133 fix 2026-03-06 09:20:51 +01:00
github-actions[bot] 7b6ca12744 chore: release v1.4.28 2026-03-06 08:18:49 +00:00
da1b5a969c fix: workflow
* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:18:19 +01:00
charlesgauthereau 5d790231ea Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:17:28 +01:00
charlesgauthereau ad363ce38c fix 2026-03-06 09:16:58 +01:00
github-actions[bot] 8e5d6eec6c chore: release v1.4.27 2026-03-06 08:13:08 +00:00
github-actions[bot] 92f1400a35 chore: release v1.4.26 2026-03-06 08:13:05 +00:00
47f1938136 fix: workflow
Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:12:30 +01:00
charlesgauthereau 13d3cfb290 Merge branch 'main' into dev 2026-03-06 09:11:05 +01:00
charlesgauthereau c657bdfb63 fix 2026-03-06 09:10:47 +01:00
github-actions[bot] f226218b3a chore: release v1.4.25 2026-03-06 08:07:38 +00:00
Charles GTEandGitHub 8f7fa0dea1 Merge pull request #153 from Portabase/dev
fix: workflow
2026-03-06 09:07:05 +01:00
charlesgauthereau 292b3c805d Merge branch 'main' into dev 2026-03-06 09:06:17 +01:00
charlesgauthereau ded26f92af fix 2026-03-06 09:05:43 +01:00
github-actions[bot] 8cce7bbaf4 chore: release v1.4.24 2026-03-06 08:03:42 +00:00
228b8c3ec7 fix: workflow
* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:03:15 +01:00
charlesgauthereau 2e298ce757 Merge branch 'main' into dev
# Conflicts:
#	.release-it.json
2026-03-06 09:02:45 +01:00
charlesgauthereau 81daa9f0b0 fix: .release-it.json 2026-03-06 09:01:55 +01:00
github-actions[bot] 713c8d9177 chore: release v1.4.23 2026-03-06 07:58:23 +00:00
f571162eed fix: workflow
* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 08:57:54 +01:00
charlesgauthereau 19d2872943 Merge branch 'main' into dev 2026-03-06 08:57:25 +01:00
charlesgauthereau 0752b8bf7f fix: .release-it.json 2026-03-06 08:57:11 +01:00
charlesgauthereau 8655b81618 Merge branch 'main' into dev 2026-03-06 08:49:05 +01:00
charlesgauthereau 1702e43373 fix: .release-it.json 2026-03-06 08:48:53 +01:00
charlesgauthereau 79425f4490 fix: .release-it.json 2026-03-06 08:45:30 +01:00
charlesgauthereau 2fcddea0fa Merge branch 'main' into dev 2026-03-06 08:45:08 +01:00
charlesgauthereau a43eedaf9f Merge remote-tracking branch 'origin/dev' into dev 2026-03-06 08:39:24 +01:00
charlesgauthereau 77bc24990e fix: .release-it.json 2026-03-06 08:38:39 +01:00
charlesgauthereau edbc79c89e fix: .release-it.json 2026-03-06 08:38:06 +01:00
charlesgauthereau 1ce3acdde0 fix: .release-it.json 2026-03-06 08:21:38 +01:00
112 changed files with 29194 additions and 2411 deletions
-1
View File
@@ -6,7 +6,6 @@ DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
# Project
PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://localhost:8887
PROJECT_SECRET=
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@soluce-technologies.com.
contact@portabase.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+1 -1
View File
@@ -18,7 +18,7 @@ We take security seriously and aim to support the following versions of the proj
If you discover a security vulnerability in this project, we appreciate your help in disclosing it responsibly.
1. **Contact Us**
Please report the vulnerability by emailing **[contact@soluce-technologies.com](mailto:contact@soluce-technologies.com)**. Include the following details:
Please report the vulnerability by emailing **[contact@portabase.io](mailto:contact@portabase.io)**. Include the following details:
- A detailed description of the issue.
- Steps to reproduce the vulnerability (if applicable).
- Any potential impacts or risks.
-47
View File
@@ -1,47 +0,0 @@
name: Auto Release bot
on:
pull_request:
types: [closed]
branches:
- main
jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
ref: ${{ github.base_ref }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: git config --global user.name 'github-actions[bot]'
- run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Run release-it
run: |
git pull origin main
pnpm exec release-it --ci
git push origin --tags
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+5 -2
View File
@@ -3,6 +3,9 @@ name: Discord Notification
on:
workflow_call:
inputs:
release_tag:
required: true
type: string
discord_title:
required: true
type: string
@@ -27,10 +30,10 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author)
RELEASE_INFO=$(gh release view "${{ inputs.release_tag }}" -R ${{ github.repository }} --json name,url,body,author)
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ inputs.release_tag }}"; fi
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
+35 -7
View File
@@ -3,6 +3,12 @@ name: Docker Publish
on:
workflow_call:
inputs:
version:
required: true
type: string
ref:
required: true
type: string
image_name:
required: false
type: string
@@ -32,10 +38,13 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [linux/amd64, linux/arm64]
platform: [ linux/amd64, linux/arm64 ]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -50,9 +59,10 @@ jobs:
id: prep
run: |
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
echo "image=${{ inputs.image_name }}:${GITHUB_REF#refs/tags/}-$ARCH" >> $GITHUB_OUTPUT
echo "image=${{ inputs.image_name }}:${{inputs.version}}-$ARCH" >> $GITHUB_OUTPUT
echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT
- name: Build and push image
uses: docker/build-push-action@v6
with:
@@ -95,20 +105,38 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Generate semantic tags
id: tags
run: |
VERSION="${{ inputs.version }}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
echo "VERSION_TAG=$VERSION" >> $GITHUB_OUTPUT
if [ -n "$MINOR" ]; then
echo "MINOR_TAG=$MAJOR.$MINOR" >> $GITHUB_OUTPUT
fi
if [ -n "$MAJOR" ]; then
echo "MAJOR_TAG=$MAJOR" >> $GITHUB_OUTPUT
fi
if [ "${{ inputs.add_latest }}" = "true" ]; then
echo "LATEST_TAG=latest" >> $GITHUB_OUTPUT
fi
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.image_name }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ inputs.add_latest }}
type=raw,value=${{ steps.tags.outputs.VERSION_TAG }}
type=raw,value=${{ steps.tags.outputs.MINOR_TAG }}
type=raw,value=${{ steps.tags.outputs.MAJOR_TAG }}
type=raw,value=${{ steps.tags.outputs.LATEST_TAG }}
- name: Create and push manifest list
working-directory: /tmp/digests
run: |
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
docker buildx imagetools create $DOCKER_IMAGES $TAG_ARGS
echo $TAG_ARGS
docker buildx imagetools create $TAG_ARGS $DOCKER_IMAGES
+77
View File
@@ -0,0 +1,77 @@
name: End-to-end testing
on:
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dockerfile/Dockerfile
push: false
load: true
tags: portabase/portabase:test
- name: Run app container
run: |
docker run -d --name myapp \
-p 8887:80 \
-e NODE_ENV=production \
-e PROJECT_URL=http://localhost:8887 \
-e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \
portabase/portabase:test
- name: Wait for app port to be listening
run: |
for i in {1..40}; do
if curl -fsS http://localhost:8887/ >/dev/null; then
echo "App is responding on port 8887"
exit 0
fi
echo "Waiting for app readiness... ($i/40)"
sleep 2
done
echo "Timeout: app never became ready"
docker logs myapp
exit 1
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium firefox webkit
- name: Run Playwright tests
run: pnpm exec playwright test --project chromium
env:
PROJECT_URL: http://localhost:8887
- name: Upload report if failed
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Cleanup
if: always()
run: docker stop myapp && docker rm myapp || true
+37
View File
@@ -0,0 +1,37 @@
name: Publish Helm Chart
on:
workflow_call:
inputs:
version:
required: true
type: string
secrets:
GH_TOKEN:
required: true
jobs:
publish-helm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Package Helm chart
run: |
mkdir -p ./helm-packages
helm package helm \
--version ${{ inputs.version }} \
--app-version ${{ inputs.version }} \
--destination ./helm-packages
- name: Authenticate to GitHub Packages
run: |
echo "${{ secrets.GH_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push Helm chart to GitHub Packages (OCI)
run: |
helm push ./helm-packages/portabase-${{ inputs.version }}.tgz oci://ghcr.io/portabase/charts
-47
View File
@@ -1,47 +0,0 @@
name: PR Checker
on:
pull_request:
branches:
- main
- dev
jobs:
validate-code:
name: Code Integrity Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check code formatting
run: pnpm run format:check
continue-on-error: true
- name: Run Linter
run: pnpm run lint
continue-on-error: true
- name: Type Check
run: pnpm run typecheck
continue-on-error: true
- name: Run Tests
run: pnpm run test
continue-on-error: true
- name: Test Application Build
run: pnpm run build
+120 -22
View File
@@ -1,45 +1,143 @@
name: Publish Docker image for release
name: Auto Release & Publish
on:
push:
tags:
- "*.*.*"
- "!*-*"
pull_request:
types: [ closed ]
branches:
- main
permissions:
contents: read
contents: write
packages: write
jobs:
docker_publish:
check-skip:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.set-skip.outputs.skip }}
steps:
- name: Determine if release should be skipped
id: set-skip
run: |
TITLE="${{ github.event.pull_request.title }}"
echo "PR title: $TITLE"
if [[ "$TITLE" == *"[skip-release]"* ]]; then
echo "PR title contains [skip-release], skipping release jobs."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
create-release:
needs: check-skip
if: ${{ needs.check-skip.outputs.skip == 'false' }}
runs-on: ubuntu-latest
outputs:
draft_tag: ${{ steps.release_step.outputs.draft_tag }}
version: ${{ steps.release_step.outputs.version }}
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
ref: main
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Run release-it
id: release_step
run: |
git pull origin main
VERSION=$(pnpm exec release-it --ci --release-version)
echo $VERSION
echo "version=$VERSION" >> $GITHUB_OUTPUT
OUTPUT=$(pnpm exec release-it --ci)
echo "$OUTPUT"
DRAFT_TAG=$(echo "$OUTPUT" | grep -oE 'untagged-[a-z0-9]+')
echo $DRAFT_TAG
echo "draft_tag=$DRAFT_TAG" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
publish-docker:
needs: create-release
if: ${{ needs.create-release.result == 'success' }}
uses: ./.github/workflows/docker.yml
with:
version: ${{ needs.create-release.outputs.version }}
ref: ${{ needs.create-release.outputs.version }}
add_latest: true
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
publish_release:
needs: docker_publish
publish-helm:
needs: create-release
if: ${{ needs.create-release.result == 'success' }}
uses: ./.github/workflows/helm.yml
with:
version: ${{ needs.create-release.outputs.version }}
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
finalize-release:
needs:
- create-release
- publish-docker
- publish-helm
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.publish_release_step.outputs.release_tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Publish GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release edit ${{ github.ref_name }} --draft=false
- name: Publish GitHub Release
id: publish_release_step
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
OUTPUT=$(gh release edit ${{ needs.create-release.outputs.draft_tag }} --draft=false)
echo "$OUTPUT"
RELEASE_TAG=$(echo "$OUTPUT" | sed -E 's|.*/releases/tag/||')
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
discord_notification:
needs: docker_publish
notify-discord:
needs:
- publish-docker
- publish-helm
- create-release
- finalize-release
uses: ./.github/workflows/discord.yml
with:
discord_title: "New Release: ${{ github.ref_name }}"
release_tag: ${{ needs.create-release.outputs.version }}
discord_title: "New Release"
discord_color: 3066993
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -2
View File
@@ -9,14 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.20.0
- uses: aquasecurity/trivy-action@0.35.0
with:
scan-type: 'fs'
format: 'table'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
secrets-gitleaks:
if: github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-latest
+7
View File
@@ -47,3 +47,10 @@ private/keys/*
seeds/pocket-id/*.db
certificates
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
+6 -11
View File
@@ -1,22 +1,17 @@
{
"github": {
"release": true,
"draft": true,
"tokenRef": "GITHUB_TOKEN"
},
"git": {
"commit": true,
"commitMessage": "chore: release v${version}",
"commitMessage": "chore: release ${version}",
"requireCleanWorkingDir": true,
"tag": true,
"tagName": "${version}",
"push": true
},
"github": {
"release": true,
"tokenRef": "GITHUB_TOKEN",
"draft": true,
"tagName": "${version}",
"push": true
},
"hooks": {
"after:git:push": "sleep 5"
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": {
+1 -1
View File
@@ -31,5 +31,5 @@ keywords:
- web-ui
- agent
license: Apache-2.0
version: 1.4.22
version: 1.9.3
date-released: '2026-03-02'
+19 -2
View File
@@ -12,7 +12,7 @@
[![License: Apache](https://img.shields.io/badge/License-apache-yellow.svg)](LICENSE)
[![Docker Pulls](https://img.shields.io/docker/pulls/solucetechnologies/portabase?color=brightgreen)](https://hub.docker.com/r/solucetechnologies/portabase)
[![Docker Pulls](https://img.shields.io/docker/pulls/portabase/portabase?color=brightgreen)](https://hub.docker.com/r/portabase/portabase)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/Portabase/portabase)
[![Support Portabase](https://img.shields.io/badge/Support-Portabase-orange)](https://www.buymeacoffee.com/portabase)
@@ -20,6 +20,8 @@
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/)
[![MariaDB](https://img.shields.io/badge/MariaDB-003545?logo=mariadb&logoColor=white)](https://mariadb.org/)
[![SQLite](https://img.shields.io/badge/-SQLite-blue?logo=sqlite&logoColor=white)](https://sqlite.org/)
[![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat&logo=Redis&logoColor=white)](https://redis.io/)
[![Valkey](https://img.shields.io/badge/Valkey-6284fc?style=flat&logo=Valkey&logoColor=white)](https://valkey.io/)
[![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?logo=mongodb&logoColor=white)](https://www.mongodb.com/)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase)
@@ -53,11 +55,26 @@ You have 4 ways to install Portabase:
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
- Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
- Kubernetes with Helm (soon)
- Kubernetes with Helm [details](https://portabase.io/docs/dashboard/setup#helm)
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
**Ensure Docker is installed on your machine before getting started.**
## Supported databases
| Engine | Support | Supported Versions | Restore |
|:-------------------|:----------|:----------------------------------|:--------|
| **PostgreSQL** | ✅ Stable | 12, 13, 14, 15, 16, 17 et 18 | Yes |
| **MySQL** | ✅ Stable | 5.7, 8 et 9 | Yes |
| **MariaDB** | ✅ Stable | 10 et 11 | Yes |
| **MongoDB** | ✅ Stable | 4, 5, 6, 7 et 8 | Yes |
| **SQLite** | ✅ Stable | 3.x | Yes |
| **Redis** | ✅ Stable | 2.8+ | No |
| **Valkey** | ✅ Stable | 7.2+ | No |
| **MSSQL Server** | ❌ Ongoing | - | Yes |
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
## Contributors
[![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors)
+1 -4
View File
@@ -1,10 +1,8 @@
import React, { Suspense } from "react";
import React from "react";
import { redirect } from "next/navigation";
import { currentUser } from "@/lib/auth/current-user";
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
import { env } from "@/env.mjs";
import { Heart } from "lucide-react";
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
export default async function Layout({
children,
@@ -31,7 +29,6 @@ export default async function Layout({
<Heart className="size-3 fill-red-500 text-red-500 animate-pulse" />{" "}
by <span className="font-medium text-foreground">Portabase</span>
</p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</footer>
</div>
);
-1
View File
@@ -8,7 +8,6 @@ import Link from "next/link";
import { Separator } from "@/components/ui/separator";
import { CardAuth } from "@/features/layout/card-auth";
import { env } from "@/env.mjs";
import { en } from "zod/v4/locales";
export const metadata: Metadata = {
title: "Login",
@@ -6,6 +6,7 @@ import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/setti
import {desc, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
export default async function RoutePage(props: PageParams<{}>) {
@@ -13,6 +14,8 @@ export default async function RoutePage(props: PageParams<{}>) {
where: (fields, {eq}) => eq(fields.name, "system"),
});
console.log(settings)
const storageChannels = await db.query.storageChannel.findMany({
with: {
organizations: true
@@ -21,7 +24,16 @@ export default async function RoutePage(props: PageParams<{}>) {
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
}) as StorageChannelWith[]
if (!settings || !storageChannels ) {
const notificationChannels = await db.query.notificationChannel.findMany({
with: {
organizations: true
},
where: isNull(drizzleDb.schemas.notificationChannel.organizationId),
orderBy: desc(drizzleDb.schemas.notificationChannel.createdAt)
}) as NotificationChannelWith[]
if (!settings || !storageChannels || !notificationChannels ) {
notFound()
}
@@ -33,7 +45,7 @@ export default async function RoutePage(props: PageParams<{}>) {
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsTabs storageChannels={storageChannels} settings={settings} />
<SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels} settings={settings} />
</PageContent>
</Page>
);
@@ -8,6 +8,7 @@ import {getOrganizationProjectDatabases} from "@/lib/services";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
export default async function RoutePage(props: PageParams<{
projectId: string;
@@ -83,6 +84,8 @@ export default async function RoutePage(props: PageParams<{
notFound();
}
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({ id: dbItem.id }) : []
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
@@ -95,6 +98,7 @@ export default async function RoutePage(props: PageParams<{
activeMember={activeMember}
settings={settings}
database={dbItem}
databaseHealthLogs={databaseHealthLogs}
isAlreadyRestore={isAlreadyRestore}
restorations={restorations}
backups={backups}
+14 -12
View File
@@ -1,7 +1,10 @@
import { NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import {NextResponse} from "next/server";
import {eq} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/helpers"});
export function withAgentCheck(handler: Function) {
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
@@ -14,23 +17,22 @@ export function withAgentCheck(handler: Function) {
if (!agent) {
return NextResponse.json(
{ error: "Agent not found" },
{ status: 404 }
{error: "Agent not found"},
{status: 404}
);
}
return handler(request, { ...context, agent });
return handler(request, {...context, agent});
} catch (err) {
console.error("Error in agent middleware:", err);
log.error({error: err, name: "withAgentCheck"}, "Error in agent middleware");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
{error: "Internal server error"},
{status: 500}
);
}
};
}
export async function getDatabaseOrThrow(generatedId: string) {
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
@@ -43,8 +45,8 @@ export async function getDatabaseOrThrow(generatedId: string) {
if (!database) {
throw NextResponse.json(
{ error: "Database associated with generatedId not found" },
{ status: 404 }
{error: "Database associated with generatedId not found"},
{status: 404}
);
}
+6 -6
View File
@@ -8,6 +8,9 @@ import {withUpdatedAt} from "@/db/utils";
import {eventEmitter} from "@/features/shared/event";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
import {EventKind} from "@/features/notifications/types";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/route"});
export type BodyPost = {
method: "manual" | "automatic"
@@ -77,7 +80,6 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
}
}
eventEmitter.emit('modification', {update: true});
return NextResponse.json(
@@ -88,7 +90,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
log.error({error: error}, "Error in POST for INIT backup");
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
@@ -102,7 +104,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
}) => {
try {
const body: BodyPatch = await request.json();
console.log("body", body);
log.debug({data: body}, "Body from PATH in backup route");
const status = body.status
const backupId = body.backupId
@@ -142,9 +144,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in PATCH backup:", error);
log.error({error: error}, "Error in PATCH backup")
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
@@ -5,6 +5,9 @@ import {db} from "@/db";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {isUuidv4} from "@/utils/verify-uuid";
import {eventEmitter} from "@/features/shared/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/upload/init"});
export type Body = {
generatedId: string
@@ -18,7 +21,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
try {
const body: Body = await request.json();
console.log("body", body);
log.debug({data: body}, "Body for backup upload init");
const generatedId = body.generatedId;
const storageChannelId = body.storageChannelId;
@@ -66,7 +69,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
log.error({error: error}, "Error in POST for INIT backup");
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
@@ -5,6 +5,9 @@ import {db as dbClient, db} from "@/db";
import {withUpdatedAt} from "@/db/utils";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {eventEmitter} from "@/features/shared/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/upload/status"});
export type Body = {
generatedId: string
@@ -27,8 +30,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
const backupStorageId = body.backupStorageId;
const backupId = body.backupId;
console.log("body", body);
log.debug({data: body}, "Body for backup upload status");
const database = await getDatabaseOrThrow(generatedId);
@@ -85,7 +87,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
log.error({error: error},"Error in POST for INIT backup");
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
+6 -5
View File
@@ -4,7 +4,9 @@ import * as drizzleDb from "@/db";
import {db} from "@/db";
import {and, eq} from "drizzle-orm";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
import {eventEmitter} from "@/features/shared/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/restore"});
export type BodyResultRestore = {
generatedId: string
@@ -66,14 +68,13 @@ export async function POST(
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
const response = {
message: true,
details: "Restoration successfully updated"
status: true,
message: "Restoration successfully updated"
}
return Response.json(response, {status: 200})
} catch (error) {
console.error('Error in POST handler:', error);
log.error({error: error}, "Error in POST handler")
return NextResponse.json(
{error: 'Internal server error'},
{status: 500}
+34 -7
View File
@@ -2,7 +2,7 @@ import {NextResponse} from "next/server";
import {Body} from "./route";
import {isUuidv4} from "@/utils/verify-uuid";
import {Agent} from "@/db/schema/08_agent";
import {Database, DatabaseWith} from "@/db/schema/07_database";
import {DatabaseWith} from "@/db/schema/07_database";
import * as drizzleDb from "@/db";
import {db, db as dbClient} from "@/db";
import {and, eq, inArray} from "drizzle-orm";
@@ -11,6 +11,9 @@ import {withUpdatedAt} from "@/db/utils";
import type {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Setting} from "@/db/schema/01_setting";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/status/helpers"});
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
const databasesResponse = [];
@@ -56,7 +59,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
}
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
console.log(`Database type not available: ${db.dbms}`);
log.error({name: "handleDatabases"},`Database type not available: ${db.dbms}`);
continue;
}
@@ -67,12 +70,24 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
name: db.name,
dbms: db.dbms as EDbmsSchema,
agentDatabaseId: db.generatedId,
lastContact: lastContact,
lastContact: db.pingStatus ? lastContact : null,
healthErrorCount: null
})
.returning();
if (databaseCreated) {
await dbClient
.insert(drizzleDb.schemas.healthcheckLog)
.values({
kind: "database",
status: db.pingStatus ? "success" : "failed",
objectId: databaseCreated.id,
date: lastContact
})
const storages = await getDatabaseStorageChannels(databaseCreated.id)
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
@@ -84,11 +99,24 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
.set(withUpdatedAt({
name: db.name,
agentId: agent.id,
lastContact: lastContact
dbms: db.dbms as EDbmsSchema,
lastContact: db.pingStatus ? lastContact : existingDatabase.lastContact,
healthErrorCount: db.pingStatus ? null : existingDatabase.healthErrorCount,
}))
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
.returning();
await dbClient
.insert(drizzleDb.schemas.healthcheckLog)
.values({
kind: "database",
status: db.pingStatus ? "success" : "failed",
objectId: databaseUpdated.id,
date: lastContact
})
const activeBackup = await dbClient.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
@@ -159,11 +187,11 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
const errorMessage = "Failed to get backup URL";
console.error("Restoration failed: ", errorMessage);
log.error({error: errorMessage, name: "handleDatabases"}, "Restoration failed");
continue;
}
} catch (err) {
console.error("Restoration crashed unexpectedly:", err);
log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly");
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "failed"})
@@ -180,7 +208,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
}
}
return databasesResponse;
}
+19 -8
View File
@@ -6,13 +6,17 @@ import {EDbmsSchema} from "@/db/schema/types";
import {eq} from "drizzle-orm";
import {isUuidv4} from "@/utils/verify-uuid";
import {withUpdatedAt} from "@/db/utils";
import {eventEmitter} from "@/features/shared/event";
import {notFound} from "next/navigation";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/status/route"});
export type databaseAgent = {
name: string,
dbms: EDbmsSchema,
generatedId: string
pingStatus: boolean
}
export type Body = {
@@ -27,14 +31,14 @@ export async function POST(
) {
try {
const agentId = (await params).agentId
console.log(agentId)
log.debug(`Agent ID: ${agentId}`)
const body: Body = await request.json();
const lastContact = new Date();
let message: string
if (!isUuidv4(agentId)) {
message = "agentId is not a valid uuid"
console.error(message)
log.error({error: message}, "An error occurred")
return NextResponse.json(
{error: "agentId is not a valid uuid"},
{status: 500}
@@ -55,17 +59,25 @@ export async function POST(
return NextResponse.json({error: "An error occured"}, {status: 404})
}
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
await db
.update(drizzleDb.schemas.agent)
.set(withUpdatedAt({
version: body.version,
lastContact: lastContact
lastContact: lastContact,
healthErrorCount: null
}))
.where(eq(drizzleDb.schemas.agent.id, agentId));
await db
.insert(drizzleDb.schemas.healthcheckLog)
.values({
kind: "agent",
status: "success",
objectId: agentId,
date: lastContact
})
const response = {
agent: {
@@ -75,10 +87,9 @@ export async function POST(
databases: databasesResponse
}
return Response.json(response)
} catch (error) {
console.error('Error in POST handler:', error);
log.error({error: error}, "Error in POST handler")
return NextResponse.json(
{error: 'Internal server error'},
{status: 500}
+6 -3
View File
@@ -2,6 +2,9 @@ import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers";
import {NextResponse} from "next/server";
import {eventEmitter} from "@/features/shared/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/events"});
export async function GET(request: Request) {
@@ -16,9 +19,9 @@ export async function GET(request: Request) {
return new Response(
new ReadableStream({
start(controller) {
console.log('Stream started');
log.info("Stream started");
const handleModification = (data: any) => {
console.log('Modification event triggered:', data);
log.info({data: data},"Modification event triggered");
controller.enqueue(`event: modification\n`);
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
};
@@ -26,7 +29,7 @@ export async function GET(request: Request) {
eventEmitter.on('modification', handleModification);
request.signal.addEventListener('abort', () => {
console.log('Client disconnected');
log.info("Client disconnected");
controller.close();
eventEmitter.off('modification', handleModification);
});
+4 -1
View File
@@ -3,6 +3,9 @@ import path from "path";
import type {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Readable} from "node:stream";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/files/backups"});
export async function GET(
request: Request,
@@ -30,7 +33,7 @@ export async function GET(
}
};
console.debug(input);
log.debug({input: input}, "Dispatch Storage");
const result = await dispatchStorage(input, undefined, storageId);
+5 -5
View File
@@ -1,12 +1,12 @@
import {NextResponse} from "next/server";
import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
import {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Readable} from "node:stream";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/files/images"});
export async function GET(
req: Request,
@@ -56,7 +56,7 @@ export async function GET(
const result = await dispatchStorage(input, undefined, storageId);
if (!result.file || !(result.file instanceof Readable)) {
console.error(`An error occurred while getting file :`, result);
log.error({error: result}, `An error occurred while getting file`);
return NextResponse.json(
{error: "Invalid file payload"},
{status: 500}
@@ -82,7 +82,7 @@ export async function GET(
});
} catch (err) {
console.error("Error streaming image:", err);
log.error({error: err}, `Error streaming image`);
return NextResponse.json({error: "Error fetching file"}, {status: 500});
}
}
+5 -5
View File
@@ -2,7 +2,9 @@ import {NextResponse} from "next/server";
import fs from "fs";
import path from "path";
import {env} from "@/env.mjs";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/tus/hooks"});
export async function POST(request: Request) {
try {
@@ -13,7 +15,7 @@ export async function POST(request: Request) {
const uploadOffset = headers["Upload-Offset"]?.[0];
const status = headers["X-Status"]?.[0];
console.log(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
log.info(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
if (status === "success") {
if (
@@ -29,9 +31,7 @@ export async function POST(request: Request) {
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
}
// const uploadDir = path.join(process.cwd(), "/private/uploads/");
const uploadDir = path.join(env.PRIVATE_PATH, "/uploads/");
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);
@@ -74,7 +74,7 @@ export async function POST(request: Request) {
}
return NextResponse.json({});
} catch (error) {
console.error("Hook error:", error);
log.error({error: error},"TUS Hook error");
return NextResponse.json({error: "Internal server error"}, {status: 500});
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { author, geistMono, poppins } from "@/fonts/fonts";
import { cn } from "@/lib/utils";
import { Providers } from "./providers";
const title = process.env.PROJECT_NAME ?? "App Title";
const title = process.env.PROJECT_NAME ?? "Portabase";
export const metadata: Metadata = {
title: {
+1 -1
View File
@@ -4,7 +4,7 @@ services:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: prod
image: portabase/portabase:1.4.0
image: portabase/portabase:1.7.1
ports:
- '8887:80'
environment:
+135
View File
@@ -0,0 +1,135 @@
import {test, expect, Page} from '@playwright/test';
type UserCredentials = {
username: string
email: string
password: string
}
const users: Record<string, UserCredentials> = {
admin: {username: 'Admin', email: 'admin@example.com', password: 'testPASS123456!'},
normal: {username: 'John Doe', email: 'john.doe@example.com', password: 'testPASS123456!'},
}
async function register(page: Page, name: string, email: string, password: string, confirmPassword: string) {
await page.locator('input[name="name"]').fill(name)
await page.locator('input[name="email"]').fill(email)
await page.locator('input[name="password"]').fill(password)
await page.locator('input[name="confirmPassword"]').fill(confirmPassword)
await page.click('button[type="submit"]')
}
async function login(page: Page, email: string, password: string) {
await page.locator('input[name="email"]').fill(email)
await page.locator('input[name="password"]').fill(password)
await page.locator('button:has-text("Login")').click()
}
async function logout() {
//TODO
}
test.describe.serial('User signup and login flows', () => {
test('Redirect to login if not connected', async ({page}) => {
await page.goto('/dashboard/projects');
await expect(page).toHaveURL("login?redirect=%2Fdashboard%2Fprojects");
});
test('Password too short', async ({page}) => {
await page.goto('')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
let password = '123456'
await register(page, users["admin"].username, users["admin"].email, password, password)
const toast = page.locator('text=Must have at least 8 character')
await expect(toast).toBeVisible()
})
test('Password too simple', async ({page}) => {
await page.goto('')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
let password = '12345678'
await register(page, users["admin"].username, users["admin"].email, password, password)
const toast = page.locator('text=Your password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.')
await expect(toast).toBeVisible()
})
test('Password and confirm password mismatch', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
let password = 'testPASS123456!'
let confirmPassword = 'testPASS123456!!'
await register(page, users["admin"].username, users["admin"].email, password, confirmPassword)
const toast = page.locator('text=The passwords did not match')
await expect(toast).toBeVisible()
})
test('Successful register for admin', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password)
await expect(page).toHaveURL('/login')
})
test('User already exists.', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password)
const toast = page.locator('text=User already exists. Use another email.')
await expect(toast).toBeVisible()
})
test('Successful register for normal', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
await register(page, users["normal"].username, users["normal"].email, users["normal"].password, users["normal"].password)
await expect(page).toHaveURL('login')
})
test('Failed login because account not active', async ({page}) => {
await page.goto('/login')
await login(page, users["normal"].email, users["normal"].password)
const toast = page.locator('text=Your account is not active.')
await expect(toast).toBeVisible()
})
test('Successful login', async ({page}) => {
await page.goto('login')
await login(page, users["admin"].email, users["admin"].password)
await expect(page).toHaveURL('/dashboard/home')
})
})
// test('Successful logout', async ({page}) => {
// await logout()
// await expect(page).toHaveURL('/login')
// })
+17
View File
@@ -0,0 +1,17 @@
.DS_Store
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
*.swp
*.bak
*.tmp
*.orig
*~
.project
.idea/
*.tmproj
.vscode/
+28
View File
@@ -0,0 +1,28 @@
apiVersion: v2
name: portabase
description: Helm chart for Portabase
type: application
version: 0.0.0
appVersion: "latest"
keywords:
- postgresql
- mariadb
- mongodb
- mysql
- sqlite
- backup
- database
- restore
home: https://github.com/Portabase/portabase
sources:
- https://github.com/Portabase/portabase
- https://github.com/Portabase/portabase/tree/main/helm
maintainers:
- name: Charles Gauthereau
url: https://github.com/RambokDev
- name: Killian Larcher
url: https://github.com/KillianLarcher
icon: https://raw.githubusercontent.com/Portabase/portabase/main/.github/assets/logo.png
+32
View File
@@ -0,0 +1,32 @@
# Development Notes
## Check that Kubernetes is reachable locally
```bash
kubectl get nodes
```
## Install the local Portabase Helm chart
```bash
helm install portabase . \
--set project.secret=$(openssl rand -hex 32)
```
## Check the pods
```bash
kubectl get pods
```
## Check the services
```bash
kubectl get svc
```
## If the service type is ClusterIP, expose it locally using port forwarding:
```bash
kubectl port-forward svc/portabase 8887:80
```
+33
View File
@@ -0,0 +1,33 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: portabase
spec:
replicas: 1
selector:
matchLabels:
app: portabase
template:
metadata:
labels:
app: portabase
spec:
containers:
- name: portabase
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 80
env:
- name: TZ
value: {{ .Values.timezone }}
- name: PROJECT_URL
value: {{ .Values.project.url }}
- name: PROJECT_SECRET
value: {{ .Values.project.secret }}
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: portabase-data
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: portabase-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.size }}
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: portabase
spec:
type: {{ .Values.service.type }}
selector:
app: portabase
ports:
- port: {{ .Values.service.port }}
targetPort: 80
nodePort: {{ .Values.service.nodePort }}
+47
View File
@@ -0,0 +1,47 @@
image:
repository: portabase/portabase
tag: latest
pullPolicy: Always
replicaCount: 1
service:
type: ClusterIP
port: 80
targetPort: 80
resources:
requests:
memory: "1Gi"
cpu: "1"
limits:
memory: "1Gi"
cpu: "1"
timezone: Europe/Paris
project:
url: http://localhost:8887
secret: "change_me_generate_secure_secret"
persistence:
enabled: true
storageClassName: ""
accessMode: ReadWriteOnce
size: 10Gi
mountPath: /data
ingress:
enabled: false
className: nginx
hosts:
- host: portabase.example.com
paths:
- path: /
pathType: Prefix
tls: []
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0
+3
View File
@@ -43,6 +43,9 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
logging: {
browserToTerminal: false,
},
experimental: {
serverActions: {
bodySizeLimit: "10gb",
+31 -28
View File
@@ -1,6 +1,6 @@
{
"name": "portabase",
"version": "1.4.22",
"version": "1.9.3",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 8887",
@@ -15,8 +15,8 @@
"release": "release-it"
},
"dependencies": {
"@better-auth/passkey": "^1.4.19",
"@better-auth/sso": "^1.4.19",
"@better-auth/passkey": "^1.5.6",
"@better-auth/sso": "^1.5.6",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -47,20 +47,20 @@
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-email/components": "^0.0.41",
"@t3-oss/env-nextjs": "^0.13.10",
"@tanstack/react-query": "^5.90.21",
"@t3-oss/env-nextjs": "^0.13.11",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-table": "^8.21.3",
"@types/nodemailer": "^6.4.23",
"@types/ws": "^8.18.1",
"@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.1",
"bcrypt": "^6.0.0",
"better-auth": "1.4.18",
"better-auth": "1.5.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dockerode": "^4.0.9",
"dockerode": "^4.0.10",
"dotenv": "^16.6.1",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
@@ -68,23 +68,25 @@
"googleapis": "^170.1.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.553.0",
"minio": "^8.0.6",
"motion": "^12.34.3",
"next": "16.1.5",
"minio": "^8.0.7",
"motion": "^12.38.0",
"next": "16.2.1",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
"node-cron": "^4.2.1",
"node-forge": "^1.3.3",
"node-forge": "^1.4.0",
"nodemailer": "^7.0.13",
"npm-check-updates": "^18.3.1",
"pg": "^8.18.0",
"pg": "^8.20.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"prettier": "^3.8.1",
"react": "^19.2.4",
"react-day-picker": "9.7.0",
"react-dom": "^19.2.4",
"react-dropzone": "^14.4.1",
"react-email": "^4.3.2",
"react-hook-form": "^7.71.2",
"react-hook-form": "^7.72.0",
"react-qr-code": "^2.0.18",
"react-resizable-panels": "^3.0.6",
"react-twc": "^1.5.1",
@@ -94,42 +96,43 @@
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sonner": "^2.0.7",
"swiper": "^12.1.2",
"swiper": "^12.1.3",
"tailwind-merge": "^3.5.0",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"ws": "^8.19.0",
"ws": "^8.20.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@iconify/react": "^6.0.2",
"@playwright/test": "1.58.2",
"@react-email/preview-server": "4.3.2",
"@react-email/render": "^2.0.4",
"@release-it/bumper": "^7.0.5",
"@release-it/conventional-changelog": "^10.0.5",
"@tailwindcss/postcss": "^4.2.1",
"@release-it/conventional-changelog": "^10.0.6",
"@tailwindcss/postcss": "^4.2.2",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.19.11",
"@types/node": "^22.19.15",
"@types/node-forge": "^1.3.14",
"@types/pg": "^8.16.0",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@zenstackhq/openapi": "^2.22.1",
"@zenstackhq/tanstack-query": "^2.22.2",
"baseline-browser-mapping": "^2.10.0",
"drizzle-kit": "^0.31.9",
"esbuild": "^0.27.3",
"eslint": "^9.39.3",
"eslint-config-next": "^16.1.6",
"baseline-browser-mapping": "^2.10.12",
"drizzle-kit": "^0.31.10",
"esbuild": "^0.27.4",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.1",
"eslint-plugin-tailwindcss": "^3.18.2",
"framer-motion": "^12.34.3",
"postcss": "^8.5.6",
"framer-motion": "^12.38.0",
"postcss": "^8.5.8",
"release-it": "^19.2.4",
"tailwindcss": "^4.2.1",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"zenstack": "2.14.2"
},
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
"packageManager": "pnpm@10.33.0"
}
+79
View File
@@ -0,0 +1,79 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: process.env.PROJECT_URL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'docker compose up -d && make up',
// url: 'http://localhost:8887',
// reuseExistingServer: !process.env.CI,
// },
});
+1411 -1242
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
+17 -6
View File
@@ -209,14 +209,25 @@ type UseZodFormProps<Z extends ZodSchema> = Exclude<
};
// const useZodForm = <Z extends ZodSchema>({
// schema,
// ...formProps
// }: UseZodFormProps<Z>) =>
// useForm({
// ...formProps,
// // @ts-ignore
// resolver: zodResolver(schema),
// });
const useZodForm = <Z extends ZodSchema>({
schema,
...formProps
}: UseZodFormProps<Z>) =>
useForm({
...formProps,
schema,
...formProps
}: UseZodFormProps<Z>): UseFormReturn<TypeOf<Z>> =>
useForm<TypeOf<Z>>({
...formProps,
// @ts-ignore
resolver: zodResolver(schema),
resolver: zodResolver(schema),
});
export {
@@ -6,6 +6,10 @@ export const S3ChannelConfigSchema = z.object({
accessKey: z.string().min(1, "Access Key is required"),
secretKey: z.string().min(1, "Secret Key is required"),
bucketName: z.string().min(1, "Bucket name is required"),
port: z.coerce.number().optional(),
port: z.union([
z.literal("").transform(() => ""),
z.literal("").transform(() => ""),
z.coerce.number()
]).optional(),
ssl: z.boolean().optional().default(true),
});
@@ -1,6 +1,5 @@
"use client";
import {Card, CardContent} from "@/components/ui/card";
import {
FormControl,
FormDescription,
@@ -0,0 +1,149 @@
"use client"
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Info, Send} from "lucide-react";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {useRouter} from "next/navigation";
import {Setting} from "@/db/schema/01_setting";
import {
Form,
FormField,
FormItem,
FormLabel,
useZodForm
} from "@/components/ui/form";
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
import {useMutation} from "@tanstack/react-query";
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
import {
DefaultNotificationSchema, DefaultNotificationType
} from "@/components/wrappers/dashboard/admin/settings/notification/settings-notification.schema";
import {
updateNotificationSettingsAction
} from "@/components/wrappers/dashboard/admin/settings/notification/settings-notification.action";
import {toast} from "sonner";
import {Badge} from "@/components/ui/badge";
export type SettingsNotificationSectionProps = {
settings: Setting;
notificationChannels: NotificationChannelWith[];
};
export const SettingsNotificationSection = ({settings, notificationChannels}: SettingsNotificationSectionProps) => {
const router = useRouter();
const form = useZodForm({
schema: DefaultNotificationSchema,
defaultValues: {
notificationChannelId: settings.defaultNotificationChannelId ?? "",
}
});
const mutation = useMutation({
mutationFn: async (values: DefaultNotificationType) => {
const result = await updateNotificationSettingsAction({name: "system", data: values})
const inner = result?.data;
if (inner?.success) {
toast.success(inner.actionSuccess?.message);
router.refresh();
} else {
toast.error(inner?.actionError?.message);
}
}
});
return (
<div className="flex flex-col h-full">
<Alert className="mt-3 flex items-start gap-2">
<Info className="h-4 w-4 mt-1"/>
<div>
<AlertTitle>Informations</AlertTitle>
<AlertDescription className="flex flex-wrap items-center gap-1">
The default notification channel will be used to send
<Badge>error_health_agent</Badge>
<Badge>error_health_database</Badge>
<Badge>error_backup</Badge>
<Badge>error_restore</Badge>
events. For more options like notify when success, please set policy at database level
</AlertDescription>
</div>
</Alert>
<div className="flex flex-col h-full py-4 gap-3">
<Form
className="space-y-4"
form={form}
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<div className="flex flex-wrap items-center gap-3">
<FormField
control={form.control}
name="notificationChannelId"
render={({ field }) => (
<FormItem className="flex-grow ">
<FormLabel>Default Notification Provider</FormLabel>
{notificationChannels.length === 0 ? (
<div className="text-sm text-muted-foreground">No channel available</div>
) : (
<Select
value={field.value ?? ""}
onValueChange={(value) => field.onChange(value)}
>
<SelectTrigger className="w-full h-full mb-0">
<SelectValue placeholder="Select a default channel" />
</SelectTrigger>
<SelectContent>
{notificationChannels.map((channel) => (
<SelectItem key={channel.id} value={channel.id}>
<div className="flex items-center gap-2">
{getChannelIcon(channel.provider)}
<span className="font-medium">{channel.name}</span>
<span className="text-[9px] uppercase bg-secondary px-1.5 py-0.5 rounded">
{channel.provider}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</FormItem>
)}
/>
</div>
<div className="flex justify-between gap-4">
{notificationChannels.length > 0 && (
<ButtonWithLoading
type="submit"
>
Confirm
</ButtonWithLoading>
)}
<div className="flex justify-end">
{notificationChannels.length > 0 && form.getValues("notificationChannelId") ? (
<ButtonWithLoading
type="button"
variant="outline"
onClick={async () => {
form.setValue("notificationChannelId", "");
await mutation.mutateAsync({
notificationChannelId: null,
});
}}
className="flex-shrink-0 w-full sm:w-auto"
>
Reset
</ButtonWithLoading>
) : null}
</div>
</div>
</Form>
</div>
</div>
);
};
@@ -0,0 +1,51 @@
"use server"
import {userAction} from "@/lib/safe-actions/actions";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
import {ServerActionResult} from "@/types/action-type";
import {Setting} from "@/db/schema/01_setting";
import {z} from "zod";
import {
DefaultNotificationSchema
} from "@/components/wrappers/dashboard/admin/settings/notification/settings-notification.schema";
export const updateNotificationSettingsAction = userAction
.schema(
z.object({
name: z.string(),
data: DefaultNotificationSchema,
})
)
.action(async ({parsedInput}): Promise<ServerActionResult<Setting>> => {
const {name, data} = parsedInput;
try {
const [updatedSettings] = await db
.update(drizzleDb.schemas.setting)
.set({
defaultNotificationChannelId: data.notificationChannelId ?? null,
})
.where(eq(drizzleDb.schemas.setting.name, name))
.returning();
return {
success: true,
value: updatedSettings,
actionSuccess: {
message: "Settings successfully updated",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed update settings.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
@@ -0,0 +1,7 @@
import {z} from "zod";
export const DefaultNotificationSchema = z.object({
notificationChannelId: z.string().optional().nullable()
});
export type DefaultNotificationType = z.infer<typeof DefaultNotificationSchema>;
@@ -7,14 +7,20 @@ import {Setting} from "@/db/schema/01_setting";
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {MailboxIcon, Save} from "lucide-react";
import {AlarmClock, MailboxIcon, Save} from "lucide-react";
import {
SettingsNotificationSection
} from "@/components/wrappers/dashboard/admin/settings/notification/settings-notification-section";
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
export type SettingsTabsProps = {
settings: Setting
storageChannels: StorageChannelWith[]
storageChannels: StorageChannelWith[],
notificationChannels: NotificationChannelWith[];
};
export const SettingsTabs = ({settings, storageChannels}: SettingsTabsProps) => {
export const SettingsTabs = ({settings, storageChannels, notificationChannels}: SettingsTabsProps) => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -47,6 +53,15 @@ export const SettingsTabs = ({settings, storageChannels}: SettingsTabsProps) =>
content: (
<SettingsStorageSection storageChannels={storageChannels} settings={settings}/>
)
},
{
name: 'Notification',
value: 'notification',
icon: AlarmClock,
content: (
<SettingsNotificationSection notificationChannels={notificationChannels} settings={settings}/>
)
}
]
@@ -109,7 +109,7 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
control={form.control}
name="storageChannelId"
render={({field}) => (
<FormItem className="flex-grow min-w-[200px] sm:flex-grow-0 sm:w-64">
<FormItem className="flex-grow">
<FormLabel>Default Storage Provider</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="w-full h-full mb-0">
@@ -52,6 +52,11 @@ export const AgentCard = (props: agentCardProps) => {
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
v{agent.version}
</Badge>
{agent.healthErrorCount && (
<Badge variant="destructive" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
down
</Badge>
)}
{isUpdateAvailable && (
<Tooltip>
<TooltipTrigger asChild>
@@ -17,6 +17,8 @@ import {Separator} from "@/components/ui/separator";
import {Badge} from "@/components/ui/badge";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {AgentDatabaseCard} from "@/components/wrappers/dashboard/agent/agent-database-card";
import {HealthCheckGraph} from "@/components/wrappers/dashboard/health/heath-grid";
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
type AgentContentPageProps = {
edgeKey: string;
@@ -32,7 +34,8 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
return result?.data;
},
initialData: {
data: initialAgent
data: initialAgent,
health: []
},
staleTime: 0,
gcTime: 0,
@@ -40,13 +43,15 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
});
const agent = data?.data ?? initialAgent;
const agentHealthLogs: HealthcheckLog[] = data?.health ?? [];
return (
<div className="space-y-10">
<div className="flex flex-col sm:flex-row sm:justify-between gap-6 ">
<Card className="w-full sm:w-auto flex-1 border-none shadow-none bg-muted/30">
<Card className="w-full sm:w-auto flex-1 transition-all border-border/50 bg-card ">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Databases</CardTitle>
<CardTitle
className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Databases</CardTitle>
<Server className="h-4 w-4 text-muted-foreground opacity-50"/>
</CardHeader>
<CardContent>
@@ -55,26 +60,34 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
</CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1 border-none shadow-none bg-muted/30">
<Card className="w-full sm:w-auto flex-1 transition-all border-border/50 bg-card ">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Last contact</CardTitle>
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Last
contact</CardTitle>
<Server className="h-4 w-4 text-muted-foreground opacity-50"/>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold tracking-tight">{formatDateLastContact(agent.lastContact)}</div>
<div
className="text-3xl font-bold tracking-tight">{formatDateLastContact(agent.lastContact)}</div>
<p className="text-xs text-muted-foreground mt-1">Status heartbeat</p>
</CardContent>
</Card>
</div>
{agent.lastContact && (
<HealthCheckGraph logs={agentHealthLogs}/>
)}
<div className="space-y-6">
<Accordion type="single" collapsible defaultValue={!agent.lastContact ? "registration" : undefined}>
<AccordionItem value="registration" className="border rounded-xl px-6 bg-card shadow-sm overflow-hidden transition-all duration-300">
<AccordionItem value="registration"
className="border rounded-xl px-6 bg-card shadow-sm overflow-hidden transition-all duration-300">
<AccordionTrigger className="hover:no-underline py-4 group">
<div className="flex items-center gap-3">
<span className="text-xl font-bold tracking-tight">Registration & Setup</span>
{!agent.lastContact && (
<Badge variant="outline" className="bg-orange-500/10 text-orange-600 border-orange-500/20 animate-pulse">
<Badge variant="outline"
className="bg-orange-500/10 text-orange-600 border-orange-500/20 animate-pulse">
Action Required
</Badge>
)}
@@ -91,23 +104,23 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
</div>
{agent.databases.length > 0 && (
<div className="space-y-6">
<div className="flex items-center justify-between px-1">
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Managed Databases</h2>
<p className="text-sm text-muted-foreground">
Resources currently connected to this agent.
</p>
<div className="space-y-6">
<div className="flex items-center justify-between px-1">
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Managed Databases</h2>
<p className="text-sm text-muted-foreground">
Resources currently connected to this agent.
</p>
</div>
</div>
<Separator className="opacity-50"/>
<CardsWithPagination
cardsPerPage={4}
numberOfColumns={2}
data={agent.databases}
cardItem={AgentDatabaseCard}
/>
</div>
<Separator className="opacity-50" />
<CardsWithPagination
cardsPerPage={4}
numberOfColumns={2}
data={agent.databases}
cardItem={AgentDatabaseCard}
/>
</div>
)}
</div>
)
@@ -21,14 +21,14 @@ interface DatabaseActionsCellProps {
backup: BackupWith;
activeMember: MemberWithUser;
isAlreadyRestore: boolean;
isBackupOnly: boolean;
}
export function DatabaseActionsCell({backup, activeMember, isAlreadyRestore}: DatabaseActionsCellProps) {
export function DatabaseActionsCell({backup, activeMember, isAlreadyRestore, isBackupOnly}: DatabaseActionsCellProps) {
const {openModal} = useBackupModal();
if (backup.deletedAt || activeMember.role === "member") return null;
return (
<div className={cn("flex items-center space-x-2")}>
<DropdownMenu>
@@ -40,18 +40,19 @@ export function DatabaseActionsCell({backup, activeMember, isAlreadyRestore}: Da
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{backup.status == "success" ? (
<>
<TooltipCustom disabled={isAlreadyRestore}
text="Already a restoration waiting">
<DropdownMenuItem
disabled={isAlreadyRestore}
onSelect={() => openModal("restore", backup)}
>
<ReloadIcon/> Restore
</DropdownMenuItem>
</TooltipCustom>
{!isBackupOnly && (
<TooltipCustom disabled={isAlreadyRestore}
text="Already a restoration waiting">
<DropdownMenuItem
disabled={isAlreadyRestore}
onSelect={() => openModal("restore", backup)}
>
<ReloadIcon/> Restore
</DropdownMenuItem>
</TooltipCustom>
)}
<DropdownMenuItem
onSelect={() => openModal("download", backup)}
>
@@ -7,6 +7,7 @@ import * as drizzleDb from "@/db";
import {BackupWith, Restoration} from "@/db/schema/07_database";
import {getOrganizationChannels} from "@/db/services/notification-channel";
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
export const getDatabaseDataAction = userAction
.schema(
@@ -50,7 +51,9 @@ export const getDatabaseDataAction = userAction
const successfulBackups = backups.filter(b => b.status === "success").length;
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
// @ts-ignore
let activeOrganizationChannels = [];
// @ts-ignore
let activeOrganizationStorageChannels = [];
if (database?.project?.organizationId) {
@@ -61,16 +64,20 @@ export const getDatabaseDataAction = userAction
activeOrganizationStorageChannels = organizationStorageChannels.filter(channel => channel.enabled);
}
return {
database,
backups,
restorations,
// @ts-ignore
activeOrganizationChannels,
// @ts-ignore
activeOrganizationStorageChannels,
stats: {
totalBackups,
availableBackups,
successRate
}
},
health: database ? await getHealthLast12hLogs({ id: database.id }) : []
};
});
@@ -22,6 +22,7 @@ import {
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
import {StorageChannel} from "@/db/schema/12_storage-channel";
import {
EVENT_KIND_BACKUP_ONLY_OPTIONS,
EVENT_KIND_OPTIONS,
PoliciesSchema,
PoliciesType,
@@ -31,11 +32,11 @@ import {
createAlertPoliciesAction, createStoragePoliciesAction, deleteAlertPoliciesAction, deleteStoragePoliciesAction,
updateAlertPoliciesAction, updateStoragePoliciesAction
} from "@/components/wrappers/dashboard/database/channels-policy/policy.action";
import {backupOnly} from "@/components/wrappers/dashboard/projects/database/database-tabs";
type ChannelPoliciesFormProps = {
onSuccess?: () => void;
channels: NotificationChannel[] | StorageChannel[];
organizationId: string;
database: DatabaseWith;
kind: ChannelKind
};
@@ -44,7 +45,6 @@ type ChannelPoliciesFormProps = {
export const ChannelPoliciesForm = ({
database,
channels,
organizationId,
onSuccess,
kind
}: ChannelPoliciesFormProps) => {
@@ -53,6 +53,9 @@ export const ChannelPoliciesForm = ({
const isMobile = useIsMobile();
const channelText = getChannelTextBasedOnKind(kind);
const isBackupOnly = backupOnly.some((type) => database.dbms === type);
const organizationChannels = channels.map(c => c.id);
const filterByChannel = <T, K extends keyof T>(
@@ -300,7 +303,7 @@ export const ChannelPoliciesForm = ({
<FormControl>
<div className="max-w-full overflow-hidden">
<MultiSelect
options={EVENT_KIND_OPTIONS}
options={isBackupOnly ? EVENT_KIND_BACKUP_ONLY_OPTIONS : EVENT_KIND_OPTIONS}
onValueChange={field.onChange}
defaultValue={field.value ?? []}
placeholder={isMobile ? "Select events..." : "Select events to trigger notifications..."}
@@ -65,7 +65,6 @@ export const ChannelPoliciesModal = ({icon, kind, database, channels, organizati
</DialogDescription>
<Separator className="mt-3 mb-3"/>
<ChannelPoliciesForm
organizationId={organizationId}
channels={channels}
database={database}
onSuccess={() => setOpen(false)}
@@ -3,7 +3,7 @@ import {z} from "zod";
export const PolicySchema = z.object({
channelId: z.string().min(1, "Please select channel"),
eventKinds: z.array(z.enum([
'error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report'
'error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report', 'error_health_database'
]))
.optional(),
enabled: z.boolean().default(true),
@@ -17,11 +17,16 @@ export const PoliciesSchema = z.object({
export type PoliciesType = z.infer<typeof PoliciesSchema>;
export type PolicyType = z.infer<typeof PolicySchema>;
export const EVENT_KIND_BACKUP_ONLY_OPTIONS = [
{label: "Error Backup", value: "error_backup"},
{label: "Success Backup", value: "success_backup"},
{label: "Health Ping Fail", value: "error_health_database"},
];
export const EVENT_KIND_OPTIONS = [
{label: "Error Backup", value: "error_backup"},
...EVENT_KIND_BACKUP_ONLY_OPTIONS,
{label: "Error Restore", value: "error_restore"},
{label: "Success Restore", value: "success_restore"},
{label: "Success Backup", value: "success_backup"},
{label: "Weekly Report", value: "weekly_report"},
];
// {label: "Weekly Report", value: "weekly_report"},
];
@@ -0,0 +1,43 @@
"use client"
import {useState} from "react";
import {Button} from "@/components/ui/button";
import {DatabaseWith} from "@/db/schema/07_database";
import {HeartPulse} from "lucide-react";
import {
Sheet,
SheetContent,
SheetTitle,
SheetTrigger
} from "@/components/ui/sheet";
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
import {HealthCheckGraph} from "@/components/wrappers/dashboard/health/heath-grid";
type HealthModalProps = {
database: DatabaseWith,
healthLogs: HealthcheckLog[]
}
export const HealthModal = ({database, healthLogs}: HealthModalProps) => {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="outline" onClick={() => setOpen(true)}>
<HeartPulse/>
</Button>
</SheetTrigger>
<SheetContent side="bottom">
<div className="flex pl-5 pt-4">
<SheetTitle>
Database Health Status
</SheetTitle>
</div>
<div className="px-4 pb-5">
<HealthCheckGraph logs={healthLogs} />
</div>
</SheetContent>
</Sheet>
)
}
@@ -0,0 +1,204 @@
"use client"
import {useMemo} from "react"
import {Card} from "@/components/ui/card"
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log"
import {useIsMobile} from "@/hooks/use-mobile";
type HealthStatus = "healthy" | "degraded" | "down" | "unknown"
interface HealthCheckData {
timestamp: Date
status: HealthStatus
}
interface Props {
logs: HealthcheckLog[]
}
const INTERVAL_MINUTES = 10
const WINDOW_HOURS = 12
function roundDateToInterval(date: Date, intervalMinutes: number): Date {
const ms = intervalMinutes * 60 * 1000
return new Date(Math.floor(date.getTime() / ms) * ms)
}
function buildTimeSeries(logs: HealthcheckLog[]): HealthCheckData[] {
const intervalMs = INTERVAL_MINUTES * 60 * 1000
const now = new Date()
const roundedNow = roundDateToInterval(now, INTERVAL_MINUTES)
const buckets = (WINDOW_HOURS * 60) / INTERVAL_MINUTES
const data: HealthCheckData[] = []
const oldestLog = logs.length > 0 ? getOldestLog(logs) : null
for (let i = buckets - 1; i >= 0; i--) {
const start = new Date(roundedNow.getTime() - i * intervalMs)
const end = new Date(start.getTime() + intervalMs)
const bucketLogs = logs.filter(
(l) =>
l.date &&
new Date(l.date) >= start &&
new Date(l.date) < end
)
let status: HealthStatus = "unknown"
if (!oldestLog || new Date(oldestLog.date!) > start) {
status = "unknown"
} else if (new Date(oldestLog.date!) < start) {
status = "down"
}
if (bucketLogs.length > 0) {
const hasFailure = bucketLogs.some((l) => l.status === "failed")
const hasSuccess = bucketLogs.some((l) => l.status === "success")
if (hasFailure && hasSuccess) {
status = "degraded"
} else if (hasFailure) {
status = "down"
} else if (hasSuccess) {
status = "healthy"
}
}
data.push({timestamp: start, status})
}
return data
}
function getStatusColor(status: HealthStatus): string {
switch (status) {
case "healthy":
return "bg-emerald-500"
case "degraded":
return "bg-emerald-700"
case "down":
return "bg-red-500"
case "unknown":
return "bg-zinc-700"
}
}
function getOldestLog(logs: HealthcheckLog[]): HealthcheckLog {
console.log(logs)
const validLogs = logs.filter(l => l.date)
return validLogs.reduce((oldest, current) =>
new Date(current.date!) < new Date(oldest.date!) ? current : oldest
)
}
function formatTime(date: Date): string {
return date.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
}
export const HealthCheckGraph = ({logs}: Props) => {
const data = useMemo(() => {
return buildTimeSeries(logs)
}, [logs])
const isMobile = useIsMobile()
const hourLabels = useMemo(() => {
if (data.length === 0) return []
return data
.map((item, index) => ({ item, index }))
.filter(({ item }) => {
const hours = item.timestamp.getHours()
const minutes = item.timestamp.getMinutes()
if (isMobile) {
return minutes === 0 && hours % 3 === 0
} else {
return minutes === 0
}
})
.map(({ item, index }) => ({
hour: formatTime(item.timestamp),
index,
}))
}, [data])
const healthyCount = data.filter((d) => d.status === "healthy").length
const uptimePercent =
data.length > 0
? ((healthyCount / data.length) * 100).toFixed(1)
: "0.0"
return (
<div className="flex items-center justify-center">
<div className="w-full">
<Card className="h-full flex flex-col p-4 border-border/50 bg-card gap-0">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold">Health</h2>
<p className="text-zinc-500 text-sm">
Last 12 hours {INTERVAL_MINUTES} minute intervals
</p>
</div>
<div className="text-right">
<p className="text-emerald-400 text-2xl font-bold">
{uptimePercent}%
</p>
<p className="text-zinc-500 text-sm">Uptime</p>
</div>
</div>
<div className="relative mb-1 h-4 text-xs text-zinc-500">
{hourLabels.map((label, i) => {
const left = (label.index / (data.length - 1)) * 100
return (
<div
key={i}
className="absolute -translate-x-1/2 whitespace-nowrap"
style={{ left: `${left}%` }}
>
{label.hour}
</div>
)
})}
</div>
<div className="flex gap-0.5">
{data.map((item, index) => (
<div
key={index}
className={`flex-1 h-8 rounded-sm ${getStatusColor(item.status)} hover:ring-2 hover:ring-zinc-400 transition-all cursor-pointer`}
title={`${formatTime(item.timestamp)} - ${item.status}`}
/>
))}
</div>
<div className="flex items-center justify-end gap-4 mt-4 text-xs text-zinc-500">
<Legend color="bg-zinc-700" label="Unknown"/>
<Legend color="bg-red-500" label="Down"/>
<Legend color="bg-emerald-700" label="Degraded"/>
<Legend color="bg-emerald-500" label="Healthy"/>
</div>
</Card>
</div>
</div>
)
}
const Legend = ({color, label}: { color: string; label: string }) => (
<div className="flex items-center gap-1.5">
<div className={`w-3 h-3 rounded-sm ${color}`}/>
<span>{label}</span>
</div>
)
@@ -20,6 +20,8 @@ import { ChannelPoliciesModal } from "@/components/wrappers/dashboard/database/c
import { HardDrive, Megaphone } from "lucide-react";
import { ImportModal } from "@/components/wrappers/dashboard/database/import/import-modal";
import { BackupButton } from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import {HealthModal} from "@/components/wrappers/dashboard/database/health/health-modal";
import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
export type DatabaseContentProps = {
settings: Setting;
@@ -34,6 +36,7 @@ export type DatabaseContentProps = {
organizationId: string;
activeOrganizationChannels: any[];
activeOrganizationStorageChannels: any[];
databaseHealthLogs: HealthcheckLog[]
};
export const DatabaseContent = (props: DatabaseContentProps) => {
@@ -64,6 +67,7 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
availableBackups: props.availableBackups,
successRate: props.successRate,
},
health: props.databaseHealthLogs
},
staleTime: 0,
gcTime: 0,
@@ -118,10 +122,13 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
organizationId={props.organizationId}
/>
<ImportModal database={database} />
<HealthModal database={database} healthLogs={data?.health ?? []}/>
</div>
<div className="flex items-center gap-2">
<BackupButton
disable={isAlreadyBackup}
disable={isAlreadyBackup || !database.lastContact}
databaseId={database.id}
/>
</div>
@@ -18,6 +18,8 @@ export type DatabaseTabsProps = {
activeMember: MemberWithUser
};
export const backupOnly = ["redis", "valkey"];
export const DatabaseTabs = (props: DatabaseTabsProps) => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -26,6 +28,7 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
useEffect(() => {
const newTab = searchParams.get("tab") ?? "backup";
// eslint-disable-next-line react-hooks/set-state-in-effect
setTab(newTab);
}, [searchParams]);
@@ -33,13 +36,13 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
router.push(`?tab=${value}`);
};
const isBackupOnly = backupOnly.some((type) => props.database.dbms === type)
return (
<Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="backup">Backup</TabsTrigger>
<TabsTrigger value="restore">Restoration</TabsTrigger>
</TabsList>
<TabsContent className="h-full justify-between" value="backup">
<>
{isBackupOnly ?
<DatabaseBackupList
isAlreadyRestore={props.isAlreadyRestore}
settings={props.settings}
@@ -47,15 +50,33 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
backups={props.backups}
activeMember={props.activeMember}
/>
</TabsContent>
<TabsContent className="h-full justify-between" value="restore">
<DatabaseRestoreList
isAlreadyRestore={props.isAlreadyRestore}
restorations={props.restorations}
activeMember={props.activeMember}
databaseId={props.database.id}
/>
</TabsContent>
</Tabs>
:
<Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="backup">Backup</TabsTrigger>
<TabsTrigger value="restore">Restoration</TabsTrigger>
</TabsList>
<TabsContent className="h-full justify-between" value="backup">
<DatabaseBackupList
isAlreadyRestore={props.isAlreadyRestore}
settings={props.settings}
database={props.database}
backups={props.backups}
activeMember={props.activeMember}
/>
</TabsContent>
<TabsContent className="h-full justify-between" value="restore">
<DatabaseRestoreList
isAlreadyRestore={props.isAlreadyRestore}
restorations={props.restorations}
activeMember={props.activeMember}
databaseId={props.database.id}
/>
</TabsContent>
</Tabs>
}
</>
);
};
+9 -4
View File
@@ -15,6 +15,9 @@ import * as notificationLog from "./schema/11_notification-log";
import * as storageChannel from "./schema/12_storage-channel";
import * as storagePolicy from "@/db/schema/13_storage-policy";
import * as backupStorage from "@/db/schema/14_storage-backup";
import * as healthcheckLog from "@/db/schema/15_healthcheck-log";
const log = logger.child({module: "db"});
import {Pool} from "pg";
@@ -22,6 +25,7 @@ import {Pool} from "pg";
// Do not delete
import dotenv from "dotenv";
import {migrate} from "drizzle-orm/node-postgres/migrator";
import {logger} from "@/lib/logger";
dotenv.config({
path: ".env",
@@ -46,7 +50,8 @@ export const schemas = {
...notificationLog,
...storageChannel,
...storagePolicy,
...backupStorage
...backupStorage,
...healthcheckLog
};
export const db = drizzle({
@@ -64,12 +69,12 @@ export async function makeMigration() {
const database = drizzle({client: pool});
console.log("Running migrations...");
log.info("Running migrations...");
try {
await migrate(database, {migrationsFolder: "./src/db/migrations"});
console.log("Migrations applied successfully.");
log.info("Migrations applied successfully.");
} catch (error) {
console.error("Error applying migrations:", error);
log.error({error: error}, "Error applying migrations:");
}
}
}
@@ -0,0 +1 @@
ALTER TYPE "public"."dbms_status" ADD VALUE 'redis';
@@ -0,0 +1 @@
ALTER TYPE "public"."dbms_status" ADD VALUE 'valkey';
+12
View File
@@ -0,0 +1,12 @@
CREATE TYPE "public"."healthcheck_status" AS ENUM('success', 'failed');--> statement-breakpoint
CREATE TYPE "public"."healthcheck_kind" AS ENUM('database', 'agent');--> statement-breakpoint
CREATE TABLE "healthcheck_log" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"kind" "healthcheck_kind" NOT NULL,
"date" timestamp,
"status" "healthcheck_status",
"object_id" uuid,
"updated_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"deleted_at" timestamp
);
@@ -0,0 +1,3 @@
ALTER TABLE "healthcheck_log" ALTER COLUMN "date" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "healthcheck_log" ALTER COLUMN "status" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "healthcheck_log" ALTER COLUMN "object_id" SET NOT NULL;
+2
View File
@@ -0,0 +1,2 @@
ALTER TABLE "settings" ADD COLUMN "default_notification_channel_id" uuid;--> statement-breakpoint
ALTER TABLE "settings" ADD CONSTRAINT "settings_default_notification_channel_id_notification_channel_id_fk" FOREIGN KEY ("default_notification_channel_id") REFERENCES "public"."notification_channel"("id") ON DELETE set null ON UPDATE no action;
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "agents" ADD COLUMN "health_error_count" integer;
+1
View File
@@ -0,0 +1 @@
ALTER TYPE "public"."event_kind" ADD VALUE 'error_health_agent';
@@ -0,0 +1 @@
ALTER TYPE "public"."event_kind" ADD VALUE 'error_health_database';
@@ -0,0 +1 @@
ALTER TABLE "databases" ADD COLUMN "health_error_count" integer;
+1
View File
@@ -0,0 +1 @@
ALTER TYPE "public"."dbms_status" ADD VALUE 'mariadb' BEFORE 'mongodb';
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+70
View File
@@ -267,6 +267,76 @@
"when": 1772200831628,
"tag": "0037_neat_talon",
"breakpoints": true
},
{
"idx": 38,
"version": "7",
"when": 1773433992397,
"tag": "0038_misty_red_hulk",
"breakpoints": true
},
{
"idx": 39,
"version": "7",
"when": 1773659096498,
"tag": "0039_conscious_solo",
"breakpoints": true
},
{
"idx": 40,
"version": "7",
"when": 1774375027038,
"tag": "0040_quick_lester",
"breakpoints": true
},
{
"idx": 41,
"version": "7",
"when": 1774378412843,
"tag": "0041_spooky_radioactive_man",
"breakpoints": true
},
{
"idx": 42,
"version": "7",
"when": 1774472168308,
"tag": "0042_breezy_namora",
"breakpoints": true
},
{
"idx": 43,
"version": "7",
"when": 1774643615673,
"tag": "0043_peaceful_chat",
"breakpoints": true
},
{
"idx": 44,
"version": "7",
"when": 1774648872657,
"tag": "0044_steep_wiccan",
"breakpoints": true
},
{
"idx": 45,
"version": "7",
"when": 1774696680244,
"tag": "0045_needy_martin_li",
"breakpoints": true
},
{
"idx": 46,
"version": "7",
"when": 1774706071024,
"tag": "0046_mysterious_menace",
"breakpoints": true
},
{
"idx": 47,
"version": "7",
"when": 1774768062658,
"tag": "0047_wet_carnage",
"breakpoints": true
}
]
}
+5 -2
View File
@@ -1,10 +1,10 @@
import {boolean, pgTable, timestamp, uuid, varchar} from "drizzle-orm/pg-core";
import {boolean, pgTable, uuid, varchar} from "drizzle-orm/pg-core";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {timestamps} from "@/db/schema/00_common";
import {storageChannel} from "@/db/schema/12_storage-channel";
import {relations} from "drizzle-orm";
import {notificationChannel} from "@/db/schema/09_notification-channel";
export const setting = pgTable("settings", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -15,6 +15,8 @@ export const setting = pgTable("settings", {
smtpPort: varchar("smtp_port", {length: 255}),
smtpUser: varchar("smtp_user", {length: 255}),
smtpSecure: boolean("smtp_secure"),
defaultNotificationChannelId: uuid('default_notification_channel_id')
.references(() => notificationChannel.id, {onDelete: "set null"}),
defaultStorageChannelId: uuid('default_storage_channel_id')
.references(() => storageChannel.id, {onDelete: "set null"}),
encryption: boolean("encryption").default(false),
@@ -23,6 +25,7 @@ export const setting = pgTable("settings", {
export const settingRelations = relations(setting, ({one, many}) => ({
storageChannel: one(storageChannel, {fields: [setting.defaultStorageChannelId], references: [storageChannel.id]}),
notificationChannel: one(notificationChannel, {fields: [setting.defaultNotificationChannelId], references: [notificationChannel.id]}),
}));
+1
View File
@@ -19,6 +19,7 @@ export const database = pgTable("databases", {
backupPolicy: text("backup_policy"),
isWaitingForBackup: boolean("is_waiting_for_backup").default(false).notNull(),
backupToRestore: text("backup_to_restore"),
healthErrorCount: integer("health_error_count"),
agentId: uuid("agent_id")
.notNull()
.references(() => agent.id, {onDelete: "cascade"}),
+2 -1
View File
@@ -1,4 +1,4 @@
import {boolean, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
import {boolean, pgTable, text, timestamp, uuid, integer} from "drizzle-orm/pg-core";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {Database, database} from "@/db/schema/07_database";
@@ -10,6 +10,7 @@ export const agent = pgTable("agents", {
slug: text("slug").notNull().unique(),
version: text("version"),
name: text("name").notNull().notNull(),
healthErrorCount: integer("health_error_count"),
description: text("description").notNull(),
isArchived: boolean("is_archived").default(false),
lastContact: timestamp("last_contact"),
+1 -1
View File
@@ -6,7 +6,7 @@ import {database} from "@/db/schema/07_database";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
export const eventKindEnum = pgEnum('event_kind', ['error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report']);
export const eventKindEnum = pgEnum('event_kind', ['error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report', 'error_health_agent', 'error_health_database']);
export const alertPolicy = pgTable('alert_policy', {
id: uuid('id').defaultRandom().primaryKey(),
+23
View File
@@ -0,0 +1,23 @@
import {pgTable, uuid, timestamp, pgEnum} from 'drizzle-orm/pg-core';
import {timestamps} from "@/db/schema/00_common";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
export const healthcheckKindEnum = pgEnum('healthcheck_kind', ['database', 'agent']);
export const healthCheckStatusEnum = pgEnum('healthcheck_status', ['success', 'failed']);
export const healthcheckLog = pgTable('healthcheck_log', {
id: uuid('id').defaultRandom().primaryKey(),
kind: healthcheckKindEnum('kind').notNull(),
date: timestamp("date").notNull(),
status: healthCheckStatusEnum("status").notNull(),
objectId: uuid('object_id').notNull(),
...timestamps
});
export const healthcheckLogSchema = createSelectSchema(healthcheckLog);
export type HealthcheckLog = z.infer<typeof healthcheckLogSchema>;
export type HealthcheckKind = (typeof healthcheckKindEnum.enumValues)[number];
export type HealthcheckStatus = (typeof healthCheckStatusEnum.enumValues)[number];
+1 -1
View File
@@ -2,7 +2,7 @@ import {pgEnum} from "drizzle-orm/pg-core";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb", "sqlite"]);
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mariadb", "mongodb", "sqlite", "redis", "valkey"]);
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
+185
View File
@@ -0,0 +1,185 @@
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {and, eq, gte, isNotNull, lt} from "drizzle-orm";
import {dispatchNotification} from "@/features/notifications/dispatch";
import {EventPayload} from "@/features/notifications/types";
import {logger} from "@/lib/logger";
const log = logger.child({module: "tasks/healthcheck"});
export async function getHealthLast12hLogs({id}: { id: string }) {
const now = new Date()
const since = new Date(now.getTime() - 12 * 60 * 60 * 1000)
return db
.select()
.from(drizzleDb.schemas.healthcheckLog)
.where(
and(
eq(drizzleDb.schemas.healthcheckLog.objectId, id),
gte(drizzleDb.schemas.healthcheckLog.date, since)
)
)
}
export async function deleteHealthLogsOlderThan12h() {
const now = new Date()
const threshold = new Date(now.getTime() - 12 * 60 * 60 * 1000)
const logsToDelete = await db
.select()
.from(drizzleDb.schemas.healthcheckLog)
.where(
lt(drizzleDb.schemas.healthcheckLog.date, threshold)
)
log.info({name: "deleteHealthLogsOlderThan12h"},`Number of logs found to delete: ${logsToDelete.length}`)
await db
.delete(drizzleDb.schemas.healthcheckLog)
.where(
lt(drizzleDb.schemas.healthcheckLog.date, threshold)
)
return logsToDelete.length
}
export async function checkAgentsHealthError() {
const agents = await db.query.agent.findMany({
where: isNotNull(drizzleDb.schemas.agent.lastContact),
});
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
if (!settings) {
throw new Error("System settings not found");
}
if (!settings.defaultNotificationChannelId) {
log.error({name: "checkAgentsHealthError"},`No default notification channel id found.`)
return
}
const now = new Date();
for (const agent of agents) {
if (!agent.lastContact) continue;
const lastContactDate = new Date(agent.lastContact);
const diffMinutes = (now.getTime() - lastContactDate.getTime()) / 1000 / 60;
if (diffMinutes > 10) {
if ((agent.healthErrorCount ?? 0) < 3) {
const newHealthErrorCount = (agent.healthErrorCount ?? 0) + 1
await db.update(drizzleDb.schemas.agent)
.set({
healthErrorCount: newHealthErrorCount,
})
.where(eq(drizzleDb.schemas.agent.id, agent.id));
const payload: EventPayload = {
title: "Agent down",
message: `Agent ${agent.name} is down, (notification number: ${newHealthErrorCount}/3)`,
level: "critical",
event: "error_health_agent",
data: {
agent: agent.name,
id: agent.id,
error: "Agent is down",
},
};
log.info({name: "checkAgentsHealthError", payload: payload},`Agent Healthcheck Notification`)
await dispatchNotification(
payload,
undefined,
settings.defaultNotificationChannelId,
undefined
);
}
}
}
}
export async function checkDatabasesHealthError() {
const databases = await db.query.database.findMany({
where: isNotNull(drizzleDb.schemas.database.lastContact),
with: {
agent: true,
alertPolicies: true
}
})
const now = new Date();
for (const database of databases) {
if (!database.lastContact) continue;
const lastContactDate = new Date(database.lastContact);
const diffMinutes = (now.getTime() - lastContactDate.getTime()) / 1000 / 60;
if (diffMinutes > 10) {
if ((database.healthErrorCount ?? 0) < 3) {
const newHealthErrorCount = (database.healthErrorCount ?? 0) + 1
await db.update(drizzleDb.schemas.database)
.set({
healthErrorCount: newHealthErrorCount,
})
.where(eq(drizzleDb.schemas.database.id, database.id));
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
with: { notificationChannel: true },
});
const defaultPolicy = settings?.notificationChannel
? [{
id: null,
notificationChannelId: settings.notificationChannel.id,
enabled: settings.notificationChannel.enabled,
eventKinds: ["error_health_database"]
}]
: [];
const policiesToUse = (database.alertPolicies && database.alertPolicies.length > 0)
? database.alertPolicies.filter(policy => policy.enabled && policy.eventKinds.includes("error_health_database"))
: defaultPolicy;
if (!policiesToUse || policiesToUse.length === 0) {
continue
}
const promises = policiesToUse.map(alertPolicy => {
const payload: EventPayload = {
title: "Database down",
message: `Database ${database.name} is down, (notification number: ${newHealthErrorCount}/3)`,
level: "critical",
event: "error_health_database",
data: {
agent: database.name,
id: database.id,
error: "Database is down",
},
};
log.info({name: "checkDatabasesHealthError", payload: payload},`Database Healthcheck Notification`)
return dispatchNotification(payload, alertPolicy.id == null ? undefined : alertPolicy.id, alertPolicy.id ? undefined : alertPolicy.notificationChannelId, undefined);
});
await Promise.all(promises);
}
}
}
}
+15
View File
@@ -39,6 +39,20 @@ export const env = createEnv({
process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *",
),
CLEANING_HEALTHCHECK_LOGS_CRON: z
.string()
.default(
process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *",
),
HEALTHCHECK_CRON: z
.string()
.default(
process.env.NODE_ENV === "production" ? "0 * * * *" : "* * * * *",
),
AUTH_OIDC_ID: z.string().optional().default("oidc"),
AUTH_OIDC_TITLE: z.string().optional(),
AUTH_OIDC_DESC: z.string().optional(),
@@ -95,6 +109,7 @@ export const env = createEnv({
SMTP_SECURE: process.env.SMTP_SECURE,
RETENTION_CRON: process.env.RETENTION_CRON,
CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON,
AUTH_OIDC_ID: process.env.AUTH_OIDC_ID,
AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE,
+4
View File
@@ -6,6 +6,7 @@ import {eq, and, ne, count} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {slugify} from "@/utils/slugify";
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
const conditions = agentId ? and(eq(drizzleDb.schemas.agent.slug, slug), ne(drizzleDb.schemas.agent.id, agentId)) : eq(drizzleDb.schemas.agent.slug, slug);
@@ -54,7 +55,10 @@ export const getAgentAction = userAction.schema(z.string()).action(async ({parse
databases: true
}
});
return {
data: agent,
health: agent ? await getHealthLast12hLogs({ id: agent.id }) : []
};
});
+6 -131
View File
@@ -11,6 +11,7 @@ import {formatLocalizedDate} from "@/utils/date-formatting";
import {formatBytes} from "@/utils/text";
import {DatabaseActionsCell} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-cell";
import { Badge as BadgeC } from "@/components/ui/badge";
import {backupOnly} from "@/components/wrappers/dashboard/projects/database/database-tabs";
export function backupColumns(
isAlreadyRestore: boolean,
@@ -18,6 +19,10 @@ export function backupColumns(
database: DatabaseWith,
activeMember: MemberWithUser
): ColumnDef<Backup>[] {
const isBackupOnly = backupOnly.some((type) => database.dbms === type)
return [
{
id: "availability",
@@ -90,137 +95,7 @@ export function backupColumns(
},
{
id: "actions",
cell: ({row}) => <DatabaseActionsCell isAlreadyRestore={isAlreadyRestore} activeMember={activeMember} backup={row.original}/>,
cell: ({row}) => <DatabaseActionsCell isAlreadyRestore={isAlreadyRestore} activeMember={activeMember} backup={row.original} isBackupOnly={isBackupOnly}/>,
},
// {
// id: "actions",
// cell: ({row, table}) => {
// const status = row.getValue("status");
// const rowData: Backup = row.original;
// const fileName = rowData.file;
//
// const router = useRouter();
//
// const mutationRestore = useMutation({
// mutationFn: async () => {
// const restoration = await createRestorationAction({
// backupId: rowData.id,
// databaseId: rowData.databaseId,
// });
// // @ts-ignore
// if (restoration.data.success) {
// // @ts-ignore
// toast.success(restoration.data.actionSuccess?.message || "Restoration created successfully!");
// router.refresh();
// } else {
// // @ts-ignore
// toast.error(restoration.serverError || "Failed to create restoration.");
// }
// },
// });
//
// const mutationDeleteBackup = useMutation({
// mutationFn: async () => {
// const deletion = await deleteBackupAction({
// backupId: rowData.id,
// databaseId: rowData.databaseId,
// status: rowData.status,
// file: rowData.file ?? "",
// projectSlug: database.project?.slug!
// });
// // @ts-ignore
// if (deletion.data.success) {
// // @ts-ignore
// toast.success(deletion.data.actionSuccess.message);
// router.refresh();
// } else {
// // @ts-ignore
// toast.error(deletion.data.actionError.message);
// }
// },
// });
//
// const handleRestore = async () => {
// await mutationRestore.mutateAsync();
// };
//
// const handleDelete = async () => {
// await mutationDeleteBackup.mutateAsync();
// };
//
// const handleDownload = async (fileName: string) => {
//
// let url: string = "";
// let data: SafeActionResult<string, ZodString, readonly [], {
// _errors?: string[] | undefined;
// }, readonly [], ServerActionResult<string>, object> | undefined
//
// if (settings.storage == "local") {
// data = await getFileUrlPresignedLocal({fileName: fileName!})
// } else if (settings.storage == "s3") {
// data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
// }
// if (data?.data?.success) {
// url = data.data.value ?? "";
// } else {
// // @ts-ignore
// const errorMessage = data?.data?.actionError?.message || "Failed to get file!";
// toast.error(errorMessage);
// }
//
// window.open(url, "_self");
// };
//
// return (
// <>
// {(rowData.deletedAt == null && activeMember.role != "member") && (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" className="h-8 w-8 p-0">
// <span className="sr-only">Open menu</span>
// <MoreHorizontal className="h-4 w-4"/>
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// <DropdownMenuLabel>Actions</DropdownMenuLabel>
// {status == "success" ? (
// <>
// <TooltipCustom disabled={isAlreadyRestore}
// text="Already a restoration waiting">
// <DropdownMenuItem
// disabled={mutationRestore.isPending || isAlreadyRestore}
// onClick={async () => {
// await handleRestore();
// }}
// >
// <ReloadIcon/> Restore
// </DropdownMenuItem>
// </TooltipCustom>
// <DropdownMenuItem
// onClick={async () => {
// await handleDownload(fileName ?? "");
// }}
// >
// <Download/> Download
// </DropdownMenuItem>
// </>
// ) : null}
// <DropdownMenuSeparator/>
// <DropdownMenuItem
// className="text-red-600"
// onClick={async () => {
// await handleDelete();
// }}
// >
// <Trash2/> Delete
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// )}
// </>
//
// );
// },
// },
];
}

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