Skip to content

Commit

Permalink
Gracefully handle startup errors (#4383)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Sep 22, 2022
1 parent 80ed0fe commit 479a3ed
Show file tree
Hide file tree
Showing 27 changed files with 673 additions and 396 deletions.
7 changes: 7 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ a:hover {
background-color: var(--evcc-accent3);
border-color: var(--evcc-accent3);
}

.btn:disabled {
color: inherit !important;
background-color: inherit !important;
border-color: inherit !important;
}

.btn-outline-primary,
.btn-outline-primary:focus {
color: var(--bs-primary);
Expand Down
8 changes: 3 additions & 5 deletions assets/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ const api = axios.create({
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.config.url !== "health") {
const url = error.config.baseURL + error.config.url;
const message = `${error.message}: API request failed ${url}`;
window.app.error({ message });
}
const url = error.config.baseURL + error.config.url;
const message = `${error.message}: API request failed ${url}`;
window.app.error({ message });
return Promise.reject(error);
}
);
Expand Down
7 changes: 0 additions & 7 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import smoothscroll from "smoothscroll-polyfill";
import "../css/app.css";
import { createApp, h } from "vue";
import { createMetaManager, plugin as metaPlugin } from "vue-meta";
import api from "./api";
import App from "./views/App.vue";
import VueNumber from "vue-number-animation";
import router from "./router";
Expand Down Expand Up @@ -73,10 +72,4 @@ app.use(featureflags);
app.use(VueNumber);
window.app = app.mount("#app");

window.setInterval(function () {
if (!document.hidden) {
api.get("health").then(window.app.setOnline).catch(window.app.setOffline);
}
}, 5000);

watchThemeChanges();
4 changes: 0 additions & 4 deletions assets/js/components/Site.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<template>
<div class="d-flex flex-column site">
<OfflineIndicator v-if="offline" />

<div class="container px-4 top-area">
<div class="d-flex justify-content-between align-items-center my-3">
<h1 class="d-block my-0">
Expand Down Expand Up @@ -29,7 +27,6 @@
<script>
import "@h2d2/shopicons/es/regular/arrowup";
import TopNavigation from "./TopNavigation.vue";
import OfflineIndicator from "./OfflineIndicator.vue";
import Notifications from "./Notifications.vue";
import Energyflow from "./Energyflow/Energyflow.vue";
import Loadpoints from "./Loadpoints.vue";
Expand All @@ -44,7 +41,6 @@ export default {
Loadpoints,
Energyflow,
Footer,
OfflineIndicator,
Notifications,
TopNavigation,
Vehicles,
Expand Down
142 changes: 142 additions & 0 deletions assets/js/components/StartupError.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<template>
<div class="container px-4">
<div class="d-flex justify-content-between align-items-center my-3">
<h1 class="d-block mt-0 d-flex">
{{ $t("startupError.title") }}
<shopicon-regular-car1 size="m" class="ms-2 icon"></shopicon-regular-car1>
</h1>
</div>
<div class="row mb-4">
<code class="fs-6 mb-3">
<div v-for="(error, index) in errors" :key="index">{{ error }}</div>
</code>
<i18n-t tag="p" keypath="startupError.description">
<a href="https://github.com/evcc-io/evcc/discussions">
{{ $t("startupError.discussions") }}
</a>
</i18n-t>
<p>
<em>{{ $t("startupError.hint") }}</em>
</p>
</div>
<div class="row mb-4">
<h5 class="mb-3">{{ $t("startupError.configuration") }}</h5>
<div class="d-md-flex justify-content-between">
<p class="me-md-4">
<span class="d-block">
{{ $t("startupError.configFile") }}
<code>{{ file }}</code>
</span>
<i18n-t v-if="line" tag="span" keypath="startupError.lineError">
<a :href="`#line${line}`" @click.prevent="scrollTo">{{
$t("startupError.lineErrorLink", [line])
}}</a>
</i18n-t>
{{ $t("startupError.fixAndRestart") }}
</p>
<p>
<button
class="btn btn-primary text-nowrap"
type="button"
:disabled="offline"
@click="shutdown"
>
{{ $t("startupError.restartButton") }}
</button>
</p>
</div>

<code v-if="config">
<div class="my-2"></div>
<div class="py-2 text-muted config">
<div
v-for="(configLine, lineNumber) in config.split('\n')"
:id="`line${lineNumber + 1}`"
:key="lineNumber"
class="m-0 px-2"
:class="{
highlighted: line === lineNumber + 1,
}"
>
{{ configLine }}&nbsp;
</div>
</div>
</code>
</div>
</div>
</template>

<script>
import "@h2d2/shopicons/es/regular/car1";
import api from "../api";
import collector from "../mixins/collector";
export default {
name: "StartupError",
mixins: [collector],
props: {
fatal: Array,
config: String,
file: String,
line: Number,
offline: Boolean,
},
computed: {
errors() {
return this.fatal || [];
},
},
methods: {
shutdown() {
api.post("shutdown");
},
scrollTo(e) {
const id = e.currentTarget.getAttribute("href").substring(1);
const el = document.getElementById(id);
console.log({ id, el });
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
},
},
};
</script>
<style scoped>
.highlighted {
position: relative;
color: var(--bs-code-color) !important;
}
.highlighted:before {
content: "";
position: absolute;
left: 0;
top: 0.2em;
border-top: 0.5em solid transparent;
border-bottom: 0.5em solid transparent;
border-left: 0.5em solid var(--bs-code-color);
}
.config {
max-width: 100%;
overflow-x: scroll;
white-space: pre;
}
.config {
border: 1px solid var(--bs-gray-400);
}
.icon {
transform-origin: 60% 40%;
animation: swinging 3.5s ease-in-out infinite;
}
@keyframes swinging {
0% {
transform: translateY(8%) rotate(170deg);
}
50% {
transform: translateY(8%) rotate(185deg);
}
100% {
transform: translateY(8%) rotate(170deg);
}
}
</style>
13 changes: 13 additions & 0 deletions assets/js/i18n/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ export default {
`,
},
},
startupError: {
title: "Fehler beim Starten",
description:
"Bitte überprüfe deine Konfigurationsdatei. Sollte dir die Fehlermeldung nicht weiterhelfen, suche in unseren {0} nach einer Lösung.",
discussions: "GitHub Discussions",
hint: "Hinweis: Ein weiterer Grund, warum du diese Meldung siehst, könnte ein fehlerhaftes Gerät (Wechselrichter, Zähler, ...) sein. Überprüfe deine Netzwerkverbindungen.",
configuration: "Konfiguration",
configFile: "Verwendete Konfigurationsdatei:",
lineError: "In {0} wurde ein Fehler gefunden.",
lineErrorLink: "Zeile {0}",
fixAndRestart: "Behebe das Problem und starte den Server neu.",
restartButton: "Server neu starten",
},
offline: {
message: "Keine Verbindung zum Server.",
reload: "Reload?",
Expand Down
13 changes: 13 additions & 0 deletions assets/js/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ export default {
`,
},
},
startupError: {
title: "Startup Error",
description:
"Please check your configuration file. If the error message does not help you, have a look at our {0}.",
discussions: "GitHub Discussions",
hint: "Note: Another reason why you see this message could be a faulty device (inverter, meter, ...). Check your network connections.",
configuration: "Config",
configFile: "Configuration file used:",
lineError: "We found an error in {0}.",
lineErrorLink: "line {0}",
fixAndRestart: "Fix the problem and restart the server.",
restartButton: "Restart server",
},
offline: {
message: "No connection to server.",
reload: "Reload?",
Expand Down
101 changes: 9 additions & 92 deletions assets/js/views/Main.vue
Original file line number Diff line number Diff line change
@@ -1,96 +1,21 @@
<template>
<div>
<Site
v-if="configured"
:notifications="notifications"
:offline="offline"
v-bind="state"
></Site>
<div v-else class="container">
<div class="row py-5">
<div class="col12">
<p class="h1 pt-5 pb-2 border-bottom">Willkommen bei evcc</p>
<p class="lead pt-2">
<b>evcc</b> ist dient zur flexiblen Ladesteuerung von Elektrofahrzeugen.
</p>
<p class="pt-2">
Es sieht aus, als wäre Dein <b>evcc</b> noch nicht konfiguriert. Um
<b>evcc</b> zu konfigurieren sind die folgenden Schritte notwendig:
</p>
<ol class="pt-2">
<li>
Erzeugen einer Konfigurationsdatei mit Namen
<code>evcc.yaml</code>. Die Standardkonfiguration
<code>evcc.dist.yaml</code> kann dafür als Vorlage dienen (<a
href="https://github.com/evcc-io/evcc/blob/master/evcc.dist.yaml"
>Download</a
>).
</li>
<li>Konfiguration der Wallbox als <code>chargers</code>.</li>
<li>
Konfiguration des EVU Zählers und evtl. weiterer Zähler unter
<code>meters</code>.
</li>
<li>
Konfiguration des Netzanschlusses unter
<code>site</code>. In einer Site wird der Netzanschluss mit dem
konfigurierten EVU Zähler (<code>meter</code>) verbunden.
</li>
<li>
Konfiguration eines Ladepunktes unter
<code>loadpoints</code>. In einem Ladepunkt wird die konfigurierte
Wallbox (<code>charger</code>) mit dem Ladepunkt verbunden.
</li>
<li>
Start von <b>evcc</b> mit der neu erstellten Konfiguration:
<code>evcc -c evcc.yaml</code>
</li>
</ol>
<p>Minimale Beispielkonfiguration für <b>evcc</b>:</p>
<p>
<code>
<pre class="mx-3">
uri: localhost:7070 # Adresse für UI
interval: 10s # Regelintervall
meters:
- name: evu-zähler
type: ... # Detailkonfiguration des EVU Zählers
- name: ladezähler
type: ... # Detailkonfiguration des Ladezählers (optional)
chargers:
- name: wallbox
type: ... # Detailkonfiguration der Wallbox
site:
title: Home
meters:
grid: evu-zähler # EVU Zähler
loadpoints:
- title: Ladepunkt # ui display name
charger: wallbox # charger
meters:
charge: ladezähler # Ladezählers (optional)
</pre
>
</code>
</p>
<p>
Viel Spass mit <b>evcc</b>! Bei Problemen kannst Du uns auf
<a href="https://github.com/evcc-io/evcc/issues">GitHub</a>
erreichen.
</p>
</div>
</div>
</div>
<OfflineIndicator v-if="offline" />

<StartupError v-if="startupErrors" v-bind="state" :offline="offline" />
<Site v-else :notifications="notifications" v-bind="state"></Site>
</div>
</template>

<script>
import Site from "../components/Site.vue";
import StartupError from "../components/StartupError.vue";
import OfflineIndicator from "../components/OfflineIndicator.vue";
import store from "../store";
export default {
name: "Main",
components: { Site },
components: { Site, StartupError, OfflineIndicator },
props: {
notifications: Array,
offline: Boolean,
Expand All @@ -99,16 +24,8 @@ export default {
return store;
},
computed: {
configured: function () {
const val = window.evcc.configured;
// for development purposes
if (val == window.evcc.configured) {
return true;
}
if (!isNaN(parseInt(val)) && parseInt(val) > 0) {
return true;
}
return false;
startupErrors: function () {
return this.state.fatal?.length > 0;
},
},
};
Expand Down
Loading

0 comments on commit 479a3ed

Please sign in to comment.