diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2db4266..0a5aabf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,6 +22,7 @@ Please put a X in the boxes as evidence of reading through the checklist. - [ ] `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 -wC frontend codestyle` runs without any issues. +- [ ] `make -w frontend-unittests` 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. diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index c8f7dca..163e7af 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -104,6 +104,9 @@ jobs: - name: unit tests run: make -w service-unittests + - name: frontend unit tests + run: make -w frontend-unittests + - name: build service run: make -w service diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 2ae1607..7009314 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -38,3 +38,6 @@ jobs: - name: frontend run: make -wC frontend codestyle + + - name: frontend unit tests + run: make -wC frontend unittests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e38c30..761ca62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,13 @@ repos: pass_filenames: false files: ^(frontend/|Makefile) + - id: frontend-unittests + name: frontend-unittests + entry: make frontend-unittests + language: system + pass_filenames: false + files: ^(frontend/|Makefile) + - id: service-unittests name: service-unittests entry: make service-unittests diff --git a/Makefile b/Makefile index 9fd420e..3ec7211 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,9 @@ service-codestyle: frontend-codestyle: $(MAKE) -wC frontend codestyle +frontend-unittests: + $(MAKE) -wC frontend unittests + it: $(MAKE) -wC integration-tests @@ -76,4 +79,4 @@ config-tool: devcheck: python3 scripts/devcheck.py $(ARGS) -.PHONY: proto service windows-resources windows-msi devcheck +.PHONY: proto service windows-resources windows-msi frontend-unittests devcheck diff --git a/config.yaml b/config.yaml index 37f555c..19da832 100644 --- a/config.yaml +++ b/config.yaml @@ -2,7 +2,8 @@ # one port (this is called the "Single HTTP Frontend") and means you just need # one open port in the container/firewalls/etc. # -# Listen on all addresses available, port 1337 +# Listen on all addresses available, port 1337. +# If the PORT environment variable is set, it overrides this port (host is kept). listenAddressSingleHTTPFrontend: 0.0.0.0:1337 # Choose from INFO (default), WARN and DEBUG @@ -73,7 +74,7 @@ actions: icon: backup onclick: execution-dialog # https://docs.olivetin.app/action_execution/oncalendar.html - execOnCalendarFile: examples/demo-olivetin-calendar.yaml + # execOnCalendarFile: examples/demo-olivetin-calendar.yaml - title: Verify backup archive shell: sleep 3 && echo "Backup archive verified" @@ -145,7 +146,7 @@ actions: title: Are you sure?! # Checklist arguments let users pick multiple predefined options. Selected - # values are passed to the action as a comma-separated string. + # values are passed to the action as a JSON array string. # # Docs: https://docs.olivetin.app/args/input_checklist.html - title: Backup selected directories @@ -165,7 +166,7 @@ actions: value: music - title: Videos value: videos - default: documents,photos + default: '["documents","photos"]' # 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. @@ -262,8 +263,8 @@ actions: icon: ping # https://docs.olivetin.app/action_execution/onfilecreated.html # mkdir -p /tmp/olivetin-demo-file-created - execOnFileCreatedInDir: - - /tmp/olivetin-demo-file-created + # execOnFileCreatedInDir: + # - /tmp/olivetin-demo-file-created - title: Start {{ .CurrentEntity.Names }} icon: box @@ -352,8 +353,10 @@ actionGroups: # # Docs: https://docs.olivetin.app/dashboards/intro.html dashboards: - # Top level items are dashboards. + # Top level items are dashboards. Optional `category` groups them under + # collapsible headings in the sidebar (same label = same group). - title: My Servers + category: Infrastructure contents: - title: All Servers type: fieldset @@ -399,8 +402,10 @@ dashboards: contents: - title: '{{ server.name }} Print server name' - # This is the second dashboard. + # Second dashboard — same category as My Servers, so both appear under + # "Infrastructure" in the sidebar. - title: My Containers + category: Infrastructure contents: - title: 'Container {{ .CurrentEntity.Names }} ({{ .CurrentEntity.Image }})' entity: container diff --git a/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc b/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc index 41f1341..c598a87 100644 --- a/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc +++ b/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc @@ -13,13 +13,15 @@ actions: shellAfterCompleted: "apprise -c /config/apprise.yml -t 'Notification: Backup script completed' -b \"$(printf 'Backup completed with exit code %s. Log: %s' {{ exitCode }} {{ output }})\"" ---- -When running shellAfterCompleted, you *cannot* use argument values - they are not passed to the command. However the following special arguments are defined; +When running shellAfterCompleted, you *cannot* use user-defined argument values - they are not passed to the command. However the following special arguments are defined; -* `{{ exitCode }}` - The exit code of the previous shell command. OliveTin substitutes this with the `EXITCODE` environment variable when running `shellAfterCompleted`, so shell metacharacters in the value cannot break quoting. -* `{{ output }}` - The standard output of the previous shell command. OliveTin substitutes this with the `OUTPUT` environment variable when running `shellAfterCompleted`, so shell metacharacters in command output cannot be executed. You can also reference `$OUTPUT` directly in your `shellAfterCompleted` command. Do not place these placeholders inside single-quoted shell arguments; single quotes prevent `$OUTPUT` and `$EXITCODE` from expanding after substitution. +* `{{ exitCode }}` / `{{ .Arguments.exitCode }}` - The exit code of the previous command. OliveTin rewrites these placeholders to the quoted `"$EXITCODE"` environment reference when running `shellAfterCompleted`, so shell metacharacters in the value cannot break quoting. Placeholders inside single-quoted shell arguments are rewritten so the environment reference can still expand. +* `{{ output }}` / `{{ .Arguments.output }}` - The standard output of the previous command. OliveTin rewrites these placeholders to the quoted `"$OUTPUT"` environment reference, so shell metacharacters in command output cannot be executed. You can also reference `$OUTPUT` directly in your `shellAfterCompleted` command. Placeholders inside single-quoted shell arguments are rewritten so the environment reference can still expand. * `{{ .Arguments.ot_executionTrackingId }}` - The unique execution tracking id for this execution (version 3k; in 2k use `{{ ot_executionTrackingId }}`) * `{{ .Arguments.ot_username }}` - The username of the user who started the execution (version 3k; in 2k use `{{ ot_username }}`). May be `guest` or `cron` for unauthenticated or automated runs. +Webhooks cannot use `shellAfterCompleted` (or `shell:`). Use `exec:` for webhook-triggered actions without an after-completion shell. See xref:action_execution/shellvsexec.adoc[Shell vs Exec]. + You can only use a single `shellAfterCompleted`, so use it for notifications, or similar. It would be an antipattern to use this do run 2 commands making up a mini script. The official OliveTin container images from version 2024.03.24 onwards include the fantastic apprise tool, which makes chat notifications on many protocols very easy. diff --git a/docs/modules/ROOT/pages/action_execution/onwebhook.adoc b/docs/modules/ROOT/pages/action_execution/onwebhook.adoc index 2fb1505..3a81982 100644 --- a/docs/modules/ROOT/pages/action_execution/onwebhook.adoc +++ b/docs/modules/ROOT/pages/action_execution/onwebhook.adoc @@ -7,7 +7,7 @@ OliveTin provides a dedicated webhook endpoint at `/webhooks` that can receive w == Basic Configuration -To configure an action to run on a webhook, add the `execOnWebhook` property to your action: +To configure an action to run on a webhook, add the `execOnWebhook` property to your action. Webhook-triggered actions **must** use `exec:` (not `shell:` or `shellAfterCompleted`). [source,yaml] .`config.yaml` @@ -15,7 +15,8 @@ To configure an action to run on a webhook, add the `execOnWebhook` property to actions: - title: Deploy Application id: deploy - shell: /opt/scripts/deploy.sh + exec: + - /opt/scripts/deploy.sh execOnWebhook: - matchHeaders: X-Event-Type: deploy @@ -51,7 +52,9 @@ Match webhooks based on HTTP header values: ---- actions: - title: Process Event - shell: echo "Processing event" + exec: + - echo + - "Processing event" execOnWebhook: - matchHeaders: X-Event-Type: my-event @@ -68,7 +71,9 @@ Match webhooks based on URL query parameters: ---- actions: - title: Process Request - shell: echo "Processing request for {{ service }}" + exec: + - echo + - "Processing request for {{ service }}" arguments: - name: service type: ascii @@ -76,9 +81,11 @@ actions: - matchQuery: action: deploy env: production + extract: + service: "$.service" ---- -A request to `/webhooks?action=deploy&env=production` would match this action. +A request to `/webhooks?action=deploy&env=production` with a JSON body containing `"service"` would match this action and pass that field into the `service` argument. === Match by JSON Body Path @@ -88,12 +95,16 @@ Match webhooks based on values in the JSON request body using JSONPath expressio ---- actions: - title: Handle Push Event - shell: echo "Push to {{ branch }}" + exec: + - echo + - "Push to {{ branch }}" arguments: - name: branch type: ascii execOnWebhook: - matchPath: "$.event_type=push" + extract: + branch: "$.branch" ---- The `matchPath` format is `jsonpath=value`. You can also just specify a JSONPath without a value to match if the path exists: @@ -112,7 +123,9 @@ Header and query parameter values can use regex patterns by prefixing with `rege ---- actions: - title: Handle Multiple Events - shell: echo "Handling event" + exec: + - echo + - "Handling event" execOnWebhook: - matchHeaders: X-Event-Type: "regex:^(push|pull_request|release)$" @@ -126,7 +139,9 @@ You can combine multiple match criteria. All criteria must match for the webhook ---- actions: - title: Production Deploy - shell: /opt/scripts/deploy.sh production + exec: + - /opt/scripts/deploy.sh + - production execOnWebhook: - matchHeaders: X-Event-Type: deploy @@ -143,9 +158,10 @@ You can extract values from the webhook payload and pass them as arguments to yo ---- actions: - title: Deploy Version - shell: | - echo "Deploying version {{ version }} to {{ environment }}" - /opt/scripts/deploy.sh "{{ version }}" "{{ environment }}" + exec: + - /opt/scripts/deploy.sh + - "{{ version }}" + - "{{ environment }}" arguments: - name: version type: ascii @@ -176,7 +192,9 @@ For example, to access the `X-Request-Id` header in your action: ---- actions: - title: Log Request - shell: echo "Request ID: {{ webhook_header_x-request-id }}" + exec: + - echo + - 'Request ID: {{ index .Arguments "webhook_header_x-request-id" }}' arguments: - name: webhook_header_x-request-id type: ascii @@ -185,6 +203,8 @@ actions: X-Event-Type: log ---- +Header names that contain hyphens become argument keys with the same hyphens (for example `webhook_header_x-request-id`). Use the `index` map-lookup form shown above, because Go templates treat hyphens in bare identifiers as subtraction. + == Webhook Authentication OliveTin supports several authentication methods to verify webhook requests: @@ -278,7 +298,8 @@ An action can have multiple webhook configurations. The action will be triggered ---- actions: - title: Deploy - shell: /opt/scripts/deploy.sh + exec: + - /opt/scripts/deploy.sh execOnWebhook: - matchHeaders: X-Event-Type: deploy-manual diff --git a/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc b/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc index 4abbc41..9b1e125 100644 --- a/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc +++ b/docs/modules/ROOT/pages/action_execution/shellvsexec.adoc @@ -1,13 +1,25 @@ = Shell vs Exec -OliveTin supports two different methods to run commands: `shell` and `exec`. The difference between these two is that "shell" accepts strings, and will wrap that whole command in a shell with "bash -c". Exec uses a syscall directly to execute commands. +OliveTin supports two different methods to run commands: `shell` and `exec`. The difference between these two is that "shell" accepts a single string and runs it via the system shell (`sh -c` on Unix; `cmd /C` on Windows). Exec passes an argument vector directly to the operating system without invoking a shell. * **Shell** is more flexible, because it allows you to chain commands (eg, using &&) and redirect or pipe output (eg: ">" or "|"). * **Exec** is more secure, because it does not invoke a shell, and thus avoids shell injection attacks. -Shell can be safe and secure with simple argument types (like ascii_identifier), but some argument types like URL can contain basically any character - /, :, ?, &, etc - which can lead to shell injection vulnerabilities while still being a valid URL. +Shell can be safe and secure with simple argument types (like `ascii_identifier`), but some argument types like `url` can contain characters such as `/`, `:`, `?`, and `&` which can lead to shell injection vulnerabilities while still being a valid URL. -OliveTin will try and prevent you from using dangerous characters in shell commands (eg, URL is no longer permitted with Shell). +OliveTin blocks unsafe argument types from being used with `shell:` (for example `url`, `email`, `password`, `regex:...`, and raw string types). See xref:args/types.adoc#shell-blocked-arg-types[Types that cannot be used with shell]. Prefer `exec:` when in doubt. + +[#shell-entity-env-trust] +== Entity and `.Env` values are not shell-sanitized + +User-supplied **argument** values are type-checked (and some types are blocked with `shell`) to reduce shell injection risk. That protection does **not** apply to: + +* Entity fields — `{{ .CurrentEntity.field }}` (and legacy forms such as `{{ server.hostname }}`) +* Process environment — `{{ .Env.VAR_NAME }}` + +Those values are substituted into `shell` / `shellAfterCompleted` as-is. OliveTin assumes they are **server-controlled** (entity files and the OliveTin process environment under the operator's control). The author of the config is responsible for ensuring that data is trustworthy, or for using `exec` and careful quoting when it might not be. + +Webhooks cannot use `shell:` or `shellAfterCompleted`; webhook-triggered actions must use `exec:` only. See xref:action_execution/onwebhook.adoc[Execute on webhook]. The way that you specify these two types of execution is different - `shell` expects a single string, while `exec` expects a list of strings (the first being the command, the rest being the arguments). diff --git a/docs/modules/ROOT/pages/advanced_configuration/config_envs.adoc b/docs/modules/ROOT/pages/advanced_configuration/config_envs.adoc index 5fb6de5..11ab73b 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/config_envs.adoc +++ b/docs/modules/ROOT/pages/advanced_configuration/config_envs.adoc @@ -51,4 +51,6 @@ actions: `.Env` uses the same Go template context as other action variables (e.g. `.Arguments`, `.CurrentEntity`, `.OliveTin`). The map is built from the process environment when OliveTin starts; values are read at template execution time. If a variable is missing, the template engine will report a missing-key error (with `missingkey=error`), so use defaulting when a variable might be unset, e.g. `{{ or .Env.OPTIONAL_VAR "default" }}`. For template functions such as JSON encoding, see xref:args/templates.adoc#json-encoding[JSON encoding with Json]. +`.Env` values are **not** sanitized for shell safety. When you use them in `shell` or `shellAfterCompleted`, OliveTin assumes the process environment is server-controlled and that you accept responsibility for those values. See xref:action_execution/shellvsexec.adoc#shell-entity-env-trust[Entity and .Env values are not shell-sanitized]. + This feature addresses the need to use environment variables in templates without changing the config loader (see link:https://github.com/OliveTin/OliveTin/issues/840[GitHub issue #840]). diff --git a/docs/modules/ROOT/pages/advanced_configuration/webui.adoc b/docs/modules/ROOT/pages/advanced_configuration/webui.adoc index 504d349..a6d1620 100644 --- a/docs/modules/ROOT/pages/advanced_configuration/webui.adoc +++ b/docs/modules/ROOT/pages/advanced_configuration/webui.adoc @@ -67,6 +67,8 @@ showNavigateOnStartIcons: false image::advanced_configuration/webui/sidebar/sidebar.png[] +Root dashboards can optionally set a `category` field so the sidebar groups them under collapsible headings. See xref:dashboards/intro.adoc#categorize-dashboards-in-the-sidebar[Categorize dashboards in the sidebar]. + === Topbar navigation style `sectionNavigationStyle: topbar` looks like this; diff --git a/docs/modules/ROOT/pages/args/input.adoc b/docs/modules/ROOT/pages/args/input.adoc index 446ad72..776ee4f 100644 --- a/docs/modules/ROOT/pages/args/input.adoc +++ b/docs/modules/ROOT/pages/args/input.adoc @@ -32,6 +32,8 @@ actions: type: ascii_sentence ---- +`description` is optional help text under the field. It is rendered as raw HTML; see xref:args/intro.adoc#arg-descriptions[Argument descriptions]. + This will give you a normal button, like this; image::args/input/args1.png[] diff --git a/docs/modules/ROOT/pages/args/input_checkbox.adoc b/docs/modules/ROOT/pages/args/input_checkbox.adoc index 09196aa..027dfea 100644 --- a/docs/modules/ROOT/pages/args/input_checkbox.adoc +++ b/docs/modules/ROOT/pages/args/input_checkbox.adoc @@ -3,6 +3,8 @@ The `checkbox` type argument is a simple checkbox that can be used to toggle a boolean value. It can be especially useful to pass flags to your actions. +Define `choices` for the checked/unchecked values. A checkbox **without** choices is not allowed with `shell:` (use `exec:` instead). + [source,yaml] ---- actions: diff --git a/docs/modules/ROOT/pages/args/input_checklist.adoc b/docs/modules/ROOT/pages/args/input_checklist.adoc index e831fd7..2e4bc3f 100644 --- a/docs/modules/ROOT/pages/args/input_checklist.adoc +++ b/docs/modules/ROOT/pages/args/input_checklist.adoc @@ -1,7 +1,7 @@ [#checklist] = Input: Checklist -The `checklist` type argument renders multiple checkboxes from predefined `choices`. Users can select one or more options, and the selected values are passed to your action as a comma-separated string. +The `checklist` type argument renders multiple checkboxes from predefined `choices`. Users can select one or more options, and the selected values are passed to your action as a **JSON array string** (for example `["documents","photos"]`). Legacy comma-separated values are rejected. [source,yaml] ---- @@ -53,7 +53,7 @@ arguments: == Choice values -Choice `value` fields must not contain commas, because commas are used to join multiple selections together. +Choice `value` fields may contain commas; selections are encoded as JSON, not joined with commas. Each `title` is shown in the web interface. If a submitted segment matches a choice `title`, OliveTin maps it to the corresponding `value` before validation, matching the behaviour of xref:args/input_checkbox.adoc[checkbox] arguments with choices. @@ -80,4 +80,4 @@ entities: name: container ---- -OliveTin expands the template once per entity instance and renders each result as a checkbox. Selected values are still passed as a comma-separated string. +OliveTin expands the template once per entity instance and renders each result as a checkbox. Selected values are still passed as a JSON array string. diff --git a/docs/modules/ROOT/pages/args/input_textarea.adoc b/docs/modules/ROOT/pages/args/input_textarea.adoc index 927144a..3204563 100644 --- a/docs/modules/ROOT/pages/args/input_textarea.adoc +++ b/docs/modules/ROOT/pages/args/input_textarea.adoc @@ -3,19 +3,24 @@ OliveTin supports multi-line text inputs, which can be useful for longer messages or scripts. You should set your argument `type` to `raw_string_multiline` to use these. -As the name implies, textareas are raw, and are NOT validated by any regex. +As the name implies, textareas are raw, and are NOT validated by any regex. For that reason they **cannot** be used with `shell:` — use `exec:` so the value is a separate argv element. [source,yaml] .`config.yaml` ---- actions: - title: Save text to file - shell: echo "$CONTENT" > file + exec: + - /bin/sh + - -c + - echo "$CONTENT" > file arguments: - type: raw_string_multiline name: content ---- +In that `exec:` example, `$CONTENT` comes from the process environment (OliveTin exports each argument as an uppercase env var), not from shell-string interpolation of the argument into `shell:`. + This renders like this; image::args/textarea/multiline-text.png[] diff --git a/docs/modules/ROOT/pages/args/intro.adoc b/docs/modules/ROOT/pages/args/intro.adoc index e8495a9..17f5e2a 100644 --- a/docs/modules/ROOT/pages/args/intro.adoc +++ b/docs/modules/ROOT/pages/args/intro.adoc @@ -10,6 +10,24 @@ Examples of valid argument names are `{{ personName }}`, `{{ customer_number }}` * numbers are allowed (argument names can also start with numbers) * all other characters are invalid for argument names. +[#arg-descriptions] +== Argument descriptions + +Each argument can include a `description:` shown under the field on the argument form. OliveTin renders that value as **raw HTML**, so you can use markup such as links, line breaks, or emphasis: + +[source,yaml] +---- +arguments: + - name: host + title: Hostname + type: ascii_identifier + description: | + Enter a host OliveTin can reach. + See the network guide. +---- + +Treat `description` as trusted markup that you control (the same trust model as xref:dashboards/4-displays.adoc[dashboard displays]). Do not put untrusted or entity-derived strings into it without careful escaping. + == What's Next? Now that you understand how arguments work, explore the different argument types and features: diff --git a/docs/modules/ROOT/pages/args/password.adoc b/docs/modules/ROOT/pages/args/password.adoc index 7016531..8fddc8d 100644 --- a/docs/modules/ROOT/pages/args/password.adoc +++ b/docs/modules/ROOT/pages/args/password.adoc @@ -1,9 +1,9 @@ = Password -Sometimes you want to mask the input you pass, and a password field is useful for this. +Sometimes you want to mask the input you pass, and a password field is useful for this. [WARNING] -Passwords are passed to the OliveTin server in cleartext (unless you're using HTTPS), and are just treated as a string on the server side. +Passwords are passed to the OliveTin server in cleartext (unless you're using HTTPS), and are just treated as a string on the server side. Password arguments are **not** type-checked for allowed characters, and they **cannot** be used with `shell:` — use `exec:` so the value is passed as a separate argument instead of being interpolated into a shell string. [source,yaml] .`config.yaml` @@ -11,9 +11,10 @@ Passwords are passed to the OliveTin server in cleartext (unless you're using HT actions: - title: echo a message icon: smile - shell: echo {{ my_password }} + exec: + - echo + - "{{ my_password }}" arguments: - name: my_password type: password ---- - diff --git a/docs/modules/ROOT/pages/args/regex.adoc b/docs/modules/ROOT/pages/args/regex.adoc index b173795..eff3530 100644 --- a/docs/modules/ROOT/pages/args/regex.adoc +++ b/docs/modules/ROOT/pages/args/regex.adoc @@ -5,13 +5,17 @@ OliveTin version 2024.02.081 and above support custom regex patterns for argumen NOTE: The regex pattern should be enclosed in single quotes, otherwise you will probably get a YAML error when starting OliveTin. +Custom `regex:...` argument types **cannot** be used with `shell:`. Use `exec:` instead (see xref:action_execution/shellvsexec.adoc[Shell vs Exec]). + [source,yaml] .`config.yaml` ---- actions: - title: echo a message icon: smile - shell: echo "{{ message }}" + exec: + - echo + - "{{ message }}" arguments: - name: message type: 'regex:^\w\w\w$' @@ -22,4 +26,6 @@ The site http://regex101.com is a good place to test your regex patterns. OliveT . **Regex in the browser** (which probably uses PCRE or Perl Compatible Regular Expressions) - this is so that the browser can give you a nice validation message. This is ignored when it reaches the server though, or if you are using the API directly. Select "PCRE" on the regex101 site when testing. . **Regex on the server** (which uses Golang's regex engine) - this is the one that actually validates the input. Select "Golang" on the regex101 site when testing. +On the server, the pattern you provide is anchored to the full string (`^(?:…)$`), so a partial match is not enough. + You cannot specify different regex patterns for the browser and server. The regex pattern you create will need to be compatible with both types of regex engine. diff --git a/docs/modules/ROOT/pages/args/templates.adoc b/docs/modules/ROOT/pages/args/templates.adoc index daeb7ee..b324dfd 100644 --- a/docs/modules/ROOT/pages/args/templates.adoc +++ b/docs/modules/ROOT/pages/args/templates.adoc @@ -10,6 +10,8 @@ In OliveTin 3k, use dotted names for template context variables: * `{{ .Env.VAR_NAME }}` — process environment (see xref:advanced_configuration/config_envs.adoc#using-env-in-template-replacements[Using .Env in template replacements]) * `{{ .OliveTin.Build.Version }}` and related build/runtime fields +IMPORTANT: Unlike argument values, `.CurrentEntity` and `.Env` are **not** sanitized for shell safety when used in `shell` or `shellAfterCompleted`. They are treated as server-controlled data; the config author is responsible for that trust. See xref:action_execution/shellvsexec.adoc#shell-entity-env-trust[Entity and .Env values are not shell-sanitized]. + In OliveTin 2k, argument and execution-request placeholders used the shorter form (for example, `{{ message }}` instead of `{{ .Arguments.message }}`). [#json-encoding] diff --git a/docs/modules/ROOT/pages/args/types.adoc b/docs/modules/ROOT/pages/args/types.adoc index e9e4cb5..49d22d4 100644 --- a/docs/modules/ROOT/pages/args/types.adoc +++ b/docs/modules/ROOT/pages/args/types.adoc @@ -7,29 +7,45 @@ A full list of argument types are below; [%header,cols="1,0,2"] |=== | Type | Rendered as | Allowed values -| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults. -| ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation -| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`. +| (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then `ascii` will be used, and a config warning is reported. It is recommended that you set the type explicitly, rather than relying on defaults. +| ascii | xref:args/input.adoc[Textbox] | `a-z`, `A-Z`, `0-9` only. No spaces or punctuation. +| ascii_identifier | xref:args/input.adoc[Textbox] | `a-z`, `A-Z`, `0-9`, `-`, `.`, and `_`. | dnsname | xref:args/input.adoc[Textbox] | A DNS hostname (RFC 1123). Short names (e.g. `webserver`) and FQDNs (e.g. `webserver.example.com`). Letters/digits/hyphens only, no underscores. Optional trailing dot allowed. | shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers. -| ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`. -| unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc. -| email | xref:args/input.adoc[Textbox] | An email address. -| password | xref:args/password.adoc[Password] | A password, which is hidden when typed. -| very_dangerous_raw_string | xref:args/input.adoc[Textbox] | Anything. This is **incredibly dangerous**, as effectively people can type anything they like, including executing additional commands beyond what you specify. Absolutely should not be used unless your OliveTin instance can only be used by people you trust entirely. -| regex:... | xref:args/input.adoc[Textbox] | Version 2024.03.081 and above support custom regex patterns. See xref:args/regex.adoc[Custom regex arguments]. -| int | xref:args/input.adoc[Textbox] | Any number, made up of the characters 0 to 9. Negative numbers are not supported. -| url | xref:args/input.adoc[Textbox] | A URL (e.g. https://example.com). Accepts any scheme, including `file://` and `ftp://`. See warning below. +| ascii_sentence | xref:args/input.adoc[Textbox] | `a-z`, `A-Z`, `0-9`, spaces, `.`, `,`, `-`, and `_`. +| unicode_identifier | xref:args/input.adoc[Textbox] | Same character class as Go's `\w` plus `-` and `.` (ASCII letters, digits, and `_`, plus `-` and `.`). Despite the name, non-ASCII letters are **not** accepted by the current server check. +| email | xref:args/input.adoc[Textbox] | An email address (parsed with Go's `mail.ParseAddress`). +| password | xref:args/password.adoc[Password] | Any string (not type-checked). Hidden in the UI. **Not allowed with `shell:`** — use `exec:`. +| very_dangerous_raw_string | xref:args/input.adoc[Textbox] | Anything. This is **incredibly dangerous**, as effectively people can type anything they like, including executing additional commands beyond what you specify. Absolutely should not be used unless your OliveTin instance can only be used by people you trust entirely. **Not allowed with `shell:`** — use `exec:`. +| regex:... | xref:args/input.adoc[Textbox] | Custom regex patterns. See xref:args/regex.adoc[Custom regex arguments]. **Not allowed with `shell:`** — use `exec:`. +| int | xref:args/input.adoc[Textbox] | Digits `0-9` only. Negative numbers are not supported. +| url | xref:args/input.adoc[Textbox] | A URL with scheme `http` or `https` only (e.g. `https://example.com`). **Not allowed with `shell:`** — use `exec:`. +| datetime | xref:args/input_datetime.adoc[Date & Time] | A local datetime in the form `YYYY-MM-DDTHH:MM:SS` (seconds may be mangled to `:00` when browsers omit them). | confirmation | xref:args/input_confirmation.adoc[Confirmation] | A UI gate that requires a checkbox before starting. Usually unnamed (nothing is substituted). If named, the value is only `0` or `1`. -| checklist | xref:args/input_checklist.adoc[Checklist] | Multiple checkboxes from predefined choices. Selected values are passed as a comma-separated string. -| n/a, but `choices` used | xref:args/input_dropdown.adoc[Dropdown] | A "hidden" argument that makes the action require a confirmation before launching. -| raw_string_multiline | xref:args/input_textarea.adoc[Textarea] | Anything. This is **dangerous**, as effectively people can type anything they like +| checkbox | xref:args/input_checkbox.adoc[Checkbox] | Typically used with `choices` for on/off flag values. A checkbox **without** choices is **not allowed with `shell:`** — use `exec:` or define choices. +| checklist | xref:args/input_checklist.adoc[Checklist] | Multiple checkboxes from predefined choices. Selected values are passed as a JSON array string (e.g. `["documents","photos"]`). +| n/a, but `choices` used | xref:args/input_dropdown.adoc[Dropdown] | Predefined choices shown as a dropdown. The submitted value must match one of the choice values (or an entity-expanded choice). +| raw_string_multiline | xref:args/input_textarea.adoc[Textarea] | Anything (not type-checked). **Dangerous**, and **not allowed with `shell:`** — use `exec:`. |=== -[WARNING] -.Security risk: URL argument type -==== -The `url` argument type does not restrict the URL scheme. Users can enter `file://` (local filesystem) URLs, `ftp://`, or other schemes. If the argument value is passed directly to curl, wget, or similar tools, a malicious or mistaken input could read local files, access internal services, or trigger unwanted network requests. +[#shell-blocked-arg-types] +== Types that cannot be used with `shell:` -If your action might be used by untrusted users, validate or filter the URL in your script (e.g. allow only `https://`) before using the value. +When an action uses `shell:` (including with arguments substituted into the command string), OliveTin rejects these argument types and asks you to use `exec:` instead: + +* `url` +* `email` +* `password` +* `raw_string_multiline` +* `very_dangerous_raw_string` +* `html` (internal/display-oriented; skips normal type checks) +* any custom `regex:...` type +* `checkbox` when it has **no** `choices` + +See xref:action_execution/shellvsexec.adoc[Shell vs Exec]. + +[NOTE] +.URL schemes +==== +The `url` type accepts only `http` and `https`. Schemes such as `file://`, `ftp://`, and `gopher://` are rejected by the server. ==== diff --git a/docs/modules/ROOT/pages/dashboards/intro.adoc b/docs/modules/ROOT/pages/dashboards/intro.adoc index 0aca815..5f30365 100644 --- a/docs/modules/ROOT/pages/dashboards/intro.adoc +++ b/docs/modules/ROOT/pages/dashboards/intro.adoc @@ -82,6 +82,39 @@ dashboards: - title: '{{ server.name }} Power Off' ---- +[#categorize-dashboards-in-the-sidebar] +== Categorize dashboards in the sidebar + +When you have several root dashboards, you can group them under collapsible category headings in the sidebar with an optional `category` field on each root dashboard: + +[source,yaml] +.`config.yaml` +---- +dashboards: + - title: My Servers + category: Infrastructure + contents: + - title: Ping All Servers + + - title: My Containers + category: Infrastructure + contents: + - title: Restart nginx + + - title: Status Board + category: Monitoring + contents: + - title: Show uptime + + - title: Misc Tools + contents: + - title: Hello World +---- + +Dashboards that share the same `category` value are grouped together. Category order follows the first time each category appears in the config among dashboards the user can see. Dashboards without a `category` appear above the category sections. Nested items under `contents` do not use `category`. + +Categories are shown when `sectionNavigationStyle` is `sidebar` (the default). See xref:advanced_configuration/webui.adoc#section-navgiation-style[Section Navigation Style]. + == What's Next? Now that you understand dashboards, explore these related features: diff --git a/docs/modules/ROOT/pages/entities/intro.adoc b/docs/modules/ROOT/pages/entities/intro.adoc index 42cd355..9ae46d9 100644 --- a/docs/modules/ROOT/pages/entities/intro.adoc +++ b/docs/modules/ROOT/pages/entities/intro.adoc @@ -11,6 +11,8 @@ Entities are just loaded from files on disk, OliveTin will also watch these file Entity data files can contain any fields you need. Those values are available in action templates as `{{ .CurrentEntity.field }}` — for example, `{{ .CurrentEntity.status }}` or `{{ .CurrentEntity.hostname }}`. +Entity field values are **not** sanitized for shell safety. If you substitute them into `shell` or `shellAfterCompleted`, OliveTin assumes the entity files are server-controlled and that you accept responsibility for that data. See xref:action_execution/shellvsexec.adoc#shell-entity-env-trust[Entity and .Env values are not shell-sanitized]. + To control which fields appear in the Entities page table and entity details view, configure `properties` on the entity definition in `config.yaml`. See xref:entities/properties.adoc[Entity properties] for details. [source,yaml] diff --git a/docs/modules/ROOT/pages/reference/multiple_instances.adoc b/docs/modules/ROOT/pages/reference/multiple_instances.adoc index 7f46cf2..db1c8f9 100644 --- a/docs/modules/ROOT/pages/reference/multiple_instances.adoc +++ b/docs/modules/ROOT/pages/reference/multiple_instances.adoc @@ -12,13 +12,13 @@ This is the easiest way to run multiple OliveTin instances. Follow the xref:inst 3. When creating the container, pass in the 2nd instance's config, eg; `-v /opt/OliveTin_two/:/config/` 4. When creating the container, set the external port, eg: `2337:1337` - 2337 is the external port) -You do not need to change the listenAddresses / ports for the other 3 ports that OliveTin uses, when you are running inside a container. +You do not need to change the listenAddresses / ports for the other 3 ports that OliveTin uses, when you are running inside a container. == Without containers - using a package (.tar.gz) If you are not using containers, then it is probably best not to use a `.deb/.rpm` installation, as those packages can only be installed for one instance. -Instead, follow the instructions for xref:install/targz.adoc[installing from a .tar.gz] archive. +Instead, follow the instructions for xref:install/targz.adoc[installing from a .tar.gz] archive. When you come to create the config.yaml file, OliveTin will look for this in it's own startup directory. Therefore it is probably best to extract the .tar.gz file like this and change the paths; @@ -28,7 +28,7 @@ When you come to create the config.yaml file, OliveTin will look for this in it' Because you are running outside of a container, you will also need to change the "internal" ports used by OliveTin so they are separate for all instances. OliveTin listens on 4 addresses (1 external, 3 internal) and needs 4 ports. You can read about these in the xref:reference/network-ports.adoc[network ports documentation]. -NOTE: OliveTin also supports reading the PORT environment variable, and will use this as a base port for the simgle frontend, will add 1 to start extra servers. For example of PORT is 2000, then the simgle frontend will start on port 2000, the REST API on 2001, and so on. +NOTE: If the `PORT` environment variable is set, OliveTin listens on that port for the single HTTP frontend only (overriding `listenAddressSingleHTTPFrontend` in the config, keeping the host from the config). When `PORT` is unset, the config value is used, or `0.0.0.0:1337` if that setting is omitted. `PORT` does not change the other internal listen addresses — set those explicitly in each instance's config as shown below. You could end up with a setup that looks like this; @@ -40,7 +40,7 @@ You could end up with a setup that looks like this; | OliveTin_three | `/opt/OliveTin_three` | `/opt/OliveTin_three/config.yaml` | `0.0.0.0:3337` | `localhost:3338` | `localhost:3339` | `localhost:3340` |=== -Note that you will also need to adjust the default systemd service file to point to your install directory, if using that. Here is an example for `OliveTin_two`; +Note that you will also need to adjust the default systemd service file to point to your install directory, if using that. Here is an example for `OliveTin_two`; .A modified systemd service file for a 2nd instance ---- @@ -55,5 +55,3 @@ Restart=always [Install] WantedBy=multi-user.target ---- - - diff --git a/docs/modules/ROOT/pages/reference/network-ports.adoc b/docs/modules/ROOT/pages/reference/network-ports.adoc index 5453241..e96abb4 100644 --- a/docs/modules/ROOT/pages/reference/network-ports.adoc +++ b/docs/modules/ROOT/pages/reference/network-ports.adoc @@ -4,7 +4,7 @@ OliveTin might surprise some people when they see it is listening on several ports when it starts up. Most of these ports are internal and localhost-only by default. It keeps the architecture of OliveTin clean and simple, and allows for -a lot of flexibility if needed. +a lot of flexibility if needed. == Network flow diagram @@ -38,18 +38,24 @@ server. Below is a detailed reference table. -== Port Reference Table +== Port Reference Table .Port reference table [%header,cols="1,2"] |=== -| Config file reference (and Default Address:Port) | Purpose +| Config file reference (and Default Address:Port) | Purpose | `listenAddressSingleHTTPFrontend: 0.0.0.0:1337` (listen on all available addresses) | This is a "micro reverse proxy" built into OliveTin. It's only purpose is to serve /ui and / (the web interface) from a single endpoint. This means that problems like CORSs and setting "external addresses" is not necessary. It does not do any caching or anything else. It can be disabled, but it makes life a lot easier for you. It's common to put your own reverse proxy like haproxy, traefik, etc in front of this single micro reverse proxy. | `listenAddressRestActions: localhost:1338` | REST - the protocol used by web pages to talk to web APIs. In the case of OliveTin, the API is used to get actions, and start actions. | `listenAddressGrpcActions:localhost:1339` | gRPC - a very popular method of service-to-service API communication. This provides the "real" API for OliveTin. | `listenAddressWebUI: localhost:1340` | Hosts a simple static web server with some HTML, stylesheets, Javascript etc for the web interface. | `listenAddressPrometheus: localhost:1341` | Hosts a prometheus endpoint, which is disabled by default. See xref:advanced_configuration/prometheus.adoc[Prometheus] to learn more. -|=== +|=== + +== PORT environment variable + +If the `PORT` environment variable is set at startup, OliveTin uses it as the listen port for `listenAddressSingleHTTPFrontend` only, keeping the host from the config (default host `0.0.0.0`). This overrides an explicit port in `config.yaml`, which is useful on platforms that assign a port via `PORT` (for example Heroku or Cloud Run). Internal listen addresses (`listenAddressRestActions`, `listenAddressWebUI`, and so on) are not derived from `PORT`; configure those separately when needed. + +When `PORT` is not set, OliveTin uses `listenAddressSingleHTTPFrontend` from the config, or `0.0.0.0:1337` if that setting is omitted. == See also diff --git a/docs/modules/ROOT/pages/reference/reference_themes_for_users.adoc b/docs/modules/ROOT/pages/reference/reference_themes_for_users.adoc index f983447..7e49ee5 100644 --- a/docs/modules/ROOT/pages/reference/reference_themes_for_users.adoc +++ b/docs/modules/ROOT/pages/reference/reference_themes_for_users.adoc @@ -8,11 +8,11 @@ You can look for themes on the link:http://www.olivetin.app/themes/[OliveTin The There are 3 ways to install a theme; -If running inside a Docker container: +If running inside a Docker container: 1. Use the `olivetin-get-theme` command to easily Git clone the theme into your `custom-webui/themes/` directory. -If running without using containers: +If running without using containers: 1. Download the theme .zip and copy it across to your `custom-webui/themes/` directory. 2. Git Clone the theme into your `custom-webui/themes/` directory. @@ -22,11 +22,14 @@ If running without using containers: The default OliveTin configuration comes with an action to get new OliveTin themes. If you deleted it from your configuration, you can add it back in by adding the following to your `config.yaml` file; -[source,bash] +[source,yaml] ---- actions: - title: Get OliveTin Theme - shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }} + exec: + - olivetin-get-theme + - "{{ themeGitRepo }}" + - "{{ themeFolderName }}" icon: theme arguments: - name: themeGitRepo @@ -93,4 +96,3 @@ body { Profit. Check out xref:reference/reference_themes_for_developers.adoc[Themes for Developers] for more information on how to develop themes. - diff --git a/frontend/Makefile b/frontend/Makefile index 0b2b71e..198081f 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -7,6 +7,9 @@ codestyle: npx eslint --fix main.js js/* resources/vue npx stylelint style.css +unittests: deps + npm test + clean: $(call delete-files,dist) @@ -18,4 +21,4 @@ build: dist: deps clean build -.PHONY: codestyle +.PHONY: codestyle unittests diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 58f11e9..8c3ba3b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,12 +18,12 @@ "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.21.1", + "picocrank": "^1.21.2", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.5", "vue": "^3.5.40", - "vue-i18n": "^11.4.7", + "vue-i18n": "^11.4.8", "vue-router": "^5.2.0" }, "devDependencies": { @@ -998,14 +998,14 @@ "license": "MIT" }, "node_modules/@intlify/core-base": { - "version": "11.4.7", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz", - "integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==", + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.8.tgz", + "integrity": "sha512-A+Q7SKm5oEcy1E/cghqd7n/St4XjTqLhiiyDuieNcMrJcrHlkY5n0jp7Q9dD3txvVHzvsmBVV5M9wD5/s1zfzw==", "license": "MIT", "dependencies": { - "@intlify/devtools-types": "11.4.7", - "@intlify/message-compiler": "11.4.7", - "@intlify/shared": "11.4.7" + "@intlify/devtools-types": "11.4.8", + "@intlify/message-compiler": "11.4.8", + "@intlify/shared": "11.4.8" }, "engines": { "node": ">= 22" @@ -1015,13 +1015,13 @@ } }, "node_modules/@intlify/devtools-types": { - "version": "11.4.7", - "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz", - "integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==", + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.8.tgz", + "integrity": "sha512-MGpID+rlfzGUbNcnC20bm5NMSBHPrvx0atLTfv9dftn3kjXw1hGKDcIcwrO99tSrZEc2i+hczRL7ks8qXsHPkQ==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.7", - "@intlify/shared": "11.4.7" + "@intlify/core-base": "11.4.8", + "@intlify/shared": "11.4.8" }, "engines": { "node": ">= 22" @@ -1031,12 +1031,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.4.7", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz", - "integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==", + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.8.tgz", + "integrity": "sha512-vbzk17dYwduYiv52EK61+FDCyhfVg1uPUtPmiD/d45W99uJIcXywrweOBcHv7n9/iEqmXiMGT52bgJbZDQqK3w==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.4.7", + "@intlify/shared": "11.4.8", "source-map-js": "^1.0.2" }, "engines": { @@ -1047,9 +1047,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.4.7", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz", - "integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==", + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.8.tgz", + "integrity": "sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==", "license": "MIT", "engines": { "node": ">= 22" @@ -5272,9 +5272,9 @@ "license": "ISC" }, "node_modules/picocrank": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.1.tgz", - "integrity": "sha512-VnfNFok7BkpLKymAIbD2zUA0bf/AiNBS63Xk2mfi8uXOkdAmf06pQaI0ssGBv2/S9p1jbCATWj9OFz1uqa39NQ==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.2.tgz", + "integrity": "sha512-bUw6789jyYCTa9waINvYRHmnfgtoMquUidkJx2yhrDCMGbu2O65Qilkl++57yl5JnFeoZOhiPuQTKMPxMQku4A==", "license": "ISC", "dependencies": { "@hugeicons/core-free-icons": "^4.2.2", @@ -7118,14 +7118,14 @@ } }, "node_modules/vue-i18n": { - "version": "11.4.7", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz", - "integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==", + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.8.tgz", + "integrity": "sha512-0ULeHP6Z9CGvAm67S77ZEp41cfGXIREGL8qfhos2BMgcQQewtQcDKuojt6jjasAD/S8GwfTp2ySPmDSpwvrCMQ==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.7", - "@intlify/devtools-types": "11.4.7", - "@intlify/shared": "11.4.7", + "@intlify/core-base": "11.4.8", + "@intlify/devtools-types": "11.4.8", + "@intlify/shared": "11.4.8", "@vue/devtools-api": "^6.5.0" }, "engines": { diff --git a/frontend/package.json b/frontend/package.json index 5762813..a0b90e7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,12 +32,12 @@ "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", "iconify-icon": "^3.0.2", - "picocrank": "^1.21.1", + "picocrank": "^1.21.2", "standard": "^17.1.2", "unplugin-vue-components": "^32.1.0", "vite": "^8.1.5", "vue": "^3.5.40", - "vue-i18n": "^11.4.7", + "vue-i18n": "^11.4.8", "vue-router": "^5.2.0" }, "engines": { diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts index 0f482a3..acc3f89 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts @@ -1890,6 +1890,11 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & { * @generated from field: int32 config_issue_count = 26; */ configIssueCount: number; + + /** + * @generated from field: repeated olivetin.api.v1.RootDashboard root_dashboard_entries = 27; + */ + rootDashboardEntries: RootDashboard[]; }; /** @@ -1898,6 +1903,27 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & { */ export declare const InitResponseSchema: GenMessage; +/** + * @generated from message olivetin.api.v1.RootDashboard + */ +export declare type RootDashboard = Message<"olivetin.api.v1.RootDashboard"> & { + /** + * @generated from field: string title = 1; + */ + title: string; + + /** + * @generated from field: string category = 2; + */ + category: string; +}; + +/** + * Describes the message olivetin.api.v1.RootDashboard. + * Use `create(RootDashboardSchema)` to create a new message. + */ +export declare const RootDashboardSchema: GenMessage; + /** * @generated from message olivetin.api.v1.AdditionalLink */ diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js index df4fcd9..5f363b2 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 * Describes the file olivetin/api/v1/olivetin.proto. */ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ - fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSIaChhTZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QiKgoZU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiowEKC0NvbmZpZ0lzc3VlEhAKCHNldmVyaXR5GAEgASgJEgwKBGNvZGUYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIRCglhY3Rpb25faWQYBCABKAkSFAoMYWN0aW9uX3RpdGxlGAUgASgJEhUKDWFyZ3VtZW50X25hbWUYBiABKAkSDgoGc291cmNlGAcgASgJEhMKC2NvbmZpZ19maWxlGAggASgJInoKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkSMwoNY29uZmlnX2lzc3VlcxgDIAMoCzIcLm9saXZldGluLmFwaS52MS5Db25maWdJc3N1ZSINCgtJbml0UmVxdWVzdCKHBgoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCBIaChJjb25maWdfaXNzdWVfY291bnQYGiABKAUiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IloKEkdldEVudGl0aWVzUmVxdWVzdBITCgtlbnRpdHlfdHlwZRgBIAEoCRIOCgZmaWx0ZXIYAiABKAkSDAoEcGFnZRgDIAEoBRIRCglwYWdlX3NpemUYBCABKAUiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiLFAQoQRW50aXR5RGVmaW5pdGlvbhINCgV0aXRsZRgBIAEoCRIqCglpbnN0YW5jZXMYAiADKAsyFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5EhoKEnVzZWRfb25fZGFzaGJvYXJkcxgDIAMoCRIMCgRpY29uGAQgASgJEjMKCnByb3BlcnRpZXMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5UHJvcGVydHkSFwoPdG90YWxfaW5zdGFuY2VzGAYgASgFIi0KDkVudGl0eVByb3BlcnR5EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMu4TChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASbAoRU2VydmVyRGlhZ25vc3RpY3MSKS5vbGl2ZXRpbi5hcGkudjEuU2VydmVyRGlhZ25vc3RpY3NSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLlNlcnZlckRpYWdub3N0aWNzUmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSIaChhTZXJ2ZXJEaWFnbm9zdGljc1JlcXVlc3QiKgoZU2VydmVyRGlhZ25vc3RpY3NSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCKZAwoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSAASNAoJaGVhcnRiZWF0GAcgASgLMh8ub2xpdmV0aW4uYXBpLnYxLkV2ZW50SGVhcnRiZWF0SABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiEAoORXZlbnRIZWFydGJlYXQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiowEKC0NvbmZpZ0lzc3VlEhAKCHNldmVyaXR5GAEgASgJEgwKBGNvZGUYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIRCglhY3Rpb25faWQYBCABKAkSFAoMYWN0aW9uX3RpdGxlGAUgASgJEhUKDWFyZ3VtZW50X25hbWUYBiABKAkSDgoGc291cmNlGAcgASgJEhMKC2NvbmZpZ19maWxlGAggASgJInoKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkSMwoNY29uZmlnX2lzc3VlcxgDIAMoCzIcLm9saXZldGluLmFwaS52MS5Db25maWdJc3N1ZSINCgtJbml0UmVxdWVzdCLHBgoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCBIaChJjb25maWdfaXNzdWVfY291bnQYGiABKAUSPgoWcm9vdF9kYXNoYm9hcmRfZW50cmllcxgbIAMoCzIeLm9saXZldGluLmFwaS52MS5Sb290RGFzaGJvYXJkIjAKDVJvb3REYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IloKEkdldEVudGl0aWVzUmVxdWVzdBITCgtlbnRpdHlfdHlwZRgBIAEoCRIOCgZmaWx0ZXIYAiABKAkSDAoEcGFnZRgDIAEoBRIRCglwYWdlX3NpemUYBCABKAUiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiLFAQoQRW50aXR5RGVmaW5pdGlvbhINCgV0aXRsZRgBIAEoCRIqCglpbnN0YW5jZXMYAiADKAsyFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5EhoKEnVzZWRfb25fZGFzaGJvYXJkcxgDIAMoCRIMCgRpY29uGAQgASgJEjMKCnByb3BlcnRpZXMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5UHJvcGVydHkSFwoPdG90YWxfaW5zdGFuY2VzGAYgASgFIi0KDkVudGl0eVByb3BlcnR5EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMu4TChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASbAoRU2VydmVyRGlhZ25vc3RpY3MSKS5vbGl2ZXRpbi5hcGkudjEuU2VydmVyRGlhZ25vc3RpY3NSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLlNlcnZlckRpYWdub3N0aWNzUmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); /** * Describes the message olivetin.api.v1.Action. @@ -493,75 +493,82 @@ export const InitRequestSchema = /*@__PURE__*/ export const InitResponseSchema = /*@__PURE__*/ messageDesc(file_olivetin_api_v1_olivetin, 68); +/** + * Describes the message olivetin.api.v1.RootDashboard. + * Use `create(RootDashboardSchema)` to create a new message. + */ +export const RootDashboardSchema = /*@__PURE__*/ + messageDesc(file_olivetin_api_v1_olivetin, 69); + /** * Describes the message olivetin.api.v1.AdditionalLink. * Use `create(AdditionalLinkSchema)` to create a new message. */ export const AdditionalLinkSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 69); + messageDesc(file_olivetin_api_v1_olivetin, 70); /** * Describes the message olivetin.api.v1.OAuth2Provider. * Use `create(OAuth2ProviderSchema)` to create a new message. */ export const OAuth2ProviderSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 70); + messageDesc(file_olivetin_api_v1_olivetin, 71); /** * Describes the message olivetin.api.v1.GetActionBindingRequest. * Use `create(GetActionBindingRequestSchema)` to create a new message. */ export const GetActionBindingRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 71); + messageDesc(file_olivetin_api_v1_olivetin, 72); /** * Describes the message olivetin.api.v1.GetActionBindingResponse. * Use `create(GetActionBindingResponseSchema)` to create a new message. */ export const GetActionBindingResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 72); + messageDesc(file_olivetin_api_v1_olivetin, 73); /** * Describes the message olivetin.api.v1.GetEntitiesRequest. * Use `create(GetEntitiesRequestSchema)` to create a new message. */ export const GetEntitiesRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 73); + messageDesc(file_olivetin_api_v1_olivetin, 74); /** * Describes the message olivetin.api.v1.GetEntitiesResponse. * Use `create(GetEntitiesResponseSchema)` to create a new message. */ export const GetEntitiesResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 74); + messageDesc(file_olivetin_api_v1_olivetin, 75); /** * Describes the message olivetin.api.v1.EntityDefinition. * Use `create(EntityDefinitionSchema)` to create a new message. */ export const EntityDefinitionSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 75); + messageDesc(file_olivetin_api_v1_olivetin, 76); /** * Describes the message olivetin.api.v1.EntityProperty. * Use `create(EntityPropertySchema)` to create a new message. */ export const EntityPropertySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 76); + messageDesc(file_olivetin_api_v1_olivetin, 77); /** * Describes the message olivetin.api.v1.GetEntityRequest. * Use `create(GetEntityRequestSchema)` to create a new message. */ export const GetEntityRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 77); + messageDesc(file_olivetin_api_v1_olivetin, 78); /** * Describes the message olivetin.api.v1.RestartActionRequest. * Use `create(RestartActionRequestSchema)` to create a new message. */ export const RestartActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 78); + messageDesc(file_olivetin_api_v1_olivetin, 79); /** * @generated from service olivetin.api.v1.OliveTinApiService diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index cc3a388..6ff12a2 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -338,40 +338,110 @@ function updateHeaderFromInit () { connectEventStreamIfNeeded() } +function getRootDashboardEntries () { + const entries = window.initResponse?.rootDashboardEntries + + if (entries && entries.length > 0) { + return entries.map((entry) => ({ + title: entry.title, + category: entry.category || '' + })) + } + + return (window.initResponse?.rootDashboards || []).map((title) => ({ + title, + category: '' + })) +} + +function addDashboardNavLink (title) { + navigation.value.addNavigationLink({ + id: title, + name: title, + title, + path: title === 'Actions' ? '/' : `/dashboards/${title}`, + icon: DashboardSquare01Icon + }) +} + +function addCategorizedDashboardLinks (entries) { + const categoryOrder = [] + const byCategory = new Map() + + for (const entry of entries) { + const category = entry.category.trim() + + if (!byCategory.has(category)) { + byCategory.set(category, []) + categoryOrder.push(category) + } + + byCategory.get(category).push(entry.title) + } + + for (const category of categoryOrder) { + navigation.value.addSection(category) + + for (const title of byCategory.get(category)) { + addDashboardNavLink(title) + } + } +} + function renderNavigation () { if (!navigation.value) { return } - const rootDashboards = window.initResponse?.rootDashboards || [] - if (typeof navigation.value.clearNavigationLinks === 'function') { navigation.value.clearNavigationLinks() } - for (const rootDashboard of rootDashboards) { - navigation.value.addNavigationLink({ - id: rootDashboard, - name: rootDashboard, - title: rootDashboard, - path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`, - icon: DashboardSquare01Icon - }) + const entries = getRootDashboardEntries() + const uncategorized = entries.filter((entry) => !entry.category.trim()) + const categorized = entries.filter((entry) => entry.category.trim()) + + for (const entry of uncategorized) { + addDashboardNavLink(entry.title) } - navigation.value.addSeparator() - navigation.value.addRouterLink('Entities', t('nav.entities')) + addCategorizedDashboardLinks(categorized) + addSystemNavLinks() +} + +function addSystemNavLinks () { + const systemLinks = [] + + systemLinks.push({ + routeName: 'Entities', + title: t('nav.entities') + }) if (showLogs.value) { - navigation.value.addRouterLink('Logs', t('nav.logs')) + systemLinks.push({ + routeName: 'Logs', + title: t('nav.logs') + }) } if (showDiagnostics.value) { const issueCount = window.initResponse?.configIssueCount || 0 - navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'), { - count: issueCount + systemLinks.push({ + routeName: 'Diagnostics', + title: t('nav.diagnostics'), + options: { count: issueCount } }) } + + if (systemLinks.length === 0) { + return + } + + navigation.value.addSection(t('nav.system')) + + for (const link of systemLinks) { + navigation.value.addRouterLink(link.routeName, link.title, link.options || {}) + } } function openLanguageDialog () { diff --git a/frontend/resources/vue/components/ActionIconGlyph.vue b/frontend/resources/vue/components/ActionIconGlyph.vue index 1b57fab..22f8317 100644 --- a/frontend/resources/vue/components/ActionIconGlyph.vue +++ b/frontend/resources/vue/components/ActionIconGlyph.vue @@ -7,10 +7,12 @@ height="1em" class="action-icon-glyph-svg" /> + + + + {{ component.title }} diff --git a/frontend/resources/vue/components/DashboardComponentDisplay.vue b/frontend/resources/vue/components/DashboardComponentDisplay.vue index 6450325..daf0867 100644 --- a/frontend/resources/vue/components/DashboardComponentDisplay.vue +++ b/frontend/resources/vue/components/DashboardComponentDisplay.vue @@ -3,7 +3,9 @@ class="display" :class="component.cssClass" > +
+
diff --git a/frontend/resources/vue/utils/logsFilterStorage.js b/frontend/resources/vue/utils/logsFilterStorage.js new file mode 100644 index 0000000..cd6fae1 --- /dev/null +++ b/frontend/resources/vue/utils/logsFilterStorage.js @@ -0,0 +1,21 @@ +export const LOGS_FILTER_STORAGE_KEY = 'olivetin-logs-filter' + +export function loadStoredLogsFilter () { + try { + return sessionStorage.getItem(LOGS_FILTER_STORAGE_KEY) || '' + } catch { + return '' + } +} + +export function storeLogsFilter (value) { + try { + if (value) { + sessionStorage.setItem(LOGS_FILTER_STORAGE_KEY, value) + } else { + sessionStorage.removeItem(LOGS_FILTER_STORAGE_KEY) + } + } catch { + // Ignore storage failures (private mode, quota, etc.) + } +} diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index 9da632c..d086d1d 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -4,17 +4,12 @@

Start action: - - - {{ title }} - + + {{ title }}

@@ -94,10 +89,12 @@ @change="handleChange(arg, $event)" /> + + @@ -684,18 +681,6 @@ onUnmounted(() => { font-size: 1.5rem; } -.action-details-title-link { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: var(--link-color, #0066cc); - text-decoration: underline; -} - -.action-details-title-link:hover { - color: var(--link-hover-color, #004499); -} - form { grid-template-columns: max-content auto auto; } diff --git a/frontend/resources/vue/views/DiagnosticsView.vue b/frontend/resources/vue/views/DiagnosticsView.vue index 4c68e44..cfb47f8 100644 --- a/frontend/resources/vue/views/DiagnosticsView.vue +++ b/frontend/resources/vue/views/DiagnosticsView.vue @@ -125,11 +125,11 @@ const browserInfoCopied = ref(false) const configIssueHeaders = computed(() => [ { key: 'severity', label: t('diagnostics.config-issue-severity'), sortable: true, width: '7rem' }, + { key: 'configFile', label: t('diagnostics.config-issue-config-file'), sortable: true, width: '14rem' }, { key: 'code', label: t('diagnostics.config-issue-code'), sortable: true, width: '12rem' }, { key: 'message', label: t('diagnostics.config-issue-message'), sortable: false }, { key: 'actionTitle', label: t('diagnostics.config-issue-action'), sortable: true, width: '10rem' }, { key: 'argumentName', label: t('diagnostics.config-issue-argument'), sortable: true, width: '8rem' }, - { key: 'configFile', label: t('diagnostics.config-issue-config-file'), sortable: true, width: '14rem' }, { key: 'source', label: t('diagnostics.config-issue-source'), sortable: false, width: '12rem' } ]) diff --git a/frontend/resources/vue/views/ExecutionView.vue b/frontend/resources/vue/views/ExecutionView.vue index 8746e65..b254d82 100644 --- a/frontend/resources/vue/views/ExecutionView.vue +++ b/frontend/resources/vue/views/ExecutionView.vue @@ -58,7 +58,7 @@ >
Duration
-
+
{{ duration }}
Status
@@ -373,7 +373,7 @@ function updateDuration (logEntryParam) { } catch (e) { console.warn('Failed to calculate delta', e) } - duration.value = logEntry.value.datetimeStarted + ' → ' + logEntry.value.datetimeFinished + duration.value = logEntry.value.datetimeStarted + ' → ' + logEntry.value.datetimeFinished if (delta !== '') { duration.value += ' (' + delta + ')' } diff --git a/frontend/resources/vue/views/LogsCalendarView.vue b/frontend/resources/vue/views/LogsCalendarView.vue index ed6d517..ceb0b81 100644 --- a/frontend/resources/vue/views/LogsCalendarView.vue +++ b/frontend/resources/vue/views/LogsCalendarView.vue @@ -5,7 +5,7 @@ >