Compare commits

...

527 Commits

Author SHA1 Message Date
jamesread
1ede041815 chore: package-lock
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-12-01 23:15:43 +00:00
James Read
23d1258384 Merge branch 'main' into next 2025-12-01 22:55:52 +00:00
James Read
25fbba8056 chore(deps): bump vite from 7.2.4 to 7.2.6 in /frontend (#775) 2025-12-01 22:38:20 +00:00
James Read
01a68d2479 Merge branch 'next' into dependabot/npm_and_yarn/frontend/next/vite-7.2.6 2025-12-01 22:24:03 +00:00
James Read
0dd56eca2d chore(deps): bump github.com/bufbuild/buf from 1.60.0 to 1.61.0 in /service (#776) 2025-12-01 22:23:26 +00:00
James Read
75a696d8c0 Merge branch 'next' into dependabot/go_modules/service/next/github.com/bufbuild/buf-1.61.0 2025-12-01 22:18:25 +00:00
jamesread
76035a56ab chore: fix typo in test 2025-12-01 22:02:23 +00:00
jamesread
a2410fc9d6 feat: Entity directories, inline actions, and all entity fields are now available in the entity details view 2025-12-01 21:48:27 +00:00
jamesread
32fea4ec30 feat: Dashboard configs can now self-contain their entities, and actions all in one neat file 2025-12-01 16:15:08 +00:00
jamesread
e58677e12c feat: inline actions on dashboards 2025-12-01 12:29:41 +00:00
jamesread
2e82a3d1ed chore: remove some dead code 2025-12-01 12:28:44 +00:00
dependabot[bot]
e13a236e0a chore(deps): bump github.com/bufbuild/buf in /service
Bumps [github.com/bufbuild/buf](https://github.com/bufbuild/buf) from 1.60.0 to 1.61.0.
- [Release notes](https://github.com/bufbuild/buf/releases)
- [Changelog](https://github.com/bufbuild/buf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bufbuild/buf/compare/v1.60.0...v1.61.0)

---
updated-dependencies:
- dependency-name: github.com/bufbuild/buf
  dependency-version: 1.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 06:12:58 +00:00
dependabot[bot]
c6fc4e7d20 chore(deps): bump vite from 7.2.4 to 7.2.6 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.2.4 to 7.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 06:11:02 +00:00
James Read
d4b8743a57 Next (#772)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-12-01 02:08:58 +00:00
jamesread
39009fcdd1 chore: code fmt
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-12-01 01:59:10 +00:00
jamesread
56365af24b feat: Mega entity improvements - entity directories, inehrit entities (#450), ordering (#762 / #703), better entity view 2025-12-01 01:38:04 +00:00
jamesread
736ad1c83b fix: Dashboard directories and displays modest style fixes 2025-12-01 00:38:02 +00:00
jamesread
fe8ecf49eb Merge branch 'next' of github.com:OliveTin/OliveTin into next 2025-12-01 00:24:13 +00:00
James Read
7282d7a9f6 Feat (#763) mre output in 3k (#771) 2025-12-01 00:24:03 +00:00
jamesread
a163347992 feat: Sidebar now closes when clicking outside of it (#714) 2025-12-01 00:23:03 +00:00
jamesread
a9b21ea645 feat: Display a big error message if OliveTin initialization fails 2025-12-01 00:22:18 +00:00
jamesread
c59bcc62db chore: fix sleep action test 2025-12-01 00:15:01 +00:00
James Read
ef072806da chore(deps-dev): bump glob from 10.4.5 to 10.5.0 in /integration-tests (#767) 2025-12-01 00:06:53 +00:00
James Read
f15f0ef329 feat: action status display on logs list (#770) 2025-12-01 00:06:20 +00:00
jamesread
caaf37e091 feat: Most Recent Execution output in dashboard 2025-12-01 00:02:42 +00:00
jamesread
abecf6f8c6 feat: Action status display on logs list 2025-11-30 22:41:06 +00:00
James Read
6bc3a7bf1f Merge branch 'next' into dependabot/npm_and_yarn/integration-tests/glob-10.5.0
Some checks failed
Codestyle checks / codestyle (push) Has been cancelled
2025-11-30 16:55:16 +00:00
jamesread
7425db53f6 fix: #718 - Clear OAuth2 authentication cookie on logout 2025-11-30 11:19:36 +00:00
James Read
09d933722e fix: #765 Page title was not being set (#769)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-11-30 10:29:07 +00:00
jamesread
6ccd10a970 feat: Added copy to clipboard buttons to Diagnostics view, with markdown backticks 2025-11-30 10:28:07 +00:00
jamesread
dca8e04518 fix: #765 Page title was not being set 2025-11-29 21:16:23 +00:00
jamesread
a91e903873 feat: add browser info to diagnostics
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
2025-11-29 20:37:08 +00:00
dependabot[bot]
4686b044e7 chore(deps-dev): bump glob from 10.4.5 to 10.5.0 in /integration-tests
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 01:10:24 +00:00
James Read
f28944bee2 Next (#766)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-11-27 01:09:06 +00:00
jamesread
470f31db6e chore: Remove unecessary OAauth2 log
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-11-27 00:41:53 +00:00
jamesread
0046ae427d chore: fix broken integration test for fieldset markup 2025-11-27 00:37:15 +00:00
jamesread
8bad1b5400 fix: #703 - Entities order was non-deterministic 2025-11-27 00:02:08 +00:00
jamesread
6b4dfddf4c Merge branch 'next' of github.com:OliveTin/OliveTin into next 2025-11-26 23:30:13 +00:00
James Read
ff77e9121c fix: (#718) OAuth Login Buttons not redirecting (#759) 2025-11-26 23:28:57 +00:00
jamesread
7f1e509e12 chore: Little fixes for OAuth2 login 2025-11-26 23:28:29 +00:00
jamesread
f33ccbd2fa fix: Log errors when loading theme CSS 2025-11-26 23:08:10 +00:00
jamesread
e9bde8b094 fix: OAuth2 scopes for GitHub 2025-11-26 23:07:55 +00:00
jamesread
853972384c security: Possible to kill actions without authentication & credential leak in logs 2025-11-26 23:07:25 +00:00
jamesread
695ad65d3e chore: Improve dashboard markup and layout 2025-11-26 22:57:26 +00:00
jamesread
b47cf7cab3 fix: Fix dashboard legends being left-aligned to centered again 2025-11-26 22:38:45 +00:00
jamesread
be7c754043 feat: Rebuild authentication system to support 3k authentication 2025-11-26 22:33:19 +00:00
jamesread
b244b42e91 fix: sort OAuth2 providers by key 2025-11-22 23:27:54 +00:00
jamesread
3d763a84df fix: Various oauth issues 2025-11-22 22:39:05 +00:00
jamesread
d05ea54f8d fix: (#718) OAuth Login Buttons not redirecting 2025-11-22 21:13:19 +00:00
jamesread
55d8f75d1b fix: Sidebar, when unstuck, clicking links will close it (#714)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-11-22 20:44:44 +00:00
jamesread
c663c9d10d chore: Update release notes to strip out nonsense 2025-11-22 20:06:07 +00:00
James Read
7f12b6d72c chore: colocate tests (#758) 2025-11-22 19:57:26 +00:00
jamesread
3cbafcb4fc chore: Remove dummy link from config.yaml 2025-11-22 19:49:53 +00:00
jamesread
458f50a6ae chore: colocate tests 2025-11-22 19:24:51 +00:00
James Read
1b89b3e217 doc: Add config tool to help support people (#713)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-11-22 10:59:12 +00:00
jamesread
3e6a751132 chore: config-tool wont overwrite original config 2025-11-22 10:49:48 +00:00
jamesread
49b8c2c4f2 chore: fix broken datetime test 2025-11-22 10:43:56 +00:00
James Read
738ec884d8 docs(contributing): update setup instructions for pre-commit (#717) 2025-11-22 09:50:55 +00:00
James Read
d2e7474eea Merge branch 'next' into chore-add-config-tool 2025-11-22 09:47:14 +00:00
jamesread
ca0a134acd chore: write back to the same config path in config-tool 2025-11-22 09:34:37 +00:00
jamesread
40d9377bbd fmt: main.js 2025-11-22 08:36:58 +00:00
jamesread
5bf2d6935b chore: dep update 2025-11-22 08:36:38 +00:00
jamesread
15390f7b80 fix: #721 Datetime args not being rendered 2025-11-22 08:34:53 +00:00
jamesread
d4c80c65c2 chore: Configure dependabot for 2 release branches
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-11-22 08:00:52 +00:00
Anton Bobov
378db80c73 docs(contributing): update setup instructions for pre-commit
Update CONTRIBUTING guide to reflect migration from custom git hooks to
standard pre-commit framework:

* Add pre-commit package to installation commands for Fedora and Windows
* Replace 'make githooks' with standard 'pre-commit install' command

This completes the migration to pre-commit started in previous refactor
commit, ensuring documentation matches current development workflow.
2025-11-20 00:08:10 +05:00
jamesread
b3e67bad75 doc: Add config tool to help support people 2025-11-16 23:08:20 +00:00
James Read
c2843aa581 3k release: terminal sizes, confirmation buttons, translations, entity fieldsets drawn in random order, code quality (#709)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-11-16 00:51:51 +00:00
jamesread
d1ec688c9a chore: cleanup executor.go
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
2025-11-16 00:47:03 +00:00
jamesread
c1508a0a65 fix: Replace custom pagination component with picocrank pagination component
- Replace custom pagination component with picocrank pagination component
- Update package.json and package-lock.json to include picocrank
- Update style.css to include new padding class
- Update ActionDetailsView.vue and LogsListView.vue to use new pagination component
2025-11-16 00:05:50 +00:00
jamesread
5b57cf2480 fix: Logs were being displayed in the wrong order 2025-11-11 23:32:00 +00:00
jamesread
b97dd23abb chore: Better fixups on lang tool and support 2025-11-11 23:24:44 +00:00
jamesread
7c66170ef5 Merge branch 'next' of github.com:OliveTin/OliveTin into next
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
2025-11-11 10:55:54 +00:00
jamesread
167e700e30 chore: cleanup websocket code 2025-11-11 10:55:42 +00:00
James Read
1f875b05a0 Merge branch 'main' into next 2025-11-11 10:55:24 +00:00
jamesread
00d7285167 chore: fix some linting issues 2025-11-11 10:43:47 +00:00
jamesread
d32d92baab Merge branch 'next' of github.com:OliveTin/OliveTin into next 2025-11-11 10:25:00 +00:00
jamesread
36c786a26d chore: Cleanup websocket/marshaller code 2025-11-11 10:20:54 +00:00
jamesread
afbd1c3abe dep updates 2025-11-11 00:30:34 +00:00
James Read
09ecc8d15c feat: Add translations and language support (#708)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-11-11 00:26:03 +00:00
jamesread
487bf83f4e chore: use koanf tag in config reloader test 2025-11-10 23:39:15 +00:00
jamesread
adae55f24b chore: translate logs list view, log statuses, and language dialog 2025-11-10 23:33:55 +00:00
jamesread
217b17a058 chore: (#708) Print key validation errors, translate the language dialog 2025-11-10 23:26:55 +00:00
jamesread
67a9d3b1d1 chore: skip env in config test in 3k 2025-11-10 23:17:53 +00:00
jamesread
de7129e1d7 chore: reduce cyclomatic complexity 2025-11-10 23:15:18 +00:00
jamesread
3df22f2ccb Merge branch 'feat-add-language-support' of github.com:OliveTin/OliveTin into feat-add-language-support 2025-11-10 22:50:58 +00:00
jamesread
1140453f30 fix: broken integration test after fieldsets changes 2025-11-10 22:50:42 +00:00
James Read
9cf79863b3 Merge branch 'next' into feat-add-language-support 2025-11-10 21:53:04 +00:00
jamesread
6a853d9c99 feat: (#708) Added language support 2025-11-10 21:52:36 +00:00
jamesread
83fe489949 fix: (#703) Fixed entity fieldsets being drawn in a random order 2025-11-10 21:08:34 +00:00
jamesread
e7d4747727 chore: fix broken integration test after fieldsets changes 2025-11-10 20:52:49 +00:00
James Read
d4d7bf8135 fix: (#704) Confirmation buttons (#707) 2025-11-10 20:46:48 +00:00
jamesread
32a062ae93 fix: (#707) Confirmation buttons and ASCII input types 2025-11-10 20:12:19 +00:00
jamesread
263601170f fix: (#702) Fixed actions being drawn in a column (an extra fieldset was being added) 2025-11-10 19:46:58 +00:00
jamesread
b071b4d036 feat: Increase terminal size, and in fullscreen mode 2025-11-10 19:46:08 +00:00
jamesread
52ff504a9d fix: (#704) Confirmation buttons 2025-11-10 13:21:54 +00:00
jamesread
2ed564a403 feat: Add translations and language support 2025-11-10 13:20:40 +00:00
James Read
209856eda9 3k release: massively improve config loading, reduce log spam, etc (#700)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-11-07 00:55:08 +00:00
jamesread
3967b91cf0 fix: Exit if no base config file is found
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
2025-11-07 00:51:04 +00:00
James Read
50abb53ace Fix config loading missing values (#699) 2025-11-07 00:40:36 +00:00
James Read
110bbd6216 Merge branch 'next' into fix-config-loading-missing-values 2025-11-07 00:34:11 +00:00
jamesread
1552c104e9 fix: Propperly merge included configs (handling actions) 2025-11-07 00:31:33 +00:00
James Read
4006fd485d fix: Various log noise and spam (#698)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-11-06 23:48:11 +00:00
jamesread
7dc99b1398 fix: Massively improve config loading 2025-11-06 23:45:54 +00:00
jamesread
581536a60f fmt: GetActionLogs 2025-11-06 23:44:56 +00:00
jamesread
58593c6f04 doc: Improve HAProxy example 2025-11-06 23:44:00 +00:00
jamesread
39664a734d fix: Various log noise 2025-11-06 23:42:07 +00:00
James Read
87f9a0b152 fix: #696 - hide login link if login interactively isn't possible (local or oauth2) (#697)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
2025-11-06 15:44:52 +00:00
jamesread
d688ab64e1 fix: #696 - hide login link if login interactively isn't possible (local user or oauth2) 2025-11-06 15:29:39 +00:00
jamesread
822f3197b6 chore: (#675) Remove dead websocket code 2025-11-06 09:52:16 +00:00
James Read
a67b5b4e8f 3k release (#693)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
2025-11-06 00:44:19 +00:00
James Read
a3c5114615 fix: #685 - show navigation (and picocrank upgrade) (#692)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
2025-11-06 00:42:56 +00:00
jamesread
28c813762f fix: #685 - show navigation (and picocrank upgrade) 2025-11-06 00:38:56 +00:00
James Read
d94f2aca1c Addresses concurrent map read and write (#688)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
2025-11-05 00:46:10 +00:00
jamesread
055472902d fix: #686 - concurrent map read and write - in entity instances 2025-11-04 23:30:14 +00:00
jamesread
2b24daa6d0 fix: Address concurrency issue in entities storage 2025-11-04 23:18:41 +00:00
jamesread
294e33d110 fix: #686 - concurrent map read and write 2025-11-04 23:03:45 +00:00
jamesread
d3cd876eec doc: Added API docs generation tool for 3k
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
2025-10-31 09:26:40 +00:00
James Read
52cd5f255a doc: Better default config that includes security examples and more doc links (#682) 2025-10-31 09:19:49 +00:00
jamesread
2b1f9a9247 doc: Better layout of default config security section 2025-10-31 09:18:45 +00:00
jamesread
6782156a58 doc: Add date id 2025-10-30 21:33:10 +00:00
jamesread
f1250f9caf doc: Add more security examples to default config 2025-10-30 21:24:13 +00:00
jamesread
0bf313a3f7 doc: Move defaultPolicy to the end in the config 2025-10-30 21:14:49 +00:00
jamesread
092661c7eb doc: Better default config that includes security examples and more doc links 2025-10-30 21:10:58 +00:00
James Read
2a6d9e4f68 3k release (#681)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-10-30 16:07:23 +00:00
jamesread
83f45d71bf fix: Several coderabbit suggestions on next branch 2025-10-30 15:51:40 +00:00
jamesread
79a71099f9 fix: fmt api.go 2025-10-30 15:26:29 +00:00
jamesread
e6a02ac614 chore: fix various cyclo checks 2025-10-30 15:25:57 +00:00
James Read
e0167c9e42 fix: Require guests to login (#678) 2025-10-30 13:22:39 +00:00
James Read
7abffedb14 Merge branch 'next' into fix-require-guests-login 2025-10-30 13:15:37 +00:00
James Read
d32db6483e chore: reduce cyclo complexity in service (#680) 2025-10-30 13:05:19 +00:00
jamesread
44b518a5b2 fix: panic when loading sessions.yaml 2025-10-30 13:04:41 +00:00
jamesread
a4e50bfb54 fix: panic when executing action with no arguments 2025-10-30 13:04:12 +00:00
jamesread
a8f5e25454 chore: Conflict in AGENTS.md 2025-10-30 12:57:59 +00:00
jamesread
c3d5da1981 chore: reduce cyclo complexity in service 2025-10-30 12:52:58 +00:00
jamesread
7a1c4d3efa fix: Test authRequireGuestsToLogin redirect
Some checks failed
Buf CI / buf (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-10-30 11:58:51 +00:00
jamesread
c89979ddb2 fix: Wait for login UI to appear in authRequireGuestsToLogin test 2025-10-30 11:05:14 +00:00
James Read
430aab638b Merge branch 'next' into fix-require-guests-login 2025-10-30 09:20:24 +00:00
jamesread
961ddac193 fix: Duplicate ACL rendering bug in UserControlPanel 2025-10-30 09:17:55 +00:00
James Read
03ac3b5fa7 feat: ActionDetailsView component, show timeout and logs for action. … (#676) 2025-10-30 02:15:01 +00:00
jamesread
d21f06e555 fix: Require guests to login 2025-10-30 02:13:40 +00:00
James Read
f25b456c3d Merge branch 'next' into fix-output-streaming 2025-10-30 00:25:49 +00:00
jamesread
e1db1e7be5 fix: Add big error handling for action details view 2025-10-30 00:24:50 +00:00
jamesread
19c3b67cdd fix: fix panic in pagination if we get a bad request 2025-10-30 00:12:06 +00:00
jamesread
b9d859ada2 fix: fix start action button in ActionDetailsView.vue 2025-10-30 00:08:12 +00:00
jamesread
61fc771ac3 fix: Race condition and speedup in accessing streaming clients 2025-10-30 00:03:37 +00:00
jamesread
e0fd10a6ec fix: calculate duration correctly in ExecutionView.vue 2025-10-29 23:35:37 +00:00
James Read
2a5732cc27 feat: enable JSON logging support with OLIVETIN_LOG_FORMAT=json envir… (#677) 2025-10-29 23:03:15 +00:00
jamesread
57390be16f feat: ActionDetailsView component, show timeout and logs for action. Fix output streaming. 2025-10-29 22:45:26 +00:00
jamesread
8a6d61c260 feat: enable JSON logging support with OLIVETIN_LOG_FORMAT=json environment variable 2025-10-29 22:44:49 +00:00
jamesread
f337e05eaf chore: start next cycle 2025-10-29 22:39:54 +00:00
jamesread
6c6d07bf4f fix: #674 Use JSON options for API handler
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-10-27 20:40:15 +00:00
jamesread
d54f2307c7 fix: Use tree for webui nfpm packages
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-10-27 16:27:32 +00:00
jamesread
49dcc7fb46 fix: goreleaser bug for webui 2025-10-27 16:20:48 +00:00
jamesread
2ea35697d0 fix: #672 Empty execution tracking ID in InternalLogEntry 2025-10-27 15:42:13 +00:00
jamesread
a551589840 feat: #428 Initial support for include directive in config files 2025-10-27 15:32:30 +00:00
jamesread
fcd3ccc59a fix: authRequireGuestsToLogin config, and config loading improvements 2025-10-27 14:56:32 +00:00
jamesread
dddc0417c2 fix: #673 Testing fix for broken deb packages 2025-10-27 14:33:40 +00:00
jamesread
d5eb74e738 fix: Include "fix" in the right place in the release notes 2025-10-27 14:22:21 +00:00
jamesread
9fbaa8671f fix: Banner message support 2025-10-27 14:20:26 +00:00
James Read
a915a654cb bugfix: #639 Exec support, disallow URL and similar arguments with (#671)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-10-26 19:02:22 +00:00
jamesread
c86bf629f9 bugfix: Added nil checks 2025-10-26 17:32:26 +00:00
jamesread
c917d1b1e7 bugfix: #639 Exec support, disallow URL and similar arguments with 2025-10-26 16:40:44 +00:00
James Read
1cb12b203e Local user login fixes (#669) 2025-10-26 14:39:03 +00:00
James Read
2a21d74e35 Merge branch 'main' into next 2025-10-26 14:22:25 +00:00
jamesread
8686a5629e fix: User Information panel and login/logout flow 2025-10-26 13:42:06 +00:00
jamesread
43cfe41378 fix: Issues with login form and local auth 2025-10-26 13:24:22 +00:00
James Read
280234b138 fix dark mode styles (#668)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-10-26 00:23:09 +00:00
jamesread
02ec8eeb65 fix: Upgraded femtocrank to fix dark mode styles 2025-10-26 01:10:43 +01:00
jamesread
ef5a67e7b8 fix: Upgrade femtocrank for dark styles 2025-10-26 00:47:46 +01:00
James Read
eb2463aa2d 3k release: Connect RPC migration and authentication refactoring (#666)
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-10-24 23:42:39 +00:00
jamesread
a7e7bf869e fix: WIP on login regression 2025-10-25 00:31:02 +01:00
jamesread
0dd9e9b2b7 fix: guard against nil session storage 2025-10-25 00:22:28 +01:00
jamesread
aa8322c354 fix: guard against nil session storage 2025-10-25 00:20:56 +01:00
jamesread
956e74a6b3 fix: Don't log SID (!) SECURITY 2025-10-25 00:20:35 +01:00
jamesread
c9ff4d1a68 fix: broken go.mod 2025-10-25 00:20:15 +01:00
jamesread
88cc1ab080 chore: Makefile whitespace 2025-10-24 23:55:42 +01:00
jamesread
3b8bc49b04 feat: Fixed session management and ripped out the rest of gRPC 2025-10-24 23:48:48 +01:00
jamesread
31ea8507f5 doc: Typo in agents, fix undefined var DATE in pipeline 2025-10-24 22:51:31 +01:00
jamesread
62af851b2c fix: Error getting absolute path for config.yaml 2025-10-24 22:38:26 +01:00
James Read
2a764acde6 chore: Don't run release pipeline on PR branches (#665) 2025-10-24 22:16:45 +01:00
James Read
02e2ac1676 fix: Listen address fields were not being loaded from config.yaml (#664) 2025-10-24 22:16:02 +01:00
jamesread
c89579840b chore: Don't run release pipeline on PR branches 2025-10-24 22:14:00 +01:00
jamesread
38d81fafe2 fix: Listen address fields were not being loaded from config.yaml 2025-10-24 22:06:32 +01:00
James Read
8b2b85c3d0 fix: Argument form start button, and input validation was also broken! (#663) 2025-10-24 21:57:23 +01:00
James Read
76a33e2e54 chore: Remove some old dead code (#662) 2025-10-24 21:57:07 +01:00
jamesread
fa94357374 chore: Stop AI agents adding superflous comments 2025-10-24 21:31:54 +01:00
James Read
439e952a25 fix: sosreport contains pwd and abs paths (#660) 2025-10-24 20:52:01 +01:00
jamesread
3dfbbcc770 doc: Switch to issue types
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-10-24 19:01:04 +01:00
jamesread
77e8c37599 fix: docker latest-3k tag 2025-10-24 18:37:47 +01:00
jamesread
d3aa3b25b0 fix: doc typo
Some checks failed
Build & Release pipeline / build (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-10-24 01:24:33 +01:00
jamesread
d944b09c51 chore: Move up the saving of integrationtests 2025-10-24 01:18:14 +01:00
jamesread
b9851adfde doc: Add upgrade guide to readme 2025-10-24 01:16:48 +01:00
jamesread
45f9c18bc3 fix: enable main releases on 3k 2025-10-24 01:13:01 +01:00
jamesread
5d947f5a32 fix: enable main releases on 3k 2025-10-24 01:01:22 +01:00
jamesread
754d216827 fix: 3k version in semrel 2025-10-24 00:54:28 +01:00
jamesread
3d902295ad chore: disable semrel on inactive 2k branch 2025-10-24 00:47:49 +01:00
jamesread
5f8cd60736 chore: disable semrel on inactive 2k branch 2025-10-24 00:46:51 +01:00
jamesread
ae360100ce chore: add build service step to build and release workflow 2025-10-23 23:43:43 +01:00
jamesread
4a851355a8 chore: run pipeline on all branches 2025-10-23 23:36:38 +01:00
jamesread
54d3c65df3 chore: upgrade icons package 2025-10-23 23:34:54 +01:00
jamesread
58ba8eeeb9 chore: Simplify semrel pipeline 2025-10-23 23:34:39 +01:00
jamesread
e1e9cd9c35 feat: Begin automating 3k releases 2025-10-23 23:24:10 +01:00
jamesread
2a5fe71458 chore: wip OliveTin3k 2025-10-23 23:18:10 +01:00
jamesread
cbb163726e chore: wip OliveTin3k 2025-10-23 23:16:39 +01:00
dependabot[bot]
6836062b00 build(deps): bump vite from 7.1.9 to 7.1.11 in /frontend (#654)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-22 09:22:04 +00:00
dependabot[bot]
339dbe6dbd build(deps): bump github.com/go-viper/mapstructure/v2 from 2.3.0 to 2.4.0 in /service (#641)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-11 06:23:50 +00:00
dependabot[bot]
a24a7fbd01 build(deps): bump github.com/quic-go/quic-go from 0.53.0 to 0.54.1 in /service (#652)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Read <contact@jread.com>
2025-10-11 06:16:47 +00:00
jamesread
c9c781b197 fix: webui directory search
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-10-11 01:16:06 +01:00
jamesread
b0f24811b2 fix: stylelint and webui cleanup 2025-10-11 01:12:07 +01:00
jamesread
3884dc6d0a fix: codestyle workflow 2025-10-11 00:59:49 +01:00
jamesread
91dfe2437e fix: increase integration test timeout 2025-10-11 00:56:56 +01:00
jamesread
60814b97e2 chore: fix gocyclo issues 2025-10-11 00:52:18 +01:00
jamesread
b330fbd1a5 fix: all broken integration tests 2025-10-11 00:45:41 +01:00
jamesread
7d5fa999e5 fix: Move Iconify to v3, and version it via npm 2025-10-01 23:28:55 +01:00
jamesread
a464e6a445 fix: broken tests after changing the way arguments are parsed
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-10-01 22:20:56 +01:00
Tim Green
a26a8bb032 chore: update the docs link for timeouts in the error message (#651)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-10-01 07:18:39 +01:00
jamesread
7345744e41 chore: frontend updates
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-09-07 01:33:46 +01:00
jamesread
570c0ba087 chore: Repair output streaming, lots of css/go lint
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
2025-09-06 08:42:13 +01:00
dependabot[bot]
60c0c5db27 build(deps-dev): bump tmp from 0.2.3 to 0.2.4 in /integration-tests (#638)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Read <contact@jread.com>
2025-09-02 20:56:57 +01:00
jamesread
4a847f0587 Merge branch 'main' of github.com:OliveTin/OliveTin
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-08-20 00:05:47 +01:00
jamesread
6b342cbedb chore: Port huge amount of code to OliveTin 3k 2025-08-20 00:05:40 +01:00
jamesread
f46a02fced chore: tests wip
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-08-19 17:29:40 +01:00
James Read
3dd7aaff88 Update restapi_auth_oauth2.go
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-08-15 18:26:50 +02:00
James Read
7d4edeb60a Update build-tag.yml (#640) 2025-08-15 18:23:19 +02:00
James Read
387f1d9c1a Update build-snapshot.yml 2025-08-15 18:21:36 +02:00
James Read
c526fa323e Update AI.md
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-08-08 00:07:16 +01:00
jamesread
17c716c599 chore: fix build error 2025-08-03 22:41:01 +01:00
James Read
c5b49b33ab Update README.md
Some checks failed
Buf CI / buf (push) Has been cancelled
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-08-03 22:22:17 +01:00
jamesread
2a73b58255 chore: merge main 2025-08-03 22:17:35 +01:00
jamesread
a62d58f119 chore: OliveTin 3k progress 2025-08-03 22:10:51 +01:00
James Read
e02ce2be4e Update README.md (#637)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-07-31 19:45:01 +00:00
James Read
21ad5871ce doc: Fix broken link (#636)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-07-31 14:02:27 +00:00
James Read
d4d3193c1d bugfix: Argument confirmations stopped working (#627) (#632)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-29 21:51:34 +00:00
dependabot[bot]
10e5a92cbe build(deps): bump github.com/docker/docker from 28.3.1+incompatible to 28.3.3+incompatible in /service (#635)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 21:19:17 +00:00
James Read
a06299bd9e feature: stylemods, for side by side buttons, and XL images (#634)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-07-28 23:02:04 +00:00
James Read
2d4a3fc048 bugfix: Crash in OAuth2 userdata, and option to log user data (#631) 2025-07-28 21:46:08 +00:00
James Read
81ef166d78 bugfix #627 (#630)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-07-28 09:24:38 +00:00
dependabot[bot]
d4fe9eaa79 build(deps): bump form-data from 4.0.0 to 4.0.4 in /integration-tests (#626)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 06:19:32 +00:00
James Read
af75afa82f bugfix: #401 Duplicate lines in output (#623)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-07-19 23:34:59 +00:00
James Read
b91c0d7e45 feat: Style update for buttons, reduce style duplication (#621)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-07-19 11:51:19 +00:00
James Read
e922baa2e0 feat: #104 Checkboxes / booleans at last! (#620)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-07-19 02:10:41 +01:00
James Read
fcd1879d09 feat: Entities in icon field, and other cleanup stuff (#619)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-07-17 15:04:15 +00:00
James Read
a8ac719af7 feat kill acl (#618)
Some checks failed
Buf CI / buf (push) Has been cancelled
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-07-13 09:08:05 +00:00
jamesread
b99b3f4345 chore: Various codestyle fixes
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-07-12 23:46:50 +01:00
jamesread
b16ce074ea chore: Various codestyle fixes 2025-07-12 23:44:24 +01:00
James Read
54447774d1 fix: #616 - JSON parsing of ints to float64 (#617) 2025-07-12 22:02:50 +00:00
James Read
260477e5e8 Update README.md (#615)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-07-09 16:09:26 +00:00
James Read
e559c32c37 doc screenshots (#614) 2025-07-09 15:54:23 +00:00
James Read
2c7b33b730 chore: Dep update 2025-07-09 (#613)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
2025-07-09 10:24:59 +00:00
James Read
1970311ff5 feat: Triggered actions get previous action args #607 (#611)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-06-22 22:08:07 +00:00
James Read
433456986d fix: #603 - Dashboards that have fieldsets without entities are hidden (#609) 2025-06-22 21:09:03 +00:00
James Read
e38361f3d7 fix: gofmt/gocylo leftover from old PR (#610) 2025-06-22 20:42:57 +00:00
James Read
1ffdd93ddf fix: Redact password arguments in logs #594 (#601)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-06-06 21:00:56 +00:00
James Read
18c5599704 fix: Datetime mangling for Android phones #564 (#602) 2025-06-06 20:41:54 +00:00
James Read
224d7f40ed feat: Add unauthenticated readyz endpoint (#600) 2025-06-06 16:02:00 +00:00
James Read
1357eae9b8 fix: Empty dashboards are hidden (#599)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-06-06 15:11:02 +00:00
James Read
b2e7509959 feat: use sans-serif font everywhere (#597)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-06-01 11:51:17 +00:00
James Read
cebab32514 bug: Hide password values in deeplinks #594 (#596)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-05-31 19:54:44 +00:00
James Read
7110399d41 fix: Bug that caused duplicate links (eg Diagnostics and Logs) in the nav bar (#595) 2025-05-31 19:20:37 +00:00
James Read
74f0930dcc feat: #582 ServiceMain for Windows (#593)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-05-30 08:30:58 +00:00
James Read
c20eea29cd feat: win unicode flag (#590)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-26 07:33:22 +00:00
James Read
8c073bf45f feature: Intelligent version comparison #575 (#580)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-02 15:53:56 +00:00
jamesread
fcfa007cec build: Add unit tests for regex (ref: #578)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-05-01 16:30:29 +01:00
dependabot[bot]
b83b7a4c42 build(deps-dev): bump base-x from 3.0.9 to 3.0.11 in /webui.dev (#577)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 06:48:12 +00:00
James Read
0a7f3f3226 bugfix: #503 Fix possible race condition in onfileindir* (#576)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-26 20:42:12 +00:00
James Read
633e513697 feature: (#568) Separator allowed in usergroup line for trusted headers (#572)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-04-22 14:35:49 +01:00
James Read
eb2721c023 chore: Cleanup argument handling (#571)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-21 08:50:59 +00:00
James Read
765c698a9b feature: Easier loading of custom-webui icons with simplified paths (#567)
Some checks failed
Buf CI / buf (push) Has been cancelled
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-19 23:06:17 +00:00
James Read
c19428f6b6 feature: Policy support - allow hiding daignostics and logs (#569) 2025-04-19 23:05:49 +00:00
James Read
f02982b451 docs: Add AI policy (#563)
Some checks failed
Buf CI / buf (push) Has been cancelled
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-04-14 23:04:34 +00:00
jamesread
6ffb0cedbc chore: Remove surious dep update 2025-04-14 17:33:24 +01:00
James Read
775b3d3ca6 feat: Smaller width buttons on mobile (#561)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-04-13 18:37:58 +00:00
jamesread
6ad001619d build: feat/feature changelog prio
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-04-09 10:34:13 +01:00
James Read
db28e8915b feature: Include kubernetes-client (#559)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-08 21:39:21 +00:00
jamesread
bb4969c9ac build: Fix webui codestyle job
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-04-08 22:27:21 +01:00
jamesread
aef70c0e1b build: Fix webui codestyle job 2025-04-08 22:19:36 +01:00
jamesread
27c287c2de build: Code format 2025-04-08 22:14:23 +01:00
Noah Perks Sloan
ec1f974f67 feat: links to argument forms (#551)
Co-authored-by: James Read <contact@jread.com>
2025-04-08 21:07:01 +00:00
James Read
4ccfd0f993 fix: 404 in docs for "big error messages" (#558) 2025-04-08 20:54:37 +00:00
jamesread
c5eaa35fb0 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2025-04-08 21:49:22 +01:00
jamesread
24ba4fb574 build: cyclo ignore "getexecutionscount 2025-04-08 21:49:10 +01:00
Noah Perks Sloan
b742bd89c4 feat: support multiple user groups (#555) 2025-04-08 20:31:55 +00:00
James Read
2fcc0a63a0 feature: #543 Rate limits are now per action-entity instead of per action (#557)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-08 00:28:32 +00:00
jamesread
ba29325c15 feature: #543 Rate limits are now per action-entity instead of per action 2025-04-08 01:18:55 +01:00
James Read
88f639d29f chore: Cleanup executor shell passing (#556)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-07 20:48:45 +00:00
James Read
fa44e958d8 chore: Big dependency update (#553)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-06 22:26:51 +00:00
David Q
182548e0dc feat: add syntax for interpolating environment variables into config file (#548)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Co-authored-by: James Read <contact@jread.com>
2025-04-05 22:36:10 +00:00
David Q
c3097e40db fix: reloading config after k8s configmap replacement (#552)
Co-authored-by: James Read <contact@jread.com>
2025-04-05 22:28:28 +00:00
jamesread
2b66414a93 build: Add missing build-snapshot job on PR 2025-04-05 23:23:47 +01:00
jamesread
7da8a5bc38 buf dep update
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Buf CI / buf (push) Has been cancelled
2025-04-05 01:02:47 +01:00
jamesread
2bc45e9a09 build: cannot set cwd with buf-action 2025-04-05 01:00:00 +01:00
jamesread
fd3c6087f0 working directory for buf 2025-04-05 00:51:32 +01:00
jamesread
c0b8dd71db Enable manual trigger for buf 2025-04-05 00:48:09 +01:00
jamesread
1164e5fae2 working directory for buf 2025-04-05 00:43:26 +01:00
jamesread
902d4ed819 build: Switch to buf2 2025-04-05 00:15:57 +01:00
James Read
2320a56dd9 build: Update cache path for go (#550)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-04-03 10:48:15 +00:00
James Read
2981fc4c1f doc: Point pull request template to contributors guide (#549)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-02 22:47:41 +00:00
Noah Perks Sloan
8865331da2 feat: enable JWT auth from a header (#547)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-04-02 19:14:43 +01:00
James Read
709d6ac2ad feature: Allow rendering output as HTML (#544)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-03-28 23:48:26 +00:00
jamesread
8d4e335dda fix: Invalid path to systemd file
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-03-23 01:02:22 +00:00
jamesread
44a9de080c docs: Windows was not bold. 2025-03-23 00:28:07 +00:00
jamesread
9bb17badad docs: Links and make targets 2025-03-23 00:26:52 +00:00
James Read
ff31abe66c refactor: Project directories (#541)
Some checks failed
Build Snapshot / build-snapshot (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
Buf CI / buf (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
2025-03-22 01:06:59 +00:00
James Read
b4c886d5d3 refactor: systemd file does not need to be in root dir (#538) 2025-03-22 01:06:22 +00:00
dependabot[bot]
e0bc1d86f6 build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#540)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-22 00:42:53 +00:00
James Read
270f20ec75 refactor: Moved proto files into own folder (#537)
Some checks failed
Buf CI / buf (push) Waiting to run
Build Snapshot / build-snapshot (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-03-21 02:41:41 +00:00
James Read
486253b253 feature: Docker ce (#536) 2025-03-21 00:15:44 +00:00
James Read
6b9c6c8b9c refactor: Switch to conventional scripts, rather than custom script (#535) 2025-03-20 23:04:36 +00:00
dependabot[bot]
1275934ac1 build(deps): bump axios from 1.7.4 to 1.8.4 in /integration-tests (#534)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Read <contact@jread.com>
2025-03-20 23:00:03 +00:00
jamesread
bbc6095e36 doc: social banner 2025-03-20 22:44:25 +00:00
jamesread
7906f2d363 refactor: dep update 2025-03-20 21:55:17 +00:00
jamesread
d45bd887c2 security: dep update 2025-03-20 21:44:06 +00:00
James Read
7788f58aac feature: persist local sessions across restart (#522)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-21 22:26:15 +00:00
James Read
f3bc82311d feature: Allow arbitary HTML on argument form (useful for descriptions) (#519) 2025-02-21 17:57:02 +00:00
James Read
cae5d296ca bugfix: Fixed broken log message for Execution finished on websocket (#520) 2025-02-21 17:56:37 +00:00
James Read
5cd5bd2a25 bugfix: Default config now uses "triggers" instead of "trigger", and shows HTML (#521) 2025-02-21 17:53:19 +00:00
James Read
6550223ee4 fmt: code style (#518)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2025-02-19 23:18:02 +00:00
James Read
39368d511a feature: Limit log history to prevent browser lag and grpc encode failures (#507)
* feature: Limit log history to prevent browser lag and grpc encode failures

* cicd: Simplify if/else chain to switch

* cicd: Lint failure in JS because of a trailing space

* cicd: Coderabbit fixes on PR

* cicd: Move config sanitize into correct place

* cicd: Handle negative startOffset, negative endOffset and empty logs

* Update internal/executor/executor.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* cicd: Fix getLogs segfault

* cicd: Fix code rabbit nitpicks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-19 23:17:06 +00:00
James Read
0a4bcc6423 cicd: Update .goreleaser.yml file to Feb 2025 syntax (#517) 2025-02-19 16:57:32 +00:00
jamesread
56ab1cec8f cicd: Textarea resizing [skip ci] 2025-02-18 17:49:54 -08:00
jamesread
12f87ca6e1 feature: Add support for raw textboxes #490 2025-02-18 17:49:54 -08:00
James Read
2fc7c23416 feature!: Trigger changed Triggers, allowing multiple actions (#515)
* feature!: Trigger changed Triggers, allowing multiple actions

* bugfix: Warning message on trigger loops, prevented NPE

* cicd: Fix cyclo complexity with triggers
2025-02-19 00:00:33 +00:00
James Read
2cf538bab1 bugfix: Systemd unit now waits for network and filesystem #495 (#514)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2025-02-18 23:17:56 +00:00
dependabot[bot]
906b6c5783 build(deps): bump serialize-javascript and mocha in /integration-tests (#513)
Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) to 6.0.2 and updates ancestor dependency [mocha](https://github.com/mochajs/mocha). These dependencies need to be updated together.


Updates `serialize-javascript` from 6.0.0 to 6.0.2
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v6.0.0...v6.0.2)

Updates `mocha` from 10.4.0 to 10.8.2
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v10.4.0...v10.8.2)

---
updated-dependencies:
- dependency-name: serialize-javascript
  dependency-type: indirect
- dependency-name: mocha
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 17:14:25 +00:00
jamesread
6d43ebef44 docs: Change maturity label
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-23 14:58:26 +00:00
James Read
c04203e671 Update README.md (#508)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2025-01-22 14:07:21 +00:00
dependabot[bot]
bf93707787 build(deps): bump github.com/MicahParks/jwkset from 0.5.17 to 0.7.0 (#506)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Bumps [github.com/MicahParks/jwkset](https://github.com/MicahParks/jwkset) from 0.5.17 to 0.7.0.
- [Release notes](https://github.com/MicahParks/jwkset/releases)
- [Commits](https://github.com/MicahParks/jwkset/compare/v0.5.17...v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/MicahParks/jwkset
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-12 21:39:18 +00:00
Bjorn Lammers
f0d70f0c15 Migrate dashboard-icons to homarr-labs
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-06 22:21:35 +00:00
jamesread
17d9e29f19 cicd: Add responsibility labels to all new issues
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2024-12-30 02:05:05 +00:00
jamesread
be7168d9c5 cicd: Issue responsibility bugs 2024-12-30 01:59:27 +00:00
jamesread
d36b23832c cicd: Issue responsibility bugs 2024-12-30 01:54:36 +00:00
jamesread
b6429e9bc7 cicd: Add issue responsibility labels 2024-12-30 01:45:51 +00:00
jamesread
7ddc112b2c doc: CONTRIBUTING - add more guidelines
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
2024-12-23 17:14:55 +00:00
jamesread
10a473ca1c security: Big dependency update
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-12-22 01:59:43 +00:00
jamesread
c7207d1ee6 security: Big dependency update 2024-12-22 01:58:19 +00:00
jamesread
c585762ba8 cicd: Standardize the automated code generation 2024-12-22 01:48:21 +00:00
James Read
476838d59a bugfix: Local users now work with a single usergroup (#486)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-11-24 21:22:24 +00:00
James Read
f0b1cefb72 feature: OAuth2 early support for CertBundles (#474) and Usergroups (#477) (#485)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2024-11-24 10:00:25 +00:00
James Read
b4a555e3da bugfix: #481 StartActionAndWait now allows arguments. (#483) 2024-11-24 06:56:27 +00:00
James Read
79a4119351 security: Update cross-spawn vulnerable dependency (#482) 2024-11-24 06:56:16 +00:00
jamesread
851d0dac84 doc: Fix #478 - easy.cfg typo [skip ci] 2024-11-23 16:31:23 -06:00
James Read
655a7f205d feature: Improved auth log messages (#476)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-11-14 22:00:08 +00:00
James Read
13e48dded2 feature: Better guest handling (#472)
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
* feature: Better guest handling

* bugfix: Better handling of requiring guests to login
2024-11-09 23:27:09 +00:00
dependabot[bot]
344df3739d build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#473)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-09 00:13:32 +00:00
jamesread
0d981773b3 fmt: gofmt 2024-11-08 23:54:32 +00:00
jamesread
8d4ddd36cd cicd: Goreleaser tikeout
Some checks failed
Build Snapshot / build-snapshot (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Codestyle checks / codestyle (push) Has been cancelled
DevSkim / DevSkim (push) Has been cancelled
2024-11-02 07:35:36 +00:00
James Read
209234c09f bugfix: Still hide an empty actions section if a section is currently selected (#469) 2024-11-02 01:32:02 +00:00
James Read
9bcb2d80dc bugfix: Logs table only updating on start, not finish (#467)
Some checks are pending
Build Snapshot / build-snapshot (push) Waiting to run
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Codestyle checks / codestyle (push) Waiting to run
DevSkim / DevSkim (push) Waiting to run
2024-11-02 00:47:13 +00:00
jamesread
de504f5f80 cicd: Lint issue 2024-11-01 23:23:02 +00:00
jamesread
8fe91101db feature: Log search box that is less visually offensive 2024-11-01 23:23:02 +00:00
James Read
8717997b0e feature: Hovering over the execution dialog title shows the action ID and execution ID (#468) 2024-11-01 22:49:09 +00:00
James Read
1bfb7c4b28 feature: Logs table creates an entry as soon as the execution is started (#464) 2024-11-01 21:23:22 +00:00
James Read
964dc7b48a feature: Rerun button on execution dialog, easily rerun actions! (#465) 2024-11-01 20:49:45 +00:00
James Read
ceb0a78180 bugfix: Logout cookies causing a conflict (#461) 2024-11-01 20:36:38 +00:00
James Read
9117163316 bugfix: Login form was not displayed if only OAuth2 was configured (#457)
* bugfix: Login form was not displayed if only OAuth2 was configured

* bugfix: npe in AuthOAuth2Provider

* test: Try and fix flakey tests
2024-10-27 08:56:28 +00:00
James Read
d3ad811ac5 bugfix: Removed hardcoded dependency on GitHub in OAuth2 flow (#458) 2024-10-26 22:30:08 +00:00
James Read
ee26fe6b50 ci github int tests settle (#459)
* ci: Allow more time for GitHub tests to settle

* ci: Run build-snapshot on any push, but dont dupe with PR
2024-10-26 22:10:33 +00:00
James Read
2950b59514 cicd: Devskim actions upgrade (#456) 2024-10-25 13:44:08 +00:00
James Read
be5ddda020 feature: freebsd support (#454) 2024-10-23 12:52:25 +00:00
James Read
16b07283b0 feature: Add riscv build! (#453) 2024-10-23 10:54:43 +00:00
jamesread
d7814ff6df bugfix: If a button is disabled in the API, render it disabled [skip ci] 2024-10-20 23:03:10 +01:00
James Read
be9b2a7c78 bugfix: Cookie expiry for OAuth2 & Local set to 1 year, not session (#451) 2024-10-20 21:58:12 +00:00
James Read
80e5b5b0c1 feature: Cleanup auth code (#444) 2024-10-19 19:33:27 +00:00
James Read
0283b51eca feature: Login/Logout links (#443)
* feature: Login/Logout links

* cicd: codestyle
2024-10-19 07:49:41 +00:00
jamesread
1af2e92132 feature: Local user login! 2024-10-18 01:20:04 +01:00
James Read
71ad5d2e3a feature: local auth! (#441)
* feature: local auth!

* cicd: contentLogin -> content-login

* bugfix: CSS codestyle
2024-10-17 13:23:01 +00:00
James Read
32b5fee108 bugfix: Fixed crash when requesting execution status that could not be found (NPE) (#440) 2024-10-15 10:48:55 +00:00
James Read
de81ec00fd feature: mix of bits (#439)
* feature: mix of bits

* bugfix: codestyle
2024-10-15 07:47:51 +00:00
Simon Dahlbacka
7cc07158ab bugfix: Turns out this should now be 403 instead (#438) 2024-10-14 13:53:23 +00:00
jamesread
3b8976fd51 feature: Login screen 2024-10-14 09:26:33 +01:00
jamesread
fb7e650267 feature: OAuth2 Redirect URL 2024-10-14 09:25:21 +01:00
jamesread
6a7187fb5b feature: OAuth2! :-D 2024-10-14 09:25:21 +01:00
James Read
b31cdf15a2 feature: Email argument type (#433)
* feature: Email argument type

* bugfix: Fix error if additional links is null
2024-10-13 18:40:16 +00:00
jamesread
6e0e0e8133 cicd: cleanup console log 2024-10-13 01:10:47 +01:00
jamesread
706441799f feature: Custom navigation links in the nav (sidebar/topbar) 2024-10-13 00:35:47 +01:00
jamesread
0b66bc7bbd cicd: Labels on issue templates [skip ci] 2024-10-13 00:35:28 +01:00
jamesread
86e876a9c9 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-10-12 23:18:29 +01:00
jamesread
8b33061bbb cicd: Remove unused Jenkinsfile 2024-10-12 23:15:06 +01:00
James Read
411f1bff1c feature: Style the currently selected section in nav, and few minor style improvements (#429)
* feature: Style the currently selected section in nav, and few minor style improvements

* bugfix: No unit on 0 length
2024-10-12 21:45:26 +00:00
James Read
eee4c4e404 bugfix: Auth in the wrong place (#430) 2024-10-12 21:14:09 +00:00
James Read
a187c8c8a0 feature: Slightly nicer looking logs and diagnostics pages (#427) 2024-10-11 17:40:40 +00:00
Simon Dahlbacka
35dca50863 Make it possible to redirect to a login url unless authenticated (#347)
* feature: Try to navigate to UrlOnUnauthenticated if set and not authenticated

* refactor: use better naming

* feature: default to allow guest
2024-10-11 17:36:31 +00:00
Simon Dahlbacka
00856f15a7 feature: pass ot_ args to shellAfterCompleted (#420)
* feature: pass ot_ args to shellAfterCompleted

* refactor: Implement this the right way

---------

Co-authored-by: James Read <contact@jread.com>
2024-10-11 14:59:02 +00:00
James Read
d1c67a9dd8 depbump: js micromatch 4.0.8 (#426) 2024-10-10 23:38:35 +00:00
James Read
6c289506c2 bugfix: Fixed unusual issues that slipped through with shellAfterCompleted (#425)
* bugfix: Fixed unusual issues that slipped through with shellAfterCompleted #419 #420

* cicd: Fix broken style lint
2024-10-10 23:15:03 +00:00
James Read
5d82ae7680 feature: Log username with each request #422 (#424) 2024-10-10 20:50:47 +00:00
James Read
9737886839 feature: Better websocket error message (#417) 2024-10-02 00:19:49 +00:00
James Read
1d9502d800 Feature better 404s (#416)
* feature: CSS on actions

* feature: Less crashes when things are not found
2024-10-01 23:21:37 +00:00
James Read
c0a18f82a5 feature: Link to log outputs (#415) 2024-10-01 23:17:09 +00:00
jamesread
9723a38ca1 bugfix: Crash on trying to start a non-existant action from the API 2024-10-01 21:19:17 +01:00
James Read
8258a758d2 bugfix: Dashboards with multiple spaces in the name could not be navigated properly (#408) 2024-09-16 22:36:40 +00:00
James Read
044613ae9a bugfix: Null action map, causing crash on startup actions (#407) 2024-09-11 16:19:52 +00:00
James Read
76c9171356 feature: Add OT_ vars to each execution (#406) 2024-09-10 22:52:46 +00:00
James Read
f8c330aae3 feature: Better feedback on shellAfterCommand, and mark OliveTin specific output with "OliveTin::" in logs (#403) 2024-09-02 22:59:19 +00:00
jamesread
d4bd7dd586 doc: shellAfterCompleted should use output, not stdout [skip ci] 2024-09-02 21:14:34 +01:00
Mitch Brown
fa20f6a63a Fixed typo 2024-09-01 00:22:45 +01:00
James Read
b9ce695616 feature: Nicer default colors (catpuccin/frappe (#395) 2024-08-30 20:48:17 +00:00
James Read
3f1dbf1130 bugfix: System users can now be used in ACLs (#396) 2024-08-30 20:48:07 +00:00
James Read
6413e51cf5 feature: Exec permission denied now marks the action as "blocked" and adds a log message. Timeouts also add a documentation link to the command output. Tags always set. (#398) 2024-08-30 20:36:13 +00:00
James Read
defcf6d26e fmt: Rebuilt ACL code to have less boilerplate (#397) 2024-08-30 20:30:44 +00:00
James Read
2671983c43 bugfix: Execution dialog duration no longer has issues displaying actions that were never started. (#394) 2024-08-30 20:28:27 +00:00
James Read
dc9653307b feature: Logs, column for tag, and left align status. Stop nav flash (#393) 2024-08-30 20:28:12 +00:00
jamesread
eb91eb33d5 bugfix: Log for http request mangling changed to DEBUG level 2024-08-30 21:27:56 +01:00
James Read
ddb803d9b5 feature: Sidebar hiding now pure CSS & theme reloading for devs (#391) 2024-08-25 23:47:43 +00:00
dependabot[bot]
c9095b4d67 build(deps): bump axios from 1.6.8 to 1.7.4 in /integration-tests (#387)
Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.8...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 22:14:35 +00:00
James Read
40158eda71 bugfix: Race condition on executor logs (#385) 2024-08-14 22:06:50 +00:00
dependabot[bot]
bbbbfceeb3 build(deps): bump github.com/docker/docker (#386)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.1.4+incompatible to 26.1.5+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.1.4...v26.1.5)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 22:06:06 +00:00
James Read
9ca1940834 feature: change all navigation to path based (#384)
* feature: change all navigation to path based

* bugfix: load default dashboard if actions is empty
2024-08-14 11:35:10 +00:00
James Read
37160a91f3 feature: take full page and display footer at bottom pf window (#383) 2024-08-14 06:56:30 +00:00
James Read
274d036f74 fmt: Cleanup terminal code (#381)
* fmt: Clean up OutputTerminal code

* fmt: Clean up OutputTerminal code
2024-08-09 19:04:57 +00:00
James Read
1fe0e49adb bugfix: #337 - Entity overwriting (#382) 2024-08-09 19:04:50 +00:00
James Read
7a7a07d9ad feature: Minor change to action timeout message to be more consistent with other logs (#380) 2024-08-09 14:53:02 +00:00
James Read
5b5fca0837 feature: enableCustomJs toggle to allow loading ./custom-webui/custom.js (#379) 2024-08-09 09:21:50 +00:00
James Read
fab0264d9b feature: Cleanup execution dialog, and log display status. (#378)
* feature: Cleanup execution dialog, and log display status.

* fmt: Remove empty block

* cicd: Use ActionStatusDisplay in test

* bugfix: missed promise

* bugfix: missed promise

* bugfix: Didnt return getText on ActionStatusDisplay
2024-08-08 20:46:46 +00:00
jamesread
8d839ee6ce bugfix: fsnotify event logs moved to DEBUG level, because info was spammy 2024-08-07 00:22:49 +01:00
jamesread
90efbf3159 fmt: #368 2024-08-07 00:21:02 +01:00
jamesread
17dd1b4158 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-08-07 00:17:42 +01:00
dependabot[bot]
e5f6d8ff50 build(deps): bump github.com/docker/docker (#372)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.0.2+incompatible to 26.1.4+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.0.2...v26.1.4)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 16:02:13 +00:00
jamesread
e183910b88 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-08-06 16:35:10 +01:00
James Read
8cd5b9fb46 feature: Trace log message on ExecRequest for debugging (#375) 2024-08-06 13:46:49 +00:00
wushuzh
6958445f83 Use windows compatible cmds and wait more time (#374)
* bugfix: change action request cnt type as Counter

* bugfix: fix type of act req count and typo

* bugfix: change to correct type of act req in TC

* bugfix: use Windows compatible cmds for test

* bugfix: wait more for slow test env

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-08-06 13:43:16 +00:00
James Read
ef91294d5e cicd: Make grpc to fix codeql (#373) 2024-07-30 13:03:56 +00:00
jamesread
a36f286f8a cicd: Make grpc to fix codeql 2024-07-30 13:58:12 +01:00
wushuzh
d9e921950f bugfix: fix type of the prometheus metric act req as count (#370)
* bugfix: change action request cnt type as Counter

* bugfix: fix type of act req count and typo

* bugfix: change to correct type of act req in TC

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-07-26 08:41:42 +00:00
wushuzh
ffcd19e748 bugfix: use cross-platform module filepath to set usedConfigDir (#369)
* bugfix: use filepath to set correct configDir in Windows

* test: improve unittest rep folder creation

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-07-22 09:33:56 +00:00
jamesread
d0eb132b95 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-07-18 15:11:19 +01:00
jamesread
1cf971c092 feature: Better format for server dashboard 2024-07-18 15:11:06 +01:00
James Read
49f745be68 fmt: Set for setdir (#367) 2024-07-18 12:08:36 +00:00
wushuzh
b50824a705 feature: support basic dev tasks in Windows (#364)
* feature: support basic dev tasks in Windows

* feature: support front-end targets in Windows

---------

Co-authored-by: wushuzh <wushuzh@outlook.com>
2024-07-17 12:51:08 +00:00
James Read
69d1cc75a7 feature: css classes on displays (#363) 2024-07-16 10:01:31 +00:00
jamesread
a54ea505c9 bugfix: Allow . in enxtended variable paths 2024-07-15 22:20:49 +01:00
James Read
a1563b72ae Feature argument grid layout (#360)
* feature: Argument form now uses a grid layout, making the input boxes take up available room

* feature: Argument form now uses a grid layout, making the input boxes take up available room

* feature: Argument form now uses a grid layout, making the input boxes take up available room

* feature: Argument form now uses a grid layout, making the input boxes take up available room
2024-07-15 16:17:08 +00:00
James Read
31d7168aac feature: #350 Environment variable PORT can be used to override the default 1337, and internal services will calculate their port based on that (#358) 2024-07-15 14:28:53 +00:00
James Read
09016e1d5f feature: Entitiy data now serializes non-strings (#357) 2024-07-15 12:10:13 +00:00
James Read
652882350c cicd: Update login-action (#356) 2024-07-15 12:09:35 +00:00
James Read
20c4423799 feature: Argument labels will end with a :, unless they end with ?, : or . already (#355) 2024-07-15 08:24:54 +00:00
jamesread
bb90a5da92 feature: Use SVG icon for window maximize #343 2024-07-13 20:46:06 +01:00
jamesread
3ca3a2dd3c Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-07-13 20:43:21 +01:00
jamesread
510c48e1af bugfix: Restore default theme 2024-07-13 20:43:04 +01:00
James Read
3ac809c234 feature: Container images upgraded to Fedora 40. (#354) 2024-07-13 18:36:10 +00:00
James Read
9dd33bc3f9 bugfix: Empty environment variable names causing exec failures on Windows, thanks @sirjmann92 ! (#353) 2024-07-12 16:22:03 +00:00
James Read
897cc0e034 depbump: Upgrade Go to 1.21 (#352) 2024-07-11 22:33:36 +00:00
James Read
482ef0e5e8 feature: Helper scripts for ssh, themes, etc (#349)
* feature: Helper scripts for ssh, themes, etc

* cicd: Update goreleaser to support v2 .goreleaser.yml files
2024-07-07 22:10:33 +00:00
dependabot[bot]
e0678fc0a9 build(deps): bump github.com/rs/cors from 1.10.1 to 1.11.0 (#348)
Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.10.1 to 1.11.0.
- [Commits](https://github.com/rs/cors/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/rs/cors
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James Read <contact@jread.com>
2024-07-07 07:01:33 +00:00
jamesread
9cb5574b99 feature: easy-ssh-docs 2024-07-07 01:06:43 +01:00
jamesread
c18b91f684 feature: Easy SSH 2024-07-07 00:55:22 +01:00
James Read
6622a6ded4 bugfix: Kill the process group, not just the parent process (#328), , thanks @ioqy, @vvrein (#346)
* bugfix: Kill the process group, not just the parent process (#328), thanks @ioqy, @vvrein

* bugfix: Kill the process group, not just the parent process (#328), thanks @ioqy, @vvrein

* bugfix: Kill the process group, not just the parent process (#328), thanks @ioqy, @vvrein
2024-07-04 00:12:28 +00:00
James Read
fb6aaa52c7 feature: New diagnostics page, showing SSH keys found. (#344) 2024-07-03 23:34:07 +00:00
James Read
a1adc2a85d bugfix: Output width in chrome (#345) 2024-07-03 23:33:58 +00:00
dependabot[bot]
3d9cb621dd build(deps-dev): bump braces from 3.0.2 to 3.0.3 in /integration-tests
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-04 00:34:34 +01:00
James Read
943b4c75aa feature: Password type (no validation) (#340) 2024-07-01 08:31:53 +00:00
James Read
fb972fae55 bugfix: Handle update failed update checks silently (#342) 2024-06-30 22:05:04 +00:00
dependabot[bot]
eb4f28dfda build(deps-dev): bump ws from 8.16.0 to 8.17.1 in /integration-tests (#335)
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 05:25:03 +00:00
James Read
e36fedf4b2 Check for updates default disabled (#333)
* feature: Checking for updates is now disabled by default (#93)

* feature: Default config has checkForUpdates: false (it defaults to false anyway) to make people aware of the update checking feature
2024-06-04 18:37:59 +00:00
James Read
362a97c59e feature: No more tracking in update checking! :-) (#93) (#332) 2024-06-04 12:21:11 +00:00
James Read
c82beb61a9 feature: Vastly improved logs and tests for killing actions (#330)
* bugfix: Sleep testing

* feature: Vastly improved testing for killing actions (#328)

* cicd: Add find-flakey-tests target

* cicd: Better debugging for domStatus

* cicd: Debug flakey test

* cicd: Debug flakey test

* cicd: Is cors messing with killaction?

* cicd: Slip broken test in CI

* cicd: Skip broken test in CI

* cicd: Skip broken test in CI
2024-06-03 16:19:15 +00:00
James Read
00a8a0bf69 fmt: Center argument-wrapper #327, point 5 (#331) 2024-06-02 22:09:24 +00:00
James Read
ffc17dd73b bugfix: Terminal width fitting (#329)
* bugfix: Terminal width fitting

* bugfix: Terminal width fitting

* fmt: css codestyle issue presumably from pkg upgrade
2024-06-02 11:44:23 +00:00
James Read
238abc95ad feature: Customizable default directory, back and action icons (#326) 2024-05-31 22:43:05 +00:00
James Read
ac0f3ab6f8 feature: Live command output! (#325)
* feature: Live command output WIP

* cicd: Fix live command output build errors

* feature: Live command output WIP, remove stdout/stderr split

* feature: Live command output

* Restore output im stepExecAfter, rename so to ost for OutputStreamer

* Update executor.go

* feature: Remove output from log message, bad for long messages, binary output, and security

* feature: Live command output WIP
2024-05-31 20:56:49 +00:00
James Read
4bac315568 feature: The executor now controls the public action map - meaning actions work without the API, and pages load faster (and much more!) (#324)
* feature: The executor now controls the public action map

* bugfix: Build the action map if there are no entities!

* bugfix: Sort by title if the actions have the same config order
2024-05-27 22:52:06 +00:00
James Read
daa48b5a73 feature: OliveTin now relies on websocket, and will show a big error if it isn't available (#323)
* feature: OliveTin now relies on websocket, and will show a big error if it isnt available

* feature: OliveTin now relies on websocket, and will show a big error if it isnt available
2024-05-26 18:21:54 +00:00
James Read
c70cc864ee feature: Websocket URL is now relocatable (#322)
* feature: Websocket URL is now relocatable

* feature: Websocket URL is now relocatable
2024-05-26 17:03:12 +00:00
James Read
dc7ff40da6 bugfix: #316 Entity file change debouce timer now uses delay after initial event (#321) 2024-05-26 17:01:37 +00:00
James Read
046ffaecf4 feature: Displays now have centered text, and "fit in" a bit better (#320) 2024-05-26 16:54:11 +00:00
James Read
3904f8563d feature: Refresh dashboards when entity files change. (#319)
* feature: Refresh dashboards when entities change

* feature: Refresh dashboards when entities change

* bugfix: Concurrency, lock around websocket write
2024-05-24 22:10:38 +00:00
James Read
18423a9888 bugfix: Default load dashboard when it contains multiple spaces (#318) 2024-05-23 08:22:08 +00:00
James Read
8fbbd9b32c feature: displays have basic styling (#317) 2024-05-23 08:21:57 +00:00
guangwu
7e34efd453 chore: remove refs to deprecated io/ioutil (#309)
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
Co-authored-by: James Read <contact@jread.com>
2024-05-13 15:20:58 +00:00
jamesread
9d33e62d34 Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-05-13 15:39:54 +01:00
jamesread
6b2bc0adc0 cicd: run build-snapshot against prs 2024-05-13 15:39:42 +01:00
James Read
80083fedab bugfix: Race condition in stringvariables (#311) 2024-05-13 14:10:44 +00:00
James Read
1ab35fdb36 feature: #Logs and similar in the address bar to load relevant section (#306) (#310) 2024-05-13 12:12:04 +00:00
James Read
f43ece4263 feature: Better sosreports (#308) 2024-05-13 07:01:45 +00:00
jamesread
ea1ce82ded fmt: Change publicid map debug URL 2024-05-13 01:04:21 +01:00
jamesread
c24adaafcb Merge branch 'main' of ssh://github.com/OliveTin/OliveTin 2024-05-06 23:01:09 +01:00
jamesread
c42875b107 cicd: update package-lock.json 2024-05-06 23:00:35 +01:00
jamesread
447ad36d4d cicd: update package-lock.json 2024-05-06 23:00:05 +01:00
jamesread
71dc467b31 cicd: Quick target to run default integration tests 2024-05-06 22:59:49 +01:00
James Read
8625e1fc0a fmt: Clean REST API Testing (#307)
* fmt: Clean REST API Testing

* fmt: Clean REST API Testing
2024-05-06 21:44:12 +00:00
James Read
638e5b7fe1 doc: fix default licenses in package.json (#304) 2024-05-01 07:38:47 +00:00
James Read
f467c69e8f Argument suggestions (#301)
* feature: Suggestions for inputs

* feature: #103 Completed suggestions support
2024-04-28 08:55:40 +00:00
James Read
8fd98874e2 feature: Save logs! #174 (#300) 2024-04-28 07:58:25 +00:00
James Read
dc6f6c2896 feature: Kill support in the UI (#297)
* feature: Kill command API (wip)

* feature: Kill support in the UI
2024-04-27 21:51:45 +00:00
James Read
a783fc8cd4 feature: Arguments are now also passed as ENV variables (#296) 2024-04-26 23:03:17 +00:00
James Read
6a16a7c6c2 fmt: onfileindir (#295) 2024-04-26 23:02:40 +00:00
James Read
ceb215a6dc bugfix: #272 if the dialog is already open, close it (#294) 2024-04-26 23:02:17 +00:00
James Read
d6cb634824 feature: Extra file info using onfileindir (#293) 2024-04-26 22:05:28 +00:00
James Read
500419307b Nullable args (#292)
* feature: Argument values can be null be default. Use RejectNull to change.

* feature: Argument values can be null be default. Use RejectNull to change.
2024-04-26 17:36:33 +00:00
James Read
12cf0013e2 feature: extra args for fileindir (#291) 2024-04-25 23:32:58 +00:00
James Read
318d4fe0d0 cicd: Reduce test flake and add lots more debugging info (#290) 2024-04-24 15:22:15 +00:00
James Read
a3aee3603f More screenshots link (#289)
* More screenshots link

* bugfix: wip

* cicd: Test flakey tests

* cicd: more tests

* cicd: more tests

* cicd: Try a 10s timeout

* cicd: Better debugging of process start / stop

* cicd: Better debugging of process start / stop

* cicd: Possibly unflaky tests
2024-04-23 20:54:54 +00:00
jamesread
7c6f36c600 doc: Update images 2024-04-22 23:34:36 +01:00
James Read
dde8a9cbb6 cicd: More test coverage that would have caught recent issues (#287)
* cicd: More test coverage that would have caught recent issues

* bugfix: test flake

* bugfix: test flake

* cicd: Reduce test flake

* cicd: more test coverage

* depbump: testing packages
2024-04-22 21:40:48 +00:00
James Read
dfca712cb1 bugfix: Update icon on buttons (#288) 2024-04-22 19:02:59 +00:00
James Read
5057ba2e1c bugfix: Start filewatcher on different goroutines (#286) 2024-04-21 19:51:49 +00:00
jamesread
8f1dfffa49 fmt: Clean up log statement 2024-04-21 20:12:47 +01:00
James Read
6991724258 bugfix: Unexpected message #283 (#284) 2024-04-20 19:58:38 +00:00
James Read
09e1de06ce bugfix: Dont rename logo filenames (#282) 2024-04-19 23:27:13 +00:00
James Read
5a644b0856 bugfix: The web interface now refreshes when the config file is changed! (#281) 2024-04-19 23:11:40 +00:00
James Read
86b2187236 cicd: Increase coverage (#280) 2024-04-19 21:56:45 +00:00
James Read
fafacfeafb bugfix: Move all inotify to filehelper (#279) 2024-04-19 21:56:21 +00:00
James Read
362019738d feature: #267 add jq to containers (#278) 2024-04-19 16:16:23 +00:00
dependabot[bot]
1a0ba6c6b1 build(deps): bump golang.org/x/net from 0.22.0 to 0.23.0 (#277)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 16:16:09 +00:00
jamesread
4c2cc5da1c bugfix: Remove debug log left in the code 2024-04-19 00:58:05 +01:00
James Read
30e2aa141b feature: Rate limit for actions (#76) (#275) 2024-04-18 23:51:59 +00:00
dependabot[bot]
3768a57eaa build(deps): bump github.com/docker/docker (#274)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 26.0.0+incompatible to 26.0.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v26.0.0...v26.0.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 23:49:41 +00:00
James Read
db5de9be97 feature: #135 Permissions for logs (#273) 2024-04-18 20:52:04 +00:00
James Read
f60ab6ce85 feature: Show username in header (#270) 2024-04-15 00:09:55 +00:00
James Read
43c48aef17 feature: Renamed authJwtSecret to authJwtHmacSecret (#271)
* feature: Renamed authJwtSecret to authJwtHmacSecret

* feature: rename authJwtSecret
2024-04-15 00:01:30 +00:00
James Read
8341b1d6d0 feature: Huge JWT improvements for CloudFlare, etc (#269) 2024-04-14 22:59:45 +00:00
jamesread
dd11961a11 bugfix: custom-webui prefix 2024-04-12 22:07:06 +01:00
James Read
3de819a0e9 feature: add prometheus metrics (#268)
* feature: add prometheus metrics

* bugfix: ixed issue with path
2024-04-11 23:23:40 +00:00
James Read
0b546eaeb5 Improve screenshots visibility 2024-04-11 11:13:10 +01:00
jamesread
558e1819bf doc: Updated mobile screenshot 2024-04-11 10:20:03 +01:00
James Read
9d6afa2fe7 doc: typo #266 (#267) 2024-04-11 06:23:24 +00:00
James Read
5f3a967515 depbump: 2024-04 (#265)
* depbump: 2024-04

* go mod tidy
2024-04-10 23:03:25 +00:00
jamesread
179f1be19a bugfix: Null JWT verifier 2024-04-11 00:04:34 +01:00
James Read
f13a5c070a feature: JWT Validation wip for Cloudflare (#264) 2024-04-10 22:14:33 +00:00
James Read
9476d052b6 cicd: Dont build snapshot on tag (#263) 2024-04-10 20:14:18 +00:00
James Read
1d446ace04 custom-webui and faster theme loading (#262)
* feature: custom-webui and faster theme loading

* feature: custom-webui and faster theme loading
2024-04-09 08:26:18 +00:00
jamesread
3a8d8706a6 bugfix: remove underline from sidebar links 2024-04-04 20:37:25 +01:00
James Read
910418925a bugfix: dark mode headr (#261) 2024-04-02 22:57:54 +00:00
James Read
1b539df2aa bugfix: rounded buttons for folders (#260) 2024-04-02 22:41:59 +00:00
James Read
f5794e57ee bugfix: sidebar nav wont tab when hidden (#259) 2024-04-02 22:40:46 +00:00
James Read
7dd1d0a7fc Markup optimisations (#258)
* feature: Cleanup markup for accessibility

* feature: Cleanup markup for accessibility

* feature: Cleanup markup for accessibility

* feature: Cleanup markup for accessibility

* bugfix: Broken test

* feature: Cleanup markup for accessibility
2024-04-02 21:58:23 +00:00
James Read
ce670cf58c Bug ignored signals (#257)
* bugfix: Signals, like SIGKILL etc were ignored. Now we capture them and add the err to stderr

* bugfix: Signals, like SIGKILL etc were ignored. Now we capture them and add the err to stderr
2024-04-01 20:30:34 +00:00
295 changed files with 33904 additions and 18678 deletions

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env python3
import sys
commitmsg = ""
with open('.git/COMMIT_EDITMSG', mode='r') as f:
commitmsg = f.readline().strip()
print("Commit message is: " + commitmsg)
ALLOWED_COMMIT_TYPES = [
"cicd",
"test",
"refactor",
"depbump",
"typo",
"fmt",
"doc",
"bugfix",
"security",
"feature",
]
for allowedType in ALLOWED_COMMIT_TYPES:
if commitmsg.startswith(allowedType + ":"):
print("Allowing commit type: ", allowedType)
sys.exit(0)
print("Commit message should start with commit type. One of: ", ", ".join(ALLOWED_COMMIT_TYPES))
sys.exit(1)

View File

@@ -2,7 +2,9 @@
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
type: bug
labels:
- "waiting-on-developer"
assignees: ''
---

View File

@@ -2,7 +2,9 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
type: feature
labels:
- "waiting-on-developer"
assignees: ''
---

View File

@@ -2,7 +2,9 @@
name: Support request
about: Need some help? Got an error message?
title: ""
labels: support
type: support
labels:
- "waiting-on-developer"
assignees: ''
---

View File

@@ -13,10 +13,14 @@ Helpful information to understand the project can be found here: [CONTRIBUTING](
# Checklist
Please put a X in the boxes as evidence of reading through the checklist.
- [ ] I have read the [CONTRIBUTORS](CONTRIBUTORS.adoc) guide
- [ ] I considered the "3 line" suggestion.
- [ ] I followed the "1 logical change" rule.
- [ ] I have forked the project, and raised this PR on a feature branch.
- [ ] `make githooks` has been run, and my git commit message was accepted by the git hook.
- [ ] `make daemon-compile` runs without any issues.
- [ ] `make daemon-codestyle` runs without any issues.
- [ ] `make daemon-unittests` runs without any issues.
- [ ] `make webui-codestyle` runs without any issues.
- [ ] I ran the `pre-commit` hooks, and my commit message was validated.
- [ ] `make -wC service compile` runs without any issues.
- [ ] `make -wC service codestyle` runs without any issues.
- [ ] `make -wC service unittests` runs without any issues.
- [ ] `make -wC webui codestyle` runs without any issues.
- [ ] `make -w it` runs without any issues.
- [ ] I understand and accept the [AGPL-3.0 license](LICENSE) and [code of conduct](CODE_OF_CONDUCT.md), and my contributions fall under these.

82
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
version: 2
updates:
# npm updates for frontend - targeting "next" branch
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# npm updates for frontend - targeting "release/2k" branch
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# npm updates for integration-tests - targeting "next" branch
- package-ecosystem: "npm"
directory: "/integration-tests"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# npm updates for integration-tests - targeting "release/2k" branch
- package-ecosystem: "npm"
directory: "/integration-tests"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# Go modules updates for service - targeting "next" branch
- package-ecosystem: "gomod"
directory: "/service"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# Go modules updates for service - targeting "release/2k" branch
- package-ecosystem: "gomod"
directory: "/service"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# Go modules updates for lang - targeting "next" branch
- package-ecosystem: "gomod"
directory: "/lang"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# Go modules updates for lang - targeting "release/2k" branch
- package-ecosystem: "gomod"
directory: "/lang"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10
# Docker updates - targeting "next" branch
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "next"
open-pull-requests-limit: 10
# Docker updates - targeting "release/2k" branch
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "release/2k"
open-pull-requests-limit: 10

View File

@@ -1,13 +1,18 @@
---
name: "Build Tag"
name: "Build & Release pipeline"
on:
pull_request:
workflow_dispatch:
push:
tags:
- '*'
branches:
- main
- next
jobs:
build-tag:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -26,52 +31,76 @@ jobs:
uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: webui.dev/package-lock.json
cache-dependency-path: frontend/package-lock.json
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '^1.18.0'
go-version-file: 'service/go.mod'
cache: true
cache-dependency-path: 'service/go.mod'
- name: Print go version
run: go version
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_KEY }}
- name: Login to ghcr
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.CONTAINER_TOKEN }}
- name: grpc
run: make -w grpc
- name: get date
run: |
echo "DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: make webui
run: make -w webui-dist
- name: goreleaser
uses: goreleaser/goreleaser-action@v5
- name: unit tests
run: make -w service-unittests
- name: build service
run: make -w service
- name: integration tests
run: cd integration-tests && make -w
- name: Archive integration tests
uses: actions/upload-artifact@v4.3.1
if: always()
with:
distribution: goreleaser
version: latest
args: release --clean --parallelism 1
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
path: |
integration-tests
!integration-tests/node_modules
- name: Install goreleaser
uses: goreleaser/goreleaser-action@v6
with:
install-only: true
- name: release
if: github.ref_type != 'tag'
uses: cycjimmy/semantic-release-action@v4
with:
extra_plugins: |
@semantic-release/commit-analyzer
@semantic-release/exec
@semantic-release/git
env:
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
- name: Archive binaries
uses: actions/upload-artifact@v4.3.1
with:
name: "OliveTin-${{ github.ref_name }}"
name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}"
path: dist/OliveTin*.*
- name: Archive integration tests
uses: actions/upload-artifact@v4.3.1
with:
name: integration-tests
path: |
integration-tests
!integration-tests/node_modules

34
.github/workflows/build-buf.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Buf CI
on:
workflow_dispatch:
push:
paths:
- 'proto/**'
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
pull-requests: write
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'service/go.mod'
cache: true
cache-dependency-path: 'service/go.mod'
- uses: bufbuild/buf-action@v1.1.0
with:
token: ${{ secrets.BUF_TOKEN }}
# Change setup_only to true if you only want to set up the Action and not execute other commands.
# Otherwise, you can delete this line--the default is false.
setup_only: false
# Optional GitHub token for API requests. Ensures requests aren't rate limited.
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,74 +0,0 @@
---
name: "Build Snapshot"
on:
- push
- workflow_dispatch
jobs:
build-snapshot:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:latest
platforms: arm64,arm
- name: Setup node
uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: webui.dev/package-lock.json
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '^1.18.0'
cache: true
- name: grpc
run: make -w grpc
- name: make daemon
run: make -w daemon-compile-x64-lin
- name: make webui
run: make -w webui-dist
- name: unit tests
run: make -w daemon-unittests
- name: integration tests
run: cd integration-tests && make -w
- name: goreleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --snapshot --clean --parallelism 1 --skip=docker
- name: get date
run: |
echo "DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
- name: Archive binaries
uses: actions/upload-artifact@v4.3.1
with:
name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}"
path: dist/OliveTin*.*
- name: Archive integration tests
uses: actions/upload-artifact@v4.3.1
with:
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
path: |
integration-tests
!integration-tests/node_modules

View File

@@ -44,6 +44,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'service/go.mod'
cache: true
cache-dependency-path: 'service/go.mod'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3

View File

@@ -21,14 +21,15 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '^1.18.0'
go-version-file: 'service/go.mod'
cache: true
cache-dependency-path: 'service/go.mod'
- name: deps
run: make -w grpc
- name: Print go version
run: go version
- name: daemon
run: make -w daemon-codestyle
- name: service
run: make -wC service codestyle
- name: webui
run: make -w webui-codestyle
- name: frontend
run: make -wC frontend codestyle

View File

@@ -16,19 +16,19 @@ on:
jobs:
lint:
name: DevSkim
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: devskim-results.sarif

View File

@@ -0,0 +1,73 @@
---
name: Issue Responsibility
on:
issue_comment:
types: [created]
jobs:
update-responsibility-labels:
runs-on: ubuntu-latest
steps:
- name: Update responsibility labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const commentAuthor = context.payload.comment.user.login;
const issueNumber = context.payload.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const skipAction = context.payload.comment.body.includes("/skip-responsibility");
if (skipAction) {
core.info("Skipping responsibility label update");
return;
}
const developers = ["jamesread"]
const commenterIsDeveloper = developers.includes(commentAuthor);
const commenterIsUser = !commenterIsDeveloper;
const issueLabels = context.payload.issue.labels.map(label => label.name);
if (issueLabels.includes("waiting-on-developer")) {
if (commenterIsDeveloper) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "waiting-on-developer",
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["waiting-on-requestor"],
});
core.info(`Switched responsibility to user for issue #${issueNumber}`);
}
}
if (issueLabels.includes("waiting-on-requestor")) {
if (commenterIsUser) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: "waiting-on-requestor",
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: ["waiting-on-developer"],
});
core.info(`Switched responsibility to developer for issue #${issueNumber}`);
}
}

22
.gitignore vendored
View File

@@ -1,14 +1,22 @@
**/*.swp
**/*.swo
gen/
/OliveTin
/OliveTin.armhf
/OliveTin.exe
reports
service/OliveTin
service/OliveTin.armhf
service/OliveTin.exe
service/reports
releases/
dist/
installation-id.txt
tmp/
frontend/dist/
frontend/node_modules
custom-frontend
integration-tests/screenshots/
.vscode/
webui/
webui.dev/node_modules
webui.dev/.parcel-cache
server.log
OliveTin
integration-tests/configs/authRequireGuestsToLogin/sessions.yaml
webui
webui.dev
sessions.yaml

View File

@@ -1,28 +1,32 @@
project_name: OliveTin
version: 2
before:
hooks:
- go mod download
- make service-prep
builds:
- env:
- CGO_ENABLED=0
binary: OliveTin
main: main.go
dir: service
goos:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- arm64
- arm
- riscv64
goarm:
- 5 # For old RPIs
- 6
- 7
main: cmd/OliveTin/main.go
ignore:
- goos: darwin
goarch: arm # Mac does not work on [32bit] arm
@@ -39,7 +43,7 @@ builds:
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Branch }}-{{ .ShortCommit }}"
version_template: "{{ .Branch }}-{{ .ShortCommit }}"
changelog:
sort: asc
groups:
@@ -47,10 +51,10 @@ changelog:
regexp: '^.*?security(\([[:word:]]+\))??!?:.+$'
order: 0
- title: 'Features'
regexp: '^.*?feature(\([[:word:]]+\))??!?:.+$'
regexp: '^.*?feat.*?(\([[:word:]]+\))??!?:.+$'
order: 1
- title: 'Bug fixes'
regexp: '^.*?bugfix(\([[:word:]]+\))??!?:.+$'
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 2
- title: Others
order: 999
@@ -59,28 +63,25 @@ changelog:
- '^docs:'
- '^test:'
- '^cicd:'
- '^chore:'
- '^release:'
- '^refactor:'
- '^Merge branch'
archives:
-
format: tar.gz
- formats: tar.gz
files:
- config.yaml
- LICENSE
- README.md
- Dockerfile
- webui
- OliveTin.service
- ./var/
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
wrap_in_directory: true
format_overrides:
- goos: windows
format: zip
formats: zip
dockers:
- image_templates:
@@ -95,9 +96,10 @@ dockers:
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Tag}}"
extra_files:
- webui
- webui/
- var/entities/
- config.yaml
- var/helper-actions/
- image_templates:
- "docker.io/jamesread/olivetin:{{ .Tag }}-arm64"
@@ -111,9 +113,10 @@ dockers:
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Tag}}"
extra_files:
- webui
- webui/
- var/entities/
- config.yaml
- var/helper-actions/
docker_manifests:
- name_template: docker.io/jamesread/olivetin:{{ .Version }}
@@ -126,6 +129,12 @@ docker_manifests:
- docker.io/jamesread/olivetin:{{ .Version }}-amd64
- docker.io/jamesread/olivetin:{{ .Version }}-arm64
- name_template: docker.io/jamesread/olivetin:latest-3k
image_templates:
- docker.io/jamesread/olivetin:{{ .Version }}-amd64
- docker.io/jamesread/olivetin:{{ .Version }}-arm64
- name_template: ghcr.io/olivetin/olivetin:{{ .Version }}
image_templates:
- ghcr.io/olivetin/olivetin:{{ .Version }}-amd64
@@ -136,6 +145,12 @@ docker_manifests:
- ghcr.io/olivetin/olivetin:{{ .Version }}-amd64
- ghcr.io/olivetin/olivetin:{{ .Version }}-arm64
- name_template: ghcr.io/olivetin/olivetin:latest-3k
image_templates:
- ghcr.io/olivetin/olivetin:{{ .Version }}-amd64
- ghcr.io/olivetin/olivetin:{{ .Version }}-arm64
nfpms:
- id: default
maintainer: James Read <contact@jread.com>
@@ -151,11 +166,12 @@ nfpms:
file_name_template: '{{ .PackageName }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
contents:
- src: OliveTin.service
- src: var/systemd/OliveTin.service
dst: /etc/systemd/system/OliveTin.service
- src: webui/*
- src: webui/
dst: /var/www/olivetin/
type: tree
- src: config.yaml
dst: /etc/OliveTin/config.yaml
@@ -184,8 +200,9 @@ nfpms:
- src: var/openrc/OliveTin
dst: /etc/init.d/OliveTin
- src: webui/*
- src: webui/
dst: /var/www/olivetin/
type: tree
- src: config.yaml
dst: /etc/OliveTin/config.yaml
@@ -214,7 +231,7 @@ release:
## Useful links
- [Which download do I need?](https://docs.olivetin.app/choose-package.html)
- [Which download do I need?](https://docs.olivetin.app/install/choose_package.html)
- [Ask for help and chat with others users in the Discord community](https://discord.gg/jhYWWpNJ3v)
Thanks for your interest in OliveTin!

View File

@@ -1,10 +1,19 @@
---
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
# Alternative semantic commit checker
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.1.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [] # optional: list of Conventional Commits types to allow e.g. [feat, fix, ci, chore, test]

16
.releaserc.yaml Normal file
View File

@@ -0,0 +1,16 @@
---
branches:
- name: main
# range: '3000.x.x'
# - name: release/2k
# range: '>=2000.0.0 <3000.0.0'
plugins:
- '@semantic-release/commit-analyzer'
- '@semantic-release/git'
- - "@semantic-release/exec"
- publishCmd: |
goreleaser release --clean --timeout 60m
tagFormat: '${version}'

69
AGENTS.md Normal file
View File

@@ -0,0 +1,69 @@
## OliveTin Agent Guide
This document helps AI agents contribute effectively to OliveTin.
If you are looking for OliveTin's AI policy, you can find it in `AI.md`.
### Project Overview
- **Service (Go)**: `service/` with business logic under `service/internal/*`
- API (Connect RPC): `service/internal/api`
- Command execution: `service/internal/executor`
- HTTP frontends/proxy: `service/internal/httpservers`
- Config/types/entities: `service/internal/config`, `service/internal/entities`
- **Frontend (Vue 3)**: `frontend/` (served by the service)
- **Integration tests**: `integration-tests/`
- **Protos/Generated**: `proto/`, `service/gen/...`
### How to Run
- Run the server (dev):
- From repo root: `go run ./service`
- Unit tests (Go):
- From repo root: `cd service && make unittests`
- Integration tests (Mocha + Selenium):
- Single test: `cd integration-tests && npx --yes mocha test/general.mjs`
- All tests: `cd integration-tests && npx --yes mocha`
### Test Notes and Gotchas
- The top-level Makefile does not expose `unittests`; use `cd service && make unittests`.
- Connect RPC API must be mounted correctly; in tests, create the handler via `GetNewHandler(ex)` and serve under `/api/`.
- Frontend “ready” state: the app sets `document.body` attribute `loaded-dashboard="<name>"` when loading a dashboard. Integration helpers that test dashboard functionality wait for this before selecting elements. Certain conditions enforcing login will mean that this attribute is not set until a user is logged in.
- Modern UI uses Vue components:
- Action buttons are rendered as `.action-button button`.
- Logs and Diagnostics are Vue router links available via `/logs` and `/diagnostics`.
- Some legacy DOM ids (e.g., `contentActions`) no longer exist; prefer class-based selectors.
- Hidden UI features:
- Footer visibility is controlled by `showFooter` from Init API; tests may assert the footer is absent when config disables it.
### Coding Standards (Go)
- Avoid adding superflous comments that explain what the code is doing. Comments are only to describe business logic decisions.
- Prefer clear, descriptive names; avoid 12 letter identifiers.
- Use early returns and handle edge cases first.
- Do not swallow errors; propagate or log meaningfully.
- Match existing formatting; avoid unrelated reformatting.
- Be safe around nils in executor steps (e.g., guard `req.Binding` and `req.Binding.Action`).
### API and Execution Flow (High-level)
1. Client calls Connect RPC (e.g., `Init`, `GetDashboard`, `StartAction`).
2. API translates requests to `executor.ExecutionRequest` and calls `Executor.ExecRequest`.
3. Executor runs a chain of steps: request binding → concurrency/rate/ACL checks → arg parsing → exec → post-exec → logging/triggering.
4. Logs are stored and can be fetched via `ExecutionStatus`/`GetLogs`.
### Common Tasks
- Add/modify actions: update `config.yaml` and ensure `executor.RebuildActionMap()` is called when needed.
- Adjust dashboard rendering: see `service/internal/api/dashboards.go` and `apiActions.go`.
- Frontend behavior:
- Router: `frontend/resources/vue/router.js`
- Main shell/layout: `frontend/resources/vue/App.vue`
- Action button behavior: `frontend/resources/vue/ActionButton.vue`
### Contributing Checklist
- Review the contributing guidelines at `CONTRIBUTING.adoc`.
- Review the AI guidance in `AI.md`.
- Review the pull request template at `.github/PULL_REQUEST_TEMPLATE.md`.
### Troubleshooting
- API tests failing with content-type errors: ensure Connect handler is served under `/api/` and the client targets that base URL.
- Executor panics: check for nil `Binding/Action` and add guards in step functions.
- Integration timeouts: wait for `loaded-dashboard` and use selectors matching the Vue UI.

32
AI.md Normal file
View File

@@ -0,0 +1,32 @@
# OliveTin's AI Policy
## Runtime:
- [x] The project does not include any AI functionality at runtime.
- [x] No data, usage, or similar is sent to, or analyized by AI.
## Development - Contributions
- [x] The project **does accept** contributions that were written with AI help. **However**:
- The contribution must be attributed to a human username who takes responsibility for the code as if they wrote it themselves.
- AI often generates very unmaintainable code as it gets longer - loads of duplication, very little function re-use amd very poor at following style guides / idiomatic design. All code contributions (AI or not) are scrutinized hard for **maintainability** and **clean merging**. Please follow the CONTRIBUTORS guide.
- AI that helps with short tab completion is generally fine.
- AI that writes lots of new code across lots of files, or makes lots of superfluous changes is generally less likely to be accepted.
- Vibe coding is not a suitable way to contribute to this project.
- [x] Contributors should declare when AI has been used to help write contributions in the pull request body message.
- [x] The project uses AI as an **optional** part of the PR process (coderabbitai). Please raise any concerns about usage within the PR.
- [x] Suggestions from coderabbitai can be accepted verbaitem, but ideally it should be the PR author that uses coderabbitai as a guide, who then re-writes the contribution.
- [x] Maintainers are the only agents permitted to accept merges.
## Development - Build process
- [x] Linters, code review tools, and others which are enabled by AI are allowed, but cannot be added as part of the standard build process.
## Community
- [x] Only project admins are allowed to run bots in the community discord server (at the time of writing, the current bot, Japella, is not AI-enabled).
- [x] Support is currently not provided by AI.
## Training
- [x] You may use the OliveTin code base, documentation and repositories to train AI, but obviously usernames and personally identifiable information may not be stored.

View File

@@ -11,6 +11,14 @@ Ideas may be discussed, purely on their merits and issues. Our Code of Conduct
discussion throughout the whole process. This project respects the
link:https://www.kernel.org/doc/html/latest/process/code-of-conduct.html[Linux Kernel code of conduct].
== Suggestion: More than 3 lines - talk to someone first
If you're planning on making a change that's more than a 3 lines, please talk to someone first (raising a GitHub issue is the best way to do that). This is so that you don't waste your time on something that might not be accepted. It's also a good way to get some feedback on your idea and make sure you're on the right track.
== Rule: A PR should be one logical change
Please try to keep your pull requests small and focused. It's almost impossible to review PRs that change lots of files for lots of different reasons. If you have a big change, it's probably best to break it down into smaller, more manageable chunks, otherwise it's likely to be rejected.
== If you're not sure, ask!
Don't be afraid to ask for advice before working on a
@@ -22,17 +30,25 @@ the general direction and roadmap of this project without asking.
The preferred way to communicate is probably via Discord or GitHub issues.
=== Dev environment setup and clean build - Fedora
== Dev environment setup and clean build
```
dnf install git go protobuf-compiler make -y
# Step1: setup compile env
# - Fedora
dnf install git go protobuf-compiler make pre-commit -y
# - Windows with chocolatey
choco install git go protoc make python nodejs-lts -y && pip install pre-commit
# Step2: clone and setup repo
git clone https://github.com/OliveTin/OliveTin.git
cd OliveTin
pre-commit install
# `make grpc` will also run `make go-tools`, which installs "buf". This binary
# Step3: compile binary for current dev env (OS, ARCH)
# `make proto` will also run `make go-tools`, which installs "buf". This binary
# will be put in your GOPATH/bin/, which should be on your path. buf is used to
# generate the protobuf / grpc stubs.
make grpc
# generate the protobuf / Connect RPC stubs.
make proto
make
./OliveTin
```
@@ -42,7 +58,7 @@ make
The project layout is reasonably straightforward;
* See the `Makefile` for common targets. This project was originally created on top of Fedora, but it should be usable on Debian/your faveourite distro with minor changes (if any).
* The API is defined in protobuf+grpc - you will need to `make grpc`.
* The API is defined in protobuf+Connect RPC - you will need to `make proto`.
* The Go daemon is built from the `cmd` and `internal` directories mostly.
* The webui is just a single page application with a bit of Javascript in the `webui` directory. This can happily be hosted on another webserver.

View File

@@ -1,18 +1,34 @@
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:38-x86_64
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:42-x86_64 AS olivetin-tmputils
RUN microdnf -y install dnf-plugins-core && \
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:42-x86_64
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
LABEL org.opencontainers.image.title=OliveTin
LABEL org.opencontainers.image.title OliveTin
RUN mkdir -p /config /config/entities/ /var/www/olivetin \
&& \
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
iputils \
openssh-clients \
kubernetes-client \
shadow-utils \
apprise \
docker \
jq \
git \
&& microdnf clean all
COPY --from=olivetin-tmputils \
/usr/bin/docker \
/usr/bin/docker
COPY --from=olivetin-tmputils \
/usr/libexec/docker/cli-plugins/docker-compose \
/usr/libexec/docker/cli-plugins/docker-compose
RUN useradd --system --create-home olivetin -u 1000
EXPOSE 1337/tcp
@@ -23,6 +39,7 @@ VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
COPY var/helper-actions/* /usr/bin/
USER olivetin

View File

@@ -1,18 +1,34 @@
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:38-aarch64
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:42-aarch64 AS olivetin-tmputils
RUN microdnf -y install dnf-plugins-core && \
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:42-aarch64
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
LABEL org.opencontainers.image.title=OliveTin
LABEL org.opencontainers.image.title OliveTin
RUN mkdir -p /config /config/entities/ /var/www/olivetin \
&& \
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
iputils \
openssh-clients \
kubernetes-client \
shadow-utils \
apprise \
docker \
jq \
git \
&& microdnf clean all
COPY --from=olivetin-tmputils \
/usr/bin/docker \
/usr/bin/docker
COPY --from=olivetin-tmputils \
/usr/libexec/docker/cli-plugins/docker-compose \
/usr/libexec/docker/cli-plugins/docker-compose
RUN useradd --system --create-home olivetin -u 1000
EXPOSE 1337/tcp
@@ -23,6 +39,7 @@ VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
COPY var/helper-actions/* /usr/bin/
USER olivetin

View File

@@ -20,6 +20,7 @@ VOLUME /config
COPY OliveTin /usr/bin/OliveTin
COPY webui /var/www/olivetin/
COPY var/helper-actions/* /usr/bin/
USER olivetin

50
Jenkinsfile vendored
View File

@@ -1,50 +0,0 @@
pipeline {
agent any
options {
skipDefaultCheckout(true)
}
stages {
stage ('Pre-Build') {
steps {
cleanWs()
checkout scm
sh 'make go-tools'
}
}
stage('Compile') {
steps {
withEnv(["PATH+GO=/root/go/bin/"]) {
sh 'go env'
sh 'echo $PATH'
sh 'buf generate'
sh 'make daemon-compile'
}
}
}
stage ('Post-Compile') {
parallel {
stage('Codestyle') {
steps {
withEnv(["PATH+GO=/root/go/bin/"]) {
sh 'make daemon-codestyle'
sh 'make webui-codestyle'
}
}
}
stage('UnitTests') {
steps {
withEnv(["PATH+GO=/root/go/bin/"]) {
sh 'make daemon-unittests'
}
}
}
}
}
}
}

View File

@@ -1,46 +1,28 @@
compile: daemon-compile-x64-lin
define delete-files
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
endef
daemon-compile-armhf:
GOARCH=arm GOARM=6 go build -o OliveTin.armhf github.com/OliveTin/OliveTin/cmd/OliveTin
service:
$(MAKE) -wC service
daemon-compile-x64-lin:
GOOS=linux go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin
service-prep:
$(MAKE) -wC service prep
daemon-compile-x64-win:
GOOS=windows GOARCH=amd64 go build -o OliveTin.exe github.com/OliveTin/OliveTin/cmd/OliveTin
service-unittests:
$(MAKE) -wC service unittests
daemon-compile: daemon-compile-armhf daemon-compile-x64-lin daemon-compile-x64-win
daemon-codestyle:
go fmt ./...
go vet ./...
gocyclo -over 4 cmd internal
gocritic check ./...
daemon-unittests:
mkdir -p reports
go test ./... -coverprofile reports/unittests.out
go tool cover -html=reports/unittests.out -o reports/unittests.html
githooks:
cp -v .githooks/* .git/hooks/
it:
$(MAKE) -wC integration-tests
go-tools:
go install "github.com/bufbuild/buf/cmd/buf"
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
go install "github.com/go-critic/go-critic/cmd/gocritic"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
go install "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
go install "google.golang.org/protobuf/cmd/protoc-gen-go"
$(MAKE) -wC service go-tools
grpc: go-tools
buf generate
proto: go-tools
$(MAKE) -wC proto
dist: protoc
dist:
echo "dist noop"
protoc:
protoc --go_out=. --go-grpc_out=. --grpc-gateway_out=. -I .:/usr/include/ OliveTin.proto
podman-image:
buildah bud -t olivetin
@@ -62,17 +44,19 @@ devrun: compile
devcontainer: compile podman-image podman-container
webui-codestyle:
cd webui.dev && npm install
cd webui.dev && ./node_modules/.bin/eslint main.js js/*
cd webui.dev && ./node_modules/.bin/stylelint style.css
webui-dist:
rm -rf webui webui.dev/dist
cd webui.dev && npm install
cd webui.dev && parcel build --public-url "." && mv dist ../webui
$(MAKE) -wC frontend dist
mv frontend/dist webui
clean:
rm -rf dist OliveTin OliveTin.armhf OliveTin.exe reports gen
$(call delete-files,dist)
$(call delete-files,OliveTin)
$(call delete-files,OliveTin.armhf)
$(call delete-files,OliveTin.exe)
$(call delete-files,reports)
$(call delete-files,gen)
.PHONY: grpc
config-tool:
cd service && go run cmd/config-tool/main.go
.PHONY: proto service

View File

@@ -1,266 +0,0 @@
syntax = "proto3";
option go_package = "gen/grpc";
import "google/api/annotations.proto";
import "google/api/httpbody.proto";
message Action {
string id = 1;
string title = 2;
string icon = 3;
bool can_exec = 4;
repeated ActionArgument arguments = 5;
string popup_on_start = 6;
}
message ActionArgument {
string name = 1;
string title = 2;
string type = 3;
string default_value = 4;
repeated ActionArgumentChoice choices = 5;
string description = 6;
map<string, string> suggestions = 7;
}
message ActionArgumentChoice {
string value = 1;
string title = 2;
}
message Entity {
string title = 1;
string icon = 2;
repeated Action actions = 3;
}
message GetDashboardComponentsResponse {
string title = 1;
repeated Action actions = 2;
repeated Entity entities = 3;
repeated DashboardComponent dashboards = 4;
}
message GetDashboardComponentsRequest {}
message DashboardComponent {
string title = 1;
string type = 2;
repeated DashboardComponent contents = 3;
}
message StartActionRequest {
string action_id = 1;
repeated StartActionArgument arguments = 2;
string unique_tracking_id = 3;
}
message StartActionArgument {
string name = 1;
string value = 2;
}
message StartActionResponse {
string execution_tracking_id = 2;
}
message StartActionAndWaitRequest {
string action_id = 1;
}
message StartActionAndWaitResponse {
LogEntry log_entry = 1;
}
message StartActionByGetRequest {
string action_id = 1;
}
message StartActionByGetResponse {
string execution_tracking_id = 2;
}
message StartActionByGetAndWaitRequest {
string action_id = 1;
}
message StartActionByGetAndWaitResponse {
LogEntry log_entry = 1;
}
message GetLogsRequest{};
message LogEntry {
string datetime_started = 1;
string action_title = 2;
string stdout = 3;
string stderr = 4;
bool timed_out = 5;
int32 exit_code = 6;
string user = 7;
string user_class = 8;
string action_icon = 9;
repeated string tags = 10;
string execution_tracking_id = 11;
string datetime_finished = 12;
string action_id = 13;
bool execution_started = 14;
bool execution_finished = 15;
bool blocked = 16;
}
message GetLogsResponse {
repeated LogEntry logs = 1;
}
message ValidateArgumentTypeRequest {
string value = 1;
string type = 2;
}
message ValidateArgumentTypeResponse {
bool valid = 1;
string description = 2;
}
message WatchExecutionRequest {
string execution_tracking_id = 1;
}
message WatchExecutionUpdate {
string update = 1;
}
message ExecutionStatusRequest {
string execution_tracking_id = 1;
string action_id = 2;
}
message ExecutionStatusResponse {
LogEntry log_entry = 1;
}
message WhoAmIRequest {}
message WhoAmIResponse {
string authenticated_user = 1;
}
message SosReportRequest {}
message SosReportResponse {
string alert = 1;
}
message DumpVarsRequest {}
message DumpVarsResponse {
string alert = 1;
map<string, string> contents = 2;
}
message ActionEntityPair {
string action_title = 1;
string entity_prefix = 2;
}
message DumpPublicIdActionMapRequest {}
message DumpPublicIdActionMapResponse {
string alert = 1;
map<string, ActionEntityPair> contents = 2;
}
message GetReadyzRequest {}
message GetReadyzResponse {
string status = 1;
}
service OliveTinApiService {
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
option (google.api.http) = {
get: "/api/GetDashboardComponents"
};
}
rpc StartAction(StartActionRequest) returns (StartActionResponse) {
option (google.api.http) = {
post: "/api/StartAction"
body: "*"
};
}
rpc StartActionAndWait(StartActionAndWaitRequest) returns (StartActionAndWaitResponse) {
option (google.api.http) = {
post: "/api/StartActionAndWait"
body: "*"
};
}
rpc StartActionByGet(StartActionByGetRequest) returns (StartActionByGetResponse) {
option (google.api.http) = {
get: "/api/StartActionByGet/{action_id}"
};
}
rpc StartActionByGetAndWait(StartActionByGetAndWaitRequest) returns (StartActionByGetAndWaitResponse) {
option (google.api.http) = {
get: "/api/StartActionByGetAndWait/{action_id}"
};
}
rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
option (google.api.http) = {
post: "/api/ExecutionStatus"
body: "*"
};
}
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
option (google.api.http) = {
get: "/api/GetLogs"
};
}
rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) {
option (google.api.http) = {
post: "/api/ValidateArgumentType"
body: "*"
};
}
rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse) {
option (google.api.http) = {
get: "/api/WhoAmI"
};
}
rpc SosReport(SosReportRequest) returns (google.api.HttpBody) {
option (google.api.http) = {
get: "/api/sosreport"
};
}
rpc DumpVars(DumpVarsRequest) returns (DumpVarsResponse) {
option (google.api.http) = {
get: "/api/DumpVars"
};
}
rpc DumpPublicIdActionMap(DumpPublicIdActionMapRequest) returns (DumpPublicIdActionMapResponse) {
option (google.api.http) = {
get: "/api/DumpPublicActionMap"
};
}
rpc GetReadyz(GetReadyzRequest) returns (GetReadyzResponse) {
option (google.api.http) = {
get: "/api/readyz"
};
}
}

100
README.md
View File

@@ -1,17 +1,23 @@
# OliveTin
<div align = "center">
<img alt = "project logo" src = "https://github.com/OliveTin/OliveTin/blob/main/frontend/OliveTinLogo.png" width = "128" />
<h1>OliveTin</h1>
<img alt = "project logo" src = "https://github.com/OliveTin/OliveTin/blob/main/webui.dev/OliveTinLogo.png" align = "right" width = "160px" />
OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface.
OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface.
[![Maturity Badge](https://img.shields.io/badge/maturity-Production-brightgreen)](#none)
[![Discord](https://img.shields.io/discord/846737624960860180?label=Discord%20Server)](https://discord.gg/jhYWWpNJ3v)
[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/awesome-selfhosted/awesome-selfhosted#automation)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
[![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
[![Build Snapshot](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml/badge.svg)](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml)
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/OliveTin-screenshot-dropshadow.png" />
[OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
</div>
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-laptop_framed.png" />
<a href = "#screenshots">More screenshots below</a>
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
## Use cases
@@ -24,7 +30,7 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
**Simplify** complex commands, make them accessible and repeatable;
* eg: Expose complex commands on touchscreen tablets stuck on walls around your house. `wake-on-lan aa:bb:cc:11:22:33`
* eg: Run long running on your servers from your cell phone. `dnf update -y`
* eg: Run long-lived commands on your servers from your cell phone. `dnf update -y`
* eg: Define complex commands with lots of preset arguments, and turn a few arguments into dropdown select boxes. `docker rm {{ container }} && docker create {{ container }} && docker start {{ container }}`
[Join the community on Discord](https://discord.gg/jhYWWpNJ3v) to talk with other users about use cases, or to ask for support in getting started.
@@ -40,76 +46,42 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
* **Dark mode** - for those of you that roll that way.
* **Accessible** - passes all the accessibility checks in Firefox, and issues with accessibility are taken seriously.
* **Container** - available for quickly testing and getting it up and running, great for the selfhosted community.
* **Integrate with anything** - OliveTin just runs Linux shell commands, so theoretially you could integrate with a bunch of stuff just by using curl, ping, etc. However, writing your own shell scripts is a great way to extend OliveTin.
* **Lightweight on resources** - uses only a few MB of RAM and barely any CPU. Written in Go, with a web interface written as a modern, responsive, Single Page App that uses the REST/gRPC API.
* **Integrate with anything** - OliveTin just runs Linux shell commands, so theoretically you could integrate with a bunch of stuff just by using curl, ping, etc. However, writing your own shell scripts is a great way to extend OliveTin.
* **Lightweight on resources** - uses only a few MB of RAM and barely any CPU. Written in Go, with a web interface written as a modern, responsive, Single Page App that uses the REST/Connect RPC API.
* **Good amount of unit tests and style checks** - helps potential contributors be consistent, and helps with maintainability.
## Screenshots
Desktop web browser;
![Desktop screenshot](media/screenshotDesktop.png)
<p align = "center">
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-laptop_framed.png" />
</p>
Desktop web browser (dark mode);
![Desktop screenshot](media/screenshotDesktopDark.png)
<p align = "center">
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-darkop_framed.png" />
</p>
Mobile screen size (responsive layout);
![Mobile screenshot](media/screenshotMobile.png)
<p align = "center">
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-phone_framed.png" style = "width: 700px;" />
</p>
## No-Nonsense Software Principles
OliveTin follows these principles:
* **Open Source & Free Software**: following the [Open Source Definition](https://opensource.org/osd) and the [Free Software Definition](https://www.gnu.org/philosophy/free-sw.html). All code and assets are available under the [AGPL-3.0 License](LICENSE).
* **Independent**: No company owns the code or is responsible for the projects' governance.
* **Inclusive**: No "core", "pro", "premium" or "enterprise" version. The only version is the one you can download and run, and it has all the features.
* **Invisible**: No usage tracking, no user tracking, no ads, and no telemetry.
* **Internal**: No internet connection required for any functionality.
## Documentation
All documentation can be found at http://docs.olivetin.app . This includes installation and usage guide, etc.
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
### Quickstart reference for `config.yaml`
This is a quick example of `config.yaml` - but again, lots of documentation for how to write your `config.yaml` can be found at [the documentation site.](https://docs.olivetin.app)
* (Recommended) [Linux package install (.rpm/.deb)](https://docs.olivetin.app/install-linuxpackage.html) install instructions
* [Container (podman/docker)](https://docs.olivetin.app/install-container.html) install instructions
* [Docker compose](https://docs.olivetin.app/install-compose.html) install instructions
* [Helm on Kubernetes](https://docs.olivetin.app/install-helm.html) install instructions
* [Kubernetes (manual)](https://docs.olivetin.app/install-k8s.html) install instructions
* [.tar.gz (manual)](https://docs.olivetin.app/install-targz.html) install instructions
Put this `config.yaml` in `/etc/OliveTin/` if you're running a standard service, or mount it at `/config` if running in a container.
```yaml
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
logLevel: "INFO"
# Actions (buttons) to show up on the WebUI:
actions:
# Docs: https://docs.olivetin.app/action-container-control.html
- title: Restart Plex
icon: restart
shell: docker restart plex
# This will send 1 ping
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping host
shell: ping {{ host }} -c {{ count }}
icon: ping
arguments:
- name: host
title: host
type: ascii_identifier
default: example.com
- name: count
title: Count
type: int
default: 1
# Restart http on host "webserver1"
# Docs: https://docs.olivetin.app/action-ssh.html
- title: restart httpd
icon: restart
shell: ssh root@webserver1 'service httpd restart'
```
A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/config.yaml).
You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **MacOS** and other platforms, too!

View File

@@ -1,19 +0,0 @@
version: v1
plugins:
- name: go
out: gen/grpc/
opt: paths=source_relative
- name: go-grpc
out: gen/grpc/
opt: paths=source_relative,require_unimplemented_servers=false
- name: grpc-gateway
out: gen/grpc/
opt: paths=source_relative
# - name: swagger
# out: reports/swagger
# - name: openapiv2
# out: reports/openapiv2

View File

@@ -1,7 +0,0 @@
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: e9fcfb66f77242e5b8fd4564d7a01033

View File

@@ -1,166 +0,0 @@
package main
import (
"flag"
log "github.com/sirupsen/logrus"
"github.com/OliveTin/OliveTin/internal/entityfiles"
"github.com/OliveTin/OliveTin/internal/executor"
grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
"github.com/OliveTin/OliveTin/internal/installationinfo"
"github.com/OliveTin/OliveTin/internal/oncalendarfile"
"github.com/OliveTin/OliveTin/internal/oncron"
"github.com/OliveTin/OliveTin/internal/onfileindir"
"github.com/OliveTin/OliveTin/internal/onstartup"
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
"github.com/OliveTin/OliveTin/internal/websocket"
"github.com/OliveTin/OliveTin/internal/httpservers"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"os"
"path"
)
var (
cfg *config.Config
version = "dev"
commit = "nocommit"
date = "nodate"
)
func init() {
initLog()
initViperConfig(initCliFlags())
initCheckEnvironment()
initInstallationInfo()
log.Info("OliveTin initialization complete")
}
func initLog() {
log.SetFormatter(&log.TextFormatter{
ForceQuote: true,
DisableTimestamp: true,
})
// Use debug this early on to catch details about startup errors. The
// default config will raise the log level later, if not set.
log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issue
}
func initCliFlags() string {
var configDir string
flag.StringVar(&configDir, "configdir", ".", "Config directory path")
var printVersion bool
flag.BoolVar(&printVersion, "version", false, "Prints the version number and exits")
flag.Parse()
// This log message should be the first log message OliveTin prints.
if printVersion {
logStartupMessage("OliveTin is just printing the startup message")
os.Exit(1)
} else {
logStartupMessage("OliveTin initializing")
}
log.WithFields(log.Fields{
"value": configDir,
}).Debugf("Value of -configdir flag")
return configDir
}
func initViperConfig(configDir string) {
viper.AutomaticEnv()
viper.SetConfigName("config.yaml")
viper.SetConfigType("yaml")
viper.AddConfigPath(configDir)
viper.AddConfigPath("/config") // For containers.
viper.AddConfigPath("/etc/OliveTin/")
if err := viper.ReadInConfig(); err != nil {
log.Errorf("Config file error at startup. %s", err)
os.Exit(1)
}
cfg = config.DefaultConfig()
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
if e.Op == fsnotify.Write {
log.Info("Config file changed:", e.String())
reloadConfig()
}
})
reloadConfig()
}
func initInstallationInfo() {
installationinfo.Config = cfg
installationinfo.Build.Version = version
installationinfo.Build.Commit = commit
installationinfo.Build.Date = date
}
func logStartupMessage(message string) {
log.WithFields(log.Fields{
"version": version,
"commit": commit,
"date": date,
}).Info(message)
}
func initCheckEnvironment() {
warnIfPuidGuid()
}
func warnIfPuidGuid() {
if os.Getenv("PUID") != "" || os.Getenv("PGID") != "" {
log.Warnf("PUID or PGID seem to be set to something, but they are ignored by OliveTin. Please check https://docs.olivetin.app/no-puid-pgid.html")
}
}
func reloadConfig() {
if err := viper.UnmarshalExact(&cfg); err != nil {
log.Errorf("Config unmarshal error %+v", err)
os.Exit(1)
}
cfg.Sanitize()
}
func main() {
configDir := path.Dir(viper.ConfigFileUsed())
log.WithFields(log.Fields{
"configDir": configDir,
}).Infof("OliveTin started")
log.Debugf("Config: %+v", cfg)
executor := executor.DefaultExecutor()
executor.AddListener(websocket.ExecutionListener)
go onstartup.Execute(cfg, executor)
go oncron.Schedule(cfg, executor)
go onfileindir.WatchFilesInDirectory(cfg, executor)
go oncalendarfile.Schedule(cfg, executor)
go entityfiles.SetupEntityFileWatchers(cfg)
go updatecheck.StartUpdateChecker(version, commit, cfg, configDir)
go grpcapi.Start(cfg, executor)
httpservers.StartServers(cfg)
}

View File

@@ -5,23 +5,24 @@
# Listen on all addresses available, port 1337
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
# Choose from INFO (default), WARN and DEBUG
# Docs: https://docs.olivetin.app/advanced_configuration/logs.html
logLevel: "INFO"
# Actions are commands that are executed by OliveTin, and normally show up as
# buttons on the WebUI.
#
# Docs: https://docs.olivetin.app/create-your-first-action.html
# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
actions:
# This is the most simple action, it just runs the command and flashes the
# button to indicate status.
#
# If you are running OliveTin in a container remember to pass through the
# docker socket! https://docs.olivetin.app/action-container-control.html
# docker socket! https://docs.olivetin.app/solutions/container-control-panel/index.html
- title: Ping the Internet
shell: ping -c 1 1.1.1.1
shell: ping -c 3 1.1.1.1
icon: ping
popupOnStart: execution-dialog-stdout-only
# This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
# the command output.
@@ -39,11 +40,17 @@ actions:
# This uses `popupOnStart: execution-button` to display a mini button that
# links to the logs.
#
# You can also rate-limit actions too.
- title: date
shell: date
id: date
timeout: 6
icon: clock
popupOnStart: execution-button
maxRate:
- limit: 3
duration: 5m
# You are not limited to operating system commands, and of course you can run
# your own scripts. Here `maxConcurrent` stops the script running multiple
@@ -51,7 +58,7 @@ actions:
# runs for too long.
- title: Run backup script
shell: /opt/backupScript.sh
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ stdout }} '"
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '"
maxConcurrent: 1
timeout: 10
icon: backup
@@ -60,14 +67,16 @@ actions:
# When you want to prompt users for input, that is when you should use
# `arguments` - this presents a popup dialog and asks for argument values.
#
# Docs: https://docs.olivetin.app/action-ping.html
# Docs: https://docs.olivetin.app/action_examples/ping.html
- title: Ping host
id: ping_host
shell: ping {{ host }} -c {{ count }}
icon: ping
timeout: 100
popupOnStart: execution-dialog-stdout-only
arguments:
- name: host
title: host
title: Host
type: ascii_identifier
default: example.com
description: The host that you want to ping
@@ -75,7 +84,7 @@ actions:
- name: count
title: Count
type: int
default: 1
default: 3
description: How many times to do you want to ping?
# OliveTin can control containers - docker is just a command line app.
@@ -83,10 +92,10 @@ actions:
# However, if you are running in a container you will need to do some setup,
# see the docs below.
#
# Docs: https://docs.olivetin.app/action-container-control.html
# Docs: https://docs.olivetin.app/solutions/container-control-panel/index.html
- title: Restart Docker Container
icon: restart
shell: docker restart {{ container }}
shell: docker restart {{ .CurrentEntity }}
arguments:
- name: container
title: Container name
@@ -98,25 +107,56 @@ actions:
# There is a special `confirmation` argument to help against accidental clicks
# on "dangerous" actions.
#
# Docs: https://docs.olivetin.app/confirmation.html
# Docs: https://docs.olivetin.app/args/input_confirmation.html
- title: Delete old backups
icon: ashtonished
shell: rm -rf /opt/oldBackups/
arguments:
- type: html
title: Description
default:
The documentation for this action can be found at <a href = "example.com">example.com</a>.
- type: confirmation
title: Are you sure?!
# This is an action that runs a script included with OliveTin, that will
# download themes. You will still need to set theme "themeName" in your config.
#
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
- title: Get OliveTin Theme
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
icon: theme
arguments:
- name: themeGitRepo
title: Theme's Git Repository
description: Find new themes at https://olivetin.app/themes
type: url
- name: themeFolderName
title: Theme's Folder Name
type: ascii_identifier
# Sometimes you want to run actions on other servers - don't overcomplicate
# it, just use SSH!
# it, just use SSH! OliveTin includes a helper to make this easier, which is
# entirely optional. You can also setup SSH manually.
#
# Docs: https://docs.olivetin.app/action-ssh.html
# Docs: https://docs.olivetin.app/action-service.html
# Docs: https://docs.olivetin.app/action_examples/ssh-easy.html
# Docs: https://docs.olivetin.app/action_examples/ssh-manual.html
- title: "Setup easy SSH"
icon: ssh
shell: olivetin-setup-easy-ssh
popupOnStart: execution-dialog
# Here's how to use SSH with the "easy" config, to restart a service on
# another server.
#
# Docs: https://docs.olivetin.app/action_examples/ssh-easy.html
# Docs: https://docs.olivetin.app/action_examples/systemd_service.html
- title: Restart httpd on server1
id: restart_httpd
icon: restart
timeout: 1
shell: ssh root@server1 'service httpd restart'
shell: ssh -F /config/ssh/easy.cfg root@server1 'service httpd restart'
# Lots of people use OliveTin to build web interfaces for their electronics
# projects. It's best to install OliveTin as a native package (eg, .deb), and
@@ -129,19 +169,19 @@ actions:
# can also just specify any HTML, this includes any unicode character,
# or a <img = "..." /> link to a custom icon.
#
# Docs: https://docs.olivetin.app/icons.html
# Docs: https://docs.olivetin.app/action_customization/icons.html
#
# Lots of people use OliveTin to easily execute ansible-playbooks. You
# probably want a much longer timeout as well (so that ansible completes).
#
# Docs: https://docs.olivetin.app/ansible-playbook.html
# Docs: https://docs.olivetin.app/action_examples/ansible.html
- title: "Run Automation Playbook"
icon: '&#129302;'
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
timeout: 120
# The following actions are "dummy" actions, used in a Dashboard. As long as
# you have these referenced in a dashboard, they will not who up in the
# you have these referenced in a dashboard, they will not show up in the
# `actions` view.
- title: Ping hypervisor1
shell: echo "hypervisor1 online"
@@ -149,29 +189,41 @@ actions:
- title: Ping hypervisor2
shell: echo "hypervisor2 online"
- title: Ping hypervisor3
shell: echo "hypervisor3 online"
- title: Ping hypervisor4
shell: echo "hypervisor4 online"
- title: "{{ server.name }} Wake on Lan"
shell: echo "Sending Wake on LAN to {{ server.hostname }}"
icon: <iconify-icon icon="carbon:awake"></iconify-icon>
entity: server
- title: "{{ server.name }} Power Off"
shell: "echo 'Power Off Server: {{ server.hostname }}'"
icon: <iconify-icon icon="carbon:flash-off"></iconify-icon>
entity: server
- title: "{{ server.name }} Print server name"
shell: 'echo "Server name: {{ server.name }}"'
entity: server
- title: Ping All Servers
shell: "echo 'Ping all servers'"
icon: ping
- title: Start {{ container.Names }}
- title: Start {{ .CurrentEntity.Names }}
icon: box
shell: docker start {{ container.Names }}
shell: docker start {{ .CurrentEntity.Names }}
entity: container
trigger: Update container entity file
triggers: ["Update container entity file"]
- title: Stop {{ container.Names }}
- title: Stop {{ .CurrentEntity.Names }}
icon: box
shell: docker stop {{ container.Names }}
shell: docker stop {{ .CurrentEntity.Names }}
entity: container
trigger: Update container entity file
triggers: ["Update container entity file"]
# Lastly, you can hide actions from the web UI, this is useful for creating
# background helpers that execute only on startup or a cron, for updating
@@ -201,13 +253,13 @@ actions:
# in your configuration as variables. For example; `container.status`,
# or `vm.hostname`.
#
# Docs: http://docs.olivetin.app/entities.html
# Docs: https://docs.olivetin.app/entities/intro.html
entities:
# YAML files are the default expected format, so you can use .yml or .yaml,
# or even .txt, as long as the file contains valid a valid yaml LIST, then it
# will load properly.
#
# Docs: https://docs.olivetin.app/entities.html
# Docs: https://docs.olivetin.app/entities/intro.html
- file: entities/servers.yaml
name: server
@@ -219,26 +271,36 @@ entities:
#
# The only way to properly use entities, are to use them with a `fieldset` on
# a dashboard.
#
# Docs: https://docs.olivetin.app/dashboards/intro.html
dashboards:
# Top level items are dashboards.
- title: My Servers
contents:
# The contents of a dashboard will try to look for an action with a
# matching title IF the `contents: ` property is empty.
- title: Ping All Servers
# If you create an item with some "contents:", OliveTin will show that as
# directory.
- title: Hypervisors
- title: All Servers
type: fieldset
contents:
- title: Ping hypervisor1
- title: Ping hypervisor2
# The contents of a dashboard will try to look for an action with a
# matching title IF the `contents: ` property is empty.
- title: Ping All Servers
# If you create an item with some "contents:", OliveTin will show that as
# directory.
- title: Hypervisors
contents:
- title: Ping hypervisor1
- title: Ping hypervisor2
- title: More hypervisors
type: directory
contents:
- title: Ping hypervisor3
- title: Ping hypervisor4
# If you specify `type: fieldset` and some `contents`, it will show your
# actions grouped together without a folder.
- type: fieldset
entity: server
title: 'Server: {{ server.hostname }}'
title: 'Server: {{ .CurrentEntity.hostname }}'
contents:
# By default OliveTin will look for an action with a matching title
# and put it on the dashboard.
@@ -254,10 +316,15 @@ dashboards:
- title: '{{ server.name }} Wake on Lan'
- title: '{{ server.name }} Power Off'
- title: More Options
type: directory
contents:
- title: '{{ server.name }} Print server name'
# This is the second dashboard.
- title: My Containers
contents:
- title: 'Container {{ container.Names }} ({{ container.Image }})'
- title: 'Container {{ .CurrentEntity.Names }} ({{ .CurrentEntity.Image }})'
entity: container
type: fieldset
contents:
@@ -265,5 +332,57 @@ dashboards:
title: |
{{ container.RunningFor }} <br /><br /><strong>{{ container.State }}</strong>
- title: 'Start {{ container.Names }}'
- title: 'Stop {{ container.Names }}'
- title: 'Start {{ .CurrentEntity.Names }}'
- title: 'Stop {{ .CurrentEntity.Names }}'
# Security - Authentication
# This setting effectively enables or disables guests.
# If set to "true", then users will have to login to do anything.
authRequireGuestsToLogin: false
# This form of auth is the simplest to setup - just define users and passwords
# in the config. OliveTin also supports header-based auth, OAuth2,
# and JWT authentication which are documented separately.
#
# Docs: https://docs.olivetin.app/security/local.html
#
# How to get a hashed password:
# Docs: https://docs.olivetin.app/security/local.html#_get_a_argon2id_hashed_password
authLocalUsers:
enabled: true
# users:
# - username: alice
# usergroup: admins
# password: "$argon2id$v=19$m=65536,t=4,p=2$puyxA0s555TSFx7hnFLCXA$PyhLGpZtvpMMvc2DgMWkM8OJMKO55euwV5gm//1iwx4"
# Security - Access Control
# Policies affect the whole app (eg: ability to view the log list).
# Docs: https://docs.olivetin.app/security/acl.html
defaultPolicy:
showDiagnostics: true
showLogList: true
# Permissions affect actions (eg: ability to view a specific log).
# Docs: https://docs.olivetin.app/security/acl.html
defaultPermissions:
view: true
exec: true
logs: true
# OliveTin uses access control lists to match up policy and permissions to users.
# Docs: https://docs.olivetin.app/security/acl.html
accessControlLists:
- name: admin_acl
matchUsergroups: ["admins"]
policy:
showDiagnostics: true
permissions:
view: true
exec: true
logs: true
# OliveTin contains many more configuration options not in this default config.
# Check out docs.olivetin.app for a setting if you feel like you're missing something.

View File

@@ -12,4 +12,4 @@
},
"rules": {
}
}
}

1
frontend/.npmrc Normal file
View File

@@ -0,0 +1 @@
fund=false

21
frontend/Makefile Normal file
View File

@@ -0,0 +1,21 @@
define delete-files
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
endef
codestyle:
npm install
npx eslint --fix main.js js/*
npx stylelint style.css
clean:
$(call delete-files,dist)
deps:
npm install
build:
npx vite build
dist: deps clean build
.PHONY: codestyle

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

69
frontend/index.html Normal file
View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset = "UTF-8" />
<meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
<meta name = "description" content = "Give safe and simple access to predefined shell commands from a web interface." />
<title>OliveTin</title>
<link rel = "stylesheet" type = "text/css" href = "/theme.css" />
<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
<link rel = "apple-touch-icon" sizes="57x57" href="OliveTinLogo-57px.png" />
<link rel = "apple-touch-icon" sizes="120x120" href="OliveTinLogo-120px.png" />
<link rel = "apple-touch-icon" sizes="180x180" href="OliveTinLogo-180px.png" />
<base href = "/" />
</head>
<body>
<slot id = "app" />
<noscript>
<div class = "error">Sorry, JavaScript is required to use OliveTin.</div>
</noscript>
<dialog title = "Big Error Message" id = "big-error" class = "error padded-content">
</dialog>
<script type = "text/javascript">
const bigErrorDialog = document.getElementById('big-error')
/**
This is the bootstrap code, which relies on very simple, old javascript
to at least display a helpful error message if we can't use OliveTin.
*/
window.showBigError = function (type, friendlyType, message, isFatal) {
console.error('Error ' + type + ': ', message)
return;
bigErrorDialog.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/troubleshooting/err-" + type + ".html' target = 'blank'/>" + type + " error in OliveTin Documentation</a></p>"
if (isFatal) {
bigErrorDialog.innerHTML += '<p>You will need to refresh your browser to clear this message.</p>'
} else {
bigErrorDialog.innerHTML += '<p>This error message will go away automatically if the problem is solved.</p>'
}
bigErrorDialog.showModal()
console.error('Error ' + type + ': ', message)
}
window.clearBigErrors = function () {
bigErrorDialog.close()
}
</script>
<script type = "text/javascript" nomodule>
showBigError("js-modules-not-supported", "Sorry, your browser does not support JavaScript modules.", null)
</script>
<script type = "module" src = "main.js"></script>
</body>
</html>

24
frontend/js/Mutex.js Normal file
View File

@@ -0,0 +1,24 @@
export class Mutex {
constructor () {
this._locked = false
this._waiting = []
}
lock () {
const unlock = () => {
const next = this._waiting.shift()
if (next) {
next(unlock)
} else {
this._locked = false
}
}
if (this._locked) {
return new Promise(resolve => this._waiting.push(resolve)).then(() => unlock)
} else {
this._locked = true
return Promise.resolve(unlock)
}
}
}

View File

@@ -0,0 +1,77 @@
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { Mutex } from './Mutex.js'
/**
* xterm.js based terminal output for the execution dialog.
*
* the xterm.js methods for write(), reset() and clear() appear to be async,
* but they do not return a Promise and instead use a callback. When calling
* these methods in quick succession, the output can get garbled due to race
* conditions.
*
* To avoid this, this class uses Mutex around those methods to ensure that
* only one write OR reset is executed at a time, is completed, and the calls
* occour in sequential order.
*/
export class OutputTerminal {
constructor (executionTrackingId) {
this.executionTrackingId = executionTrackingId
this.writeMutex = new Mutex()
this.terminal = new Terminal({
convertEol: true
})
const fitAddon = new FitAddon()
this.terminal.loadAddon(fitAddon)
this.terminal.fit = fitAddon
}
async write (out, then) {
const unlock = await this.writeMutex.lock()
try {
await new Promise(resolve => {
this.terminal.write(out, () => {
resolve()
})
})
} finally {
unlock()
if (then != null && then !== undefined) {
then()
}
}
}
async reset () {
const unlock = await this.writeMutex.lock()
try {
await new Promise(resolve => {
this.terminal.clear()
this.terminal.reset()
resolve()
})
} finally {
unlock()
}
}
fit () {
this.terminal.fit.fit()
}
open (el) {
this.terminal.open(el)
}
close () {
this.terminal.dispose()
}
resize (cols, rows) {
this.terminal.resize(cols, rows)
}
}

61
frontend/js/websocket.js Normal file
View File

@@ -0,0 +1,61 @@
import { buttonResults } from '../resources/vue/stores/buttonResults.js'
export function initWebsocket () {
window.addEventListener('EventOutputChunk', onOutputChunk)
reconnectWebsocket()
}
window.websocketAvailable = false
async function reconnectWebsocket () {
if (window.websocketAvailable) {
return
}
try {
window.websocketAvailable = true
for await (const e of window.client.eventStream()) {
handleEvent(e)
}
} catch (err) {
console.error('Websocket connection failed: ', err)
}
window.websocketAvailable = false
console.log('Reconnecting websocket...')
}
function handleEvent (msg) {
const typeName = msg.event.value.$typeName.replace('olivetin.api.v1.', '')
const j = new Event(typeName)
j.payload = msg.event.value
switch (typeName) {
case 'EventOutputChunk':
case 'EventConfigChanged':
case 'EventEntityChanged':
window.dispatchEvent(j)
break
case 'EventExecutionFinished':
case 'EventExecutionStarted':
buttonResults[msg.event.value.logEntry.executionTrackingId] = msg.event.value.logEntry
window.dispatchEvent(j)
break
default:
console.warn('Unhandled websocket message type from server: ', typeName)
window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + typeName, true)
}
}
function onOutputChunk (evt) {
const chunk = evt.payload
if (window.terminal) {
if (chunk.executionTrackingId === window.terminal.executionTrackingId) {
window.terminal.write(chunk.output)
}
}
}

123
frontend/main.js Normal file
View File

@@ -0,0 +1,123 @@
'use strict'
import 'femtocrank/style.css'
import 'femtocrank/dark.css'
import './style.css'
import 'iconify-icon'
import { createClient } from '@connectrpc/connect'
import { createConnectTransport } from '@connectrpc/connect-web'
import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
import { createApp, h } from 'vue'
import { createI18n } from 'vue-i18n'
import router from './resources/vue/router.js'
import App from './resources/vue/App.vue'
import { initWebsocket } from './js/websocket.js'
import combinedTranslations from '../lang/combined_output.json'
function getSelectedLanguage () {
const storedLanguage = localStorage.getItem('olivetin-language')
if (storedLanguage && storedLanguage !== 'auto') {
return storedLanguage
}
if (storedLanguage === 'auto') {
localStorage.removeItem('olivetin-language')
}
if (navigator.languages && navigator.languages.length > 0) {
const available = Object.keys(combinedTranslations.messages || {})
for (const candidate of navigator.languages) {
const lowerCandidate = candidate.toLowerCase()
const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
if (exact) {
return exact
}
const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
if (prefix) {
return prefix
}
}
}
return 'en'
}
async function initClient () {
const transport = createConnectTransport({
baseUrl: window.location.protocol + '//' + window.location.host + '/api/'
})
window.client = createClient(OliveTinApiService, transport)
window.initResponse = await window.client.init({})
const i18nSettings = createI18n({
legacy: false,
locale: getSelectedLanguage(),
fallbackLocale: 'en',
messages: combinedTranslations.messages,
postTranslation: (translated) => {
const params = new URLSearchParams(window.location.search)
if (params.has('debug-translations')) {
return '____'
} else {
return translated
}
}
})
return i18nSettings
}
function setupVue (i18nSettings) {
const app = createApp(App)
app.use(router)
app.use(i18nSettings)
window.i18n = i18nSettings.global
app.mount('#app')
}
function setupErrorDisplay (errorMessage) {
const ErrorApp = {
render() {
return h('section', { class: 'bad', style: 'padding: 2em; text-align: center; margin: 2em auto;' }, [
h('h2', 'OliveTin Init Failed'),
h('p', errorMessage),
h('p', 'Please check your browser console for more details.')
])
}
}
const app = createApp(ErrorApp)
app.mount('#app')
}
async function main () {
try {
const i18nSettings = await initClient()
initWebsocket()
setupVue(i18nSettings)
} catch (err) {
const errorMessage = err.message || 'Failed to initialize. Please check your configuration and try again.'
console.error('Init failed:', err)
setupErrorDisplay(errorMessage)
}
}
main()

6436
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
frontend/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "olivetin-webui",
"version": "1.0.0",
"description": "The WebUI for OliveTin",
"repository": "https://github.com/OliveTin/OliveTin",
"source": "index.html",
"devDependencies": {
"process": "^0.11.10",
"stylelint": "^16.26.1",
"stylelint-config-standard": "^39.0.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"parcelIgnore": [
"theme.css",
"OliveTinLogo.png",
"OliveTinLogo-57px.png",
"OliveTinLogo-120px.png",
"OliveTinLogo-180px.png"
],
"license": "AGPL-3.0-only",
"dependencies": {
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-web": "^2.1.1",
"@hugeicons/core-free-icons": "^2.0.0",
"@hugeicons/vue": "^1.0.3",
"@vitejs/plugin-vue": "^6.0.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"iconify-icon": "^3.0.2",
"picocrank": "^1.9.0",
"standard": "^17.1.2",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.2.6",
"vue-i18n": "^11.2.2",
"vue-router": "^4.6.3"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,312 @@
<template>
<div :id="`actionButton-${actionId}`" role="none" class="action-button">
<button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
:class="buttonClasses" @click="handleClick">
<div class="navigate-on-start-container">
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
</div>
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
<HugeiconsIcon :icon="TypeCursorIcon" />
</div>
<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background">
<HugeiconsIcon :icon="WorkoutRunIcon" />
</div>
</div>
<span class="icon" v-html="unicodeIcon"></span>
<span class="title" aria-live="polite">{{ displayTitle }}
</span>
</button>
</div>
</template>
<script setup>
import ArgumentForm from './views/ArgumentForm.vue'
import { buttonResults } from './stores/buttonResults'
import { useRouter } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue'
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons'
import { ref, watch, onMounted, inject } from 'vue'
const router = useRouter()
const navigateOnStart = ref('')
const props = defineProps({
actionData: {
type: Object,
required: true
}
})
const actionId = ref('')
const title = ref('')
const canExec = ref(true)
const popupOnStart = ref('')
// Display properties
const unicodeIcon = ref('&#x1f4a9;')
const displayTitle = ref('')
// State
const isDisabled = ref(false)
const showArgumentForm = ref(false)
// Animation classes
const buttonClasses = ref([])
// Timestamps
const updateIterationTimestamp = ref(0)
function getUnicodeIcon(icon) {
if (icon === '') {
console.log('icon not found ', icon)
return '&#x1f4a9;'
} else {
return unescape(icon)
}
}
function constructFromJson(json) {
updateIterationTimestamp.value = 0
updateFromJson(json)
actionId.value = json.bindingId
title.value = json.title
canExec.value = json.canExec
popupOnStart.value = json.popupOnStart
if (popupOnStart.value.includes('execution-dialog')) {
navigateOnStart.value = 'pop'
} else if (props.actionData.arguments.length > 0) {
navigateOnStart.value = 'arg'
}
isDisabled.value = !json.canExec
displayTitle.value = title.value
unicodeIcon.value = getUnicodeIcon(json.icon)
}
function updateFromJson(json) {
// Fields that should not be updated
// title - as the callback URL relies on it
unicodeIcon.value = getUnicodeIcon(json.icon)
}
async function handleClick() {
if (props.actionData.arguments && props.actionData.arguments.length > 0) {
router.push(`/actionBinding/${props.actionData.bindingId}/argumentForm`)
} else {
await startAction()
}
}
function getUniqueId() {
if (window.isSecureContext) {
return window.crypto.randomUUID()
} else {
return Date.now().toString()
}
}
async function startAction(actionArgs) {
buttonClasses.value = [] // Removes old animation classes
if (actionArgs === undefined) {
actionArgs = []
}
// UUIDs are create client side, so that we can setup a "execution-button"
// to track the execution before we send the request to the server.
const startActionArgs = {
bindingId: props.actionData.bindingId,
arguments: actionArgs,
uniqueTrackingId: getUniqueId()
}
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
watch(
() => buttonResults[startActionArgs.uniqueTrackingId],
(newResult, oldResult) => {
onLogEntryChanged(newResult)
}
)
try {
await window.client.startAction(startActionArgs)
} catch (err) {
console.error('Failed to start action:', err)
}
}
function onLogEntryChanged(logEntry) {
if (logEntry.executionFinished) {
onExecutionFinished(logEntry)
} else {
onExecutionStarted(logEntry)
}
}
function onExecutionStarted(logEntry) {
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
router.push(`/logs/${logEntry.executionTrackingId}`)
}
isDisabled.value = true
}
function onExecutionFinished(logEntry) {
if (logEntry.timedOut) {
renderExecutionResult('action-timeout', 'Timed out')
} else if (logEntry.blocked) {
renderExecutionResult('action-blocked', 'Blocked!')
} else if (logEntry.exitCode !== 0) {
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
} else {
const ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
renderExecutionResult('action-success', 'Success!')
}
}
function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
onExecStatusChanged()
}
function updateDom(resultCssClass, newTitle) {
if (resultCssClass == null) {
buttonClasses.value = []
} else {
buttonClasses.value = [resultCssClass]
}
displayTitle.value = newTitle
}
function onExecStatusChanged() {
isDisabled.value = false
setTimeout(() => {
updateDom(null, title.value)
}, 2000)
}
onMounted(() => {
constructFromJson(props.actionData)
})
watch(
() => props.actionData,
(newData) => {
updateFromJson(newData)
},
{ deep: true }
)
</script>
<style scoped>
.action-button {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.action-button button {
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 0 .6em #aaa;
font-size: .85em;
border-radius: .7em;
}
.action-button button:hover:not(:disabled) {
background: #f5f5f5;
border-color: #999;
}
.action-button button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-button button .icon {
font-size: 3em;
flex-grow: 1;
align-content: center;
}
.action-button button .title {
font-weight: 500;
padding: 0.2em;
}
/* Animation classes */
.action-button button.action-timeout {
background: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.action-button button.action-blocked {
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-nonzero-exit {
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-success {
background: #d4edda !important;
border-color: #c3e6cb;
color: #155724;
}
.action-button-footer {
margin-top: 0.5em;
}
.navigate-on-start-container {
position: relative;
margin-left: auto;
height: 0;
right: 0;
top: 0;
}
@media (prefers-color-scheme: dark) {
.action-button button {
background: #111;
border-color: #000;
box-shadow: 0 0 6px #000;
color: #fff;
}
.action-button button:hover:not(:disabled) {
background: #222;
border-color: #000;
box-shadow: 0 0 6px #444;
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,350 @@
<template>
<Header :title="pageTitle" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar" :sidebarEnabled="showNavigation">
<template #toolbar>
<div id="banner" v-if="bannerMessage" :style="bannerCss">
<p>{{ bannerMessage }}</p>
</div>
</template>
<template #user-info>
<div class="flex-row user-info" style="gap: .5em;">
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">{{ t('login-button') }}</router-link></span>
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
<span id="username-text">{{ username }}</span>
</router-link>
<HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" />
</div>
</template>
</Header>
<div id="layout">
<Sidebar ref="sidebar" id = "mainnav" v-if="showNavigation" />
<div id="content" initial-martial-complete="{{ hasLoaded }}">
<main title="Main content">
<router-view :key="$route.fullPath" />
</main>
<footer title="footer" v-if="showFooter">
<p>
<img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
OliveTin {{ currentVersion }}
</p>
<p>
<span>
<a href="https://docs.olivetin.app" target="_new">{{ t('docs') }}</a>
</span>
<span>
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">{{ t('raise-issue') }}</a>
</span>
<span>
<a href="#" @click.prevent="openLanguageDialog">{{ currentLanguageName }}</a>
</span>
<span>{{ t('connected') }}</span>
</p>
<p>
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
</p>
</footer>
</div>
</div>
<dialog ref="languageDialog" class="language-dialog" @click="handleDialogClick">
<div class="dialog-content" @click.stop>
<h2>{{ t('language-dialog.title') }}</h2>
<select v-model="selectedLanguage" @change="changeLanguage" class="language-select">
<option v-for="(name, code) in availableLanguages" :key="code" :value="code">
{{ code === 'auto' ? name : `${name} (${code})` }}
</option>
</select>
<p class="browser-languages">
{{ t('language-dialog.browser-languages') }}:
<span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
<span v-else>{{ t('language-dialog.not-available') }}</span>
</p>
<div class="dialog-buttons">
<button @click="closeLanguageDialog">{{ t('language-dialog.close') }}</button>
</div>
</div>
</dialog>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import Sidebar from 'picocrank/vue/components/Sidebar.vue';
import Header from 'picocrank/vue/components/Header.vue';
import { HugeiconsIcon } from '@hugeicons/vue'
import { Menu01Icon } from '@hugeicons/core-free-icons'
import { UserCircle02Icon } from '@hugeicons/core-free-icons'
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
import logoUrl from '../../OliveTinLogo.png';
import { useI18n } from 'vue-i18n';
import combinedTranslations from '../../../lang/combined_output.json';
const { t, locale } = useI18n();
const router = useRouter();
const sidebar = ref(null);
const username = ref('notset');
const isLoggedIn = ref(false);
const serverConnection = ref(true);
const currentVersion = ref('?');
const pageTitle = ref('OliveTin');
const bannerMessage = ref('');
const bannerCss = ref('');
const hasLoaded = ref(false);
const showFooter = ref(true)
const showNavigation = ref(true)
const showLogs = ref(true)
const showDiagnostics = ref(true)
const showLoginLink = ref(true)
const languageDialog = ref(null)
const browserLanguages = ref([])
const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.getItem('olivetin-language') : null
const languagePreference = ref(initialLanguagePreference || 'auto')
const selectedLanguage = ref(languagePreference.value)
// Available languages with display names
const availableLanguages = {
'auto': 'Browser Language',
'en': 'English',
'de-DE': 'Deutsch',
'es-ES': 'Español',
'it-IT': 'Italiano',
'zh-Hans-CN': '简体中文'
}
// Computed property to get current language display name
const currentLanguageName = computed(() => {
if (languagePreference.value === 'auto') {
return availableLanguages['auto']
}
return availableLanguages[languagePreference.value] || languagePreference.value
})
function normalizeBrowserLanguage() {
const available = Object.keys(combinedTranslations.messages || {})
if (navigator.languages && navigator.languages.length > 0) {
for (const candidate of navigator.languages) {
const lowerCandidate = candidate.toLowerCase()
// Try exact match (case-insensitive)
const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
if (exact) {
return exact
}
// Try prefix match (e.g., "zh-CN" -> "zh-Hans-CN")
const prefix = available.find(locale => locale.toLowerCase().startsWith(lowerCandidate.split('-')[0] + '-'))
if (prefix) {
return prefix
}
}
}
return 'en'
}
function toggleSidebar() {
if (sidebar.value && showNavigation.value) {
sidebar.value.toggle()
}
}
function updateHeaderFromInit() {
if (!window.initResponse) {
return
}
username.value = window.initResponse.authenticatedUser
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
currentVersion.value = window.initResponse.currentVersion
pageTitle.value = window.initResponse.pageTitle || 'OliveTin'
bannerMessage.value = window.initResponse.bannerMessage || ''
bannerCss.value = window.initResponse.bannerCss || ''
showFooter.value = window.initResponse.showFooter
showNavigation.value = window.initResponse.showNavigation
showLogs.value = window.initResponse.showLogList
showDiagnostics.value = window.initResponse.showDiagnostics
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
showLoginLink.value = false
}
renderSidebar()
if (window.initResponse.loginRequired) {
router.push('/login')
return
}
}
function renderSidebar() {
if (!sidebar.value) {
return
}
const rootDashboards = window.initResponse?.rootDashboards || []
if (typeof sidebar.value.clear === 'function') {
sidebar.value.clear()
}
for (const rootDashboard of rootDashboards) {
sidebar.value.addNavigationLink({
id: rootDashboard,
name: rootDashboard,
title: rootDashboard,
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
icon: DashboardSquare01Icon,
})
}
sidebar.value.addSeparator()
sidebar.value.addRouterLink('Entities', t('nav.entities'))
if (showLogs.value) {
sidebar.value.addRouterLink('Logs', t('nav.logs'))
}
if (showDiagnostics.value) {
sidebar.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
}
}
function openLanguageDialog() {
selectedLanguage.value = languagePreference.value
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
browserLanguages.value = navigator.languages
} else {
browserLanguages.value = []
}
if (languageDialog.value) {
languageDialog.value.showModal()
}
}
function closeLanguageDialog() {
if (languageDialog.value) {
languageDialog.value.close()
}
}
function changeLanguage() {
if (!window.i18n || !selectedLanguage.value) {
return
}
if (selectedLanguage.value === 'auto') {
localStorage.removeItem('olivetin-language')
languagePreference.value = 'auto'
window.i18n.locale.value = normalizeBrowserLanguage()
} else {
window.i18n.locale.value = selectedLanguage.value
localStorage.setItem('olivetin-language', selectedLanguage.value)
languagePreference.value = selectedLanguage.value
}
// Update sidebar with new translations
if (sidebar.value) {
renderSidebar()
}
closeLanguageDialog()
}
function handleDialogClick(event) {
// Close dialog when clicking on the backdrop
if (event.target === languageDialog.value) {
closeLanguageDialog()
}
}
window.updateHeaderFromInit = updateHeaderFromInit
onMounted(() => {
serverConnection.value = true;
updateHeaderFromInit()
// Initialize selected language from stored preference
selectedLanguage.value = languagePreference.value
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
browserLanguages.value = navigator.languages
}
})
</script>
<style scoped>
.user-info span {
margin-left: 1em;
}
.user-link {
text-decoration: none;
color: inherit;
}
.user-link:hover {
text-decoration: underline;
}
.language-dialog {
border: 1px solid var(--border-color, #ccc);
border-radius: 0.5rem;
padding: 0;
max-width: 400px;
width: 90%;
}
.language-dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.dialog-content {
padding: 1.5rem;
}
.dialog-content h2 {
margin-top: 0;
margin-bottom: 1rem;
}
.language-select {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
font-size: 1rem;
border: 1px solid var(--border-color, #ccc);
border-radius: 0.25rem;
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.dialog-buttons button {
padding: 0.5rem 1rem;
cursor: pointer;
}
.browser-languages {
font-size: 0.875rem;
color: var(--fg2, #555);
margin-bottom: 1rem;
}
</style>

View File

@@ -0,0 +1,298 @@
<template>
<section v-if="!dashboard && !initError" style = "text-align: center; padding: 2em;">
<HugeiconsIcon :icon="Loading03Icon" width="3em" height="3em" style="animation: spin 1s linear infinite;" />
<p>Loading dashboard...</p>
<p style="color: var(--fg2);">{{ loadingTime }}s</p>
</section>
<section v-if="initError" style="text-align: center; padding: 2em;" class = "bad">
<h2 style="color: var(--error);">Initialization Failed</h2>
<p>{{ initError }}</p>
<p style="color: var(--fg2);">Please check your configuration and try again.</p>
</section>
<template v-else-if="dashboard">
<section v-if="dashboard.contents.length == 0">
<div class="back-button-container" v-if="isDirectory">
<button @click="goBack" class="back-button">
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
<span>Back</span>
</button>
</div>
<h2>{{ dashboard.title }}</h2>
<p style = "text-align: center" class = "padding">This dashboard is empty.</p>
</section>
<section class="transparent" v-else>
<div class="back-button-container" v-if="isDirectory">
<button @click="goBack" class="back-button">
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
<span>Back</span>
</button>
</div>
<div class = "dashboard-row" v-for="component in dashboard.contents" :key="component.title">
<h2 v-if = "dashboard.title != 'Default'">
<router-link
v-if="component.entityType && component.entityKey"
:to="{
name: 'EntityDetails',
params: {
entityType: component.entityType,
entityKey: component.entityKey
}
}"
class="entity-link">
{{ component.title }}
</router-link>
<span v-else>{{ component.title }}</span>
</h2>
<fieldset>
<template v-for="subcomponent in component.contents">
<DashboardComponent :component="subcomponent" />
</template>
</fieldset>
</div>
</section>
</template>
</template>
<script setup>
import DashboardComponent from './components/DashboardComponent.vue'
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue'
import { Loading03Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
const props = defineProps({
title: {
type: String,
required: false
},
entityType: {
type: String,
required: false
},
entityKey: {
type: String,
required: false
}
})
const router = useRouter()
const dashboard = ref(null)
const loadingTime = ref(0)
const initError = ref(null)
let loadingTimer = null
let checkInitInterval = null
const isDirectory = computed(() => {
if (!dashboard.value || !window.initResponse) {
return false
}
const rootDashboards = window.initResponse.rootDashboards || []
return !rootDashboards.includes(dashboard.value.title) && dashboard.value.title !== 'Actions'
})
function goBack() {
if (window.history.length > 1) {
router.back()
} else {
const rootDashboards = window.initResponse?.rootDashboards || []
if (rootDashboards.length > 0) {
router.push({ name: 'Dashboard', params: { title: rootDashboards[0] } })
} else {
router.push({ name: 'Actions' })
}
}
}
async function getDashboard() {
let title = props.title
// If no specific title was provided or it's the placeholder 'default',
// prefer the first configured root dashboard (e.g., "Test").
if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
title = window.initResponse.rootDashboards[0]
}
try {
const request = {
title: title,
}
if (props.entityType && props.entityKey) {
request.entityType = props.entityType
request.entityKey = props.entityKey
}
const ret = await window.client.getDashboard(request)
if (!ret || !ret.dashboard) {
throw new Error('No dashboard found')
}
dashboard.value = ret.dashboard
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = ret.dashboard.title + ' - ' + pageTitle
// Clear any previous init error since we successfully loaded
initError.value = null
// Stop the loading timer once dashboard is loaded
if (loadingTimer) {
clearInterval(loadingTimer)
loadingTimer = null
}
// Set attribute to indicate dashboard is loaded successfully
document.body.setAttribute('loaded-dashboard', title || 'default')
} catch (e) {
// On error, provide a safe fallback state
console.error('Failed to load dashboard', e)
dashboard.value = { title: title || 'Default', contents: [] }
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = 'Error - ' + pageTitle
// Stop the loading timer on error
if (loadingTimer) {
clearInterval(loadingTimer)
loadingTimer = null
}
// Set attribute even on error so tests can proceed
document.body.setAttribute('loaded-dashboard', title || 'error')
}
}
function waitForInitAndLoadDashboard() {
// Start the loading timer
loadingTime.value = 0
loadingTimer = setInterval(() => {
loadingTime.value++
}, 1000)
// Check if init has completed successfully
if (window.initResponse) {
getDashboard()
} else if (window.initError) {
// Init failed, show error immediately
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
// Stop the loading timer since we're showing an error
if (loadingTimer) {
clearInterval(loadingTimer)
loadingTimer = null
}
} else {
// Init hasn't completed yet, poll for completion
checkInitInterval = setInterval(() => {
if (window.initResponse) {
clearInterval(checkInitInterval)
checkInitInterval = null
getDashboard()
} else if (window.initError) {
clearInterval(checkInitInterval)
checkInitInterval = null
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
// Stop the loading timer since we're showing an error
if (loadingTimer) {
clearInterval(loadingTimer)
loadingTimer = null
}
}
}, 100) // Check every 100ms
}
}
onMounted(() => {
waitForInitAndLoadDashboard()
})
onUnmounted(() => {
// Clean up the timers when component is unmounted
if (loadingTimer) {
clearInterval(loadingTimer)
loadingTimer = null
}
if (checkInitInterval) {
clearInterval(checkInitInterval)
checkInitInterval = null
}
})
</script>
<style scoped>
h2 {
font-weight: bold;
text-align: center;
padding: 1em;
padding-top: 1.5em;
grid-column: 1 / -1;
}
h2 .entity-link {
color: inherit;
text-decoration: none;
transition: opacity 0.2s;
}
h2 .entity-link:hover {
opacity: 0.7;
text-decoration: underline;
}
fieldset {
display: grid;
grid-template-columns: repeat(auto-fit, 180px);
grid-auto-rows: 1fr;
justify-content: center;
place-items: stretch;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.back-button-container {
display: flex;
justify-content: flex-start;
padding: 1em;
padding-bottom: 0;
}
.back-button {
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.5em 1em;
background-color: var(--bg, #fff);
border: 1px solid var(--border-color, #ccc);
border-radius: 0.5em;
cursor: pointer;
font-size: 0.9em;
box-shadow: 0 0 .3em rgba(0, 0, 0, 0.1);
transition: background-color 0.2s, box-shadow 0.2s;
}
.back-button:hover {
background-color: var(--bg-hover, #f5f5f5);
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.15);
}
@media (prefers-color-scheme: dark) {
.back-button {
background-color: var(--bg, #111);
border-color: var(--border-color, #333);
}
.back-button:hover {
background-color: var(--bg-hover, #222);
}
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div
:id="`execution-${executionTrackingId}`"
class="execution-button"
>
<button
:title="`${ellapsed}s`"
@click="show"
>
{{ buttonText }}
</button>
</div>
</template>
<script>
//import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
export default {
name: 'ExecutionButton',
// mixins: [ExecutionFeedbackButton],
props: {
executionTrackingId: {
type: String,
required: true
}
},
data() {
return {
ellapsed: 0,
isWaiting: true
}
},
computed: {
buttonText() {
if (this.isWaiting) {
return 'Executing...'
} else {
return `${this.ellapsed}s`
}
}
},
mounted() {
this.constructFromJson(this.executionTrackingId)
},
methods: {
constructFromJson(json) {
this.executionTrackingId = json
this.ellapsed = 0
this.isWaiting = true
},
show() {
this.$emit('show')
if (window.executionDialog) {
window.executionDialog.reset()
window.executionDialog.show()
window.executionDialog.fetchExecutionResult(this.executionTrackingId)
}
},
onExecStatusChanged() {
this.isWaiting = false
this.domTitle = this.ellapsed + 's'
},
// Override from ExecutionFeedbackButton
onExecutionFinished(logEntry) {
if (logEntry.timedOut) {
this.renderExecutionResult('action-timeout', 'Timed out')
} else if (logEntry.blocked) {
this.renderExecutionResult('action-blocked', 'Blocked!')
} else if (logEntry.exitCode !== 0) {
this.renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
} else {
this.ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
this.renderExecutionResult('action-success', 'Success!')
}
},
renderExecutionResult(resultCssClass, temporaryStatusMessage) {
this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
this.onExecStatusChanged()
},
updateDom(resultCssClass, title) {
// For execution button, we don't need to update classes as much
// since it's a simpler component
if (resultCssClass) {
this.$el.classList.add(resultCssClass)
}
}
}
}
</script>
<style scoped>
.execution-button {
display: inline-block;
}
.execution-button button {
padding: 0.25em 0.5em;
border: 1px solid #ccc;
border-radius: 3px;
background: #fff;
cursor: pointer;
font-size: 0.9em;
transition: all 0.2s ease;
}
.execution-button button:hover {
background: #f5f5f5;
border-color: #999;
}
/* Animation classes */
.execution-button button.action-timeout {
background: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.execution-button button.action-blocked {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.execution-button button.action-nonzero-exit {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.execution-button button.action-success {
background: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div :class = "statusClass + ' annotation'">
<span>{{ statusText }}</span><span>{{ exitCodeText }}</span>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
logEntry: {
type: Object,
required: true
}
})
const statusText = computed(() => {
const logEntry = props.logEntry
if (!logEntry) return 'unknown'
if (logEntry.executionFinished) {
if (logEntry.blocked) {
return 'Blocked'
} else if (logEntry.timedOut) {
return 'Timed out'
} else {
return 'Completed'
}
} else {
return 'Still running...'
}
})
const exitCodeText = computed(() => {
const logEntry = props.logEntry
if (!logEntry) return ''
if (logEntry.exitCode === 0) {
return ''
}
if (logEntry.executionFinished) {
if (logEntry.blocked || logEntry.timedOut) {
return ''
}
return ' (Exit code: ' + logEntry.exitCode + ')'
}
return ''
})
const statusClass = computed(() => {
const logEntry = props.logEntry
if (!logEntry) return ''
if (logEntry.executionFinished) {
if (logEntry.blocked) {
return 'status-blocked'
} else if (logEntry.timedOut) {
return 'status-timeout'
} else if (logEntry.exitCode === 0) {
return 'status-success'
} else {
return 'status-nonzero-exit'
}
}
return ''
})
</script>
<style scoped>
.status-success {
color: var(--karma-good-fg);
}
.status-nonzero-exit {
color: var(--karma-bad-fg);
}
.status-timeout {
color: var(--karma-warning-fg);
}
.status-blocked {
color: #ca79ff;
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div id = "breadcrumbs">
<template v-for="(link, index) in links" :key="link.name">
<router-link :to="link.href">{{ link.name }}</router-link>
<span v-if="index < links.length - 1" class="separator">
&raquo;
</span>
</template>
</div>
</template>
<style scoped>
span {
color: #bbb;
}
a {
text-decoration: none;
padding: 0.4em;
border-radius: 0.2em;
}
a:hover {
text-decoration: underline;
background-color: #000;
}
</style>
<script setup>
import { ref } from 'vue';
import { watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const links = ref([]);
watch(() => route.matched, (matched) => {
links.value = [];
matched.forEach((record) => {
if (record.meta && record.meta.breadcrumb) {
record.meta.breadcrumb.forEach((item) => {
links.value.push({
name: item.name,
href: item.href || record.path || '/'
});
});
} else if (record.name) {
links.value.push({
name: record.name,
href: record.path || '/'
});
}
});
}, { immediate: true });
</script>

View File

@@ -0,0 +1,35 @@
<template>
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :key="component.title" />
<DashboardComponentDirectory v-else-if="component.type == 'directory'" :component="component" />
<DashboardComponentDisplay v-else-if="component.type == 'display'" :component="component" />
<DashboardComponentMostRecentExecution v-else-if="component.type == 'stdout-most-recent-execution'" :component="component" />
<template v-else-if="component.type == 'fieldset'">
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
<DashboardComponent :component="subcomponent" />
</template>
</template>
<div v-else>
OTHER: {{ component.type }}
{{ component }}
</div>
</template>
<script setup>
import ActionButton from '../ActionButton.vue'
import DashboardComponentMostRecentExecution from './DashboardComponentMostRecentExecution.vue'
import DashboardComponentDirectory from './DashboardComponentDirectory.vue'
import DashboardComponentDisplay from './DashboardComponentDisplay.vue'
const props = defineProps({
component: {
type: Object,
required: true
}
})
</script>

View File

@@ -0,0 +1,60 @@
<template>
<button @click="navigateToDirectory">
{{ component.title }}
</button>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps({
component: {
type: Object,
required: true
}
})
function navigateToDirectory() {
const params = { title: props.component.title }
if (props.component.entityType && props.component.entityKey) {
params.entityType = props.component.entityType
params.entityKey = props.component.entityKey
}
router.push({ name: 'Dashboard', params })
}
</script>
<style scoped>
.folder-container {
display: grid;
}
button {
box-shadow: 0 0 .6em #aaa;
background-color: #fff;
border-radius: .7em;
}
button:hover {
background-color: #f5f5f5;
border-color: #999;
}
@media (prefers-color-scheme: dark) {
button {
box-shadow: 0 0 .6em #000;
background-color: #111;
border-color: #000;
}
button:hover {
background-color: #222;
border-color: #000;
}
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="display">
<div v-html="component.title" />
</div>
</template>
<script setup>
const props = defineProps({
component: {
type: Object,
required: true
}
})
</script>
<style scoped>
.display {
padding: 1em;
border-radius: .7em;
box-shadow: 0 0 .6em #aaa;
text-align: center;
font-size: small;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
align-items: center;
}
@media (prefers-color-scheme: dark) {
.display {
border-color: #000;
box-shadow: 0 0 .6em #000;
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div class="mre-container">
<router-link
v-if="executionTrackingId"
:to="`/logs/${executionTrackingId}`"
class="mre-link"
>
<pre class="mre-output">{{ output }}</pre>
</router-link>
<pre v-else class="mre-output fg-important">{{ output }}</pre>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps({
component: {
type: Object,
required: true
}
})
const output = ref('Waiting...')
const executionTrackingId = ref(null)
let eventListener = null
async function fetchMostRecentExecution() {
if (!props.component.title) {
output.value = 'Error: No action ID specified'
executionTrackingId.value = null
return
}
if (!window.client) {
output.value = 'Error: Client not initialized'
executionTrackingId.value = null
return
}
try {
const executionStatusArgs = {
actionId: props.component.title
}
const result = await window.client.executionStatus(executionStatusArgs)
if (result.logEntry) {
if (result.logEntry.output !== undefined) {
output.value = result.logEntry.output
} else {
output.value = 'No output available'
}
if (result.logEntry.executionTrackingId) {
executionTrackingId.value = result.logEntry.executionTrackingId
}
} else {
output.value = 'No output available'
executionTrackingId.value = null
}
} catch (err) {
if (err.code === 'NotFound' || err.status === 404) {
output.value = 'No execution found'
executionTrackingId.value = null
} else {
output.value = 'Error: ' + (err.message || 'Failed to fetch execution')
console.error('Failed to fetch most recent execution:', err)
executionTrackingId.value = null
}
}
}
function handleExecutionFinished(event) {
// The dashboard component "title" field is used for lots of things
// and in this context for MreOutput it's just to refer to an actionId.
//
// So this is not a typo.
const logEntry = event.payload.logEntry
if (logEntry && logEntry.actionId === props.component.title) {
if (logEntry.output !== undefined) {
output.value = logEntry.output
}
if (logEntry.executionTrackingId) {
executionTrackingId.value = logEntry.executionTrackingId
}
}
}
onMounted(() => {
fetchMostRecentExecution()
eventListener = (event) => handleExecutionFinished(event)
window.addEventListener('EventExecutionFinished', eventListener)
})
onBeforeUnmount(() => {
if (eventListener) {
window.removeEventListener('EventExecutionFinished', eventListener)
}
})
</script>
<style scoped>
.mre-container {
display: grid;
grid-column: span 2;
}
.mre-link {
text-decoration: none;
color: inherit;
display: grid;
cursor: pointer;
grid-column: span 2;
}
.mre-link:hover .mre-output {
border-color: #999;
}
.mre-output {
box-shadow: 0 0 .6em #aaa;
border: 1px dashed #ccc;
border-radius: .7em;
padding: 1em;
margin: 0;
min-height: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: monospace;
font-size: 0.9em;
overflow-x: auto;
overflow-y: auto;
transition: border-color 0.2s ease;
max-height: 20em;
}
@media (prefers-color-scheme: dark) {
.mre-output {
border: 1px dashed #444;
box-shadow: 0 0 .6em #444;
}
.mre-link:hover .mre-output {
border-color: #666;
}
}
</style>

View File

@@ -0,0 +1,149 @@
import { createRouter, createWebHistory } from 'vue-router'
import { Wrench01Icon } from '@hugeicons/core-free-icons'
import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
import { CellsIcon } from '@hugeicons/core-free-icons'
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
const routes = [
{
path: '/',
name: 'Actions',
component: () => import('./Dashboard.vue'),
meta: { title: 'Actions', icon: DashboardSquare01Icon }
},
{
path: '/dashboards/:title/:entityType?/:entityKey?',
name: 'Dashboard',
component: () => import('./Dashboard.vue'),
props: true,
meta: { title: 'Dashboard' }
},
{
path: '/actionBinding/:bindingId/argumentForm',
name: 'ActionBinding',
component: () => import('./views/ArgumentForm.vue'),
props: true,
meta: { title: 'Action Binding' }
},
{
path: '/logs',
name: 'Logs',
component: () => import('./views/LogsListView.vue'),
meta: {
title: 'Logs',
icon: LeftToRightListDashIcon
}
},
{
path: '/entities',
name: 'Entities',
component: () => import('./views/EntitiesView.vue'),
meta: {
title: 'Entities',
icon: CellsIcon
}
},
{
path: '/entity-details/:entityType/:entityKey',
name: 'EntityDetails',
component: () => import('./views/EntityDetailsView.vue'),
props: true,
meta: {
title: 'OliveTin - Entity Details',
breadcrumb: [
{ name: "Entities", href: "/entities" },
{ name: "Entity Details" }
]
}
},
{
path: '/logs/:executionTrackingId',
name: 'Execution',
component: () => import('./views/ExecutionView.vue'),
props: true,
meta: {
title: 'Execution',
breadcrumb: [
{ name: "Logs", href: "/logs" },
{ name: "Execution" },
]
}
},
{
path: '/action/:actionId',
name: 'ActionDetails',
component: () => import('./views/ActionDetailsView.vue'),
props: true,
meta: {
title: 'Action Details',
breadcrumb: [
{ name: "Actions", href: "/" },
{ name: "Action Details" },
]
}
},
{
path: '/diagnostics',
name: 'Diagnostics',
component: () => import('./views/DiagnosticsView.vue'),
meta: {
title: 'Diagnostics',
icon: Wrench01Icon
}
},
{
path: '/login',
name: 'Login',
component: () => import('./views/LoginView.vue'),
meta: { title: 'Login' }
},
{
path: '/user',
name: 'UserInformation',
component: () => import('./views/UserControlPanel.vue'),
meta: { title: 'User Information' }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('./views/NotFoundView.vue'),
meta: { title: 'Page Not Found' }
}
]
// Create router instance
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
// Navigation guard to update page title
router.beforeEach((to, from, next) => {
if (to.meta && to.meta.title) {
const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = to.meta.title + " - " + pageTitle
}
next()
})
// Navigation guard for authentication (if needed)
router.beforeEach((to, from, next) => {
// Check if user is authenticated for protected routes
const isAuthenticated = window.isAuthenticated || true // Default to true for now
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
next()
}
})
export default router

View File

@@ -0,0 +1,3 @@
import { reactive } from 'vue'
export const buttonResults = reactive({})

View File

@@ -0,0 +1,389 @@
<template>
<Section :title="'Action Details: ' + actionTitle" :padding="false">
<template #toolbar>
<button v-if="action" @click="startAction" title="Start this action" class="button neutral">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" d="M8 6v12l8-6z" />
</svg>
Start
</button>
</template>
<div class = "flex-row padding" v-if="action">
<div class = "fg1">
<dl>
<dt>Title</dt>
<dd>{{ action.title }}</dd>
<dt>Timeout</dt>
<dd>{{ action.timeout }} seconds</dd>
</dl>
<p v-if="action" class = "fg1">
Execution history for this action. You can filter by execution tracking ID.
</p>
</div>
<div style = "align-self: start; text-align: right;">
<span class="icon" v-html="action.icon"></span>
<div class="filter-container">
<label class="input-with-icons">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
</svg>
<input placeholder="Filter current page" v-model="searchText" />
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
</svg>
</button>
</label>
</div>
</div>
</div>
<div v-show="filteredLogs.length > 0">
<table class="logs-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Execution ID</th>
<th>Metadata</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
<td>
<router-link :to="`/logs/${log.executionTrackingId}`">
{{ log.executionTrackingId }}
</router-link>
</td>
<td class="tags">
<span class="annotation">
<span class="annotation-key">User:</span>
<span class="annotation-val">{{ log.user }}</span>
</span>
<span v-if="log.tags && log.tags.length > 0" class="tag-list">
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
</span>
</td>
<td class="exit-code">
<span :class="getStatusClass(log) + ' annotation'">
{{ getStatusText(log) }}
</span>
</td>
</tr>
</tbody>
</table>
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" @page-change="handlePageChange" class="padding"
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
</div>
<div v-show="logs.length === 0 && !loading" class="empty-state">
<p>This action has no execution history.</p>
<router-link to="/">Return to index</router-link>
</div>
</Section>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Pagination from 'picocrank/vue/components/Pagination.vue'
import Section from 'picocrank/vue/components/Section.vue'
const route = useRoute()
const router = useRouter()
const logs = ref([])
const action = ref(null)
const actionTitle = ref('Action Details')
const searchText = ref('')
const pageSize = ref(10)
const currentPage = ref(1)
const loading = ref(false)
const totalCount = ref(0)
const filteredLogs = computed(() => {
if (!searchText.value) {
return logs.value
}
const searchLower = searchText.value.toLowerCase()
return logs.value.filter(log =>
log.executionTrackingId.toLowerCase().includes(searchLower) ||
log.actionTitle.toLowerCase().includes(searchLower)
)
})
async function fetchActionLogs() {
loading.value = true
try {
const actionId = route.params.actionId
const startOffset = (currentPage.value - 1) * pageSize.value
const args = {
"actionId": actionId,
"startOffset": BigInt(startOffset),
"pageSize": BigInt(Number(pageSize.value)),
}
const response = await window.client.getActionLogs(args)
logs.value = response.logs
const serverPageSize = Number(response.pageSize)
if (Number.isFinite(serverPageSize) && serverPageSize > 0) {
pageSize.value = serverPageSize
}
totalCount.value = Number(response.totalCount) || 0
} catch (err) {
console.error('Failed to fetch action logs:', err)
window.showBigError('fetch-action-logs', 'getting action logs', err, false)
} finally {
loading.value = false
}
}
async function fetchAction() {
try {
const actionId = route.params.actionId
const args = {
"bindingId": actionId
}
const response = await window.client.getActionBinding(args)
action.value = response.action
actionTitle.value = action.value?.title || 'Unknown Action'
} catch (err) {
console.error('Failed to fetch action:', err)
window.showBigError('fetch-action', 'getting action details', err, false)
}
}
function resetState() {
action.value = null
actionTitle.value = 'Action Details'
logs.value = []
totalCount.value = 0
currentPage.value = 1
searchText.value = ''
loading.value = true
}
function clearSearch() {
searchText.value = ''
}
function formatTimestamp(timestamp) {
if (!timestamp) return 'Unknown'
try {
const date = new Date(timestamp)
return date.toLocaleString()
} catch (err) {
return timestamp
}
}
function getStatusClass(log) {
if (log.timedOut) return 'status-timeout'
if (log.blocked) return 'status-blocked'
if (log.exitCode !== 0) return 'status-error'
return 'status-success'
}
function getStatusText(log) {
if (log.timedOut) return 'Timed out'
if (log.blocked) return 'Blocked'
if (log.exitCode !== 0) return `Exit code ${log.exitCode}`
return 'Completed'
}
function handlePageChange(page) {
currentPage.value = page
fetchActionLogs()
}
function handlePageSizeChange(newPageSize) {
pageSize.value = newPageSize
currentPage.value = 1
fetchActionLogs()
}
async function startAction() {
if (!action.value || !action.value.bindingId) {
console.error('Cannot start action: no binding ID')
return
}
try {
const args = {
"bindingId": action.value.bindingId,
"arguments": []
}
const response = await window.client.startAction(args)
router.push(`/logs/${response.executionTrackingId}`)
} catch (err) {
console.error('Failed to start action:', err)
window.showBigError('start-action', 'starting action', err, false)
}
}
onMounted(() => {
fetchAction()
fetchActionLogs()
})
watch(
() => route.params.actionId,
() => {
resetState()
fetchAction()
fetchActionLogs()
},
{ immediate: false }
)
</script>
<style scoped>
.action-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.action-header h2 {
margin: 0;
}
.icon {
font-size: 1.5rem;
}
.logs-table {
width: 100%;
border-collapse: collapse;
}
.logs-table th {
background-color: var(--section-background);
padding: 0.5rem;
text-align: left;
font-weight: 600;
}
.logs-table td {
padding: 0.5rem;
border-top: 1px solid var(--border-color);
}
.log-row:hover {
background-color: var(--hover-background);
}
.timestamp {
font-family: monospace;
font-size: 0.9rem;
color: var(--text-secondary);
}
.empty-state {
padding: 2rem;
text-align: center;
color: var(--text-secondary);
}
.filter-container {
display: flex;
justify-content: flex-end;
padding: 0.5rem 1rem;
}
.input-with-icons {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
background: var(--section-background);
width: 100%;
max-width: 300px;
}
.input-with-icons input {
border: none;
outline: none;
background: transparent;
flex: 1;
color: var(--text-primary);
}
.input-with-icons button {
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
}
.input-with-icons button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.annotation {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.85rem;
}
.annotation-key {
font-weight: 600;
color: var(--text-secondary);
}
.annotation-val {
color: var(--text-primary);
}
.tag-list {
display: inline-flex;
gap: 0.25rem;
}
.tag {
background-color: var(--accent-color);
color: var(--accent-text);
padding: 0.1rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.85rem;
}
.exit-code .status-success {
color: #28a745;
}
.exit-code .status-error {
color: #dc3545;
}
.exit-code .status-timeout {
color: #ffc107;
}
.exit-code .status-blocked {
color: #6c757d;
}
.padding {
padding: 1rem;
}
</style>

View File

@@ -0,0 +1,408 @@
<template>
<section id = "argument-popup">
<div class="section-header">
<h2>Start action: {{ title }}</h2>
</div>
<div class="section-content padding">
<form @submit="handleSubmit">
<template v-if="actionArguments.length > 0">
<template v-for="arg in actionArguments" :key="arg.name">
<label :for="arg.name">
{{ formatLabel(arg.title) }}
</label>
<datalist v-if="arg.suggestions && Object.keys(arg.suggestions).length > 0" :id="`${arg.name}-choices`">
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
{{ suggestion }}
</option>
</datalist>
<select v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
:required="arg.required" @input="handleInput(arg, $event)" @change="handleChange(arg, $event)">
<option v-for="choice in arg.choices" :key="choice.value" :value="choice.value">
{{ choice.title || choice.value }}
</option>
</select>
<component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
:list="arg.suggestions ? `${arg.name}-choices` : undefined"
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
@input="handleInput(arg, $event)" @change="handleChange(arg, $event)" />
<span class="argument-description" v-html="arg.description"></span>
</template>
</template>
<div v-else>
<p>No arguments required</p>
</div>
<div class="buttons">
<button name="start" type="submit" :disabled="hasConfirmation && !confirmationChecked">
Start
</button>
<button name="cancel" type="button" @click="handleCancel">
Cancel
</button>
</div>
</form>
</div>
</section>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// Reactive data
const dialog = ref(null)
const title = ref('')
const icon = ref('')
//const arguments = ref([])
const argValues = ref({})
const confirmationChecked = ref(false)
const hasConfirmation = ref(false)
const formErrors = ref({})
const actionArguments = ref([])
// Computed properties
const props = defineProps({
bindingId: {
type: String,
required: true
}
})
// Methods
async function setup() {
const ret = await window.client.getActionBinding({
bindingId: props.bindingId
})
const action = ret.action
title.value = action.title
icon.value = action.icon
actionArguments.value = action.arguments || []
argValues.value = {}
formErrors.value = {}
confirmationChecked.value = false
hasConfirmation.value = false
// Initialize values from query params or defaults
actionArguments.value.forEach(arg => {
if (arg.type === 'confirmation') {
hasConfirmation.value = true
const paramValue = getQueryParamValue(arg.name)
let checkedValue = false
if (paramValue !== null) {
checkedValue = paramValue === '1' || paramValue === 'true' || paramValue === true
} else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
}
argValues.value[arg.name] = checkedValue
confirmationChecked.value = checkedValue
} else {
const paramValue = getQueryParamValue(arg.name)
argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
}
})
// Run initial validation on all fields after DOM is updated
await nextTick()
for (const arg of actionArguments.value) {
if (arg.type && !arg.type.startsWith('regex:') && arg.type !== 'select' && arg.type !== '' && arg.type !== 'confirmation') {
await validateArgument(arg, argValues.value[arg.name])
}
}
}
function getQueryParamValue(paramName) {
const params = new URLSearchParams(window.location.search.substring(1))
return params.get(paramName)
}
function formatLabel(title) {
const lastChar = title.charAt(title.length - 1)
if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
return title
}
return title + ':'
}
function getInputComponent(arg) {
if (arg.type === 'html') {
return 'div'
} else if (arg.type === 'raw_string_multiline') {
return 'textarea'
} else if (arg.choices && arg.choices.length > 0 && (arg.type === 'select' || arg.type === '')) {
return 'select'
} else {
return 'input'
}
}
function getInputType(arg) {
if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
return undefined
}
if (arg.type === 'confirmation') {
return 'checkbox'
}
if (arg.type === 'ascii_identifier' || arg.type === 'ascii') {
return 'text'
}
if (arg.type === 'datetime') {
return 'datetime-local'
}
return arg.type
}
function getPattern(arg) {
if (arg.type && arg.type.startsWith('regex:')) {
return arg.type.replace('regex:', '')
}
return undefined
}
function getArgumentValue(arg) {
if (arg.type === 'checkbox' || arg.type === 'confirmation') {
return argValues.value[arg.name] === '1' || argValues.value[arg.name] === true || argValues.value[arg.name] === 'true'
}
return argValues.value[arg.name] || ''
}
function handleInput(arg, event) {
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
argValues.value[arg.name] = value
updateUrlWithArg(arg.name, value)
}
function handleChange(arg, event) {
if (arg.type === 'confirmation') {
confirmationChecked.value = event.target.checked
return
}
// Validate the input
validateArgument(arg, event.target.value)
}
async function validateArgument(arg, value) {
if (!arg.type || arg.type.startsWith('regex:')) {
return
}
// Skip validation for datetime - backend will handle mangling values without seconds
if (arg.type === 'datetime') {
const inputElement = document.getElementById(arg.name)
if (inputElement) {
inputElement.setCustomValidity('')
}
delete formErrors.value[arg.name]
return
}
try {
const validateArgumentTypeArgs = {
value: value,
type: arg.type
}
const validation = await window.client.validateArgumentType(validateArgumentTypeArgs)
// Get the input element to set custom validity
const inputElement = document.getElementById(arg.name)
if (validation.valid) {
delete formErrors.value[arg.name]
// Clear custom validity message
if (inputElement) {
inputElement.setCustomValidity('')
}
} else {
formErrors.value[arg.name] = validation.description
// Set custom validity message
if (inputElement) {
inputElement.setCustomValidity(validation.description)
}
}
} catch (err) {
console.warn('Validation failed:', err)
// On error, clear any custom validity
const inputElement = document.getElementById(arg.name)
if (inputElement) {
inputElement.setCustomValidity('')
}
}
}
function updateUrlWithArg(name, value) {
if (name && value !== undefined) {
const url = new URL(window.location.href)
// Don't add passwords to URL
const arg = actionArguments.value.find(a => a.name === name)
if (arg && arg.type === 'password') {
return
}
url.searchParams.set(name, value)
window.history.replaceState({}, '', url.toString())
}
}
function getArgumentValues() {
const ret = []
for (const arg of actionArguments.value) {
let value = argValues.value[arg.name] || ''
if (arg.type === 'checkbox' || arg.type === 'confirmation') {
value = value ? '1' : '0'
}
ret.push({
name: arg.name,
value: value
})
}
return ret
}
function getUniqueId() {
if (window.isSecureContext) {
return window.crypto.randomUUID()
} else {
return Date.now().toString()
}
}
async function startAction(actionArgs) {
const startActionArgs = {
bindingId: props.bindingId,
arguments: actionArgs,
uniqueTrackingId: getUniqueId()
}
try {
const response = await window.client.startAction(startActionArgs)
console.log('Action started successfully with tracking ID:', response.executionTrackingId)
return response
} catch (err) {
console.error('Failed to start action:', err)
throw err
}
}
async function handleSubmit(event) {
// Set custom validity for required fields
for (const arg of actionArguments.value) {
const value = argValues.value[arg.name]
const inputElement = document.getElementById(arg.name)
if (arg.required && (!value || value === '')) {
formErrors.value[arg.name] = 'This field is required'
// Set custom validity for required field validation
if (inputElement) {
inputElement.setCustomValidity('This field is required')
}
}
}
const form = event.target
if (!form.checkValidity()) {
console.log('argument form has elements that failed validation')
return
}
event.preventDefault()
const argvs = getArgumentValues()
console.log('argument form has elements that passed validation')
try {
const response = await startAction(argvs)
router.push(`/logs/${response.executionTrackingId}`)
} catch (err) {
console.error('Failed to start action:', err)
}
}
function handleCancel() {
router.back()
clearBookmark()
}
function clearBookmark() {
window.history.replaceState({
path: window.location.pathname
}, '', window.location.pathname)
}
function show() {
if (dialog.value) {
dialog.value.showModal()
}
}
function close() {
if (dialog.value) {
dialog.value.close()
}
}
// Expose methods for parent components
defineExpose({
show,
close
})
// Lifecycle
onMounted(() => {
setup()
})
</script>
<style scoped>
form {
grid-template-columns: max-content auto auto;
}
.argument-description {
font-size: 0.875rem;
color: #666;
margin-top: 0.25rem;
}
.buttons {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
padding-top: 1rem;
border-top: 1px solid #eee;
}
/* Checkbox specific styling */
.argument-group input[type="checkbox"] {
width: auto;
margin-right: 0.5rem;
}
.argument-group input[type="checkbox"]+label {
display: inline;
font-weight: normal;
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<Section :title="t('diagnostics.get-support')">
<p>{{ t('diagnostics.get-support-description') }}
</p>
<ul>
<li>
<a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">{{ t('diagnostics.where-to-find-help') }}</a>
</li>
</ul>
</Section>
<Section :title="t('diagnostics.ssh')">
<dl>
<dt>{{ t('diagnostics.found-key') }}</dt>
<dd>{{ diagnostics.sshFoundKey || '?' }}</dd>
<dt>{{ t('diagnostics.found-config') }}</dt>
<dd>{{ diagnostics.sshFoundConfig || '?' }}</dd>
</dl>
</Section>
<Section :title="t('diagnostics.sos-report')">
<p>{{ t('diagnostics.sos-report-description') }}</p>
<p>
<a href="https://docs.olivetin.app/troubleshooting/sosreport.html" target="_blank">{{ t('diagnostics.sos-report-docs') }}</a>
</p>
<div role="toolbar">
<button @click="generateSosReport" :disabled="loading" class = "good">{{ t('diagnostics.generate-sos-report') }}</button>
<button @click="copySosReport" :disabled="!sosReport || loading" :class="sosReportCopied ? 'good' : ''">{{ sosReportCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
</div>
<textarea v-model="sosReport" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
</Section>
<Section :title="t('diagnostics.browser-info')">
<p>{{ t('diagnostics.browser-info-description') }}</p>
<div role="toolbar">
<button @click="generateBrowserInfo" :disabled="loading" class = "good">{{ t('diagnostics.generate-browser-info') }}</button>
<button @click="copyBrowserInfo" :disabled="!browserInfo || loading" :class="browserInfoCopied ? 'good' : ''">{{ browserInfoCopied ? t('diagnostics.copied') : t('diagnostics.copy-to-clipboard') }}</button>
</div>
<textarea v-model="browserInfo" readonly style="flex: 1; min-height: 200px; resize: vertical; width: 100%; box-sizing: border-box;"></textarea>
</Section>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const diagnostics = ref({})
const loading = ref(false)
const sosReport = ref('')
const browserInfo = ref('')
const sosReportCopied = ref(false)
const browserInfoCopied = ref(false)
async function fetchDiagnostics() {
loading.value = true
try {
const response = await window.client.getDiagnostics();
diagnostics.value = {
sshFoundKey: response.SshFoundKey,
sshFoundConfig: response.SshFoundConfig
};
} catch (err) {
console.error('Failed to fetch diagnostics:', err);
diagnostics.value = {
sshFoundKey: t('diagnostics.unknown'),
sshFoundConfig: t('diagnostics.unknown')
}
}
loading.value = false
}
function formatKey(key) {
return key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim()
}
async function generateSosReport() {
const response = await window.client.sosReport()
console.log("response", response)
sosReport.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
}
async function copySosReport() {
try {
await navigator.clipboard.writeText(sosReport.value)
sosReportCopied.value = true
setTimeout(() => {
sosReportCopied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy SOS report to clipboard:', err)
}
}
async function generateBrowserInfo() {
loading.value = true
try {
let userAgentData = 'N/A'
if (navigator.userAgentData) {
try {
const uaData = await navigator.userAgentData.getHighEntropyValues([
'platform',
'platformVersion',
'architecture',
'model',
'uaFullVersion',
'bitness',
'fullVersionList'
])
userAgentData = JSON.stringify(uaData, null, 2)
} catch (err) {
userAgentData = `${t('diagnostics.useragent-data-error')}: ${err.message}`
}
}
const info = {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
languages: navigator.languages?.join(', ') || 'N/A',
cookieEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
screenWidth: screen.width,
screenHeight: screen.height,
screenColorDepth: screen.colorDepth,
screenPixelDepth: screen.pixelDepth,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio || 'N/A',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timezoneOffset: new Date().getTimezoneOffset(),
localStorageEnabled: (() => {
try {
localStorage.setItem('test', 'test')
localStorage.removeItem('test')
return true
} catch {
return false
}
})(),
sessionStorageEnabled: (() => {
try {
sessionStorage.setItem('test', 'test')
sessionStorage.removeItem('test')
return true
} catch {
return false
}
})(),
hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
maxTouchPoints: navigator.maxTouchPoints || 'N/A',
userAgentData: userAgentData
}
const olivetinVersion = window.initResponse?.currentVersion || t('diagnostics.unknown')
const currentLanguage = locale.value || t('diagnostics.unknown')
let output = '';
output += `\`\`\`\n`
output += '### BROWSER INFO START (copy all text to BROWSER INFO END)\n'
output += `# OliveTin Information\n`
output += `olivetinVersion: ${olivetinVersion}\n`
output += `currentLanguage: ${currentLanguage}\n`
output += `\n# Browser Information\n`
output += `userAgent: ${info.userAgent}\n`
output += `platform: ${info.platform}\n`
output += `language: ${info.language}\n`
output += `languages: ${info.languages}\n`
output += `\n# User Agent Data\n`
output += `userAgentData:\n${info.userAgentData}\n`
output += `\n# Display Information\n`
output += `screenWidth: ${info.screenWidth}\n`
output += `screenHeight: ${info.screenHeight}\n`
output += `screenColorDepth: ${info.screenColorDepth}\n`
output += `screenPixelDepth: ${info.screenPixelDepth}\n`
output += `viewportWidth: ${info.viewportWidth}\n`
output += `viewportHeight: ${info.viewportHeight}\n`
output += `devicePixelRatio: ${info.devicePixelRatio}\n`
output += `\n# Feature Support\n`
output += `cookieEnabled: ${info.cookieEnabled}\n`
output += `localStorageEnabled: ${info.localStorageEnabled}\n`
output += `sessionStorageEnabled: ${info.sessionStorageEnabled}\n`
output += `onLine: ${info.onLine}\n`
output += `hardwareConcurrency: ${info.hardwareConcurrency}\n`
output += `maxTouchPoints: ${info.maxTouchPoints}\n`
output += `\n# Location & Time\n`
output += `timezone: ${info.timezone}\n`
output += `timezoneOffset: ${info.timezoneOffset}\n`
output += `\n### BROWSER INFO END (copy all text from BROWSER INFO START)`
output += `\n\`\`\`\n`
browserInfo.value = output
} finally {
loading.value = false
}
}
async function copyBrowserInfo() {
try {
await navigator.clipboard.writeText(browserInfo.value)
browserInfoCopied.value = true
setTimeout(() => {
browserInfoCopied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy browser info to clipboard:', err)
}
}
onMounted(() => {
fetchDiagnostics()
})
</script>
<style scoped>
.diagnostics-view {
padding: 1rem;
}
.diagnostics-content {
max-width: 800px;
margin: 0 auto;
}
.note {
background: #f8f9fa;
border-left: 4px solid #007bff;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0 4px 4px 0;
font-size: 0.875rem;
color: #495057;
}
.note a {
color: #007bff;
text-decoration: none;
}
.note a:hover {
text-decoration: underline;
}
.diagnostics-table {
width: 100%;
border-collapse: collapse;
}
.diagnostics-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #f1f3f4;
}
.diagnostics-table td:first-child {
font-weight: 500;
color: #495057;
background: #f8f9fa;
}
.diagnostics-table tr:last-child td {
border-bottom: none;
}
.error-list {
padding: 1rem;
}
.error-item {
background: #f8d7da;
color: #721c24;
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 4px;
border-left: 4px solid #dc3545;
font-family: monospace;
font-size: 0.875rem;
}
.error-item:last-child {
margin-bottom: 0;
}
.flex-col {
display: flex;
flex-direction: column;
}
.section-content {
display: flex;
flex-direction: column;
gap: 1em;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<Section class = "with-header-and-content" v-if="entityDefinitions.length === 0" title="Loading entity definitions...">
<div class = "section-header">
<h2 class="loading-message">
Loading entity definitions...
</h2>
</div>
</Section>
<template v-else>
<Section v-for="def in entityDefinitions" :key="def.title" :title="'Entity: ' + def.title ">
<div class = "section-content">
<p>{{ def.instances.length }} instances.</p>
<ul>
<li v-for="inst in def.instances" :key="inst.uniqueKey">
<router-link :to="{ name: 'EntityDetails', params: { entityType: inst.type, entityKey: inst.uniqueKey } }">
{{ inst.title }}
</router-link>
</li>
</ul>
<h3>Used on Dashboards:</h3>
<ul>
<li v-for="dash in filteredDashboards(def.usedOnDashboards)" :key="dash">
<template v-if="isEntityDirectory(dash)">
{{ getDashboardTitle(dash) }} <span class="entity-directory-label">[Entity Directory]</span>
</template>
<router-link v-else-if="!dash.includes('entity:')" :to="{ name: 'Dashboard', params: { title: getDashboardTitle(dash) } }">
{{ getDashboardTitle(dash) }}
</router-link>
<span v-else>{{ dash }}</span>
</li>
</ul>
</div>
</Section>
</template>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue'
const entityDefinitions = ref([])
async function fetchEntities() {
const ret = await window.client.getEntities()
entityDefinitions.value = ret.entityDefinitions
}
function filteredDashboards(dashboards) {
return dashboards.filter(d => d && !d.includes('{{'))
}
function isEntityDirectory(dashboardTitle) {
return dashboardTitle.endsWith(' [Entity Directory]')
}
function getDashboardTitle(dashboardTitle) {
if (isEntityDirectory(dashboardTitle)) {
return dashboardTitle.slice(0, -' [Entity Directory]'.length)
}
return dashboardTitle
}
onMounted(() => {
fetchEntities()
})
</script>

View File

@@ -0,0 +1,176 @@
<template>
<Section title="Entity Details">
<template #toolbar>
<button @click="goBack" class="back-button">
<HugeiconsIcon :icon="ArrowLeftIcon" width="1.2em" height="1.2em" />
<span>Back</span>
</button>
</template>
<div v-if="!entityDetails">
<p>Loading entity details...</p>
</div>
<template v-else>
<dl>
<dt>Type</dt>
<dd>
<router-link :to="{ name: 'Entities' }" class="entity-type-link">
{{ entityType }}
</router-link>
</dd>
<dt v-if="entityDetails.title">Title</dt>
<dd v-if="entityDetails.title">{{ entityDetails.title }}</dd>
<template v-if="entityDetails.fields">
<template v-for="(value, key) in entityDetails.fields" :key="key">
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
</template>
</template>
</dl>
<p v-if="!entityDetails.title && (!entityDetails.fields || Object.keys(entityDetails.fields).length === 0)">No details available for this entity.</p>
<hr />
<h3>Dashboard Entity Directories</h3>
<div v-if="filteredDirectories.length > 0" class="directories-section">
<ul class="directory-list">
<li v-for="(directory, idx) in filteredDirectories" :key="idx">
<router-link
:to="{
name: 'Dashboard',
params: {
title: directory,
entityType: entityType,
entityKey: entityKey
}
}">
{{ directory }}
</router-link>
</li>
</ul>
</div>
<p v-else>No directories found for this entity.
<a href = "https://docs.olivetin.app/dashboards/entity-directories.html" target = "_blank">Learn more</a>
</p>
</template>
</Section>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue'
import { ArrowLeftIcon } from '@hugeicons/core-free-icons'
import Section from 'picocrank/vue/components/Section.vue'
const router = useRouter()
const entityDetails = ref(null)
const props = defineProps({
entityType: String,
entityKey: String
})
const filteredDirectories = computed(() => {
if (!entityDetails.value?.directories) {
return []
}
return entityDetails.value.directories.filter(d => d)
})
function goBack() {
router.push({ name: 'Entities' })
}
async function fetchEntityDetails() {
try {
const response = await window.client.getEntity({
type: props.entityType,
uniqueKey: props.entityKey
})
entityDetails.value = response
} catch (err) {
console.error('Failed to fetch entity details:', err)
window.showBigError('fetch-entity-details', 'getting entity details', err, false)
}
}
onMounted(() => {
fetchEntityDetails()
})
</script>
<style scoped>
.back-button {
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.5em 1em;
background-color: var(--bg, #fff);
border: 1px solid var(--border-color, #ccc);
border-radius: 0.5em;
cursor: pointer;
font-size: 0.9em;
box-shadow: 0 0 .3em rgba(0, 0, 0, 0.1);
transition: background-color 0.2s, box-shadow 0.2s;
}
.back-button:hover {
background-color: var(--bg-hover, #f5f5f5);
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.15);
}
.directories-section h3 {
margin-bottom: 0.5em;
font-size: 1.1em;
}
.directory-list a {
text-decoration: none;
padding: 0.5em;
display: inline-block;
border-radius: 0.3em;
transition: background-color 0.2s;
}
.directory-list a:hover {
background-color: var(--bg-hover, #f5f5f5);
text-decoration: underline;
}
.entity-type-link {
text-decoration: none;
transition: opacity 0.2s;
}
.entity-type-link:hover {
text-decoration: underline;
opacity: 0.8;
}
hr {
border: 0;
border-top: 1px solid var(--border-color, #ccc);
}
@media (prefers-color-scheme: dark) {
.back-button {
background-color: var(--bg, #111);
border-color: var(--border-color, #333);
}
.back-button:hover {
background-color: var(--bg-hover, #222);
}
.directories-section {
border-top-color: var(--border-color, #333);
}
.directory-list a:hover {
background-color: var(--bg-hover, #222);
}
}
</style>

View File

@@ -0,0 +1,413 @@
<template>
<Section :title="'Execution Results: ' + title" id = "execution-results-popup">
<template #toolbar>
<router-link v-if="actionId" :to="`/action/${actionId}`" title="View all executions for this action" class="button neutral">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.22 2.34 1.8 0 .53-.39 1.39-2.1 1.39-1.6 0-2.05-.56-2.13-1.45H8.04c.08 1.5 1.18 2.37 2.82 2.69V19h2.34v-1.63c1.65-.35 2.48-1.24 2.48-2.77-.01-1.88-1.51-2.87-3.7-3.23z"/>
</svg>
Action Details
</router-link>
<button @click="toggleSize" title="Toggle dialog size" class = "neutral">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z" />
</svg>
</button>
</template>
<div v-if="logEntry" class = "flex-row">
<dl class = "fg1">
<dt>Duration</dt>
<dd><span v-html="duration"></span></dd>
<dt>Status</dt>
<dd>
<ActionStatusDisplay :log-entry="logEntry" id = "execution-dialog-status" />
</dd>
</dl>
<span class="icon" role="img" v-html="icon" style = "align-self: start"></span>
</div>
<div v-if="notFound" class="error-message padded-content">
<h3>Execution Not Found</h3>
<p>{{ errorMessage }}</p>
<p>The execution with ID <code>{{ executionTrackingId }}</code> could not be found.</p>
<router-link to="/logs">View all logs</router-link> or <router-link to="/">return to home</router-link>.
</div>
<div ref="xtermOutput"></div>
<br />
<div class="flex-row g1 buttons padded-content">
<button @click="goBack" title="Go back">
<HugeiconsIcon :icon="ArrowLeftIcon" />
Back
</button>
<div class = "fg1" />
<button :disabled="!canRerun" @click="rerunAction" title="Rerun">
<HugeiconsIcon :icon="WorkoutRunIcon" />
Rerun
</button>
<button :disabled="!canKill" @click="killAction" title="Kill" id = "execution-dialog-kill-action">
<HugeiconsIcon :icon="Cancel02Icon" />
Kill
</button>
</div>
</Section>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
import Section from 'picocrank/vue/components/Section.vue'
import { OutputTerminal } from '../../../js/OutputTerminal.js'
import { HugeiconsIcon } from '@hugeicons/vue'
import { WorkoutRunIcon, Cancel02Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
import { useRouter } from 'vue-router'
import { buttonResults } from '../stores/buttonResults'
const router = useRouter()
// Refs for DOM elements
const xtermOutput = ref(null)
const props = defineProps({
executionTrackingId: {
type: String,
required: true
}
})
const executionTrackingId = ref(props.executionTrackingId)
const hideBasics = ref(false)
const hideDetails = ref(false)
const hideDetailsOnResult = ref(false)
const executionSeconds = ref(0)
const icon = ref('')
const title = ref('Waiting for result...')
const titleTooltip = ref('')
const duration = ref('')
const logEntry = ref(null)
const canRerun = ref(false)
const canKill = ref(false)
const actionId = ref('')
const notFound = ref(false)
const errorMessage = ref('')
let executionTicker = null
let terminal = null
function initializeTerminal() {
terminal = new OutputTerminal(executionTrackingId.value)
terminal.open(xtermOutput.value)
terminal.resize(80, 40)
window.terminal = terminal
}
function toggleSize() {
if (!xtermOutput.value) {
return
}
if (xtermOutput.value.requestFullscreen) {
xtermOutput.value.requestFullscreen()
} else if (xtermOutput.value.webkitRequestFullscreen) {
xtermOutput.value.webkitRequestFullscreen()
} else if (xtermOutput.value.mozRequestFullScreen) {
xtermOutput.value.mozRequestFullScreen()
} else if (xtermOutput.value.msRequestFullscreen) {
xtermOutput.value.msRequestFullscreen()
}
}
async function reset() {
executionSeconds.value = 0
executionTrackingId.value = 'notset'
hideBasics.value = false
hideDetails.value = false
hideDetailsOnResult.value = false
icon.value = ''
title.value = 'Waiting for result...'
titleTooltip.value = ''
duration.value = ''
canRerun.value = false
canKill.value = false
logEntry.value = null
notFound.value = false
errorMessage.value = ''
if (terminal) {
await terminal.reset()
terminal.fit()
}
}
function show(actionButton) {
if (actionButton) {
icon.value = actionButton.domIcon.innerText
}
canKill.value = true
// Clear existing ticker
if (executionTicker) {
clearInterval(executionTicker)
}
executionSeconds.value = 0
executionTick()
executionTicker = setInterval(() => {
executionTick()
}, 1000)
}
async function rerunAction() {
if (!logEntry.value || !logEntry.value.actionId) {
console.error('Cannot rerun: no action ID available')
return
}
try {
const startActionArgs = {
"bindingId": logEntry.value.actionId,
"arguments": []
}
const res = await window.client.startAction(startActionArgs)
router.push(`/logs/${res.executionTrackingId}`)
} catch (err) {
console.error('Failed to rerun action:', err)
window.showBigError('rerun-action', 'rerunning action', err, false)
}
}
async function killAction() {
if (!executionTrackingId.value || executionTrackingId.value === 'notset') {
return
}
const killActionArgs = {
executionTrackingId: executionTrackingId.value
}
try {
await window.client.killAction(killActionArgs)
} catch (err) {
console.error('Failed to kill action:', err)
}
}
function executionTick() {
executionSeconds.value++
updateDuration(null)
}
function hideEverythingApartFromOutput() {
hideDetailsOnResult.value = true
hideBasics.value = true
hideDetailsOnResult.value = true
hideBasics.value = true
}
async function fetchExecutionResult(executionTrackingIdParam) {
console.log("fetchExecutionResult", executionTrackingIdParam)
executionTrackingId.value = executionTrackingIdParam
notFound.value = false
errorMessage.value = ''
const executionStatusArgs = {
executionTrackingId: executionTrackingId.value
}
try {
const logEntryResult = await window.client.executionStatus(executionStatusArgs)
await renderExecutionResult(logEntryResult)
} catch (err) {
// Check if it's a "not found" error (404 or similar)
if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
notFound.value = true
errorMessage.value = err.message || 'The execution could not be found in the system.'
} else {
renderError(err)
}
throw err
}
}
function updateDuration(logEntryParam) {
logEntry.value = logEntryParam
if (logEntry.value == null) {
duration.value = executionSeconds.value + ' seconds'
duration.value = duration.value
} else if (!logEntry.value.executionStarted) {
duration.value = logEntry.value.datetimeStarted + ' (request time). Not executed.'
} else if (logEntry.value.executionStarted && !logEntry.value.executionFinished) {
duration.value = logEntry.value.datetimeStarted
} else {
let delta = ''
try {
delta = (new Date(logEntry.value.datetimeFinished) - new Date(logEntry.value.datetimeStarted)) / 1000
delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
} catch (e) {
console.warn('Failed to calculate delta', e)
}
duration.value = logEntry.value.datetimeStarted + ' &rarr; ' + logEntry.value.datetimeFinished
if (delta !== '') {
duration.value += ' (' + delta + ')'
}
}
}
async function renderExecutionResult(res) {
logEntry.value = res.logEntry
// Clear ticker
if (executionTicker) {
clearInterval(executionTicker)
}
executionTicker = null
if (hideDetailsOnResult.value) {
hideDetails.value = true
}
executionTrackingId.value = res.logEntry.executionTrackingId
canRerun.value = res.logEntry.executionFinished
canKill.value = res.logEntry.canKill
icon.value = res.logEntry.actionIcon
title.value = res.logEntry.actionTitle
titleTooltip.value = 'Action ID: ' + res.logEntry.actionId + '\nExecution ID: ' + res.logEntry.executionTrackingId
actionId.value = res.logEntry.actionId
updateDuration(res.logEntry)
if (terminal) {
await terminal.reset()
await terminal.write(res.logEntry.output, () => {
terminal.fit()
})
}
}
function renderError(err) {
window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
}
function handleClose() {
if (executionTicker) {
clearInterval(executionTicker)
}
executionTicker = null
}
function cleanup() {
if (executionTicker) {
clearInterval(executionTicker)
}
executionTicker = null
if (terminal != null) {
terminal.close()
}
terminal = null
}
function goBack() {
router.back()
}
onMounted(() => {
document.addEventListener('fullscreenchange', (e) => {
setTimeout(() => { // Wait for the DOM to settle
if (document.fullscreenElement) {
terminal.fit()
} else {
terminal.resize(80, 40)
terminal.fit()
}
}, 100)
})
initializeTerminal()
fetchExecutionResult(props.executionTrackingId)
watch(
() => buttonResults[props.executionTrackingId],
(newResult, oldResult) => {
if (newResult) {
renderExecutionResult({
logEntry: newResult
})
}
}
)
})
onBeforeUnmount(() => {
cleanup()
})
// Expose methods for parent/imperative use
defineExpose({
reset,
show,
rerunAction,
killAction,
fetchExecutionResult,
renderExecutionResult,
hideEverythingApartFromOutput,
handleClose
})
</script>
<style scoped>
.action-history-link {
color: var(--link-color, #007bff);
text-decoration: none;
display: inline-block;
font-size: 0.9rem;
}
.error-message {
background-color: #f8d7da;
border: 1px solid #f5c2c7;
border-radius: 0.25rem;
padding: 1.5rem;
margin: 1rem 0;
}
.error-message h3 {
margin: 0 0 0.5rem 0;
color: #721c24;
}
.error-message p {
margin: 0.5rem 0;
color: #721c24;
}
.error-message code {
background-color: #f8d7da;
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
font-family: monospace;
}
.error-message a {
color: #721c24;
text-decoration: underline;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<Section title="Login to OliveTin" class="small">
<div class="login-form">
<div v-if="!hasOAuth && !hasLocalLogin" class="login-disabled">
<span>This server is not configured with either OAuth, or local users, so you cannot login.</span>
</div>
<div v-if="hasOAuth" class="login-oauth2">
<h3>OAuth Login</h3>
<div class="oauth-providers">
<button v-for="provider in oauthProviders" :key="provider.key" class="oauth-button"
@click="loginWithOAuth(provider)">
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
<span class="provider-name">Login with {{ provider.title }}</span>
</button>
</div>
</div>
<div v-if="hasLocalLogin" class="login-local">
<h3>Local Login</h3>
<form @submit.prevent="handleLocalLogin" class="local-login-form">
<div v-if="loginError" class="bad">
{{ loginError }}
</div>
<input id="username" v-model="username" type="text" name="username" autocomplete="username" required placeholder="Username" />
<input id="password" v-model="password" type="password" name="password" autocomplete="current-password" placeholder="Password"
required />
<button type="submit" :disabled="loading" class="login-button">
{{ loading ? 'Logging in...' : 'Login' }}
</button>
</form>
</div>
</div>
</Section>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import Section from 'picocrank/vue/components/Section.vue'
const router = useRouter()
const username = ref('')
const password = ref('')
const loading = ref(false)
const loginError = ref('')
const hasOAuth = ref(false)
const hasLocalLogin = ref(false)
const oauthProviders = ref([])
function loadLoginOptions() {
// Use the init response data that was loaded in App.vue
if (window.initResponse) {
hasOAuth.value = window.initResponse.oAuth2Providers && window.initResponse.oAuth2Providers.length > 0
hasLocalLogin.value = window.initResponse.authLocalLogin
if (hasOAuth.value) {
oauthProviders.value = window.initResponse.oAuth2Providers
}
} else {
console.warn('Init response not available yet, login options will be empty')
}
}
async function handleLocalLogin() {
loading.value = true
loginError.value = ''
try {
const response = await window.client.localUserLogin({
username: username.value,
password: password.value
})
if (response.success) {
// Re-initialize to get updated user context
try {
const initResponse = await window.client.init({})
window.initResponse = initResponse
window.initError = false
window.initErrorMessage = ''
window.initCompleted = true
// Update the header with new user info
if (window.updateHeaderFromInit) {
window.updateHeaderFromInit()
}
} catch (initErr) {
console.error('Failed to reinitialize after login:', initErr)
}
// Redirect to home page on successful login
router.push('/')
} else {
loginError.value = 'Login failed. Please check your credentials.'
}
} catch (err) {
console.error('Login error:', err)
loginError.value = err.message || 'Network error. Please try again.'
} finally {
loading.value = false
}
}
function loginWithOAuth(provider) {
if (!provider.key) {
console.error('OAuth provider missing key:', provider)
return
}
const providerKey = encodeURIComponent(provider.key)
window.location.href = `/oauth/login?provider=${providerKey}`
}
onMounted(() => {
loadLoginOptions()
// Also watch for when init response becomes available
const stopWatcher = watch(() => window.initResponse, () => {
loadLoginOptions()
}, { immediate: true })
})
</script>
<style scoped>
section {
margin: auto;
}
.login-view {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
form {
grid-template-columns: 1fr;
gap: 1em;
}
.provider-icon {
width: 1em;
height: 1em;
margin-right: .4em;
display: inline-flex;
vertical-align: middle;
}
</style>

View File

@@ -0,0 +1,230 @@
<template>
<Section :title="t('logs.title')" :padding="false">
<template #toolbar>
<label class="input-with-icons">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
</svg>
<input :placeholder="t('search-filter')" v-model="searchText" />
<button :title="t('logs.clear-filter')" :disabled="!searchText" @click="clearSearch">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor"
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
</svg>
</button>
</label>
</template>
<p class = "padding">{{ t('logs.page-description') }}</p>
<div v-show="filteredLogs.length > 0">
<table class="logs-table">
<thead>
<tr>
<th>{{ t('logs.timestamp') }}</th>
<th>{{ t('logs.action') }}</th>
<th>{{ t('logs.metadata') }}</th>
<th>{{ t('logs.status') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
<td>
<span class="icon" v-html="log.actionIcon"></span>
<router-link :to="`/logs/${log.executionTrackingId}`">
{{ log.actionTitle }}
</router-link>
</td>
<td class="tags">
<span class="annotation">
<span class="annotation-key">User:</span>
<span class="annotation-val">{{ log.user }}</span>
</span>
<span v-if="log.tags && log.tags.length > 0" class="tag-list">
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
</span>
</td>
<td class="exit-code">
<ActionStatusDisplay :logEntry="log" />
</td>
</tr>
</tbody>
</table>
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" @page-change="handlePageChange" class = "padding"
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
</div>
<div v-show="logs.length === 0" class="empty-state">
<p>{{ t('logs.no-logs-to-display') }}</p>
<router-link to="/">{{ t('return-to-index') }}</router-link>
</div>
</Section>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import Pagination from 'picocrank/vue/components/Pagination.vue'
import Section from 'picocrank/vue/components/Section.vue'
import { useI18n } from 'vue-i18n'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
const logs = ref([])
const searchText = ref('')
const pageSize = ref(10)
const currentPage = ref(1)
const loading = ref(false)
const totalCount = ref(0)
const { t } = useI18n()
const filteredLogs = computed(() => {
let result = logs.value
if (searchText.value) {
const searchLower = searchText.value.toLowerCase()
result = logs.value.filter(log =>
log.actionTitle.toLowerCase().includes(searchLower)
)
}
// Sort by timestamp with most recent first
return [...result].sort((a, b) => {
const dateA = a.datetimeStarted ? new Date(a.datetimeStarted).getTime() : 0
const dateB = b.datetimeStarted ? new Date(b.datetimeStarted).getTime() : 0
return dateB - dateA // Descending order (most recent first)
})
})
async function fetchLogs() {
loading.value = true
try {
const startOffset = (currentPage.value - 1) * pageSize.value
const args = {
"startOffset": BigInt(startOffset),
}
const response = await window.client.getLogs(args)
logs.value = response.logs
pageSize.value = Number(response.pageSize) || 0
totalCount.value = Number(response.totalCount) || 0
} catch (err) {
console.error('Failed to fetch logs:', err)
window.showBigError('fetch-logs', 'getting logs', err, false)
} finally {
loading.value = false
}
}
function clearSearch() {
searchText.value = ''
}
function formatTimestamp(timestamp) {
if (!timestamp) return 'Unknown'
try {
const date = new Date(timestamp)
return date.toLocaleString()
} catch (err) {
return timestamp
}
}
function handlePageChange(page) {
currentPage.value = page
fetchLogs()
}
function handlePageSizeChange(newPageSize) {
pageSize.value = newPageSize
currentPage.value = 1 // Reset to first page
}
onMounted(() => {
fetchLogs()
})
</script>
<style scoped>
.logs-view {
padding: 1rem;
}
.input-with-icons {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
background: var(--section-background);
width: 100%;
max-width: 300px;
}
.input-with-icons input {
border: none;
outline: none;
background: transparent;
flex: 1;
color: var(--text-primary);
}
.input-with-icons button {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
border-radius: 3px;
}
.input-with-icons button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.timestamp {
font-family: monospace;
font-size: 0.875rem;
color: #666;
}
.icon {
margin-right: 0.5rem;
font-size: 1.2em;
}
.content {
color: #007bff;
text-decoration: none;
cursor: pointer;
}
.content:hover {
text-decoration: underline;
}
.annotation {
font-weight: 500;
font-size: smaller;
}
.empty-state {
text-align: center;
padding: 2rem;
color: #666;
}
.empty-state a {
color: #007bff;
text-decoration: none;
}
.empty-state a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="not-found-view">
<div class="not-found-container">
<div class="not-found-content">
<h1>404</h1>
<h2>Page Not Found</h2>
<div class="actions">
<button class = "button good" @click="goToHome">
Go to Home
</button>
<button class="button neutral" @click="goBack">
Go Back
</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'NotFoundView',
methods: {
goBack() {
this.$router.go(-1)
},
goToHome() {
this.$router.push('/')
}
}
}
</script>
<style scoped>
.not-found-content {
padding: 3rem 2rem;
text-align: center;
}
.not-found-content h1 {
font-size: 6rem;
margin: 0;
font-weight: 700;
line-height: 1;
}
.not-found-content h2 {
font-size: 2rem;
margin: 0 0 1rem 0;
color: #333;
}
.not-found-content p {
font-size: 1.1rem;
color: #666;
margin-bottom: 2rem;
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<Section title="User Information" class="small">
<div v-if="!isLoggedIn" class="user-not-logged-in">
<p>You are not currently logged in.</p>
<p>To access user settings and logout, please <router-link to="/login">log in</router-link>.</p>
</div>
<div v-else class="user-control-panel">
<dl class="user-info">
<dt>Username</dt>
<dd>{{ username }}</dd>
<dt v-if="userProvider !== 'system'">Provider</dt>
<dd v-if="userProvider !== 'system'">{{ userProvider }}</dd>
<dt v-if="usergroup">Group</dt>
<dd v-if="usergroup">{{ usergroup }}</dd>
<dt v-if="acls && acls.length > 0">Matched ACLs</dt>
<dd v-if="acls && acls.length > 0">
<span class="acl-tag" v-for="(acl, idx) in acls" :key="`acl-${idx}`">{{ acl }}</span>
</dd>
</dl>
<div class="user-actions">
<div class="action-buttons">
<button @click="handleLogout" class="button bad" :disabled="loggingOut">
{{ loggingOut ? 'Logging out...' : 'Logout' }}
</button>
</div>
</div>
</div>
</Section>
</template>
<script setup>
import { ref, onMounted, watch, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import Section from 'picocrank/vue/components/Section.vue'
const router = useRouter()
const isLoggedIn = ref(false)
const username = ref('guest')
const userProvider = ref('system')
const usergroup = ref('')
const loggingOut = ref(false)
const acls = ref([])
function updateUserInfo() {
if (window.initResponse) {
isLoggedIn.value = window.initResponse.authenticatedUser !== '' && window.initResponse.authenticatedUser !== 'guest'
username.value = window.initResponse.authenticatedUser
userProvider.value = window.initResponse.authenticatedUserProvider || 'system'
usergroup.value = window.initResponse.effectivePolicy?.usergroup || ''
}
}
async function fetchWhoAmI() {
try {
const res = await window.client.whoAmI({})
acls.value = res.acls || []
// Update usergroup from authoritative WhoAmI response
if (res.usergroup) {
usergroup.value = res.usergroup
}
} catch (e) {
console.warn('Failed to fetch WhoAmI for ACLs', e)
acls.value = []
}
}
async function handleLogout() {
loggingOut.value = true
try {
await window.client.logout({})
// Re-initialize to get updated user context (should be guest)
try {
const initResponse = await window.client.init({})
window.initResponse = initResponse
window.initError = false
window.initErrorMessage = ''
window.initCompleted = true
// Update the header with new user info
if (window.updateHeaderFromInit) {
window.updateHeaderFromInit()
}
} catch (initErr) {
console.error('Failed to reinitialize after logout:', initErr)
}
// Redirect based on init response: if login is required, go to login page
if (window.initResponse && window.initResponse.loginRequired) {
router.push('/login')
} else {
router.push('/')
}
} catch (err) {
console.error('Logout error:', err)
} finally {
loggingOut.value = false
}
}
let watchInterval = null
onMounted(() => {
updateUserInfo()
fetchWhoAmI()
})
onUnmounted(() => {
if (watchInterval) {
clearInterval(watchInterval)
}
})
</script>
<style scoped>
section {
margin: auto;
}
.user-not-logged-in {
padding: 2rem;
text-align: center;
}
.user-not-logged-in p {
margin: 1rem 0;
}
.user-control-panel {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
.action-buttons {
display: flex;
gap: 1rem;
}
.acl-tag {
display: inline-block;
background: var(--section-background);
border: 1px solid var(--border-color);
border-radius: 0.25rem;
padding: 0.1rem 0.4rem;
margin: 0 0.25rem 0.25rem 0;
font-size: 0.85rem;
}
.button {
padding: 0.75rem 1.5rem;
border-radius: 4px;
border: none;
cursor: pointer;
text-align: center;
font-weight: 500;
transition: background-color 0.2s;
}
.button.bad {
background-color: #dc3545;
color: white;
}
.button.bad:hover:not(:disabled) {
background-color: #c82333;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>

39
frontend/style.css Normal file
View File

@@ -0,0 +1,39 @@
header {
position: fixed;
width: 100%;
z-index: 5;
}
aside {
padding-top: 4em;
z-index: 3; /* Make sure the sidebar is on top of the terminal */
}
main {
padding-top: 4em;
}
dialog {
border-radius: 1em;
}
section {
padding: 0;
}
aside .flex-row {
padding-left: 1em;
padding-right: .5em;
}
#sidebar-toggler-button {
margin-right: .5em;
}
div.buttons button svg {
vertical-align: middle;
}
section.small {
border-radius: .4em;
}

24
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
Components({
dirs: ['resources/vue/'],
extensions: ['vue'],
deep: true,
dts: false,
}),
vue(),
],
server: {
proxy: {
'/api': {
target: 'http://localhost:1337',
changeOrigin: true,
secure: false,
}
},
},
})

123
go.mod
View File

@@ -1,123 +0,0 @@
module github.com/OliveTin/OliveTin
go 1.18
require (
github.com/bufbuild/buf v1.29.0
github.com/fsnotify/fsnotify v1.6.0
github.com/fzipp/gocyclo v0.6.0
github.com/go-critic/go-critic v0.11.1
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.4.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c
google.golang.org/grpc v1.62.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
google.golang.org/protobuf v1.32.0
gopkg.in/yaml.v3 v3.0.1
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1 // indirect
connectrpc.com/connect v1.15.0 // indirect
connectrpc.com/otelconnect v0.7.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/bufbuild/protocompile v0.8.0 // indirect
github.com/bufbuild/protovalidate-go v0.5.2 // indirect
github.com/bufbuild/protoyaml-go v0.1.8 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/cristalhq/acmd v0.11.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/cli v25.0.3+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v25.0.5+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/fgprof v0.9.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/pkgload v1.2.2 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/cel-go v0.20.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-containerregistry v0.19.0 // indirect
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jdx/go-netrc v1.0.0 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quasilyte/go-ruleguard v0.4.2 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/cors v1.10.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tetratelabs/wazero v1.6.0 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.18.0 // indirect
google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

697
go.sum
View File

@@ -1,697 +0,0 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1 h1:AmmAwHbvaeOIxDKG2+aTn5C36HjmFIMkrdTp49rp80Q=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.32.0-20240221180331-f05a6f4403ce.1/go.mod h1:tiTMKD8j6Pd/D2WzREoweufjzaJKHZg35f/VGcZ2v3I=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
connectrpc.com/connect v1.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo=
connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA=
connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY=
connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/bufbuild/buf v1.29.0 h1:llP6HqOcCaSGBxOfnrp/mwvcY1O/dciEOl1QaMEOB3M=
github.com/bufbuild/buf v1.29.0/go.mod h1:UTjvPXTObvKQiGqxod32wt9zRz70TJsMpaigpbIZGuc=
github.com/bufbuild/protocompile v0.8.0 h1:9Kp1q6OkS9L4nM3FYbr8vlJnEwtbpDPQlQOVXfR+78s=
github.com/bufbuild/protocompile v0.8.0/go.mod h1:+Etjg4guZoAqzVk2czwEQP12yaxLJ8DxuqCJ9qHdH94=
github.com/bufbuild/protovalidate-go v0.5.2 h1:MPNZd6F2ekGWjWVQDv8lEYOX8ndSOzMnmTaGbDZWIcg=
github.com/bufbuild/protovalidate-go v0.5.2/go.mod h1:DWCNjFl/HwtBiHyN5/3lKA+0MgXOlAoc3jk8Ps3iN+s=
github.com/bufbuild/protoyaml-go v0.1.8 h1:X9QDLfl9uEllh4gsXUGqPanZYCOKzd92uniRtW2OnAQ=
github.com/bufbuild/protoyaml-go v0.1.8/go.mod h1:R8vE2+l49bSiIExP4VJpxOXleHE+FDzZ6HVxr3cYunw=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/cristalhq/acmd v0.11.2 h1:ITIWtBRiYbmzk+i8xQgH2RzfCVMII+dOd0CtGWVIhaU=
github.com/cristalhq/acmd v0.11.2/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284=
github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-critic/go-critic v0.11.1 h1:/zBseUSUMytnRqxjlsYNbDDxpu3R2yH8oLXo/FOE8b8=
github.com/go-critic/go-critic v0.11.1/go.mod h1:aZVQR7+gazH6aDEQx4356SD7d8ez8MipYjXbEl5JAKA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=
github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/cel-go v0.20.0 h1:h4n6DOCppEMpWERzllyNkntl7JrDyxoE543KWS6BLpc=
github.com/google/cel-go v0.20.0/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic=
github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
github.com/jhump/protoreflect v1.15.4 h1:mrwJhfQGGljwvR/jPEocli8KA6G9afbQpH8NY2wORcI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs=
github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225 h1:BzKNaIRXh1bD+1557OcFIHlpYBiVbK4zEyn8zBHi1SE=
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c h1:Zmyn5CV/jxzKnF+3d+xzbomACPwLQqVpLTpyXN5uTaQ=
google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c h1:9g7erC9qu44ks7UK4gDNlnk4kOxZG707xKm4jVniy6o=
google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,3 +1,4 @@
---
recursive: true
require:
- mochaSetup.mjs

View File

@@ -1,6 +1,15 @@
default:
default: test-install test-run
test-install:
npm install --no-fund
./node_modules/.bin/mocha
test-run:
# GitHub Actions fails badly on the default timeout of 2000ms
npx mocha tests --recursive -t 10000
find-flakey-tests:
echo "Running test-run infinately"
sh -c "while make test-run; do :; done"
nginx:
podman-compose up -d nginx

View File

@@ -4,7 +4,8 @@
Vagrant.configure("2") do |config|
config.vm.provision "shell", inline: "mkdir /etc/OliveTin && chmod o+w /etc/OliveTin/ && mkdir -p /opt/OliveTin-configs/ && chmod 0777 /opt/OliveTin-configs", privileged: true
config.vm.provision "file", source: "configs/.", destination: "/opt/OliveTin-configs/"
config.vm.provision "file", source: "tests/.", destination: "/tmp/test-configs/"
config.vm.provision "shell", inline: "for dir in /tmp/test-configs/*/; do if [ -f \"$dir/config.yaml\" ]; then cp -r \"$dir\" /opt/OliveTin-configs/$(basename \"$dir\")/; fi; done", privileged: true
config.vm.provider :libvirt do |libvirt|
libvirt.management_network_device = 'virbr0'

View File

@@ -1,5 +1,139 @@
import { By } from 'selenium-webdriver'
import fs from 'fs'
import { expect } from 'chai'
import { Condition } from 'selenium-webdriver'
export async function getActionButtons (webdriver) {
return await webdriver.findElement(By.id('contentActions')).findElements(By.tagName('button'))
export async function getActionButtons () {
// Currently, only the active dashboard's contents are rendered,
// so we don't need to scope the selector by dashboard title.
return await webdriver.findElements(By.css('.action-button button'))
}
export async function getExecutionDialogOutput() {
await webdriver.wait(new Condition('Dialog with long int is visible', async () => {
const dialog = await webdriver.findElement({ id: 'execution-results-popup' })
return await dialog.isDisplayed()
}));
const ret = await webdriver.executeScript('return window.logEntries.get(window.executionDialog.executionTrackingId).output')
return ret
}
export async function closeExecutionDialog() {
const btnClose = await webdriver.findElements(By.css('[title="Close"]'))
await btnClose[0].click()
}
export function takeScreenshotOnFailure (test, webdriver) {
if (test.state === 'failed') {
const title = test.fullTitle();
console.log(`Test failed, taking screenshot: ${title}`);
takeScreenshot(webdriver, title);
}
}
export function takeScreenshot (webdriver, title) {
return webdriver.takeScreenshot().then((img) => {
fs.mkdirSync('screenshots', { recursive: true });
title = title.replaceAll('config: ', '')
title = title.replaceAll(/[\(\)\|\*\<\>\:]/g, "_")
title = title + '.failed-test'
fs.writeFileSync('screenshots/' + title + '.png', img, 'base64')
})
}
export async function getRootAndWait() {
await webdriver.get(runner.baseUrl())
await webdriver.wait(new Condition('wait for loaded-dashboard', async function() {
const body = await webdriver.findElement(By.tagName('body'))
const attr = await body.getAttribute('loaded-dashboard')
console.log('loaded-dashboard: ', attr)
if (attr) {
return true
} else {
return false
}
}))
}
export async function closeSidebar() {
await webdriver.findElement(By.id('sidebar-toggler-button')).click()
const sidebar = await webdriver.findElement(By.id('mainnav'))
const neededLeft = '-250px' // Assuming sidebar is closed at this position
let lastLeft = ''
await webdriver.wait(new Condition('wait for sidebar to close', async function() {
const left = await sidebar.getCssValue('left')
if (left !== lastLeft) {
lastLeft = left
console.log('Sidebar left changed to: ', left)
return false
} else {
console.log('Sidebar closed, left is: *' + left, left === neededLeft ? ' (as expected)' : '')
return left === neededLeft
}
}), 10000); // Wait up to 10 seconds for the sidebar to close
}
export async function openSidebar() {
await webdriver.findElement(By.id('sidebar-toggler-button')).click()
const sidebar = await webdriver.findElement(By.id('mainnav'))
let lastLeft = 0
await webdriver.wait(new Condition('wait for sidebar to open', async function() {
const left = await sidebar.getCssValue('left')
if (left !== lastLeft) {
lastLeft = left
console.log('Sidebar left changed to: ', left)
return false
} else {
console.log('Sidebar opened, left is: ', left)
return true
}
}));
}
export async function getNavigationLinks() {
const navigationLinks = await webdriver.findElements(By.css('.navigation-links li'))
return navigationLinks
}
export async function requireExecutionDialogStatus (webdriver, expected) {
await webdriver.wait(new Condition('wait for action to be running', async function () {
const dialogStatus = await webdriver.findElement(By.id('execution-dialog-status'))
const actual = await dialogStatus.getText()
if (actual === expected) {
return true
} else {
console.log('Waiting for domStatus text to be: ', expected, ', it is currently: ', actual)
return false
}
}))
}
export async function findExecutionDialog (webdriver) {
return webdriver.findElement(By.id('execution-results-popup'))
}
export async function getActionButton (webdriver, title) {
const buttons = await webdriver.findElements(By.css('[title="' + title + '"]'))
expect(buttons).to.have.length(1)
return buttons[0]
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,14 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"license": "AGPL-3.0-only",
"devDependencies": {
"chai": "^4.3.10",
"eslint": "^8.51.0",
"mocha": "^10.2.0",
"selenium-webdriver": "4.12.0"
"chai": "^6.2.1",
"eslint": "^9.39.1",
"mocha": "^11.7.5",
"selenium-webdriver": "^4.38.0"
},
"dependencies": {
"wait-on": "^7.2.0"
"wait-on": "^9.0.3"
}
}

View File

@@ -0,0 +1,2 @@
make:
haproxy -f ./haproxy.conf

View File

@@ -1,15 +1,32 @@
global
stats socket /var/run/haproxy/admin.sock mode 660 level admin
log stdout local0
frontend cleartext_frontend
bind 0.0.0.0:80
bind 0.0.0.0:8080
timeout client 10s
stats enable
stats uri /stats
stats refresh 30s
log global
option httplog
option dontlognull
log-format "[%t] %ci:%cp -> BACKEND:%b/%s | %HM %HU | Status:%ST | Bytes:%B"
mode http
use_backend be_olivetin_webs if { hdr(Host) -i olivetin.example.com && path_beg /websocket }
use_backend be_olivetin_http if { hdr(Host) -i olivetin.example.com }
use_backend be_olivetin if { hdr(Host) -i -m beg olivetin.example.com }
backend be_olivetin_http
server olivetinServer 127.0.0.1:1337 check
backend be_olivetin_webs
backend be_olivetin
mode http
timeout connect 10s
timeout server 10s
timeout tunnel 1h
option http-server-close
server olivetinServer 127.0.0.1:1337
http-request set-header X-Forwarded-User "Alice"
http-request set-header X-Forwarded-Group "Group1,Group2"
server olivetinServer 127.0.0.1:1337 check

View File

@@ -25,44 +25,86 @@ class OliveTinTestRunner {
baseUrl() {
return this.BASE_URL
}
metricsUrl() {
return new URL('metrics', this.baseUrl());
}
}
class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
async start (cfg) {
this.ot = spawn('./../OliveTin', ['-configdir', 'configs/' + cfg + '/'])
let stdout = ""
let stderr = ""
const logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
console.log(" OliveTin starting local process...")
if (logStdout) {
this.ot.stdout.on('data', (data) => {
console.log(`stdout: ${data}`)
})
this.ot = spawn('./../service/OliveTin', ['-configdir', 'tests/' + cfg + '/'])
this.ot.stderr.on('data', (data) => {
console.error(`stderr: ${data}`)
})
let logStdout = false
if (process.env.CI === 'true') {
logStdout = true;
} else {
logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
}
this.ot.on('close', (code) => {
if (code != null) {
console.log(`child process exited with code ${code}`)
this.ot.stdout.on('data', (data) => {
stdout += data
if (logStdout) {
console.log(`stdout: ${data}`)
}
})
/*
this.server = await startSomeServer({port: process.env.TEST_PORT});
console.log(`server running on port ${this.server.port}`);
*/
this.ot.stderr.on('data', (data) => {
stderr += data
this.BASE_URL = 'http://localhost:1337/'
await waitOn({
resources: [this.BASE_URL]
if (logStdout) {
console.log(`stderr: ${data}`)
}
})
this.ot.on('close', (code) => {
if (code != null) {
console.log(`OliveTin local process exited with code ${code}`)
console.log(stdout)
console.log(stderr)
console.log(this.ot.exitCode)
}
})
if (this.ot.exitCode == null) {
this.BASE_URL = 'http://localhost:1337/'
console.log(" OliveTin waiting for local process to start...")
await waitOn({
resources: [this.BASE_URL]
})
console.log(" OliveTin local process started and webUI accessible")
} else {
console.log(" OliveTin local process start FAILED!")
console.log(stdout)
console.log(stderr)
console.log(this.ot.exitCode)
}
}
async stop () {
await this.ot.kill()
if ((await this.ot.exitCode) != null) {
console.log(" OliveTin local process tried stop(), but it already exited with code", this.ot.exitCode)
} else {
await this.ot.kill()
console.log(" OliveTin local process killed")
}
if (process.env.CI === 'true') {
// GitHub runners seem to need a bit more time to clean up
await new Promise((res) => setTimeout(res, 3000))
} else {
await new Promise((res) => setTimeout(res, 100))
}
}
}

View File

@@ -1,37 +0,0 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By } from 'selenium-webdriver'
//import * as waitOn from 'wait-on'
describe('config: general', function () {
before(async function () {
await runner.start('general')
})
after(async () => {
await runner.stop()
})
it('Page title', async function () {
await webdriver.get(runner.baseUrl())
const title = await webdriver.getTitle()
expect(title).to.be.equal("OliveTin")
})
it('Footer contains promo', async function () {
const ftr = await webdriver.findElement(By.tagName('footer')).getText()
expect(ftr).to.contain('Documentation')
})
it('Default buttons are rendered', async function() {
await webdriver.get(runner.baseUrl())
// await webdriver.manage().setTimeouts({ implicit: 2000 })
const buttons = await webdriver.findElement(By.id('root-group')).findElements(By.tagName('button'))
expect(buttons).to.have.length(6)
})
})

View File

@@ -1,20 +0,0 @@
import { expect } from 'chai'
import { By } from 'selenium-webdriver'
describe('config: hiddenNav', function () {
before(async function () {
await runner.start('hiddenNav')
})
after(async () => {
await runner.stop()
})
it('nav is hidden', async () => {
await webdriver.get(runner.baseUrl())
const toggler = await webdriver.findElement(By.id('sidebar-toggle-wrapper'))
expect(await toggler.isDisplayed()).to.be.false
})
})

View File

@@ -1,46 +0,0 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until } from 'selenium-webdriver'
import { getActionButtons } from '../lib/elements.js'
describe('config: multipleDropdowns', function () {
before(async function () {
await runner.start('multipleDropdowns')
})
after(async () => {
await runner.stop()
})
it('Multiple dropdowns are possible', async function() {
await webdriver.get(runner.baseUrl())
await webdriver.manage().setTimeouts({ implicit: 2000 })
const buttons = await getActionButtons(webdriver)
let button = null
for (const b of buttons) {
const title = await b.getAttribute('title')
console.log('title: ' + title)
if (title === 'Test multiple dropdowns') {
button = b
}
}
expect(buttons).to.have.length(2)
expect(button).to.not.be.null
await button.click()
const dialog = await webdriver.findElement(By.id('argument-popup'))
await webdriver.wait(until.elementIsVisible(dialog), 3500)
const selects = await dialog.findElements(By.tagName('select'))
expect(selects).to.have.length(2)
expect(await selects[0].findElements(By.tagName('option'))).to.have.length(2)
expect(await selects[1].findElements(By.tagName('option'))).to.have.length(3)
})
})

View File

@@ -0,0 +1,39 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until } from 'selenium-webdriver'
import {
getRootAndWait,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: authRequireGuestsToLogin', function () {
this.timeout(30000)
before(async function () {
await runner.start('authRequireGuestsToLogin')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Guest is redirected to login', async function () {
// Don't use getRootAndWait here because we want to test the redirect, and getRootAndWait waits for the dashboard to load
await webdriver.get(runner.baseUrl())
await webdriver.wait(until.urlContains('/login'), 10000)
// Verify login UI elements are present
const loginElements = await webdriver.findElements(By.css('form.local-login-form, .login-oauth2, .login-disabled'))
expect(loginElements.length).to.be.greaterThan(0)
console.log('✓ Login page loaded correctly')
})
})

View File

@@ -0,0 +1,53 @@
#
# Integration Test Config: Require Guests to Login
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
# Require guests to login
authRequireGuestsToLogin: true
# Enable local user authentication
authLocalUsers:
enabled: true
users:
- username: "alice"
usergroup: "admins"
password: "$argon2id$v=19$m=65536,t=4,p=6$ORxyZZGW6E3FWZnbQmHJ9Q$BzIOWeXry/BZ6+JV1T4UASBnebVLB9QJ4f5TmUPXsg4" # notsecret: password
- username: "bob"
usergroup: "users"
password: "$argon2id$v=19$m=65536,t=4,p=6$ORxyZZGW6E3FWZnbQmHJ9Q$BzIOWeXry/BZ6+JV1T4UASBnebVLB9QJ4f5TmUPXsg4" # notsecret: password
accessControlLists:
- name: "admin"
matchUsergroups: ["admins"]
addToEveryAction: true
permissions:
view: true
exec: true
logs: true
kill: true
- name: "users"
matchUsergroups: ["users"]
addToEveryAction: true
permissions:
view: true
exec: false
logs: false
kill: false
# Simple actions for testing
actions:
- title: Ping Google.com
shell: ping google.com -c 1
icon: ping
- title: sleep 2 seconds
shell: sleep 2
icon: "&#x1F971"

View File

@@ -0,0 +1,36 @@
logLevel: debug
actions:
- title: Ping
shell: echo "Ping executed"
icon: ping
- title: Action 1
shell: echo "Action 1 executed"
icon: check
- title: Action 2
shell: echo "Action 2 executed"
icon: check
- title: Action 3
shell: echo "Action 3 executed"
icon: check
- title: Action 4
shell: echo "Action 4 executed"
icon: check
dashboards:
- title: Test
contents:
# Uncomment to see the dashboard with the "Ping" action only
- title: Ping
- title: Fieldset 1
type: fieldset
contents:
- title: Action 1
- title: Action 2
- title: Fieldset 2
type: fieldset
contents:
- title: Action 3
- title: Action 4

View File

@@ -0,0 +1,63 @@
import { describe, it, before, after } from 'mocha'
import { expect, assert } from 'chai'
import { By, until, Condition } from 'selenium-webdriver'
//import * as waitOn from 'wait-on'
import {
getRootAndWait,
getActionButtons,
openSidebar,
getNavigationLinks,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: dashboards with basic fieldsets', function () {
before(async function () {
await runner.start('dashboardsWithBasicFieldsets')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Dashboards with basic fieldsets', async function () {
await getRootAndWait()
const title = await webdriver.getTitle()
expect(title).to.be.equal("Test - OliveTin")
await openSidebar()
const navigationLinks = await getNavigationLinks()
assert.equal(navigationLinks.length, 5, 'Expected the nav to only have 5 links') // test dashboard + logs + diagnostics + entities + separator
const firstLink = await navigationLinks[0]
expect(await firstLink.getAttribute('title')).to.be.equal('Test', 'Expected the first link to be the actions link')
const actionButtons = await getActionButtons()
expect(actionButtons).to.have.length(5, 'Expected 5 action buttons')
// Check that we have the expected number of fieldsets
const dashboardRows = await webdriver.findElements(By.css('.dashboard-row'))
expect(dashboardRows).to.have.length(3, 'Expected 3 dashboard rows total')
// Check that we have fieldsets with the expected titles
const fieldsetTitles = []
for (let i = 0; i < dashboardRows.length; i++) {
const titleElements = await dashboardRows[i].findElements(By.css('h2'))
if (titleElements.length > 0) {
const title = await titleElements[0].getText()
fieldsetTitles.push(title)
}
}
// We should have fieldsets for: Fieldset 1, Fieldset 2, and Actions fieldsets
expect(fieldsetTitles).to.include('Fieldset 1')
expect(fieldsetTitles).to.include('Fieldset 2')
})
})

View File

@@ -0,0 +1,16 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: Test datetime argument
shell: "echo 'Selected datetime: {{ datetime }}'"
icon: ping
arguments:
- name: datetime
title: Select a date and time
type: datetime
description: Choose a date and time for the action

View File

@@ -0,0 +1,118 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, Condition } from 'selenium-webdriver'
import {
getRootAndWait,
getActionButton,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: datetime', function () {
before(async function () {
await runner.start('datetime')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver)
})
it('Datetime argument uses datetime-local input type', async function () {
await getRootAndWait()
const btn = await getActionButton(webdriver, 'Test datetime argument')
await btn.click()
// Wait for navigation to argument form page
await webdriver.wait(
new Condition('wait for argument form page', async () => {
const url = await webdriver.getCurrentUrl()
return url.includes('/actionBinding/') && url.includes('/argumentForm')
}),
8000
)
// Find the datetime input field
const datetimeInput = await webdriver.findElement(By.id('datetime'))
// Verify it's a datetime-local input type
const inputType = await datetimeInput.getAttribute('type')
expect(inputType).to.equal('datetime-local', 'Input type should be datetime-local')
// Verify it has the step attribute set to 1 (for seconds precision)
const step = await datetimeInput.getAttribute('step')
expect(step).to.equal('1', 'Step attribute should be 1')
// Verify the label is present
const label = await webdriver.findElement(By.css('label[for="datetime"]'))
expect(await label.getText()).to.contain('Select a date and time')
})
it('Datetime argument can be filled and submitted', async function () {
await getRootAndWait()
const btn = await getActionButton(webdriver, 'Test datetime argument')
await btn.click()
// Wait for navigation to argument form page
await webdriver.wait(
new Condition('wait for argument form page', async () => {
const url = await webdriver.getCurrentUrl()
return url.includes('/actionBinding/') && url.includes('/argumentForm')
}),
8000
)
// Find the datetime input field
const datetimeInput = await webdriver.findElement(By.id('datetime'))
// Set a datetime value (format: YYYY-MM-DDTHH:mm)
// datetime-local returns values without seconds, backend will add :00
const testDateTime = '2023-12-25T15:30'
// Use JavaScript to set the value directly (more reliable for datetime-local inputs)
await webdriver.executeScript(
'arguments[0].value = arguments[1]',
datetimeInput,
testDateTime
)
// Trigger input event to ensure Vue reactivity
await webdriver.executeScript(
'arguments[0].dispatchEvent(new Event("input", { bubbles: true }))',
datetimeInput
)
// Small wait for Vue to process the change
await webdriver.sleep(100)
// Verify the value was set
const value = await datetimeInput.getAttribute('value')
expect(value).to.equal(testDateTime)
// Find and click the submit button
const submitButton = await webdriver.findElement(
By.css('button[name="start"]')
)
await submitButton.click()
// Wait for navigation to logs page
await webdriver.wait(
new Condition('wait for logs page', async () => {
const url = await webdriver.getCurrentUrl()
return url.includes('/logs/')
}),
8000
)
// Verify we're on the logs page (action was executed)
const url = await webdriver.getCurrentUrl()
expect(url).to.include('/logs/')
})
})

View File

@@ -0,0 +1,21 @@
#
# Integration Test Config: emptyDashboardsAreHidden
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: Ping
shell: ping example.com
icon: ping
entity: server
dashboards:
- title: Empty Dashboard
contents: []

View File

@@ -0,0 +1,37 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until, Condition } from 'selenium-webdriver'
//import * as waitOn from 'wait-on'
import {
getRootAndWait,
openSidebar,
getNavigationLinks,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: empty dashboards are hidden', function () {
before(async function () {
await runner.start('emptyDashboardsAreHidden')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Test hidden dashboard', async function () {
await getRootAndWait()
await openSidebar()
const title = await webdriver.getTitle()
expect(title).to.be.equal("Actions - OliveTin")
const navigationLinks = await getNavigationLinks()
expect(navigationLinks).to.not.be.empty
expect(navigationLinks.length).to.be.equal(4, 'Expected the nav to only have 4 links')
})
})

View File

@@ -0,0 +1,18 @@
#
# Integration Test Config: Entities
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: Ping {{ server.hostname }}
shell: ping {{ server.hostname }}
icon: ping
entity: server
entities:
- file: entities/servers.yaml
name: server

View File

@@ -0,0 +1,42 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until } from 'selenium-webdriver'
import {
getRootAndWait,
takeScreenshot,
takeScreenshotOnFailure,
} from '../../lib/elements.js'
describe('config: entities', function () {
before(async function () {
await runner.start('entities')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Entity buttons are rendered', async function() {
await getRootAndWait()
// The old test was looking for #root-group, but that doesn't exist in the new Vue UI
// Instead, we should look for action buttons directly
const actionButtons = await webdriver.findElements(By.css('.action-button button'))
expect(actionButtons).to.not.be.null
expect(actionButtons).to.have.length(3)
expect(await actionButtons[0].getAttribute('title')).to.be.equal('Ping server1')
expect(await actionButtons[1].getAttribute('title')).to.be.equal('Ping server2')
expect(await actionButtons[2].getAttribute('title')).to.be.equal('Ping server3')
// Check that there's no error dialog visible
const dialogErr = await webdriver.findElements(By.id('big-error'))
if (dialogErr.length > 0) {
expect(await dialogErr[0].isDisplayed()).to.be.false
}
})
})

View File

@@ -0,0 +1,3 @@
- hostname: server1
- hostname: server2
- hostname: server3

View File

@@ -0,0 +1,9 @@
actions:
- title: 'Test me {{ test_me.name }}'
popupOnStart: execution-dialog-stdout-only
entity: testrows
shell: echo "{{ test_me.val }}"
entities:
- name: testrows
file: entities/data.json

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