mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-10 10:56:34 +00:00
Render script execution state (#8789)
This commit is contained in:
parent
b6f59d3c98
commit
401064d3c8
@ -2,8 +2,7 @@ import { DemoTrace } from "./types";
|
|||||||
|
|
||||||
export const basicTrace: DemoTrace = {
|
export const basicTrace: DemoTrace = {
|
||||||
trace: {
|
trace: {
|
||||||
last_action: "action/2",
|
last_step: "action/2",
|
||||||
last_condition: "condition/0",
|
|
||||||
run_id: "0",
|
run_id: "0",
|
||||||
state: "stopped",
|
state: "stopped",
|
||||||
timestamp: {
|
timestamp: {
|
||||||
@ -14,6 +13,12 @@ export const basicTrace: DemoTrace = {
|
|||||||
domain: "automation",
|
domain: "automation",
|
||||||
item_id: "1615419646544",
|
item_id: "1615419646544",
|
||||||
trace: {
|
trace: {
|
||||||
|
"trigger/0": [
|
||||||
|
{
|
||||||
|
path: "trigger/0",
|
||||||
|
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||||
|
},
|
||||||
|
],
|
||||||
"condition/0": [
|
"condition/0": [
|
||||||
{
|
{
|
||||||
path: "condition/0",
|
path: "condition/0",
|
||||||
@ -284,45 +289,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
parent_id: "664d6d261450a9ecea6738e97269a149",
|
parent_id: "664d6d261450a9ecea6738e97269a149",
|
||||||
user_id: null,
|
user_id: null,
|
||||||
},
|
},
|
||||||
variables: {
|
script_execution: "finished",
|
||||||
trigger: {
|
|
||||||
platform: "state",
|
|
||||||
entity_id: "input_boolean.toggle_1",
|
|
||||||
from_state: {
|
|
||||||
entity_id: "input_boolean.toggle_1",
|
|
||||||
state: "on",
|
|
||||||
attributes: {
|
|
||||||
editable: true,
|
|
||||||
friendly_name: "Toggle 1",
|
|
||||||
},
|
|
||||||
last_changed: "2021-03-24T19:03:59.141440+00:00",
|
|
||||||
last_updated: "2021-03-24T19:03:59.141440+00:00",
|
|
||||||
context: {
|
|
||||||
id: "5d0918eb379214d07554bdab6a08bcff",
|
|
||||||
parent_id: null,
|
|
||||||
user_id: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
to_state: {
|
|
||||||
entity_id: "input_boolean.toggle_1",
|
|
||||||
state: "off",
|
|
||||||
attributes: {
|
|
||||||
editable: true,
|
|
||||||
friendly_name: "Toggle 1",
|
|
||||||
},
|
|
||||||
last_changed: "2021-03-25T04:36:51.220696+00:00",
|
|
||||||
last_updated: "2021-03-25T04:36:51.220696+00:00",
|
|
||||||
context: {
|
|
||||||
id: "664d6d261450a9ecea6738e97269a149",
|
|
||||||
parent_id: null,
|
|
||||||
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
for: null,
|
|
||||||
attribute: null,
|
|
||||||
description: "state of input_boolean.toggle_1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
logbookEntries: [
|
logbookEntries: [
|
||||||
{
|
{
|
||||||
|
44
gallery/src/data/traces/mock-demo-trace.ts
Normal file
44
gallery/src/data/traces/mock-demo-trace.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { LogbookEntry } from "../../../../src/data/logbook";
|
||||||
|
import { AutomationTraceExtended } from "../../../../src/data/trace";
|
||||||
|
import { DemoTrace } from "./types";
|
||||||
|
|
||||||
|
export const mockDemoTrace = (
|
||||||
|
tracePartial: Partial<AutomationTraceExtended>,
|
||||||
|
logbookEntries?: LogbookEntry[]
|
||||||
|
): DemoTrace => ({
|
||||||
|
trace: {
|
||||||
|
last_step: "",
|
||||||
|
run_id: "0",
|
||||||
|
state: "stopped",
|
||||||
|
timestamp: {
|
||||||
|
start: "2021-03-25T04:36:51.223693+00:00",
|
||||||
|
finish: "2021-03-25T04:36:51.266132+00:00",
|
||||||
|
},
|
||||||
|
trigger: "mocked trigger",
|
||||||
|
domain: "automation",
|
||||||
|
item_id: "1615419646544",
|
||||||
|
trace: {
|
||||||
|
"trigger/0": [
|
||||||
|
{
|
||||||
|
path: "trigger/0",
|
||||||
|
changed_variables: {
|
||||||
|
trigger: {
|
||||||
|
description: "mocked trigger",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
trigger: [],
|
||||||
|
action: [],
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
id: "abcd",
|
||||||
|
},
|
||||||
|
script_execution: "finished",
|
||||||
|
...tracePartial,
|
||||||
|
},
|
||||||
|
logbookEntries: logbookEntries || [],
|
||||||
|
});
|
@ -2,8 +2,7 @@ import { DemoTrace } from "./types";
|
|||||||
|
|
||||||
export const motionLightTrace: DemoTrace = {
|
export const motionLightTrace: DemoTrace = {
|
||||||
trace: {
|
trace: {
|
||||||
last_action: "action/3",
|
last_step: "action/3",
|
||||||
last_condition: null,
|
|
||||||
run_id: "1",
|
run_id: "1",
|
||||||
state: "stopped",
|
state: "stopped",
|
||||||
timestamp: {
|
timestamp: {
|
||||||
@ -14,6 +13,12 @@ export const motionLightTrace: DemoTrace = {
|
|||||||
domain: "automation",
|
domain: "automation",
|
||||||
item_id: "1614732497392",
|
item_id: "1614732497392",
|
||||||
trace: {
|
trace: {
|
||||||
|
"trigger/0": [
|
||||||
|
{
|
||||||
|
path: "trigger/0",
|
||||||
|
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||||
|
},
|
||||||
|
],
|
||||||
"action/0": [
|
"action/0": [
|
||||||
{
|
{
|
||||||
path: "action/0",
|
path: "action/0",
|
||||||
@ -171,45 +176,7 @@ export const motionLightTrace: DemoTrace = {
|
|||||||
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
|
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
|
||||||
user_id: null,
|
user_id: null,
|
||||||
},
|
},
|
||||||
variables: {
|
script_execution: "finished",
|
||||||
trigger: {
|
|
||||||
platform: "state",
|
|
||||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
|
||||||
from_state: {
|
|
||||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
|
||||||
state: "off",
|
|
||||||
attributes: {
|
|
||||||
friendly_name: "Paulus’s MacBook Pro Camera In Use",
|
|
||||||
icon: "mdi:camera-off",
|
|
||||||
},
|
|
||||||
last_changed: "2021-03-14T06:06:29.235325+00:00",
|
|
||||||
last_updated: "2021-03-14T06:06:29.235325+00:00",
|
|
||||||
context: {
|
|
||||||
id: "ad4864c5ce957c38a07b50378eeb245d",
|
|
||||||
parent_id: null,
|
|
||||||
user_id: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
to_state: {
|
|
||||||
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
|
|
||||||
state: "on",
|
|
||||||
attributes: {
|
|
||||||
friendly_name: "Paulus’s MacBook Pro Camera In Use",
|
|
||||||
icon: "mdi:camera",
|
|
||||||
},
|
|
||||||
last_changed: "2021-03-14T06:07:01.762009+00:00",
|
|
||||||
last_updated: "2021-03-14T06:07:01.762009+00:00",
|
|
||||||
context: {
|
|
||||||
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
|
|
||||||
parent_id: null,
|
|
||||||
user_id: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
for: null,
|
|
||||||
attribute: null,
|
|
||||||
description: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
logbookEntries: [
|
logbookEntries: [
|
||||||
{
|
{
|
||||||
|
87
gallery/src/demos/demo-automation-trace-timeline.ts
Normal file
87
gallery/src/demos/demo-automation-trace-timeline.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
property,
|
||||||
|
} from "lit-element";
|
||||||
|
import "../../../src/components/ha-card";
|
||||||
|
import "../../../src/components/trace/hat-script-graph";
|
||||||
|
import "../../../src/components/trace/hat-trace-timeline";
|
||||||
|
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import { mockDemoTrace } from "../data/traces/mock-demo-trace";
|
||||||
|
import { DemoTrace } from "../data/traces/types";
|
||||||
|
|
||||||
|
const traces: DemoTrace[] = [
|
||||||
|
mockDemoTrace({ state: "running" }),
|
||||||
|
mockDemoTrace({ state: "debugged" }),
|
||||||
|
mockDemoTrace({ state: "stopped", script_execution: "failed_condition" }),
|
||||||
|
mockDemoTrace({ state: "stopped", script_execution: "failed_single" }),
|
||||||
|
mockDemoTrace({ state: "stopped", script_execution: "failed_max_runs" }),
|
||||||
|
mockDemoTrace({ state: "stopped", script_execution: "finished" }),
|
||||||
|
mockDemoTrace({ state: "stopped", script_execution: "aborted" }),
|
||||||
|
mockDemoTrace({
|
||||||
|
state: "stopped",
|
||||||
|
script_execution: "error",
|
||||||
|
error: 'Variable "beer" cannot be None',
|
||||||
|
}),
|
||||||
|
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-automation-trace-timeline")
|
||||||
|
export class DemoAutomationTraceTimeline extends LitElement {
|
||||||
|
@property({ attribute: false }) hass?: HomeAssistant;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
${traces.map(
|
||||||
|
(trace) => html`
|
||||||
|
<ha-card .header=${trace.trace.config.alias}>
|
||||||
|
<div class="card-content">
|
||||||
|
<hat-trace-timeline
|
||||||
|
.hass=${this.hass}
|
||||||
|
.trace=${trace.trace}
|
||||||
|
.logbookEntries=${trace.logbookEntries}
|
||||||
|
></hat-trace-timeline>
|
||||||
|
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
const hass = provideHass(this);
|
||||||
|
hass.updateTranslations(null, "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-automation-trace-timeline": DemoAutomationTraceTimeline;
|
||||||
|
}
|
||||||
|
}
|
@ -20,9 +20,11 @@ import { HomeAssistant } from "../../types";
|
|||||||
import "./ha-timeline";
|
import "./ha-timeline";
|
||||||
import type { HaTimeline } from "./ha-timeline";
|
import type { HaTimeline } from "./ha-timeline";
|
||||||
import {
|
import {
|
||||||
|
mdiAlertCircle,
|
||||||
mdiCircle,
|
mdiCircle,
|
||||||
mdiCircleOutline,
|
mdiCircleOutline,
|
||||||
mdiPauseCircleOutline,
|
mdiProgressClock,
|
||||||
|
mdiProgressWrench,
|
||||||
mdiRecordCircleOutline,
|
mdiRecordCircleOutline,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { LogbookEntry } from "../../data/logbook";
|
import { LogbookEntry } from "../../data/logbook";
|
||||||
@ -34,6 +36,7 @@ import {
|
|||||||
import relativeTime from "../../common/datetime/relative_time";
|
import relativeTime from "../../common/datetime/relative_time";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { describeAction } from "../../data/script_i18n";
|
import { describeAction } from "../../data/script_i18n";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
const LOGBOOK_ENTRIES_BEFORE_FOLD = 2;
|
const LOGBOOK_ENTRIES_BEFORE_FOLD = 2;
|
||||||
|
|
||||||
@ -273,7 +276,7 @@ class ActionRenderer {
|
|||||||
`Triggered ${
|
`Triggered ${
|
||||||
triggerStep.path === "trigger"
|
triggerStep.path === "trigger"
|
||||||
? "manually"
|
? "manually"
|
||||||
: `by the ${triggerStep.changed_variables.trigger.description}`
|
: `by the ${this.trace.trigger}`
|
||||||
} at
|
} at
|
||||||
${formatDateTimeWithSeconds(
|
${formatDateTimeWithSeconds(
|
||||||
new Date(triggerStep.timestamp),
|
new Date(triggerStep.timestamp),
|
||||||
@ -421,29 +424,92 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
|
|
||||||
logbookRenderer.flush();
|
logbookRenderer.flush();
|
||||||
|
|
||||||
|
// Render footer
|
||||||
|
const renderFinishedAt = () =>
|
||||||
|
formatDateTimeWithSeconds(
|
||||||
|
new Date(this.trace!.timestamp.finish!),
|
||||||
|
this.hass.locale
|
||||||
|
);
|
||||||
|
const renderRuntime = () => `(runtime:
|
||||||
|
${(
|
||||||
|
(new Date(this.trace!.timestamp.finish!).getTime() -
|
||||||
|
new Date(this.trace!.timestamp.start).getTime()) /
|
||||||
|
1000
|
||||||
|
).toFixed(2)}
|
||||||
|
seconds)`;
|
||||||
|
|
||||||
|
let entry: {
|
||||||
|
description: TemplateResult | string;
|
||||||
|
icon: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.trace.state === "running") {
|
||||||
|
entry = {
|
||||||
|
description: "Still running",
|
||||||
|
icon: mdiProgressClock,
|
||||||
|
};
|
||||||
|
} else if (this.trace.state === "debugged") {
|
||||||
|
entry = {
|
||||||
|
description: "Debugged",
|
||||||
|
icon: mdiProgressWrench,
|
||||||
|
};
|
||||||
|
} else if (this.trace.script_execution === "finished") {
|
||||||
|
entry = {
|
||||||
|
description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`,
|
||||||
|
icon: mdiCircle,
|
||||||
|
};
|
||||||
|
} else if (this.trace.script_execution === "aborted") {
|
||||||
|
entry = {
|
||||||
|
description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`,
|
||||||
|
icon: mdiAlertCircle,
|
||||||
|
};
|
||||||
|
} else if (this.trace.script_execution === "cancelled") {
|
||||||
|
entry = {
|
||||||
|
description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`,
|
||||||
|
icon: mdiAlertCircle,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let reason: string;
|
||||||
|
let isError = false;
|
||||||
|
let extra: TemplateResult | undefined;
|
||||||
|
|
||||||
|
switch (this.trace.script_execution) {
|
||||||
|
case "failed_condition":
|
||||||
|
reason = "a condition failed";
|
||||||
|
break;
|
||||||
|
case "failed_single":
|
||||||
|
reason = "only a single execution is allowed";
|
||||||
|
break;
|
||||||
|
case "failed_max_runs":
|
||||||
|
reason = "maximum number of parallel runs reached";
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
reason = "an error was encountered";
|
||||||
|
isError = true;
|
||||||
|
extra = html`<br /><br />${this.trace.error!}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reason = `of unknown reason "${this.trace.script_execution}"`;
|
||||||
|
isError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
description: html`Stopped because ${reason} at ${renderFinishedAt()}
|
||||||
|
${renderRuntime()}${extra || ""}`,
|
||||||
|
icon: mdiAlertCircle,
|
||||||
|
className: isError ? "error" : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
// null means it was stopped by a condition
|
// null means it was stopped by a condition
|
||||||
if (this.trace.last_action !== null) {
|
if (entry) {
|
||||||
entries.push(html`
|
entries.push(html`
|
||||||
<ha-timeline
|
<ha-timeline
|
||||||
lastItem
|
lastItem
|
||||||
.icon=${this.trace.timestamp.finish
|
.icon=${entry.icon}
|
||||||
? mdiCircle
|
class=${ifDefined(entry.className)}
|
||||||
: mdiPauseCircleOutline}
|
|
||||||
>
|
>
|
||||||
${this.trace.timestamp.finish
|
${entry.description}
|
||||||
? html`Finished at
|
|
||||||
${formatDateTimeWithSeconds(
|
|
||||||
new Date(this.trace.timestamp.finish),
|
|
||||||
this.hass.locale
|
|
||||||
)}
|
|
||||||
(runtime:
|
|
||||||
${(
|
|
||||||
(new Date(this.trace.timestamp.finish!).getTime() -
|
|
||||||
new Date(this.trace.timestamp.start).getTime()) /
|
|
||||||
1000
|
|
||||||
).toFixed(2)}
|
|
||||||
seconds)`
|
|
||||||
: "Still running"}
|
|
||||||
</ha-timeline>
|
</ha-timeline>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@ -506,6 +572,10 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
ha-timeline[data-path] {
|
ha-timeline[data-path] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.error {
|
||||||
|
--timeline-ball-color: var(--error-color);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -54,22 +54,40 @@ export type ActionTraceStep =
|
|||||||
export interface AutomationTrace {
|
export interface AutomationTrace {
|
||||||
domain: string;
|
domain: string;
|
||||||
item_id: string;
|
item_id: string;
|
||||||
last_action: string | null;
|
last_step: string | null;
|
||||||
last_condition: string | null;
|
|
||||||
run_id: string;
|
run_id: string;
|
||||||
state: "running" | "stopped" | "debugged";
|
state: "running" | "stopped" | "debugged";
|
||||||
timestamp: {
|
timestamp: {
|
||||||
start: string;
|
start: string;
|
||||||
finish: string | null;
|
finish: string | null;
|
||||||
};
|
};
|
||||||
trigger: unknown;
|
script_execution:
|
||||||
|
| // The script was not executed because the automation's condition failed
|
||||||
|
"failed_condition"
|
||||||
|
// The script was not executed because the run mode is single
|
||||||
|
| "failed_single"
|
||||||
|
// The script was not executed because max parallel runs would be exceeded
|
||||||
|
| "failed_max_runs"
|
||||||
|
// All script steps finished:
|
||||||
|
| "finished"
|
||||||
|
// Script execution stopped by the script itself because a condition fails, wait_for_trigger timeouts etc:
|
||||||
|
| "aborted"
|
||||||
|
// Details about failing condition, timeout etc. is in the last element of the trace
|
||||||
|
// Script execution stops because of an unexpected exception:
|
||||||
|
| "error"
|
||||||
|
// The exception is in the trace itself or in the last element of the trace
|
||||||
|
// Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc:
|
||||||
|
| "cancelled"
|
||||||
|
| string;
|
||||||
|
// Automation only, should become it's own type when we support script in frontend
|
||||||
|
trigger: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationTraceExtended extends AutomationTrace {
|
export interface AutomationTraceExtended extends AutomationTrace {
|
||||||
trace: Record<string, ActionTraceStep[]>;
|
trace: Record<string, ActionTraceStep[]>;
|
||||||
context: Context;
|
context: Context;
|
||||||
variables: Record<string, unknown>;
|
|
||||||
config: AutomationConfig;
|
config: AutomationConfig;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TraceTypes {
|
interface TraceTypes {
|
||||||
|
@ -1926,7 +1926,7 @@
|
|||||||
"@codemirror/state" "^0.18.0"
|
"@codemirror/state" "^0.18.0"
|
||||||
"@codemirror/view" "^0.18.0"
|
"@codemirror/view" "^0.18.0"
|
||||||
|
|
||||||
"@codemirror/highlight@^0.18.0":
|
"@codemirror/highlight@^0.18.0", "@codemirror/highlight@^0.18.1":
|
||||||
version "0.18.3"
|
version "0.18.3"
|
||||||
resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.18.3.tgz#50e268630f113c322a2dc97c9f68d71934fffcb0"
|
resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.18.3.tgz#50e268630f113c322a2dc97c9f68d71934fffcb0"
|
||||||
integrity sha512-NmRmkmWl8ht6Y6Y39ghov84AMPCqhUPIH9fmILs2NaWxZFZf4jGCTzrULnmREGsTie+26+LbKUncIU+PBu1Qng==
|
integrity sha512-NmRmkmWl8ht6Y6Y39ghov84AMPCqhUPIH9fmILs2NaWxZFZf4jGCTzrULnmREGsTie+26+LbKUncIU+PBu1Qng==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user