diff --git a/.github/SECURITY_ADVISORY_DUPLICATES.md b/.github/SECURITY_ADVISORY_DUPLICATES.md new file mode 100644 index 0000000..22f59fc --- /dev/null +++ b/.github/SECURITY_ADVISORY_DUPLICATES.md @@ -0,0 +1,116 @@ +# Security Advisory Duplicates — Maintainer Guide + +This document lists known duplicate security advisory clusters for [OliveTin/OliveTin](https://github.com/OliveTin/OliveTin). When triaging new reports, check here and [open advisories](https://github.com/OliveTin/OliveTin/security/advisories) before accepting. + +**Duplicate policy:** the earliest reporter on the canonical advisory receives primary credit. Later reporters are credited on the canonical advisory when closed as duplicates. See [SECURITY.md](../SECURITY.md). + +## Triage checklist + +1. Search open advisories for the same component and attack path. +2. Match against clusters below. +3. If duplicate: close the newer advisory, link to canonical, add reporter to canonical credits. +4. If unique: accept, patch on a private branch, reassess CVSS with OliveTin context (see SECURITY.md — OliveTin is intentional RCE by design). +5. Merge fix to `next`, publish advisory, credit reporters in advisory body (not commit message). + +--- + +## shellAfterCompleted command injection + +**Canonical:** [GHSA-vc6p-m6vx-6cwq](https://github.com/OliveTin/OliveTin/security/advisories/GHSA-vc6p-m6vx-6cwq) — reporter **knight-yagami** (2026-03-04) + +Untrusted command output or template variables interpolated into `shellAfterCompleted` and executed via `sh -c`. + +| GHSA | Reporter | Status | Notes | +|------|----------|--------|-------| +| GHSA-vc6p-m6vx-6cwq | knight-yagami | closed (canonical) | Original report | +| GHSA-v5gc-hqpq-227p | 0xkakash1 | closed (duplicate) | Output template variant | +| GHSA-m7wr-wj5j-7459 | Ryu7zz | duplicate | Webhook `exec` → output → `shellAfterCompleted` | +| GHSA-cjxm-x848-6vmc | Yesuhei | duplicate | Missing shell safety on after-completion | +| GHSA-j9p9-36jc-2v8w | anushkavirgaonkar | duplicate | Same root cause | + +**Fix:** shell-quote `output`/`exitCode` before template render; block `shellAfterCompleted` for webhook-tagged actions. + +**CVSS note:** requires admin-configured `shellAfterCompleted` and attacker influence on output — typically PR:H not PR:N. + +--- + +## OAuth2 state map memory exhaustion (DoS) + +**Canonical:** [GHSA-xpxj-f2fm-rqch](https://github.com/OliveTin/OliveTin/security/advisories/GHSA-xpxj-f2fm-rqch) — reporter **knight-yagami** (2026-03-04) + +Unauthenticated `/oauth/login` grows `registeredStates` without TTL or cap. + +| GHSA | Reporter | Status | Notes | +|------|----------|--------|-------| +| GHSA-xpxj-f2fm-rqch | knight-yagami | closed (canonical) | Original report | +| GHSA-cj96-c55v-2f3c | Dredsen | duplicate | Same unbounded map | + +**Fix:** TTL sweep (match 15-minute cookie MaxAge), max map size, cleanup on failed callback. + +**CVSS note:** unauthenticated DoS — reported 7.5 is appropriate. + +--- + +## URL argument type — unrestricted URI schemes (SSRF / file read) + +**Canonical:** [GHSA-45pc-w4ph-hrq4](https://github.com/OliveTin/OliveTin/security/advisories/GHSA-45pc-w4ph-hrq4) — reporter **fg0x0** (2026-03-09) + +`url` type accepts `file://`, `gopher://`, etc. Blocked in `shell:` mode but still validated weakly for `exec:` actions. + +| GHSA | Reporter | Status | Notes | +|------|----------|--------|-------| +| GHSA-45pc-w4ph-hrq4 | fg0x0 | closed (canonical) | Original report | +| GHSA-cchg-25m4-q6rj | anushkavirgaonkar | duplicate | Same scheme validation gap | + +**Fix:** allowlist `http`/`https` in `typeSafetyCheckUrl`. + +**CVSS note:** admin must configure `exec:` action passing URL to external tool — PR:H. + +--- + +## Custom `regex:` argument type in shell actions + +**Canonical:** [GHSA-xc5w-4v5w-7x65](https://github.com/OliveTin/OliveTin/security/advisories/GHSA-xc5w-4v5w-7x65) — reporter **Ayantaker** (2026-05-06) + +`regex:` types not in shell denylist; partial `MatchString` allows injection suffixes. + +| GHSA | Reporter | Status | Notes | +|------|----------|--------|-------| +| GHSA-xc5w-4v5w-7x65 | Ayantaker | canonical | Missing denylist entry | +| GHSA-gvxq-7gvp-4ggr | anushkavirgaonkar | duplicate | Unanchored partial match | + +**Fix:** deny `regex:` in shell mode; enforce full-string match for custom regex types. + +--- + +## Shell denylist incomplete (post CVE-2026-27626) + +**Canonical:** [GHSA-c26w-h42g-jfp9](https://github.com/OliveTin/OliveTin/security/advisories/GHSA-c26w-h42g-jfp9) — reporter **sec-reex** (2026-07-03) + +CVE-2026-27626 added `password` to denylist only; `html`, `confirmation`, and choiceless `checkbox` still skip validation and are allowed in `shell:` actions. + +**Fix:** extend `checkShellArgumentSafety` denylist. + +--- + +## StartActionAndWait logs ACL bypass + +**Canonical:** [GHSA-jm28-2wcr-qf3h](https://github.com/OliveTin/OliveTin/security/advisories/GHSA-jm28-2wcr-qf3h) — reporter **offset** (2026-03-12) + +`StartActionAndWait` / `StartActionByGetAndWait` return full log output without `logs` ACL check. + +No known duplicates. + +**Fix:** apply `isLogEntryAllowed` before returning `LogEntry`. + +**CVSS note:** requires authenticated user with `exec` but not `logs` — typically 4.3–5.3 not 6.5. + +--- + +## Easy to confuse (not duplicates) + +| Topic | Advisories | Distinction | +|-------|------------|-------------| +| OAuth2 state DoS vs OAuth2 auth bypass | GHSA-xpxj vs GHSA-3v7p | DoS fills state map; bypass spoofs `authHttpHeaderUsername` | +| `shellAfterCompleted` vs direct `shell` injection | GHSA-vc6p vs GHSA-49gm | Second-order via output vs first-order argument injection | +| `ValidateArgumentType` enumeration | GHSA-f637 vs GHSA-x6q3 | Same issue; GHSA-f637 published | diff --git a/Makefile b/Makefile index 186b56e..9fd420e 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,7 @@ devrun: compile devcontainer: compile podman-image podman-container webui-dist: + $(call delete-files,webui) $(MAKE) -wC frontend dist mv frontend/dist webui @@ -72,4 +73,7 @@ clean: config-tool: cd service && go run cmd/config-tool/main.go -.PHONY: proto service windows-resources windows-msi +devcheck: + python3 scripts/devcheck.py $(ARGS) + +.PHONY: proto service windows-resources windows-msi devcheck diff --git a/config.yaml b/config.yaml index c50b2df..37f555c 100644 --- a/config.yaml +++ b/config.yaml @@ -133,7 +133,8 @@ actions: # Docs: https://docs.olivetin.app/args/input_confirmation.html - title: Delete old backups icon: ashtonished - justification: true + # A single space requires justification with no prefilled template (empty disables it). + justification: " " shell: rm -rf /opt/oliveTinOldBackups/ && sleep 5 arguments: - type: html @@ -143,6 +144,29 @@ actions: - type: confirmation title: Are you sure?! + # Checklist arguments let users pick multiple predefined options. Selected + # values are passed to the action as a comma-separated string. + # + # Docs: https://docs.olivetin.app/args/input_checklist.html + - title: Backup selected directories + icon: backup + shell: 'echo "Backing up: {{ directories }}"' + arguments: + - name: directories + title: Directories to back up + type: checklist + description: Select one or more directories to include in the backup. + choices: + - title: Documents + value: documents + - title: Photos + value: photos + - title: Music + value: music + - title: Videos + value: videos + 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. # @@ -299,6 +323,12 @@ entities: # Docs: https://docs.olivetin.app/entities/intro.html - file: entities/servers.yaml name: server + icon: ssh + properties: + - name: hostname + title: Hostname + - name: ip + title: IP - file: entities/containers.json name: container diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 29896a8..ac12883 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -73,6 +73,7 @@ ** xref:args/regex.adoc[Input: Regex] ** xref:args/password.adoc[Input: Password] ** xref:args/input_checkbox.adoc[Input: Checkbox/Boolean] +** xref:args/input_checklist.adoc[Input: Checklist] ** xref:args/input_dropdown.adoc[Input: Dropdown] ** xref:args/input_datetime.adoc[Input: Date & Time] ** xref:args/input_confirmation.adoc[Input: Confirmation] diff --git a/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc b/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc index 0de2767..41f1341 100644 --- a/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc +++ b/docs/modules/ROOT/pages/action_execution/aftercompletion.adoc @@ -10,13 +10,13 @@ actions: - title: Check date and send notification via apprise icon: date shell: date - shellAfterCompleted: "apprise -c /config/apprise.yml -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '" + 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 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 -* `{{ output }}` - The standard output of the previous shell command +* `{{ 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. * `{{ .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. diff --git a/docs/modules/ROOT/pages/args/input_checklist.adoc b/docs/modules/ROOT/pages/args/input_checklist.adoc new file mode 100644 index 0000000..229814e --- /dev/null +++ b/docs/modules/ROOT/pages/args/input_checklist.adoc @@ -0,0 +1,80 @@ +[#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. + +[source,yaml] +---- +actions: + - title: Backup selected directories + shell: echo "Backing up: {{ directories }}" + arguments: + - name: directories + title: Directories to back up + type: checklist + choices: + - title: Documents + value: documents + - title: Photos + value: photos + - title: Music + value: music + default: documents,photos +---- + +When the example above runs with Documents and Photos selected, the shell command becomes: + +[source,shell] +---- +echo "Backing up: documents,photos" +---- + +== Select all / Select none + +The web interface includes **Select all** and **Select none** controls above the checkbox list. + +== Empty selections + +If no options are selected, the argument value is an empty string. Use `rejectNull: true` when at least one selection is required. + +[source,yaml] +---- +arguments: + - name: directories + type: checklist + rejectNull: true + choices: + - value: documents + - value: photos +---- + +== Choice values + +Choice `value` fields must not contain commas, because commas are used to join multiple selections together. + +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. + +== Using Entities + +Checklist options can be generated from entities, using the same pattern as xref:args/input_dropdown.adoc#args-dropdown-entities[entity-backed dropdowns]. Define one choice template and set `entity` to the entity type name: + +[source,yaml] +---- +actions: + - title: Restart selected containers + shell: 'docker restart {{ containers }}' + arguments: + - name: containers + title: Containers to restart + type: checklist + entity: container + choices: + - value: '{{ container.Names }}' + title: '{{ container.Names }}' + +entities: + - file: entities/containers.json + 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. diff --git a/docs/modules/ROOT/pages/args/types.adoc b/docs/modules/ROOT/pages/args/types.adoc index ffdfed0..6d6f687 100644 --- a/docs/modules/ROOT/pages/args/types.adoc +++ b/docs/modules/ROOT/pages/args/types.adoc @@ -9,9 +9,9 @@ A full list of argument types are below; | 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 `_`. +| ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`. | 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 `,`. +| 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. @@ -20,6 +20,7 @@ A full list of argument types are below; | 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. | confirmation | xref:args/input_confirmation.adoc[Confirmation] | A "hidden" argument that makes the action require a confirmation before launching. +| 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 |=== @@ -31,4 +32,3 @@ The `url` argument type does not restrict the URL scheme. Users can enter `file: 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. ==== - 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 03a2c00..4f0085f 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 @@ -92,9 +92,9 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & { execOnWebhooks: ActionWebhookExecHint[]; /** - * @generated from field: bool justification = 16; + * @generated from field: string justification = 20; */ - justification: boolean; + justification: string; /** * @generated from field: bool has_running_instance = 17; @@ -247,6 +247,27 @@ export declare type ActionArgumentChoice = Message<"olivetin.api.v1.ActionArgume */ export declare const ActionArgumentChoiceSchema: GenMessage; +/** + * @generated from message olivetin.api.v1.EntityRelatedAction + */ +export declare type EntityRelatedAction = Message<"olivetin.api.v1.EntityRelatedAction"> & { + /** + * @generated from field: olivetin.api.v1.Action action = 1; + */ + action?: Action | undefined; + + /** + * @generated from field: map prefilled_arguments = 2; + */ + prefilledArguments: { [key: string]: string }; +}; + +/** + * Describes the message olivetin.api.v1.EntityRelatedAction. + * Use `create(EntityRelatedActionSchema)` to create a new message. + */ +export declare const EntityRelatedActionSchema: GenMessage; + /** * @generated from message olivetin.api.v1.Entity */ @@ -275,6 +296,16 @@ export declare type Entity = Message<"olivetin.api.v1.Entity"> & { * @generated from field: map fields = 5; */ fields: { [key: string]: string }; + + /** + * @generated from field: repeated olivetin.api.v1.EntityRelatedAction related_actions = 6; + */ + relatedActions: EntityRelatedAction[]; + + /** + * @generated from field: string icon = 7; + */ + icon: string; }; /** @@ -1894,6 +1925,25 @@ export declare const GetActionBindingResponseSchema: GenMessage & { + /** + * @generated from field: string entity_type = 1; + */ + entityType: string; + + /** + * @generated from field: string filter = 2; + */ + filter: string; + + /** + * @generated from field: int32 page = 3; + */ + page: number; + + /** + * @generated from field: int32 page_size = 4; + */ + pageSize: number; }; /** @@ -1936,6 +1986,21 @@ export declare type EntityDefinition = Message<"olivetin.api.v1.EntityDefinition * @generated from field: repeated string used_on_dashboards = 3; */ usedOnDashboards: string[]; + + /** + * @generated from field: string icon = 4; + */ + icon: string; + + /** + * @generated from field: repeated olivetin.api.v1.EntityProperty properties = 5; + */ + properties: EntityProperty[]; + + /** + * @generated from field: int32 total_instances = 6; + */ + totalInstances: number; }; /** @@ -1944,6 +2009,27 @@ export declare type EntityDefinition = Message<"olivetin.api.v1.EntityDefinition */ export declare const EntityDefinitionSchema: GenMessage; +/** + * @generated from message olivetin.api.v1.EntityProperty + */ +export declare type EntityProperty = Message<"olivetin.api.v1.EntityProperty"> & { + /** + * @generated from field: string name = 1; + */ + name: string; + + /** + * @generated from field: string title = 2; + */ + title: string; +}; + +/** + * Describes the message olivetin.api.v1.EntityProperty. + * Use `create(EntityPropertySchema)` to create a new message. + */ +export declare const EntityPropertySchema: GenMessage; + /** * @generated from message olivetin.api.v1.GetEntityRequest */ @@ -2194,3 +2280,4 @@ export declare const OliveTinApiService: GenService<{ output: typeof EntitySchema; }, }>; + 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 5e993ca..0f5d513 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("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLABAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgIEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcCJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSKUBAoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkSNwoJYXJndW1lbnRzGBggAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAki1AEKE0VudGl0eVJlbGF0ZWRBY3Rpb24SJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJZChNwcmVmaWxsZWRfYXJndW1lbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkVudGl0eVJlbGF0ZWRBY3Rpb24uUHJlZmlsbGVkQXJndW1lbnRzRW50cnkaOQoXUHJlZmlsbGVkQXJndW1lbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL/AQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Ej0KD3JlbGF0ZWRfYWN0aW9ucxgGIAMoCzIkLm9saXZldGluLmFwaS52MS5FbnRpdHlSZWxhdGVkQWN0aW9uEgwKBGljb24YByABKAkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSKUAQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJEhUKDWp1c3RpZmljYXRpb24YBCABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIn4KGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50EhUKDWp1c3RpZmljYXRpb24YAyABKAkiSgoaU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IiwKF1N0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSI5ChhTdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJIjMKHlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiTwofU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiXgoOR2V0TG9nc1JlcXVlc3QSFAoMc3RhcnRfb2Zmc2V0GAEgASgDEhMKC2RhdGVfZmlsdGVyGAIgASgJEhEKCXBhZ2Vfc2l6ZRgDIAEoAxIOCgZmaWx0ZXIYBCABKAkilAQKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCRIOCgZxdWV1ZWQYFSABKAgSGAoQcXVldWVkX2Zvcl9ncm91cBgWIAEoCRIVCg1qdXN0aWZpY2F0aW9uGBcgASgJEjcKCWFyZ3VtZW50cxgYIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IpEBCg9HZXRMb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyI/ChRHZXRBY3Rpb25Mb2dzUmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSFAoMc3RhcnRfb2Zmc2V0GAIgASgDIpcBChVHZXRBY3Rpb25Mb2dzUmVzcG9uc2USJwoEbG9ncxgBIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeRIXCg9jb3VudF9yZW1haW5pbmcYAiABKAMSEQoJcGFnZV9zaXplGAMgASgDEhMKC3RvdGFsX2NvdW50GAQgASgDEhQKDHN0YXJ0X29mZnNldBgFIAEoAyIaChhHZXRFeGVjdXRpb25RdWV1ZVJlcXVlc3QixgEKFEV4ZWN1dGlvblF1ZXVlQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEhMKC2FjdGlvbl9pY29uGAMgASgJEhYKDm1heF9jb25jdXJyZW50GAQgASgFEhQKDGFjdGl2ZV9jb3VudBgFIAEoBRIVCg1lbnRpdHlfcHJlZml4GAYgASgJEioKB2VudHJpZXMYByADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiwQEKE0V4ZWN1dGlvblF1ZXVlR3JvdXASDAoEbmFtZRgBIAEoCRIMCgRpY29uGAIgASgJEhYKDm1heF9jb25jdXJyZW50GAMgASgFEhQKDGFjdGl2ZV9jb3VudBgEIAEoBRI2CgdhY3Rpb25zGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlQWN0aW9uEhQKDHF1ZXVlZF9jb3VudBgGIAEoBRISCgpxdWV1ZV9zaXplGAcgASgFImcKGUdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2USNAoGZ3JvdXBzGAEgAygLMiQub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblF1ZXVlR3JvdXASFAoMdG90YWxfYWN0aXZlGAIgASgFImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJhChlEYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkSDAoEcGF0aBgEIAEoCSKPAQoXRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0Ig8KDVdob0FtSVJlcXVlc3QibAoOV2hvQW1JUmVzcG9uc2USGgoSYXV0aGVudGljYXRlZF91c2VyGAEgASgJEhEKCXVzZXJncm91cBgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIMCgRhY2xzGAQgAygJEgsKA3NpZBgFIAEoCSISChBTb3NSZXBvcnRSZXF1ZXN0IiIKEVNvc1JlcG9ydFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJIhEKD0R1bXBWYXJzUmVxdWVzdCKVAQoQRHVtcFZhcnNSZXNwb25zZRINCgVhbGVydBgBIAEoCRJBCghjb250ZW50cxgCIAMoCzIvLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlLkNvbnRlbnRzRW50cnkaLwoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjsKDERlYnVnQmluZGluZxIUCgxhY3Rpb25fdGl0bGUYASABKAkSFQoNZW50aXR5X3ByZWZpeBgCIAEoCSIeChxEdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Is4BCh1EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZRINCgVhbGVydBgBIAEoCRJOCghjb250ZW50cxgCIAMoCzI8Lm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZS5Db250ZW50c0VudHJ5Gk4KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEiwKBXZhbHVlGAIgASgLMh0ub2xpdmV0aW4uYXBpLnYxLkRlYnVnQmluZGluZzoCOAEiEgoQR2V0UmVhZHl6UmVxdWVzdCIjChFHZXRSZWFkeXpSZXNwb25zZRIOCgZzdGF0dXMYASABKAkiFAoSRXZlbnRTdHJlYW1SZXF1ZXN0IpkDChNFdmVudFN0cmVhbVJlc3BvbnNlEj0KDmVudGl0eV9jaGFuZ2VkGAIgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50RW50aXR5Q2hhbmdlZEgAEj0KDmNvbmZpZ19jaGFuZ2VkGAMgASgLMiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50Q29uZmlnQ2hhbmdlZEgAEkUKEmV4ZWN1dGlvbl9maW5pc2hlZBgEIAEoCzInLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvbkZpbmlzaGVkSAASQwoRZXhlY3V0aW9uX3N0YXJ0ZWQYBSABKAsyJi5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25TdGFydGVkSAASOQoMb3V0cHV0X2NodW5rGAYgASgLMiEub2xpdmV0aW4uYXBpLnYxLkV2ZW50T3V0cHV0Q2h1bmtIABI0CgloZWFydGJlYXQYByABKAsyHy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRIZWFydGJlYXRIAEIHCgVldmVudCJBChBFdmVudE91dHB1dENodW5rEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZvdXRwdXQYAiABKAkiFAoSRXZlbnRFbnRpdHlDaGFuZ2VkIhQKEkV2ZW50Q29uZmlnQ2hhbmdlZCIQCg5FdmVudEhlYXJ0YmVhdCJGChZFdmVudEV4ZWN1dGlvbkZpbmlzaGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJFChVFdmVudEV4ZWN1dGlvblN0YXJ0ZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IjIKEUtpbGxBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSJtChJLaWxsQWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBmtpbGxlZBgCIAEoCBIZChFhbHJlYWR5X2NvbXBsZXRlZBgDIAEoCBINCgVmb3VuZBgEIAEoCCI7ChVMb2NhbFVzZXJMb2dpblJlcXVlc3QSEAoIdXNlcm5hbWUYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiKQoWTG9jYWxVc2VyTG9naW5SZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIicKE1Bhc3N3b3JkSGFzaFJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkiJAoUUGFzc3dvcmRIYXNoUmVzcG9uc2USDAoEaGFzaBgBIAEoCSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlIhcKFUdldERpYWdub3N0aWNzUmVxdWVzdCJFChZHZXREaWFnbm9zdGljc1Jlc3BvbnNlEhMKC1NzaEZvdW5kS2V5GAEgASgJEhYKDlNzaEZvdW5kQ29uZmlnGAIgASgJIg0KC0luaXRSZXF1ZXN0IusFCgxJbml0UmVzcG9uc2USEgoKc2hvd0Zvb3RlchgBIAEoCBIWCg5zaG93TmF2aWdhdGlvbhgCIAEoCBIXCg9zaG93TmV3VmVyc2lvbnMYAyABKAgSGAoQYXZhaWxhYmxlVmVyc2lvbhgEIAEoCRIWCg5jdXJyZW50VmVyc2lvbhgFIAEoCRIRCglwYWdlVGl0bGUYBiABKAkSHgoWc2VjdGlvbk5hdmlnYXRpb25TdHlsZRgHIAEoCRIaChJkZWZhdWx0SWNvbkZvckJhY2sYCCABKAkSFgoOZW5hYmxlQ3VzdG9tSnMYCSABKAgSFAoMYXV0aExvZ2luVXJsGAogASgJEhYKDmF1dGhMb2NhbExvZ2luGAsgASgIEhEKCXN0eWxlTW9kcxgMIAMoCRI4Cg9vQXV0aDJQcm92aWRlcnMYDSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuT0F1dGgyUHJvdmlkZXISOAoPYWRkaXRpb25hbExpbmtzGA4gAygLMh8ub2xpdmV0aW4uYXBpLnYxLkFkZGl0aW9uYWxMaW5rEhYKDnJvb3REYXNoYm9hcmRzGA8gAygJEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgQIAEoCRIjChthdXRoZW50aWNhdGVkX3VzZXJfcHJvdmlkZXIYESABKAkSOgoQZWZmZWN0aXZlX3BvbGljeRgSIAEoCzIgLm9saXZldGluLmFwaS52MS5FZmZlY3RpdmVQb2xpY3kSFgoOYmFubmVyX21lc3NhZ2UYEyABKAkSEgoKYmFubmVyX2NzcxgUIAEoCRIYChBzaG93X2RpYWdub3N0aWNzGBUgASgIEhUKDXNob3dfbG9nX2xpc3QYFiABKAgSFgoObG9naW5fcmVxdWlyZWQYFyABKAgSGAoQYXZhaWxhYmxlX3RoZW1lcxgYIAMoCRIkChxzaG93X25hdmlnYXRlX29uX3N0YXJ0X2ljb25zGBkgASgIIiwKDkFkZGl0aW9uYWxMaW5rEg0KBXRpdGxlGAEgASgJEgsKA3VybBgCIAEoCSI6Cg5PQXV0aDJQcm92aWRlchINCgV0aXRsZRgBIAEoCRIMCgRpY29uGAMgASgJEgsKA2tleRgEIAEoCSItChdHZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBISCgpiaW5kaW5nX2lkGAEgASgJIosBChhHZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2USJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhJGChJiYWNrX3RvX2Rhc2hib2FyZHMYAiADKAsyKi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkTmF2aWdhdGlvblRhcmdldCJaChJHZXRFbnRpdGllc1JlcXVlc3QSEwoLZW50aXR5X3R5cGUYASABKAkSDgoGZmlsdGVyGAIgASgJEgwKBHBhZ2UYAyABKAUSEQoJcGFnZV9zaXplGAQgASgFIlQKE0dldEVudGl0aWVzUmVzcG9uc2USPQoSZW50aXR5X2RlZmluaXRpb25zGAEgAygLMiEub2xpdmV0aW4uYXBpLnYxLkVudGl0eURlZmluaXRpb24ixQEKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkSDAoEaWNvbhgEIAEoCRIzCgpwcm9wZXJ0aWVzGAUgAygLMh8ub2xpdmV0aW4uYXBpLnYxLkVudGl0eVByb3BlcnR5EhcKD3RvdGFsX2luc3RhbmNlcxgGIAEoBSItCg5FbnRpdHlQcm9wZXJ0eRIMCgRuYW1lGAEgASgJEg0KBXRpdGxlGAIgASgJIjQKEEdldEVudGl0eVJlcXVlc3QSEgoKdW5pcXVlX2tleRgBIAEoCRIMCgR0eXBlGAIgASgJIjUKFFJlc3RhcnRBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCTLWEwoST2xpdmVUaW5BcGlTZXJ2aWNlEl0KDEdldERhc2hib2FyZBIkLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlc3BvbnNlIgASWgoLU3RhcnRBY3Rpb24SIy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJvChJTdGFydEFjdGlvbkFuZFdhaXQSKi5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBorLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZSIAEmkKEFN0YXJ0QWN0aW9uQnlHZXQSKC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlIgASfgoXU3RhcnRBY3Rpb25CeUdldEFuZFdhaXQSLy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0GjAub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2UiABJeCg1SZXN0YXJ0QWN0aW9uEiUub2xpdmV0aW4uYXBpLnYxLlJlc3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJXCgpLaWxsQWN0aW9uEiIub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXF1ZXN0GiMub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXNwb25zZSIAEmYKD0V4ZWN1dGlvblN0YXR1cxInLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXF1ZXN0Gigub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlIgASTgoHR2V0TG9ncxIfLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVxdWVzdBogLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVzcG9uc2UiABJgCg1HZXRBY3Rpb25Mb2dzEiUub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXF1ZXN0GiYub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXNwb25zZSIAEmwKEUdldEV4ZWN1dGlvblF1ZXVlEikub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdBoqLm9saXZldGluLmFwaS52MS5HZXRFeGVjdXRpb25RdWV1ZVJlc3BvbnNlIgASdQoUVmFsaWRhdGVBcmd1bWVudFR5cGUSLC5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Gi0ub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2UiABJLCgZXaG9BbUkSHi5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVxdWVzdBofLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXNwb25zZSIAElQKCVNvc1JlcG9ydBIhLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlc3BvbnNlIgASUQoIRHVtcFZhcnMSIC5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXF1ZXN0GiEub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UiABJ4ChVEdW1wUHVibGljSWRBY3Rpb25NYXASLS5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdBouLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXNwb25zZSIAElQKCUdldFJlYWR5ehIhLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXF1ZXN0GiIub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlc3BvbnNlIgASYwoOTG9jYWxVc2VyTG9naW4SJi5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVzcG9uc2UiABJdCgxQYXNzd29yZEhhc2gSJC5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXNwb25zZSIAEksKBkxvZ291dBIeLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlc3BvbnNlIgASXAoLRXZlbnRTdHJlYW0SIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVzcG9uc2UiADABEmMKDkdldERpYWdub3N0aWNzEiYub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1Jlc3BvbnNlIgASRQoESW5pdBIcLm9saXZldGluLmFwaS52MS5Jbml0UmVxdWVzdBodLm9saXZldGluLmFwaS52MS5Jbml0UmVzcG9uc2UiABJpChBHZXRBY3Rpb25CaW5kaW5nEigub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkJpbmRpbmdSZXNwb25zZSIAEloKC0dldEVudGl0aWVzEiMub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1Jlc3BvbnNlIgASSQoJR2V0RW50aXR5EiEub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0eVJlcXVlc3QaFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5IgBCOFo2Z2l0aHViLmNvbS9PbGl2ZVRpbi9PbGl2ZVRpbi9nZW4vb2xpdmV0aW4vYXBpL3YxO2FwaXYxYgZwcm90bzM"); /** * Describes the message olivetin.api.v1.Action. @@ -45,505 +45,520 @@ export const ActionArgumentSchema = /*@__PURE__*/ export const ActionArgumentChoiceSchema = /*@__PURE__*/ messageDesc(file_olivetin_api_v1_olivetin, 4); +/** + * Describes the message olivetin.api.v1.EntityRelatedAction. + * Use `create(EntityRelatedActionSchema)` to create a new message. + */ +export const EntityRelatedActionSchema = /*@__PURE__*/ + messageDesc(file_olivetin_api_v1_olivetin, 5); + /** * Describes the message olivetin.api.v1.Entity. * Use `create(EntitySchema)` to create a new message. */ export const EntitySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 5); + messageDesc(file_olivetin_api_v1_olivetin, 6); /** * Describes the message olivetin.api.v1.GetDashboardResponse. * Use `create(GetDashboardResponseSchema)` to create a new message. */ export const GetDashboardResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 6); + messageDesc(file_olivetin_api_v1_olivetin, 7); /** * Describes the message olivetin.api.v1.EffectivePolicy. * Use `create(EffectivePolicySchema)` to create a new message. */ export const EffectivePolicySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 7); + messageDesc(file_olivetin_api_v1_olivetin, 8); /** * Describes the message olivetin.api.v1.GetDashboardRequest. * Use `create(GetDashboardRequestSchema)` to create a new message. */ export const GetDashboardRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 8); + messageDesc(file_olivetin_api_v1_olivetin, 9); /** * Describes the message olivetin.api.v1.Dashboard. * Use `create(DashboardSchema)` to create a new message. */ export const DashboardSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 9); + messageDesc(file_olivetin_api_v1_olivetin, 10); /** * Describes the message olivetin.api.v1.DashboardComponent. * Use `create(DashboardComponentSchema)` to create a new message. */ export const DashboardComponentSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 10); + messageDesc(file_olivetin_api_v1_olivetin, 11); /** * Describes the message olivetin.api.v1.StartActionRequest. * Use `create(StartActionRequestSchema)` to create a new message. */ export const StartActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 11); + messageDesc(file_olivetin_api_v1_olivetin, 12); /** * Describes the message olivetin.api.v1.StartActionArgument. * Use `create(StartActionArgumentSchema)` to create a new message. */ export const StartActionArgumentSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 12); + messageDesc(file_olivetin_api_v1_olivetin, 13); /** * Describes the message olivetin.api.v1.StartActionResponse. * Use `create(StartActionResponseSchema)` to create a new message. */ export const StartActionResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 13); + messageDesc(file_olivetin_api_v1_olivetin, 14); /** * Describes the message olivetin.api.v1.StartActionAndWaitRequest. * Use `create(StartActionAndWaitRequestSchema)` to create a new message. */ export const StartActionAndWaitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 14); + messageDesc(file_olivetin_api_v1_olivetin, 15); /** * Describes the message olivetin.api.v1.StartActionAndWaitResponse. * Use `create(StartActionAndWaitResponseSchema)` to create a new message. */ export const StartActionAndWaitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 15); + messageDesc(file_olivetin_api_v1_olivetin, 16); /** * Describes the message olivetin.api.v1.StartActionByGetRequest. * Use `create(StartActionByGetRequestSchema)` to create a new message. */ export const StartActionByGetRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 16); + messageDesc(file_olivetin_api_v1_olivetin, 17); /** * Describes the message olivetin.api.v1.StartActionByGetResponse. * Use `create(StartActionByGetResponseSchema)` to create a new message. */ export const StartActionByGetResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 17); + messageDesc(file_olivetin_api_v1_olivetin, 18); /** * Describes the message olivetin.api.v1.StartActionByGetAndWaitRequest. * Use `create(StartActionByGetAndWaitRequestSchema)` to create a new message. */ export const StartActionByGetAndWaitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 18); + messageDesc(file_olivetin_api_v1_olivetin, 19); /** * Describes the message olivetin.api.v1.StartActionByGetAndWaitResponse. * Use `create(StartActionByGetAndWaitResponseSchema)` to create a new message. */ export const StartActionByGetAndWaitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 19); + messageDesc(file_olivetin_api_v1_olivetin, 20); /** * Describes the message olivetin.api.v1.GetLogsRequest. * Use `create(GetLogsRequestSchema)` to create a new message. */ export const GetLogsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 20); + messageDesc(file_olivetin_api_v1_olivetin, 21); /** * Describes the message olivetin.api.v1.LogEntry. * Use `create(LogEntrySchema)` to create a new message. */ export const LogEntrySchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 21); + messageDesc(file_olivetin_api_v1_olivetin, 22); /** * Describes the message olivetin.api.v1.GetLogsResponse. * Use `create(GetLogsResponseSchema)` to create a new message. */ export const GetLogsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 22); + messageDesc(file_olivetin_api_v1_olivetin, 23); /** * Describes the message olivetin.api.v1.GetActionLogsRequest. * Use `create(GetActionLogsRequestSchema)` to create a new message. */ export const GetActionLogsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 23); + messageDesc(file_olivetin_api_v1_olivetin, 24); /** * Describes the message olivetin.api.v1.GetActionLogsResponse. * Use `create(GetActionLogsResponseSchema)` to create a new message. */ export const GetActionLogsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 24); + messageDesc(file_olivetin_api_v1_olivetin, 25); /** * Describes the message olivetin.api.v1.GetExecutionQueueRequest. * Use `create(GetExecutionQueueRequestSchema)` to create a new message. */ export const GetExecutionQueueRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 25); + messageDesc(file_olivetin_api_v1_olivetin, 26); /** * Describes the message olivetin.api.v1.ExecutionQueueAction. * Use `create(ExecutionQueueActionSchema)` to create a new message. */ export const ExecutionQueueActionSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 26); + messageDesc(file_olivetin_api_v1_olivetin, 27); /** * Describes the message olivetin.api.v1.ExecutionQueueGroup. * Use `create(ExecutionQueueGroupSchema)` to create a new message. */ export const ExecutionQueueGroupSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 27); + messageDesc(file_olivetin_api_v1_olivetin, 28); /** * Describes the message olivetin.api.v1.GetExecutionQueueResponse. * Use `create(GetExecutionQueueResponseSchema)` to create a new message. */ export const GetExecutionQueueResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 28); + messageDesc(file_olivetin_api_v1_olivetin, 29); /** * Describes the message olivetin.api.v1.ValidateArgumentTypeRequest. * Use `create(ValidateArgumentTypeRequestSchema)` to create a new message. */ export const ValidateArgumentTypeRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 29); + messageDesc(file_olivetin_api_v1_olivetin, 30); /** * Describes the message olivetin.api.v1.ValidateArgumentTypeResponse. * Use `create(ValidateArgumentTypeResponseSchema)` to create a new message. */ export const ValidateArgumentTypeResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 30); + messageDesc(file_olivetin_api_v1_olivetin, 31); /** * Describes the message olivetin.api.v1.WatchExecutionRequest. * Use `create(WatchExecutionRequestSchema)` to create a new message. */ export const WatchExecutionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 31); + messageDesc(file_olivetin_api_v1_olivetin, 32); /** * Describes the message olivetin.api.v1.WatchExecutionUpdate. * Use `create(WatchExecutionUpdateSchema)` to create a new message. */ export const WatchExecutionUpdateSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 32); + messageDesc(file_olivetin_api_v1_olivetin, 33); /** * Describes the message olivetin.api.v1.ExecutionStatusRequest. * Use `create(ExecutionStatusRequestSchema)` to create a new message. */ export const ExecutionStatusRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 33); + messageDesc(file_olivetin_api_v1_olivetin, 34); /** * Describes the message olivetin.api.v1.DashboardNavigationTarget. * Use `create(DashboardNavigationTargetSchema)` to create a new message. */ export const DashboardNavigationTargetSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 34); + messageDesc(file_olivetin_api_v1_olivetin, 35); /** * Describes the message olivetin.api.v1.ExecutionStatusResponse. * Use `create(ExecutionStatusResponseSchema)` to create a new message. */ export const ExecutionStatusResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 35); + messageDesc(file_olivetin_api_v1_olivetin, 36); /** * Describes the message olivetin.api.v1.WhoAmIRequest. * Use `create(WhoAmIRequestSchema)` to create a new message. */ export const WhoAmIRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 36); + messageDesc(file_olivetin_api_v1_olivetin, 37); /** * Describes the message olivetin.api.v1.WhoAmIResponse. * Use `create(WhoAmIResponseSchema)` to create a new message. */ export const WhoAmIResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 37); + messageDesc(file_olivetin_api_v1_olivetin, 38); /** * Describes the message olivetin.api.v1.SosReportRequest. * Use `create(SosReportRequestSchema)` to create a new message. */ export const SosReportRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 38); + messageDesc(file_olivetin_api_v1_olivetin, 39); /** * Describes the message olivetin.api.v1.SosReportResponse. * Use `create(SosReportResponseSchema)` to create a new message. */ export const SosReportResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 39); + messageDesc(file_olivetin_api_v1_olivetin, 40); /** * Describes the message olivetin.api.v1.DumpVarsRequest. * Use `create(DumpVarsRequestSchema)` to create a new message. */ export const DumpVarsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 40); + messageDesc(file_olivetin_api_v1_olivetin, 41); /** * Describes the message olivetin.api.v1.DumpVarsResponse. * Use `create(DumpVarsResponseSchema)` to create a new message. */ export const DumpVarsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 41); + messageDesc(file_olivetin_api_v1_olivetin, 42); /** * Describes the message olivetin.api.v1.DebugBinding. * Use `create(DebugBindingSchema)` to create a new message. */ export const DebugBindingSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 42); + messageDesc(file_olivetin_api_v1_olivetin, 43); /** * Describes the message olivetin.api.v1.DumpPublicIdActionMapRequest. * Use `create(DumpPublicIdActionMapRequestSchema)` to create a new message. */ export const DumpPublicIdActionMapRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 43); + messageDesc(file_olivetin_api_v1_olivetin, 44); /** * Describes the message olivetin.api.v1.DumpPublicIdActionMapResponse. * Use `create(DumpPublicIdActionMapResponseSchema)` to create a new message. */ export const DumpPublicIdActionMapResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 44); + messageDesc(file_olivetin_api_v1_olivetin, 45); /** * Describes the message olivetin.api.v1.GetReadyzRequest. * Use `create(GetReadyzRequestSchema)` to create a new message. */ export const GetReadyzRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 45); + messageDesc(file_olivetin_api_v1_olivetin, 46); /** * Describes the message olivetin.api.v1.GetReadyzResponse. * Use `create(GetReadyzResponseSchema)` to create a new message. */ export const GetReadyzResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 46); + messageDesc(file_olivetin_api_v1_olivetin, 47); /** * Describes the message olivetin.api.v1.EventStreamRequest. * Use `create(EventStreamRequestSchema)` to create a new message. */ export const EventStreamRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 47); + messageDesc(file_olivetin_api_v1_olivetin, 48); /** * Describes the message olivetin.api.v1.EventStreamResponse. * Use `create(EventStreamResponseSchema)` to create a new message. */ export const EventStreamResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 48); + messageDesc(file_olivetin_api_v1_olivetin, 49); /** * Describes the message olivetin.api.v1.EventOutputChunk. * Use `create(EventOutputChunkSchema)` to create a new message. */ export const EventOutputChunkSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 49); + messageDesc(file_olivetin_api_v1_olivetin, 50); /** * Describes the message olivetin.api.v1.EventEntityChanged. * Use `create(EventEntityChangedSchema)` to create a new message. */ export const EventEntityChangedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 50); + messageDesc(file_olivetin_api_v1_olivetin, 51); /** * Describes the message olivetin.api.v1.EventConfigChanged. * Use `create(EventConfigChangedSchema)` to create a new message. */ export const EventConfigChangedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 51); + messageDesc(file_olivetin_api_v1_olivetin, 52); /** * Describes the message olivetin.api.v1.EventHeartbeat. * Use `create(EventHeartbeatSchema)` to create a new message. */ export const EventHeartbeatSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 52); + messageDesc(file_olivetin_api_v1_olivetin, 53); /** * Describes the message olivetin.api.v1.EventExecutionFinished. * Use `create(EventExecutionFinishedSchema)` to create a new message. */ export const EventExecutionFinishedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 53); + messageDesc(file_olivetin_api_v1_olivetin, 54); /** * Describes the message olivetin.api.v1.EventExecutionStarted. * Use `create(EventExecutionStartedSchema)` to create a new message. */ export const EventExecutionStartedSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 54); + messageDesc(file_olivetin_api_v1_olivetin, 55); /** * Describes the message olivetin.api.v1.KillActionRequest. * Use `create(KillActionRequestSchema)` to create a new message. */ export const KillActionRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 55); + messageDesc(file_olivetin_api_v1_olivetin, 56); /** * Describes the message olivetin.api.v1.KillActionResponse. * Use `create(KillActionResponseSchema)` to create a new message. */ export const KillActionResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 56); + messageDesc(file_olivetin_api_v1_olivetin, 57); /** * Describes the message olivetin.api.v1.LocalUserLoginRequest. * Use `create(LocalUserLoginRequestSchema)` to create a new message. */ export const LocalUserLoginRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 57); + messageDesc(file_olivetin_api_v1_olivetin, 58); /** * Describes the message olivetin.api.v1.LocalUserLoginResponse. * Use `create(LocalUserLoginResponseSchema)` to create a new message. */ export const LocalUserLoginResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 58); + messageDesc(file_olivetin_api_v1_olivetin, 59); /** * Describes the message olivetin.api.v1.PasswordHashRequest. * Use `create(PasswordHashRequestSchema)` to create a new message. */ export const PasswordHashRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 59); + messageDesc(file_olivetin_api_v1_olivetin, 60); /** * Describes the message olivetin.api.v1.PasswordHashResponse. * Use `create(PasswordHashResponseSchema)` to create a new message. */ export const PasswordHashResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 60); + messageDesc(file_olivetin_api_v1_olivetin, 61); /** * Describes the message olivetin.api.v1.LogoutRequest. * Use `create(LogoutRequestSchema)` to create a new message. */ export const LogoutRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 61); + messageDesc(file_olivetin_api_v1_olivetin, 62); /** * Describes the message olivetin.api.v1.LogoutResponse. * Use `create(LogoutResponseSchema)` to create a new message. */ export const LogoutResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 62); + messageDesc(file_olivetin_api_v1_olivetin, 63); /** * Describes the message olivetin.api.v1.GetDiagnosticsRequest. * Use `create(GetDiagnosticsRequestSchema)` to create a new message. */ export const GetDiagnosticsRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 63); + messageDesc(file_olivetin_api_v1_olivetin, 64); /** * Describes the message olivetin.api.v1.GetDiagnosticsResponse. * Use `create(GetDiagnosticsResponseSchema)` to create a new message. */ export const GetDiagnosticsResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 64); + messageDesc(file_olivetin_api_v1_olivetin, 65); /** * Describes the message olivetin.api.v1.InitRequest. * Use `create(InitRequestSchema)` to create a new message. */ export const InitRequestSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 65); + messageDesc(file_olivetin_api_v1_olivetin, 66); /** * Describes the message olivetin.api.v1.InitResponse. * Use `create(InitResponseSchema)` to create a new message. */ export const InitResponseSchema = /*@__PURE__*/ - messageDesc(file_olivetin_api_v1_olivetin, 66); + messageDesc(file_olivetin_api_v1_olivetin, 67); /** * 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, 67); + messageDesc(file_olivetin_api_v1_olivetin, 68); /** * 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, 68); + messageDesc(file_olivetin_api_v1_olivetin, 69); /** * 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, 69); + messageDesc(file_olivetin_api_v1_olivetin, 70); /** * 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, 70); + messageDesc(file_olivetin_api_v1_olivetin, 71); /** * 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, 71); + messageDesc(file_olivetin_api_v1_olivetin, 72); /** * 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, 72); + messageDesc(file_olivetin_api_v1_olivetin, 73); /** * 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, 73); + messageDesc(file_olivetin_api_v1_olivetin, 74); + +/** + * 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, 75); /** * 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, 74); + messageDesc(file_olivetin_api_v1_olivetin, 76); /** * 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, 75); + messageDesc(file_olivetin_api_v1_olivetin, 77); /** * @generated from service olivetin.api.v1.OliveTinApiService */ export const OliveTinApiService = /*@__PURE__*/ serviceDesc(file_olivetin_api_v1_olivetin, 0); + diff --git a/frontend/resources/vue/ActionButton.vue b/frontend/resources/vue/ActionButton.vue index d1df11a..649979c 100644 --- a/frontend/resources/vue/ActionButton.vue +++ b/frontend/resources/vue/ActionButton.vue @@ -61,6 +61,11 @@ const props = defineProps({ type: String, required: false, default: '' + }, + prefilledArguments: { + type: Object, + required: false, + default: () => ({}) } }) @@ -248,7 +253,16 @@ async function handleClick() { return } if (needsArgumentForm(props.actionData)) { - router.push(`/actionBinding/${props.actionData.bindingId}/argumentForm`) + const bindingId = props.actionData.bindingId + const prefilled = props.prefilledArguments || {} + if (Object.keys(prefilled).length > 0) { + router.push({ + path: `/actionBinding/${bindingId}/argumentForm`, + state: { prefilledArguments: prefilled } + }) + } else { + router.push(`/actionBinding/${bindingId}/argumentForm`) + } } else { await startAction() } diff --git a/frontend/resources/vue/components/ChoiceChecklist.vue b/frontend/resources/vue/components/ChoiceChecklist.vue new file mode 100644 index 0000000..04235d6 --- /dev/null +++ b/frontend/resources/vue/components/ChoiceChecklist.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/frontend/resources/vue/components/ChoiceCombobox.vue b/frontend/resources/vue/components/ChoiceCombobox.vue index d19ec35..3c11692 100644 --- a/frontend/resources/vue/components/ChoiceCombobox.vue +++ b/frontend/resources/vue/components/ChoiceCombobox.vue @@ -88,6 +88,7 @@ const rootRef = ref(null) const searchInputRef = ref(null) const isOpen = ref(false) const query = ref('') +const isUserFiltering = ref(false) const highlightedIndex = ref(0) const listboxId = computed(() => `${props.id}-listbox`) @@ -109,6 +110,10 @@ const placeholderText = computed(() => { }) const filteredChoices = computed(() => { + if (!isUserFiltering.value) { + return props.choices + } + const search = query.value.trim().toLowerCase() if (!search) { return props.choices @@ -140,14 +145,23 @@ function syncFromModelValue() { } } +function selectedChoiceIndex(choices) { + const index = choices.findIndex(choice => choice.value === props.modelValue) + return index >= 0 ? index : 0 +} + function openList() { document.dispatchEvent(new CustomEvent(closeOthersEvent, { detail: { id: props.id } })) + const wasClosed = !isOpen.value isOpen.value = true - highlightedIndex.value = 0 + if (wasClosed) { + highlightedIndex.value = selectedChoiceIndex(filteredChoices.value) + } } function closeList() { isOpen.value = false + isUserFiltering.value = false syncFromModelValue() } @@ -164,12 +178,14 @@ function selectChoice(choice) { function handleFocus() { if (!isOpen.value) { syncFromModelValue() + isUserFiltering.value = false } openList() } function handleSearchInput(event) { + isUserFiltering.value = true query.value = event.target.value openList() highlightedIndex.value = 0 diff --git a/frontend/resources/vue/components/EntityDefinitionSection.vue b/frontend/resources/vue/components/EntityDefinitionSection.vue new file mode 100644 index 0000000..b6defea --- /dev/null +++ b/frontend/resources/vue/components/EntityDefinitionSection.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/frontend/resources/vue/components/EntityInstancesTable.vue b/frontend/resources/vue/components/EntityInstancesTable.vue new file mode 100644 index 0000000..1ff84e2 --- /dev/null +++ b/frontend/resources/vue/components/EntityInstancesTable.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/frontend/resources/vue/components/EntityListFilter.vue b/frontend/resources/vue/components/EntityListFilter.vue new file mode 100644 index 0000000..16a14c5 --- /dev/null +++ b/frontend/resources/vue/components/EntityListFilter.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/frontend/resources/vue/utils/choiceChecklistHelpers.js b/frontend/resources/vue/utils/choiceChecklistHelpers.js new file mode 100644 index 0000000..8c48f10 --- /dev/null +++ b/frontend/resources/vue/utils/choiceChecklistHelpers.js @@ -0,0 +1,54 @@ +function parseLegacyChecklistValue(value) { + return value.split(',').map((segment) => segment.trim()).filter((segment) => segment !== '') +} + +export function parseChecklistValue(value) { + if (!value || value === '') { + return [] + } + + const trimmed = value.trim() + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed) + if (!Array.isArray(parsed)) { + return [] + } + + return parsed.map((segment) => String(segment).trim()).filter((segment) => segment !== '') + } catch { + return [] + } + } + + return parseLegacyChecklistValue(value) +} + +export function formatChecklistValue(selected) { + if (!Array.isArray(selected) || selected.length === 0) { + return '' + } + + return JSON.stringify(selected) +} + +export function toggleChoice(selected, value) { + const current = Array.isArray(selected) ? [...selected] : [] + const index = current.indexOf(value) + + if (index === -1) { + current.push(value) + return current + } + + current.splice(index, 1) + return current +} + +export function choiceLabel(choice) { + return choice.title || choice.value || '' +} + +export function allChoiceValues(choices) { + return choices.map((choice) => choice.value) +} diff --git a/frontend/resources/vue/utils/choiceChecklistHelpers.test.mjs b/frontend/resources/vue/utils/choiceChecklistHelpers.test.mjs new file mode 100644 index 0000000..bee56b1 --- /dev/null +++ b/frontend/resources/vue/utils/choiceChecklistHelpers.test.mjs @@ -0,0 +1,46 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { + allChoiceValues, + choiceLabel, + formatChecklistValue, + parseChecklistValue, + toggleChoice +} from './choiceChecklistHelpers.js' + +const choices = [ + { title: 'Documents', value: 'documents' }, + { title: 'Photos', value: 'photos' } +] + + test('parseChecklistValue parses JSON-encoded values', () => { + assert.deepEqual(parseChecklistValue('["documents","photos"]'), ['documents', 'photos']) + assert.deepEqual(parseChecklistValue('["kitchen,bedroom","hallway"]'), ['kitchen,bedroom', 'hallway']) + assert.deepEqual(parseChecklistValue(''), []) +}) + +test('parseChecklistValue accepts legacy comma-delimited values', () => { + assert.deepEqual(parseChecklistValue('documents,photos'), ['documents', 'photos']) + assert.deepEqual(parseChecklistValue('documents, photos'), ['documents', 'photos']) +}) + +test('formatChecklistValue joins selected values as JSON', () => { + assert.equal(formatChecklistValue(['documents', 'photos']), '["documents","photos"]') + assert.equal(formatChecklistValue(['kitchen,bedroom']), '["kitchen,bedroom"]') + assert.equal(formatChecklistValue([]), '') +}) + +test('toggleChoice adds and removes values', () => { + assert.deepEqual(toggleChoice([], 'documents'), ['documents']) + assert.deepEqual(toggleChoice(['documents'], 'photos'), ['documents', 'photos']) + assert.deepEqual(toggleChoice(['documents', 'photos'], 'documents'), ['photos']) +}) + +test('choiceLabel prefers title over value', () => { + assert.equal(choiceLabel(choices[0]), 'Documents') + assert.equal(choiceLabel({ value: 'music' }), 'music') +}) + +test('allChoiceValues returns every choice value', () => { + assert.deepEqual(allChoiceValues(choices), ['documents', 'photos']) +}) diff --git a/frontend/resources/vue/utils/entityRoutes.js b/frontend/resources/vue/utils/entityRoutes.js new file mode 100644 index 0000000..48cd5d6 --- /dev/null +++ b/frontend/resources/vue/utils/entityRoutes.js @@ -0,0 +1,9 @@ +export function entityDetailsRoute (entity) { + return { + name: 'EntityDetails', + params: { + entityType: entity.type, + entityKey: entity.uniqueKey + } + } +} diff --git a/frontend/resources/vue/utils/justificationTemplate.js b/frontend/resources/vue/utils/justificationTemplate.js new file mode 100644 index 0000000..39cec76 --- /dev/null +++ b/frontend/resources/vue/utils/justificationTemplate.js @@ -0,0 +1,15 @@ +export function applyArgumentTemplate(template, args) { + if (!template) { + return '' + } + + return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, name) => args[name] ?? '') +} + +export function actionRequiresJustification(justification) { + return (justification ?? '').length > 0 +} + +export function actionJustificationTemplate(justification) { + return justification ?? '' +} diff --git a/frontend/resources/vue/utils/needsArgumentForm.js b/frontend/resources/vue/utils/needsArgumentForm.js index 387d5ec..11b61bd 100644 --- a/frontend/resources/vue/utils/needsArgumentForm.js +++ b/frontend/resources/vue/utils/needsArgumentForm.js @@ -1,3 +1,5 @@ +import { actionRequiresJustification } from './justificationTemplate.js' + export function needsArgumentForm (action) { - return (action?.arguments?.length > 0) || action?.justification + return (action?.arguments?.length > 0) || actionRequiresJustification(action?.justification) } diff --git a/frontend/resources/vue/utils/prefilledArguments.js b/frontend/resources/vue/utils/prefilledArguments.js new file mode 100644 index 0000000..78a2554 --- /dev/null +++ b/frontend/resources/vue/utils/prefilledArguments.js @@ -0,0 +1,21 @@ +export function readPrefilledArgumentsFromNavigation() { + const state = window.history.state + if (state?.prefilledArguments && typeof state.prefilledArguments === 'object') { + return { ...state.prefilledArguments } + } + + return {} +} + +export function getInitialArgumentValue(paramName, prefilledArguments = {}) { + const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object' + ? prefilledArguments + : {} + + if (Object.prototype.hasOwnProperty.call(safePrefilledArguments, paramName)) { + return safePrefilledArguments[paramName] + } + + const params = new URLSearchParams(window.location.search) + return params.get(paramName) +} diff --git a/frontend/resources/vue/utils/prefilledArguments.test.mjs b/frontend/resources/vue/utils/prefilledArguments.test.mjs new file mode 100644 index 0000000..b18bd44 --- /dev/null +++ b/frontend/resources/vue/utils/prefilledArguments.test.mjs @@ -0,0 +1,39 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { getInitialArgumentValue, readPrefilledArgumentsFromNavigation } from './prefilledArguments.js' + +test('readPrefilledArgumentsFromNavigation returns navigation state values', () => { + const originalState = window.history.state + window.history.replaceState({ prefilledArguments: { ansible_host: '10.0.0.1' } }, '') + + assert.deepEqual(readPrefilledArgumentsFromNavigation(), { ansible_host: '10.0.0.1' }) + + window.history.replaceState(originalState, '') +}) + +test('getInitialArgumentValue prefers navigation state over query params', () => { + const originalState = window.history.state + const originalSearch = window.location.search + + window.history.replaceState({ prefilledArguments: { ansible_host: '10.0.0.1' } }, '') + window.history.replaceState(window.history.state, '', '?ansible_host=10.0.0.2') + + assert.equal(getInitialArgumentValue('ansible_host', readPrefilledArgumentsFromNavigation()), '10.0.0.1') + + window.history.replaceState(originalState, '') + window.history.replaceState(window.history.state, '', originalSearch || '/') +}) + +test('getInitialArgumentValue falls back to query params when state is absent', () => { + const originalState = window.history.state + const originalSearch = window.location.search + + window.history.replaceState({}, '') + window.history.replaceState(window.history.state, '', '?ansible_host=10.0.0.2') + + assert.equal(getInitialArgumentValue('ansible_host', readPrefilledArgumentsFromNavigation()), '10.0.0.2') + + window.history.replaceState(originalState, '') + window.history.replaceState(window.history.state, '', originalSearch || '/') +}) diff --git a/frontend/resources/vue/utils/rerunArguments.js b/frontend/resources/vue/utils/rerunArguments.js index 180711b..d214960 100644 --- a/frontend/resources/vue/utils/rerunArguments.js +++ b/frontend/resources/vue/utils/rerunArguments.js @@ -1,4 +1,5 @@ import { needsArgumentForm } from './needsArgumentForm.js' +import { actionRequiresJustification } from './justificationTemplate.js' const nonStorableArgumentTypes = new Set([ 'password', @@ -17,7 +18,7 @@ export function logEntryArgumentsToStartActionArgs (logEntry) { } export function rerunNeedsArgumentForm (action, logEntry) { - if (action?.justification && !logEntry?.justification) { + if (actionRequiresJustification(action?.justification) && !logEntry?.justification) { return true } @@ -60,7 +61,7 @@ export function buildRerunStartActionArgs (bindingId, logEntry, action) { arguments: logEntryArgumentsToStartActionArgs(logEntry) } - if (action?.justification && logEntry?.justification) { + if (actionRequiresJustification(action?.justification) && logEntry?.justification) { startActionArgs.justification = logEntry.justification } diff --git a/frontend/resources/vue/utils/rerunArguments.test.mjs b/frontend/resources/vue/utils/rerunArguments.test.mjs index 1caf5e7..4f34b9f 100644 --- a/frontend/resources/vue/utils/rerunArguments.test.mjs +++ b/frontend/resources/vue/utils/rerunArguments.test.mjs @@ -75,7 +75,7 @@ test('rerunNeedsArgumentForm can start directly when stored args are complete', }) test('rerunNeedsArgumentForm opens the form when justification is missing', () => { - const action = { justification: true, arguments: [] } + const action = { justification: ' ', arguments: [] } assert.equal(rerunNeedsArgumentForm(action, {}), true) assert.equal( @@ -90,7 +90,7 @@ test('buildRerunStartActionArgs includes stored justification', () => { arguments: [{ name: 'host', value: 'db-1' }], justification: 'maintenance window' }, { - justification: true, + justification: ' ', arguments: [{ name: 'host', type: 'ascii_identifier' }] }), { diff --git a/frontend/resources/vue/views/ActionDetailsView.vue b/frontend/resources/vue/views/ActionDetailsView.vue index 4e8738e..15519b4 100644 --- a/frontend/resources/vue/views/ActionDetailsView.vue +++ b/frontend/resources/vue/views/ActionDetailsView.vue @@ -96,7 +96,9 @@ {{ formatExecutionDuration(log) }} - {{ log.executionTrackingId }} + + {{ log.executionTrackingId }} + @@ -134,6 +136,7 @@ import Section from 'picocrank/vue/components/Section.vue' import ActionIconGlyph from '../components/ActionIconGlyph.vue' import ActionStatusDisplay from '../components/ActionStatusDisplay.vue' import ActionGroupLimitsLabel from '../components/ActionGroupLimitsLabel.vue' +import LogActionTitle from '../components/LogActionTitle.vue' import { HugeiconsIcon } from '@hugeicons/vue' import { DashboardSquare01Icon, WorkoutRunIcon } from '@hugeicons/core-free-icons' import { requestReconnectNow } from '../../../js/websocket.js' @@ -162,7 +165,8 @@ const filteredLogs = computed(() => { const searchLower = searchText.value.toLowerCase() return logs.value.filter(log => log.executionTrackingId.toLowerCase().includes(searchLower) || - log.actionTitle.toLowerCase().includes(searchLower) + log.actionTitle.toLowerCase().includes(searchLower) || + (log.justification || '').toLowerCase().includes(searchLower) ) }) diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index f62bf89..e7a92ad 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -1,16 +1,30 @@