Merge branch 'main' into feature/adguard-home-customservices-doc

This commit is contained in:
Bastien Wirtz 2022-04-07 22:33:20 +02:00 committed by GitHub
commit 7341d7634b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 973 additions and 549 deletions

View File

@ -1,7 +1,7 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI name: Integration
on: on:
push: push:

View File

@ -10,7 +10,7 @@ COPY . .
RUN yarn build RUN yarn build
# production stage # production stage
FROM alpine:3.11 FROM alpine:3.15
ENV USER darkhttpd ENV USER darkhttpd
ENV GROUP darkhttpd ENV GROUP darkhttpd

View File

@ -56,7 +56,7 @@
- Search - Search
- Grouping - Grouping
- Theme customization - Theme customization
- Offline heath check - Offline health check
- keyboard shortcuts: - keyboard shortcuts:
- `/` Start searching. - `/` Start searching.
- `Escape` Stop searching. - `Escape` Stop searching.

View File

@ -5,7 +5,7 @@ Title, icons, links, colors, and services can be configured in the `config.yml`
```yaml ```yaml
--- ---
# Homepage configuration # Homepage configuration
# See https://fontawesome.com/icons for icons options # See https://fontawesome.com/v5/search for icons options
# Optional: Use external configuration file. # Optional: Use external configuration file.
# Using this will ignore remaining config in this file # Using this will ignore remaining config in this file
@ -29,9 +29,13 @@ connectivityCheck: true # whether you want to display a message when the apps ar
# Optional: Proxy / hosting option # Optional: Proxy / hosting option
proxy: proxy:
# NOT All custom services implements this new option YET. Support will be extended very soon.
useCredentials: false # send cookies & authorization headers when fetching service specific data. Set to `true` if you use an authentication proxy. Can be overrided on service level. useCredentials: false # send cookies & authorization headers when fetching service specific data. Set to `true` if you use an authentication proxy. Can be overrided on service level.
# Set the default layout and color scheme
defaults:
layout: columns # Either 'columns', or 'list'
colorTheme: auto # One of 'auto', 'light', or 'dark'
# Optional theming # Optional theming
theme: default # 'default' or one of the themes available in 'src/assets/themes'. theme: default # 'default' or one of the themes available in 'src/assets/themes'.

View File

@ -1,8 +1,22 @@
# Custom Services # Custom Services
Some service can use a specific a component that provides some extra features by adding a `type` key to the service yaml Some service can use a specific a component that provides some extra features by adding a `type` key to the service yaml
configuration. Available services are in `src/components/`. Here is an overview of all custom services that are available configuration and, where applicable, an apikey. Note that config.yml is exposed at /assets/config.yml via HTTP and any
within Homer. apikey included in the configuration file is exposed to anyone who can access the homer instance. Only include an apikey
if your homer instance is secured behind some form of authentication or access restriction.
Available services are in `src/components/`. Here is an overview of all custom services that are available
within Homer:
+ [PiHole](#pihole)
+ [OpenWeatherMap](#openweathermap)
+ [Medusa](#medusa)
+ [Lidarr, Prowlarr, Sonarr and Radarr](#lidarr-prowlarr-sonarr-and-radarr)
+ [PaperlessNG](#paperlessng)
+ [Ping](#ping)
+ [Prometheus](#prometheus)
+ [AdGuard Home](#adguard-home)
+ [Portainer](#portainer)
+ [Emby](#emby)
If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page. If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page.
@ -18,8 +32,6 @@ If you experiencing any issue, please have a look to the [troubleshooting](troub
type: "<type>" type: "<type>"
``` ```
⚠️🚧 `endpoint` & `useCredentials` new options are not yet supported by all custom services (but will be very soon).
## PiHole ## PiHole
Using the PiHole service you can display info about your local PiHole instance right on your Homer dashboard. Using the PiHole service you can display info about your local PiHole instance right on your Homer dashboard.
@ -65,18 +77,28 @@ Two lines are needed in the config.yml :
The url must be the root url of Medusa application. The url must be the root url of Medusa application.
The Medusa API key can be found in General configuration > Interface. It is needed to access Medusa API. The Medusa API key can be found in General configuration > Interface. It is needed to access Medusa API.
## Sonarr/Radarr ## Lidarr, Prowlarr, Sonarr and Radarr
This service displays Activity (blue), Warning (orange) or Error (red) notifications bubbles from the Radarr/Sonarr application. This service displays Activity (blue), Warning (orange) or Error (red) notifications bubbles from the Lidarr, Radarr or Sonarr application.
Two lines are needed in the config.yml : Two lines are needed in the config.yml :
```yaml ```yaml
type: "Radarr" or "Sonarr" type: "Lidarr", "Prowlarr", "Radarr" or "Sonarr"
apikey: "01234deb70424befb1f4ef6a23456789" apikey: "01234deb70424befb1f4ef6a23456789"
``` ```
The url must be the root url of Radarr/Sonarr application. The url must be the root url of Lidarr, Prowlarr, Radarr or Sonarr application.
The Radarr/Sonarr API key can be found in Settings > General. It is needed to access the API. The Lidarr, Prowlarr, Radarr or Sonarr API key can be found in Settings > General. It is needed to access the API.
If you are using an older version of Radarr or Sonarr which don't support the new V3 api endpoints, add the following line to your service config "legacyApi: true", example:
```yaml
- name: "Radarr"
type: "Radarr"
url: "http://localhost:7878/"
apikey: "MY-SUPER-SECRET-API-KEY"
target: "_blank"
legacyApi: true
```
## PaperlessNG ## PaperlessNG
@ -124,3 +146,36 @@ For AdGuard Home you need to set the type to AdGuard, if you have somes issues a
target: "_blank" target: "_blank"
type: "AdGuardHome" type: "AdGuardHome"
``` ```
## Portainer
This service displays info about the total number of containers managed by your Portainer instance.
In order to use it, you must be using Portainer version 1.11 or later. Generate an access token from the UI and pass
it to the apikey field.
By default, every connected environments will be checked. To select specific ones,add an "environments" entry which can be a simple string or an array containing all the selected environments name.
See https://docs.portainer.io/v/ce-2.11/user/account-settings#access-tokens
```yaml
- name: "Portainer"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151/"
type: "Portainer"
apikey: "MY-SUPER-SECRET-API-KEY"
# environments:
# - "raspberry"
# - "local"
```
## Emby
You need to set the type to Emby, provide an api key and choose which stats to show if the subtitle is disabled.
```yaml
- name: "Emby"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151/"
type: "Emby"
apikey: "MY-SUPER-SECRET-API-KEY"
libraryType: "music" #Choose which stats to show. Can be one of: music, series or movies.
```

View File

@ -51,7 +51,7 @@ and then simply reference these pre-defined (anchored) tags in each item like so
- name: "VS Code" - name: "VS Code"
logo: "/assets/vscode.png" logo: "/assets/vscode.png"
subtitle: "Develop Code Anywhere, On Anything!" subtitle: "Develop Code Anywhere, On Anything!"
<<: *App # Reference to the predefined "App" Tag <<: *Apps # Reference to the predefined "App" Tag
url: "https://vscode.example.com/" url: "https://vscode.example.com/"
target: "_blank" # optional html tag target attribute target: "_blank" # optional html tag target attribute
```` ````

View File

@ -6,7 +6,7 @@ You might by facing a [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/C
It happens when the targeted service is hosted on a different domain or port. It happens when the targeted service is hosted on a different domain or port.
Web browsers will not allow to fetch information from a different site without explicit permissions (the targeted service Web browsers will not allow to fetch information from a different site without explicit permissions (the targeted service
must include a special `Access-Control-Allow-Origin: *` HTTP headers). must include a special `Access-Control-Allow-Origin: *` HTTP headers).
If this happens your web console (`ctrl+shit+i` or `F12`) will be filled with this kind of errors: If this happens your web console (`ctrl+shift+i` or `F12`) will be filled with this kind of errors:
```text ```text
Access to fetch at 'https://<target-service>' from origin 'https://<homer>' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. Access to fetch at 'https://<target-service>' from origin 'https://<homer>' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
@ -15,5 +15,5 @@ Access to fetch at 'https://<target-service>' from origin 'https://<homer>' has
To resolve this, you can either: To resolve this, you can either:
* Host all your target service under the same domain & port. * Host all your target service under the same domain & port.
* Modify the target sever configuration so that the response of the server included following header- `Access-Control-Allow-Origin: *` (<https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests>). It might be an option in the targeted service, otherwise depending on how the service is hosted, the proxy or web server can seamlessly add it. * Modify the target server configuration so that the response of the server included following header- `Access-Control-Allow-Origin: *` (<https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests>). It might be an option in the targeted service, otherwise depending on how the service is hosted, the proxy or web server can seamlessly add it.
* Use a cors proxy sever like [`cors-container`](https://github.com/imjacobclark/cors-container), [`cors-anywhere`](https://github.com/Rob--W/cors-anywhere) or many others. * Use a cors proxy server like [`cors-container`](https://github.com/imjacobclark/cors-container), [`cors-anywhere`](https://github.com/Rob--W/cors-anywhere) or many others.

View File

@ -1,7 +1,6 @@
{ {
"name": "homer", "name": "homer",
"version": "21.09.1", "version": "21.09.1",
"license": "Apache-2.0",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
@ -10,17 +9,17 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"bulma": "^0.9.3", "bulma": "^0.9.3",
"core-js": "^3.17.3", "core-js": "^3.21.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"vue": "^2.6.14" "vue": "^2.6.14"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-pwa": "~4.5.0", "@vue/cli-plugin-pwa": "~4.5.15",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.15",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
@ -31,5 +30,6 @@
"sass": "^1.26.5", "sass": "^1.26.5",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.12" "vue-template-compiler": "^2.6.12"
} },
"license": "Apache-2.0"
} }

View File

@ -1,7 +1,7 @@
--- ---
# Additionnal page configuration # Additional page configuration
# Additionnal configurations are loaded using its file name, minus the extension, as an anchor (https://<mydashboad>#<config>). # Additional configurations are loaded using its file name, minus the extension, as an anchor (https://<mydashboad>#<config>).
# `config.yml` is still used as a base configuration, and all values here will overwrite it, so you don't have to re-defined everything # `config.yml` is still used as a base configuration, and all values here will overwrite it, so you don't have to re-defined everything

View File

@ -1,6 +1,6 @@
--- ---
# Homepage configuration # Homepage configuration
# See https://fontawesome.com/icons for icons options # See https://fontawesome.com/v5/search for icons options
title: "Demo dashboard" title: "Demo dashboard"
subtitle: "Homer" subtitle: "Homer"
@ -58,11 +58,11 @@ links:
- name: "Wiki" - name: "Wiki"
icon: "fas fa-book" icon: "fas fa-book"
url: "https://www.wikipedia.org/" url: "https://www.wikipedia.org/"
# this will link to a second homer page that will load config from additionnal-page.yml and keep default config values as in config.yml file # this will link to a second homer page that will load config from additional-page.yml and keep default config values as in config.yml file
# see url field and assets/additionnal-page.yml.dist used in this example: # see url field and assets/additional-page.yml.dist used in this example:
- name: "another page!" - name: "another page!"
icon: "fas fa-file-alt" icon: "fas fa-file-alt"
url: "#additionnal-page" url: "#additional-page"
# Services # Services
# First level array represent a group. # First level array represent a group.

View File

@ -1,6 +1,6 @@
--- ---
# Homepage configuration # Homepage configuration
# See https://fontawesome.com/icons for icons options # See https://fontawesome.com/v5/search for icons options
title: "Hello beautiful!" title: "Hello beautiful!"
subtitle: "App dashboard" subtitle: "App dashboard"

View File

@ -30,18 +30,22 @@
:links="config.links" :links="config.links"
@navbar-toggle="showMenu = !showMenu" @navbar-toggle="showMenu = !showMenu"
> >
<DarkMode @updated="isDark = $event" /> <DarkMode
@updated="isDark = $event"
:defaultValue="this.config.defaults.colorTheme"
/>
<SettingToggle <SettingToggle
@updated="vlayout = $event" @updated="vlayout = $event"
name="vlayout" name="vlayout"
icon="fa-list" icon="fa-list"
iconAlt="fa-columns" iconAlt="fa-columns"
:defaultValue="this.config.defaults.layout == 'columns'"
/> />
<SearchInput <SearchInput
class="navbar-item is-inline-block-mobile" class="navbar-item is-inline-block-mobile"
:hotkey=searchHotkey() :hotkey="searchHotkey()"
@input="filterServices" @input="filterServices"
@search-focus="showMenu = true" @search-focus="showMenu = true"
@search-open="navigateToFirstService" @search-open="navigateToFirstService"
@ -56,6 +60,9 @@
v-if="config.connectivityCheck" v-if="config.connectivityCheck"
@network-status-update="offline = $event" @network-status-update="offline = $event"
/> />
<GetStarted v-if="loaded && !services" />
<div v-if="!offline"> <div v-if="!offline">
<!-- Optional messages --> <!-- Optional messages -->
<Message :item="config.message" /> <Message :item="config.message" />
@ -130,6 +137,7 @@ const jsyaml = require("js-yaml");
const merge = require("lodash.merge"); const merge = require("lodash.merge");
import Navbar from "./components/Navbar.vue"; import Navbar from "./components/Navbar.vue";
import GetStarted from "./components/GetStarted.vue";
import ConnectivityChecker from "./components/ConnectivityChecker.vue"; import ConnectivityChecker from "./components/ConnectivityChecker.vue";
import Service from "./components/Service.vue"; import Service from "./components/Service.vue";
import Message from "./components/Message.vue"; import Message from "./components/Message.vue";
@ -144,6 +152,7 @@ export default {
name: "App", name: "App",
components: { components: {
Navbar, Navbar,
GetStarted,
ConnectivityChecker, ConnectivityChecker,
Service, Service,
Message, Message,
@ -154,6 +163,7 @@ export default {
}, },
data: function () { data: function () {
return { return {
loaded: false,
config: null, config: null,
services: null, services: null,
offline: false, offline: false,
@ -166,6 +176,7 @@ export default {
created: async function () { created: async function () {
this.buildDashboard(); this.buildDashboard();
window.onhashchange = this.buildDashboard; window.onhashchange = this.buildDashboard;
this.loaded = true;
}, },
methods: { methods: {
searchHotkey() { searchHotkey() {
@ -193,6 +204,7 @@ export default {
} }
this.config = merge(defaults, config); this.config = merge(defaults, config);
this.services = this.config.services; this.services = this.config.services;
document.title = document.title =
this.config.documentTitle || this.config.documentTitle ||
`${this.config.title} | ${this.config.subtitle}`; `${this.config.title} | ${this.config.subtitle}`;
@ -211,6 +223,7 @@ export default {
window.location.href = response.url; window.location.href = response.url;
return; return;
} }
if (!response.ok) { if (!response.ok) {
throw Error(`${response.statusText}: ${response.body}`); throw Error(`${response.statusText}: ${response.body}`);
} }

View File

@ -10,6 +10,12 @@ footer: '<p>Created with <span class="has-text-danger">❤️</span> with <a hre
columns: 3 columns: 3
connectivityCheck: true connectivityCheck: true
defaults:
# columns, list
layout: columns
# auto, light, dark
colorTheme: auto
theme: default theme: default
colors: colors:
light: light:

View File

@ -15,6 +15,9 @@
<script> <script>
export default { export default {
name: "Darkmode", name: "Darkmode",
props: {
defaultValue: String,
},
data: function () { data: function () {
return { return {
isDark: null, isDark: null,
@ -30,6 +33,17 @@ export default {
if ("overrideDark" in localStorage) { if ("overrideDark" in localStorage) {
// Light theme is 1 and Dark theme is 2 // Light theme is 1 and Dark theme is 2
this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1; this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1;
} else {
switch (this.defaultValue) {
case "light":
this.mode = 1;
break;
case "dark":
this.mode = 2;
break;
default:
this.mode = 0;
}
} }
this.isDark = this.getIsDark(); this.isDark = this.getIsDark();
this.$emit("updated", this.isDark); this.$emit("updated", this.isDark);

View File

@ -0,0 +1,35 @@
<template>
<article>
<div class="m-6 has-text-centered py-6">
<p class="is-size-5 mb-0">No configured service found!</p>
<p>Check out the documentation to start building your Homer dashboard.</p>
<p>
<a
class="button is-primary mt-5 has-text-weight-bold"
href="https://github.com/bastienwirtz/homer/blob/main/README.md#getting-started"
target="_blank"
>
Get started
</a>
</p>
</div>
</article>
</template>
<script>
export default {
name: "GetStarted",
};
</script>
<style lang="scss" scoped>
p {
color: #4a4a4a;
}
body #app a {
font-weight: 900;
color: #ffffff;
font-family: "Lato", sans-serif;
}
</style>

View File

@ -19,8 +19,8 @@ export default {
value: String, value: String,
hotkey: { hotkey: {
type: String, type: String,
default: "/" default: "/",
} },
}, },
mounted() { mounted() {
this._keyListener = function (event) { this._keyListener = function (event) {

View File

@ -12,6 +12,7 @@ export default {
name: String, name: String,
icon: String, icon: String,
iconAlt: String, iconAlt: String,
defaultValue: Boolean,
}, },
data: function () { data: function () {
return { return {
@ -24,6 +25,8 @@ export default {
if (this.name in localStorage) { if (this.name in localStorage) {
this.value = JSON.parse(localStorage[this.name]); this.value = JSON.parse(localStorage[this.name]);
} else {
this.value = this.defaultValue;
} }
this.$emit("updated", this.value); this.$emit("updated", this.value);

View File

@ -76,9 +76,6 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.media-left img {
max-height: 100%;
}
.status { .status {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-title); color: var(--text-title);

View File

@ -0,0 +1,118 @@
<template>
<Generic :item="item">
<template #content>
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">
<template v-if="item.subtitle">
{{ item.subtitle }}
</template>
<template v-else>
{{ embyCount }}
</template>
</p>
</template>
<template #indicator>
<div v-if="status" class="status" :class="status">
{{ status }}
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Emby",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
status: "",
albumCount: 0,
songCount: 0,
movieCount: 0,
seriesCount: 0,
episodeCount: 0,
}),
computed: {
embyCount: function () {
if (this.item.libraryType === "music")
return `${this.songCount} songs, ${this.albumCount} albums`;
else if (this.item.libraryType === "movies")
return `${this.movieCount} movies`;
else if (this.item.libraryType === "series")
return `${this.episodeCount} eps, ${this.seriesCount} series`;
else return `wrong library type 💀`;
},
},
created() {
this.fetchServerStatus();
if (!this.item.subtitle && this.status !== "dead")
this.fetchServerMediaStats();
},
methods: {
fetchServerStatus: async function () {
this.fetch("/System/info/public")
.then((response) => {
if (response.Id) this.status = "running";
else throw new Error();
})
.catch((e) => {
console.log(e);
this.status = "dead";
});
},
fetchServerMediaStats: async function () {
const headers = {
"X-Emby-Token": this.item.apikey,
};
var data = await this.fetch("/items/counts", { headers }).catch((e) => {
console.log(e);
});
this.albumCount = data.AlbumCount;
this.songCount = data.SongCount;
this.movieCount = data.MovieCount;
this.seriesCount = data.SeriesCount;
this.episodeCount = data.EpisodeCount;
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
&.running:before {
background-color: #94e185;
border-color: #78d965;
box-shadow: 0 0 5px 1px #94e185;
}
&.dead:before {
background-color: #c9404d;
border-color: #c42c3b;
box-shadow: 0 0 5px 1px #c9404d;
}
&:before {
content: " ";
display: inline-block;
width: 7px;
height: 7px;
margin-right: 10px;
border: 1px solid #000;
border-radius: 7px;
}
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="activity > 0" class="notif activity" title="Activity">
{{ activity }}
</strong>
<strong v-if="warnings > 0" class="notif warnings" title="Warning">
{{ warnings }}
</strong>
<strong v-if="errors > 0" class="notif errors" title="Error">
{{ errors }}
</strong>
<strong
v-if="serverError"
class="notif errors"
title="Connection error to Lidarr API, check url and apikey in config.yml"
>?</strong
>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Lidarr",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => {
return {
activity: null,
warnings: null,
errors: null,
serverError: false,
};
},
created: function () {
this.fetchConfig();
},
methods: {
fetchConfig: function () {
this.fetch(`/api/v1/health?apikey=${this.item.apikey}`)
.then((health) => {
this.warnings = 0;
this.errors = 0;
for (var i = 0; i < health.length; i++) {
if (health[i].type == "warning") {
this.warnings++;
} else if (health[i].type == "error") {
this.errors++;
}
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
this.fetch(`/api/v1/queue/status?apikey=${this.item.apikey}`)
.then((queue) => {
this.activity = queue.totalCount;
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
},
},
};
</script>
<style scoped lang="scss">
.notifs {
position: absolute;
color: white;
font-family: sans-serif;
top: 0.3em;
right: 0.5em;
.notif {
display: inline-block;
padding-right: 0.35em;
padding-left: 0.35em;
padding-top: 0.2em;
padding-bottom: 0.2em;
border-radius: 0.25em;
position: relative;
margin-left: 0.3em;
font-size: 0.8em;
&.activity {
background-color: #4fb5d6;
}
&.warnings {
background-color: #d08d2e;
}
&.errors {
background-color: #e51111;
}
}
}
</style>

View File

@ -1,20 +1,6 @@
<template> <template>
<div> <Generic :item="item">
<div class="card" :class="item.class"> <template #content>
<a :href="item.url" :target="item.target" rel="noreferrer">
<div class="card-content">
<div class="media">
<div v-if="item.logo" class="media-left">
<figure class="image is-48x48">
<img :src="item.logo" :alt="`${item.name} logo`" />
</figure>
</div>
<div v-if="item.icon" class="media-left">
<figure class="image is-48x48">
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ item.name }}</p> <p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6"> <p class="subtitle is-6">
<template v-if="item.subtitle"> <template v-if="item.subtitle">
@ -25,23 +11,23 @@
happily keeping {{ stats.totalRecipes }} recipes organized happily keeping {{ stats.totalRecipes }} recipes organized
</template> </template>
</p> </p>
</div> </template>
</div> </Generic>
<div class="tag" :class="item.tagstyle" v-if="item.tag">
<strong class="tag-text">#{{ item.tag }}</strong>
</div>
</div>
</a>
</div>
</div>
</template> </template>
<script> <script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default { export default {
name: "Mealie", name: "Mealie",
mixins: [service],
props: { props: {
item: Object, item: Object,
}, },
components: {
Generic,
},
data: () => ({ data: () => ({
stats: null, stats: null,
meal: null, meal: null,
@ -51,44 +37,20 @@ export default {
}, },
methods: { methods: {
fetchStatus: async function () { fetchStatus: async function () {
if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing const headers = {
this.meal = await fetch(`${this.item.url}/api/meal-plans/today/`, {
headers: {
Authorization: "Bearer " + this.item.apikey, Authorization: "Bearer " + this.item.apikey,
Accept: "application/json", Accept: "application/json",
}, };
})
.then(function (response) { if (this.item.subtitle != null) return;
if (!response.ok) {
throw new Error("Not 2xx response"); this.meal = await this.fetch("/api/meal-plans/today/", { headers }).catch(
} else { (e) => console.log(e)
if (response != null) { );
return response.json(); this.stats = await this.fetch("/api/debug/statistics/", {
} headers,
} }).catch((e) => console.log(e));
})
.catch((e) => console.log(e));
this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, {
headers: {
Authorization: "Bearer " + this.item.apikey,
Accept: "application/json",
},
})
.then(function (response) {
if (!response.ok) {
throw new Error("Not 2xx response");
} else {
return response.json();
}
})
.catch((e) => console.log(e));
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss">
.media-left img {
max-height: 100%;
}
</style>

View File

@ -1,23 +1,6 @@
<template> <template>
<div> <Generic :item="item">
<div class="card" :class="item.class"> <template #indicator>
<a :href="item.url" :target="item.target" rel="noreferrer">
<div class="card-content">
<div class="media">
<div v-if="item.logo" class="media-left">
<figure class="image is-48x48">
<img :src="item.logo" :alt="`${item.name} logo`" />
</figure>
</div>
<div v-if="item.icon" class="media-left">
<figure class="image is-48x48">
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">{{ item.subtitle }}</p>
</div>
<div class="notifs"> <div class="notifs">
<strong <strong
v-if="config !== null && config.system.news.unread > 0" v-if="config !== null && config.system.news.unread > 0"
@ -44,22 +27,23 @@
>?</strong >?</strong
> >
</div> </div>
</div> </template>
<div class="tag" :class="item.tagstyle" v-if="item.tag"> </Generic>
<strong class="tag-text">#{{ item.tag }}</strong>
</div>
</div>
</a>
</div>
</div>
</template> </template>
<script> <script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default { export default {
name: "Medusa", name: "Medusa",
mixins: [service],
props: { props: {
item: Object, item: Object,
}, },
components: {
Generic,
},
data: () => { data: () => {
return { return {
config: null, config: null,
@ -71,15 +55,8 @@ export default {
}, },
methods: { methods: {
fetchConfig: function () { fetchConfig: function () {
fetch(`${this.item.url}/api/v2/config`, { this.fetch("/api/v2/config", {
credentials: "include", headers: { "X-Api-Key": this.item.apikey },
headers: { "X-Api-Key": `${this.item.apikey}` },
})
.then((response) => {
if (response.status != 200) {
throw new Error(response.statusText);
}
return response.json();
}) })
.then((conf) => { .then((conf) => {
this.config = conf; this.config = conf;
@ -94,16 +71,12 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.media-left img {
max-height: 100%;
}
.notifs { .notifs {
position: absolute; position: absolute;
color: white; color: white;
font-family: sans-serif; font-family: sans-serif;
top: 0.3em; top: 0.3em;
right: 0.5em; right: 0.5em;
}
.notif { .notif {
padding-right: 0.35em; padding-right: 0.35em;
padding-left: 0.35em; padding-left: 0.35em;
@ -113,16 +86,17 @@ export default {
position: relative; position: relative;
margin-left: 0.3em; margin-left: 0.3em;
font-size: 0.8em; font-size: 0.8em;
} &.news {
.news {
background-color: #777777; background-color: #777777;
} }
.warnings { &.warnings {
background-color: #d08d2e; background-color: #d08d2e;
} }
.errors { &.errors {
background-color: #e51111; background-color: #e51111;
} }
}
}
</style> </style>

View File

@ -1,20 +1,6 @@
<template> <template>
<div> <Generic :item="item">
<div class="card" :class="item.class"> <template #content>
<a :href="item.url" :target="item.target" rel="noreferrer">
<div class="card-content">
<div class="media">
<div v-if="item.logo" class="media-left">
<figure class="image is-48x48">
<img :src="item.logo" :alt="`${item.name} logo`" />
</figure>
</div>
<div v-if="item.icon" class="media-left">
<figure class="image is-48x48">
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ item.name }}</p> <p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6"> <p class="subtitle is-6">
<template v-if="item.subtitle"> <template v-if="item.subtitle">
@ -24,23 +10,23 @@
happily storing {{ api.count }} documents happily storing {{ api.count }} documents
</template> </template>
</p> </p>
</div> </template>
</div> </Generic>
<div class="tag" :class="item.tagstyle" v-if="item.tag">
<strong class="tag-text">#{{ item.tag }}</strong>
</div>
</div>
</a>
</div>
</div>
</template> </template>
<script> <script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default { export default {
name: "Paperless", name: "Paperless",
mixins: [service],
props: { props: {
item: Object, item: Object,
}, },
components: {
Generic,
},
data: () => ({ data: () => ({
api: null, api: null,
}), }),
@ -49,36 +35,21 @@ export default {
}, },
methods: { methods: {
fetchStatus: async function () { fetchStatus: async function () {
if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing if (this.item.subtitle != null) return;
var apikey = this.item.apikey;
const apikey = this.item.apikey;
if (!apikey) { if (!apikey) {
console.error( console.error(
"apikey is not present in config.yml for the paperless entry!" "apikey is not present in config.yml for the paperless entry!"
); );
return; return;
} }
const url = `${this.item.url}/api/documents/`; this.api = await this.fetch("/api/documents/", {
this.api = await fetch(url, {
credentials: "include",
headers: { headers: {
Authorization: "Token " + this.item.apikey, Authorization: "Token " + this.item.apikey,
}, },
}) }).catch((e) => console.log(e));
.then(function (response) {
if (!response.ok) {
throw new Error("Not 2xx response");
} else {
return response.json();
}
})
.catch((e) => console.log(e));
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss">
.media-left img {
max-height: 100%;
}
</style>

View File

@ -1,59 +1,45 @@
<template> <template>
<div> <Generic :item="item">
<div class="card" :class="item.class"> <template #content>
<a :href="item.url" :target="item.target" rel="noreferrer">
<div class="card-content">
<div class="media">
<div v-if="item.logo" class="media-left">
<figure class="image is-48x48">
<img :src="item.logo" :alt="`${item.name} logo`" />
</figure>
</div>
<div v-if="item.icon" class="media-left">
<figure class="image is-48x48">
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ item.name }}</p> <p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6"> <p class="subtitle is-6">
<template v-if="item.subtitle"> <template v-if="item.subtitle">
{{ item.subtitle }} {{ item.subtitle }}
</template> </template>
<template v-else-if="api"> <template v-else-if="percentage">
{{ percentage }}&percnt; blocked {{ percentage }}&percnt; blocked
</template> </template>
</p> </p>
</template>
<template #indicator>
<div v-if="status" class="status" :class="status">
{{ status }}
</div> </div>
<div v-if="api" class="status" :class="api.status"> </template>
{{ api.status }} </Generic>
</div>
</div>
<div class="tag" :class="item.tagstyle" v-if="item.tag">
<strong class="tag-text">#{{ item.tag }}</strong>
</div>
</div>
</a>
</div>
</div>
</template> </template>
<script> <script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default { export default {
name: "PiHole", name: "PiHole",
mixins: [service],
props: { props: {
item: Object, item: Object,
}, },
components: {
Generic,
},
data: () => ({ data: () => ({
api: {
status: "", status: "",
ads_percentage_today: 0, ads_percentage_today: 0,
},
}), }),
computed: { computed: {
percentage: function () { percentage: function () {
if (this.api) { if (this.ads_percentage_today) {
return this.api.ads_percentage_today.toFixed(1); return this.ads_percentage_today.toFixed(1);
} }
return ""; return "";
}, },
@ -63,21 +49,16 @@ export default {
}, },
methods: { methods: {
fetchStatus: async function () { fetchStatus: async function () {
const url = `${this.item.url}/api.php`; const result = await this.fetch("/api.php").catch((e) => console.log(e));
this.api = await fetch(url, {
credentials: "include", this.status = result.status;
}) this.ads_percentage_today = result.ads_percentage_today;
.then((response) => response.json())
.catch((e) => console.log(e));
}, },
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.media-left img {
max-height: 100%;
}
.status { .status {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-title); color: var(--text-title);

View File

@ -0,0 +1,139 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="running > 0" class="notif running" title="Running">
{{ running }}
</strong>
<strong v-if="dead > 0" class="notif dead" title="Dead">
{{ dead }}
</strong>
<strong
v-if="misc > 0"
class="notif misc"
title="Other (creating, paused, exited, etc.)"
>
{{ misc }}
</strong>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Portainer",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
endpoints: null,
containers: null,
}),
computed: {
running: function () {
if (!this.containers) {
return "";
}
return this.containers.filter((container) => {
return container.State.toLowerCase() === "running";
}).length;
},
dead: function () {
if (!this.containers) {
return "";
}
return this.containers.filter((container) => {
return container.State.toLowerCase() === "dead";
}).length;
},
misc: function () {
if (!this.containers) {
return "";
}
return this.containers.filter((container) => {
return (
container.State.toLowerCase() !== "running" &&
container.State.toLowerCase() !== "dead"
);
}).length;
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
const headers = {
"X-Api-Key": this.item.apikey,
};
this.endpoints = await this.fetch("/api/endpoints", { headers }).catch(
(e) => {
console.error(e);
}
);
let containers = [];
for (let endpoint of this.endpoints) {
if (
this.item.environments &&
!this.item.environments.includes(endpoint.Name)
) {
continue;
}
const uri = `/api/endpoints/${endpoint.Id}/docker/containers/json?all=1`;
const endpointContainers = await this.fetch(uri, { headers }).catch(
(e) => {
console.error(e);
}
);
if (endpointContainers) {
containers = containers.concat(endpointContainers);
}
}
this.containers = containers;
},
},
};
</script>
<style scoped lang="scss">
.notifs {
position: absolute;
color: white;
font-family: sans-serif;
top: 0.3em;
right: 0.5em;
.notif {
display: inline-block;
padding: 0.2em 0.35em;
border-radius: 0.25em;
position: relative;
margin-left: 0.3em;
font-size: 0.8em;
&.running {
background-color: #4fd671;
}
&.dead {
background-color: #e51111;
}
&.misc {
background-color: #2ed0c8;
}
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="warnings > 0" class="notif warnings" title="Warning">
{{ warnings }}
</strong>
<strong v-if="errors > 0" class="notif errors" title="Error">
{{ errors }}
</strong>
<strong
v-if="serverError"
class="notif errors"
title="Connection error to Prowlarr API, check url and apikey in config.yml"
>
?
</strong>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Prowlarr",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => {
return {
warnings: null,
errors: null,
serverError: false,
};
},
created: function () {
this.fetchConfig();
},
methods: {
fetchConfig: function () {
this.fetch(`/api/v1/health?apikey=${this.item.apikey}`)
.then((health) => {
this.warnings = 0;
this.errors = 0;
for (var i = 0; i < health.length; i++) {
if (health[i].type == "warning") {
this.warnings++;
} else if (health[i].type == "error") {
this.errors++;
}
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
},
},
};
</script>
<style scoped lang="scss">
.notifs {
position: absolute;
color: white;
font-family: sans-serif;
top: 0.3em;
right: 0.5em;
.notif {
display: inline-block;
padding: 0.2em 0.35em;
border-radius: 0.25em;
position: relative;
margin-left: 0.3em;
font-size: 0.8em;
&.warnings {
background-color: #d08d2e;
}
&.errors {
background-color: #e51111;
}
}
}
</style>

View File

@ -1,39 +1,16 @@
<template> <template>
<div> <Generic :item="item">
<div class="card" :class="item.class"> <template #indicator>
<a :href="item.url" :target="item.target" rel="noreferrer">
<div class="card-content">
<div class="media">
<div v-if="item.logo" class="media-left">
<figure class="image is-48x48">
<img :src="item.logo" :alt="`${item.name} logo`" />
</figure>
</div>
<div v-if="item.icon" class="media-left">
<figure class="image is-48x48">
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">{{ item.subtitle }}</p>
</div>
<div class="notifs"> <div class="notifs">
<strong <strong v-if="activity > 0" class="notif activity" title="Activity">
v-if="activity > 0" {{ activity }}
class="notif activity" </strong>
title="Activity" <strong v-if="warnings > 0" class="notif warnings" title="Warning">
>{{ activity }}</strong {{ warnings }}
> </strong>
<strong <strong v-if="errors > 0" class="notif errors" title="Error">
v-if="warnings > 0" {{ errors }}
class="notif warnings" </strong>
title="Warning"
>{{ warnings }}</strong
>
<strong v-if="errors > 0" class="notif errors" title="Error">{{
errors
}}</strong>
<strong <strong
v-if="serverError" v-if="serverError"
class="notif errors" class="notif errors"
@ -41,22 +18,26 @@
>?</strong >?</strong
> >
</div> </div>
</div> </template>
<div class="tag" :class="item.tagstyle" v-if="item.tag"> </Generic>
<strong class="tag-text">#{{ item.tag }}</strong>
</div>
</div>
</a>
</div>
</div>
</template> </template>
<script> <script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const V3_API = "/api/v3";
const LEGACY_API = "/api";
export default { export default {
name: "Radarr", name: "Radarr",
mixins: [service],
props: { props: {
item: Object, item: Object,
}, },
components: {
Generic,
},
data: () => { data: () => {
return { return {
activity: null, activity: null,
@ -68,17 +49,14 @@ export default {
created: function () { created: function () {
this.fetchConfig(); this.fetchConfig();
}, },
computed: {
apiPath() {
return this.item.legacyApi ? LEGACY_API : V3_API;
},
},
methods: { methods: {
fetchConfig: function () { fetchConfig: function () {
fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`)
credentials: "include",
})
.then((response) => {
if (response.status != 200) {
throw new Error(response.statusText);
}
return response.json();
})
.then((health) => { .then((health) => {
this.warnings = 0; this.warnings = 0;
this.errors = 0; this.errors = 0;
@ -94,22 +72,19 @@ export default {
console.error(e); console.error(e);
this.serverError = true; this.serverError = true;
}); });
fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`)
credentials: "include",
})
.then((response) => {
if (response.status != 200) {
throw new Error(response.statusText);
}
return response.json();
})
.then((queue) => { .then((queue) => {
this.activity = 0; this.activity = 0;
if (this.item.legacyApi) {
for (var i = 0; i < queue.length; i++) { for (var i = 0; i < queue.length; i++) {
if (queue[i].movie) { if (queue[i].movie) {
this.activity++; this.activity++;
} }
} }
} else {
this.activity = queue.totalRecords;
}
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
@ -121,35 +96,30 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.media-left img {
max-height: 100%;
}
.notifs { .notifs {
position: absolute; position: absolute;
color: white; color: white;
font-family: sans-serif; font-family: sans-serif;
top: 0.3em; top: 0.3em;
right: 0.5em; right: 0.5em;
}
.notif { .notif {
padding-right: 0.35em; display: inline-block;
padding-left: 0.35em; padding: 0.2em 0.35em;
padding-top: 0.2em;
padding-bottom: 0.2em;
border-radius: 0.25em; border-radius: 0.25em;
position: relative; position: relative;
margin-left: 0.3em; margin-left: 0.3em;
font-size: 0.8em; font-size: 0.8em;
} &.activity {
.activity {
background-color: #4fb5d6; background-color: #4fb5d6;
} }
.warnings { &.warnings {
background-color: #d08d2e; background-color: #d08d2e;
} }
.errors { &.errors {
background-color: #e51111; background-color: #e51111;
} }
}
}
</style> </style>

View File

@ -1,62 +1,49 @@
<template> <template>
<div> <Generic :item="item">
<div class="card" :class="item.class"> <template #indicator>
<a :href="item.url" :target="item.target" rel="noreferrer">
<div class="card-content">
<div class="media">
<div v-if="item.logo" class="media-left">
<figure class="image is-48x48">
<img :src="item.logo" :alt="`${item.name} logo`" />
</figure>
</div>
<div v-if="item.icon" class="media-left">
<figure class="image is-48x48">
<i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">{{ item.subtitle }}</p>
</div>
<div class="notifs"> <div class="notifs">
<strong <strong v-if="activity > 0" class="notif activity" title="Activity">
v-if="activity > 0" {{ activity }}
class="notif activity" </strong>
title="Activity" <strong v-if="warnings > 0" class="notif warnings" title="Warning">
>{{ activity }}</strong {{ warnings }}
> </strong>
<strong <strong v-if="errors > 0" class="notif errors" title="Error">
v-if="warnings > 0" {{ errors }}
class="notif warnings" </strong>
title="Warning"
>{{ warnings }}</strong
>
<strong v-if="errors > 0" class="notif errors" title="Error">{{
errors
}}</strong>
<strong <strong
v-if="serverError" v-if="serverError"
class="notif errors" class="notif errors"
title="Connection error to Sonarr API, check url and apikey in config.yml" title="Connection error to Sonarr API, check url and apikey in config.yml"
>?</strong
> >
?
</strong>
</div> </div>
</div> </template>
<div class="tag" :class="item.tagstyle" v-if="item.tag"> </Generic>
<strong class="tag-text">#{{ item.tag }}</strong>
</div>
</div>
</a>
</div>
</div>
</template> </template>
<script> <script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const V3_API = "/api/v3";
const LEGACY_API = "/api";
export default { export default {
name: "Sonarr", name: "Sonarr",
mixins: [service],
props: { props: {
item: Object, item: Object,
}, },
components: {
Generic,
},
computed: {
apiPath() {
return this.item.legacyApi ? LEGACY_API : V3_API;
},
},
data: () => { data: () => {
return { return {
activity: null, activity: null,
@ -70,15 +57,7 @@ export default {
}, },
methods: { methods: {
fetchConfig: function () { fetchConfig: function () {
fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`)
credentials: "include",
})
.then((response) => {
if (response.status != 200) {
throw new Error(response.statusText);
}
return response.json();
})
.then((health) => { .then((health) => {
this.warnings = 0; this.warnings = 0;
this.errors = 0; this.errors = 0;
@ -94,22 +73,18 @@ export default {
console.error(e); console.error(e);
this.serverError = true; this.serverError = true;
}); });
fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`)
credentials: "include",
})
.then((response) => {
if (response.status != 200) {
throw new Error(response.statusText);
}
return response.json();
})
.then((queue) => { .then((queue) => {
this.activity = 0; this.activity = 0;
if (this.item.legacyApi) {
for (var i = 0; i < queue.length; i++) { for (var i = 0; i < queue.length; i++) {
if (queue[i].series) { if (queue[i].series) {
this.activity++; this.activity++;
} }
} }
} else {
this.activity = queue.totalRecords;
}
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
@ -121,35 +96,32 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.media-left img {
max-height: 100%;
}
.notifs { .notifs {
position: absolute; position: absolute;
color: white; color: white;
font-family: sans-serif; font-family: sans-serif;
top: 0.3em; top: 0.3em;
right: 0.5em; right: 0.5em;
}
.notif { .notif {
padding-right: 0.35em; display: inline-block;
padding-left: 0.35em; padding: 0.2em 0.35em;
padding-top: 0.2em;
padding-bottom: 0.2em;
border-radius: 0.25em; border-radius: 0.25em;
position: relative; position: relative;
margin-left: 0.3em; margin-left: 0.3em;
font-size: 0.8em; font-size: 0.8em;
}
.activity { &.activity {
background-color: #4fb5d6; background-color: #4fb5d6;
} }
.warnings { &.warnings {
background-color: #d08d2e; background-color: #d08d2e;
} }
.errors { &.errors {
background-color: #e51111; background-color: #e51111;
} }
}
}
</style> </style>

View File

@ -31,7 +31,13 @@ export default {
path = path.slice(1); path = path.slice(1);
} }
return fetch(`${this.endpoint}/${path}`, options).then((response) => { let url = this.endpoint;
if (path) {
url = `${this.endpoint}/${path}`;
}
return fetch(url, options).then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error("Not 2xx response"); throw new Error("Not 2xx response");
} }

122
yarn.lock
View File

@ -1173,10 +1173,10 @@
lodash.kebabcase "^4.1.1" lodash.kebabcase "^4.1.1"
svg-tags "^1.0.0" svg-tags "^1.0.0"
"@vue/babel-preset-app@^4.5.13": "@vue/babel-preset-app@^4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.5.13.tgz#cb475321e4c73f7f110dac29a48c2a9cb80afeb6" resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.5.15.tgz#f6bc08f8f674e98a260004234cde18b966d72eb0"
integrity sha512-pM7CR3yXB6L8Gfn6EmX7FLNE3+V/15I3o33GkSNsWvgsMp6HVGXKkXgojrcfUUauyL1LZOdvTmu4enU2RePGHw== integrity sha512-J+YttzvwRfV1BPczf8r3qCevznYk+jh531agVF+5EYlHF4Sgh/cGXTz9qkkiux3LQgvhEGXgmCteg1n38WuuKg==
dependencies: dependencies:
"@babel/core" "^7.11.0" "@babel/core" "^7.11.0"
"@babel/helper-compilation-targets" "^7.9.6" "@babel/helper-compilation-targets" "^7.9.6"
@ -1258,61 +1258,61 @@
"@vue/babel-plugin-transform-vue-jsx" "^1.2.1" "@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
camelcase "^5.0.0" camelcase "^5.0.0"
"@vue/cli-overlay@^4.5.13": "@vue/cli-overlay@^4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-4.5.13.tgz#4f1fd2161be8f69d6cba8079f3f0d7dc4dee47a7" resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-4.5.15.tgz#0700fd6bad39336d4189ba3ff7d25e638e818c9c"
integrity sha512-jhUIg3klgi5Cxhs8dnat5hi/W2tQJvsqCxR0u6hgfSob0ORODgUBlN+F/uwq7cKIe/pzedVUk1y07F13GQvPqg== integrity sha512-0zI0kANAVmjFO2LWGUIzdGPMeE3+9k+KeRDXsUqB30YfRF7abjfiiRPq5BU9pOzlJbVdpRkisschBrvdJqDuDg==
"@vue/cli-plugin-babel@~4.5.0": "@vue/cli-plugin-babel@~4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-4.5.13.tgz#a89c482edcc4ea1d135645cec502a7f5fd4c30e7" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-4.5.15.tgz#ae4fb2ed54255fe3d84df381dab68509641179ed"
integrity sha512-ykvEAfD8PgGs+dGMGqr7l/nRmIS39NRzWLhMluPLTvDV1L+IxcoB73HNLGA/aENDpl8CuWrTE+1VgydcOhp+wg== integrity sha512-hBLrwYfFkHldEe34op/YNgPhpOWI5n5DB2Qt9I/1Epeif4M4iFaayrgjvOR9AVM6WbD3Yx7WCFszYpWrQZpBzQ==
dependencies: dependencies:
"@babel/core" "^7.11.0" "@babel/core" "^7.11.0"
"@vue/babel-preset-app" "^4.5.13" "@vue/babel-preset-app" "^4.5.15"
"@vue/cli-shared-utils" "^4.5.13" "@vue/cli-shared-utils" "^4.5.15"
babel-loader "^8.1.0" babel-loader "^8.1.0"
cache-loader "^4.1.0" cache-loader "^4.1.0"
thread-loader "^2.1.3" thread-loader "^2.1.3"
webpack "^4.0.0" webpack "^4.0.0"
"@vue/cli-plugin-eslint@~4.5.0": "@vue/cli-plugin-eslint@~4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.13.tgz#8baf22d0d96d76720c7506646b96f4f62c05bdfa" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.15.tgz#5781824a941f34c26336a67b1f6584a06c6a24ff"
integrity sha512-yc2uXX6aBiy3vEf5TwaueaDqQbdIXIhk0x0KzEtpPo23jBdLkpOSoU5NCgE06g/ZiGAcettpmBSv73Hfp4wHEw== integrity sha512-/2Fl6wY/5bz3HD035oSnFRMsKNxDxU396KqBdpCQdwdvqk4mm6JAbXqihpcBRTNPeTO6w+LwGe6FE56PVbJdbg==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.13" "@vue/cli-shared-utils" "^4.5.15"
eslint-loader "^2.2.1" eslint-loader "^2.2.1"
globby "^9.2.0" globby "^9.2.0"
inquirer "^7.1.0" inquirer "^7.1.0"
webpack "^4.0.0" webpack "^4.0.0"
yorkie "^2.0.0" yorkie "^2.0.0"
"@vue/cli-plugin-pwa@~4.5.0": "@vue/cli-plugin-pwa@~4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-pwa/-/cli-plugin-pwa-4.5.13.tgz#a800639814b6f62a38f04198c340cfaee7295c3f" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-pwa/-/cli-plugin-pwa-4.5.15.tgz#eb800c418d96b496deec9d063a1798fe6e9c2db8"
integrity sha512-uU5pp94VU0YscfKq/mNRsKOdxG+CTqVlZWaYkRc+HCcwkJ/m/CnxgaEqQFr0QpHC8zmlX4gILO1RVYygJoR9tw== integrity sha512-yQzsspaIkjeQyN6btF8ATgbJFU023q1HC8uUpmiBa4QE9EyBlR8fSrKFhcJ0EmT6KnU7PMwlnOJ/OqjguFnufA==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.13" "@vue/cli-shared-utils" "^4.5.15"
webpack "^4.0.0" webpack "^4.0.0"
workbox-webpack-plugin "^4.3.1" workbox-webpack-plugin "^4.3.1"
"@vue/cli-plugin-router@^4.5.13": "@vue/cli-plugin-router@^4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-router/-/cli-plugin-router-4.5.13.tgz#0b67c8898a2bf132941919a2a2e5f3aacbd9ffbe" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-router/-/cli-plugin-router-4.5.15.tgz#1e75c8c89df42c694f143b9f1028de3cf5d61e1e"
integrity sha512-tgtMDjchB/M1z8BcfV4jSOY9fZSMDTPgF9lsJIiqBWMxvBIsk9uIZHxp62DibYME4CCKb/nNK61XHaikFp+83w== integrity sha512-q7Y6kP9b3k55Ca2j59xJ7XPA6x+iSRB+N4ac0ZbcL1TbInVQ4j5wCzyE+uqid40hLy4fUdlpl4X9fHJEwuVxPA==
dependencies: dependencies:
"@vue/cli-shared-utils" "^4.5.13" "@vue/cli-shared-utils" "^4.5.15"
"@vue/cli-plugin-vuex@^4.5.13": "@vue/cli-plugin-vuex@^4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.13.tgz#98646d8bc1e69cf6c6a6cba2fed3eace0356c360" resolved "https://registry.yarnpkg.com/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.15.tgz#466c1f02777d02fef53a9bb49a36cc3a3bcfec4e"
integrity sha512-I1S9wZC7iI0Wn8kw8Zh+A2Qkf6s1M6vTGBkx8boXjuzfwEEyEHRxadsVCecZc8Mkpydo0nykj+MyYF96TKFuVA== integrity sha512-fqap+4HN+w+InDxlA3hZTOGE0tzBTgXhKLoDydhywqgmhQ1D9JA6Feh94ze6tG8DsWX58/ujYUqA8jAz17FJtg==
"@vue/cli-service@~4.5.0": "@vue/cli-service@~4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-4.5.13.tgz#a09e684a801684b6e24e5414ad30650970eec9ed" resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-4.5.15.tgz#0e9a186d51550027d0e68e95042077eb4d115b45"
integrity sha512-CKAZN4iokMMsaUyJRU22oUAz3oS/X9sVBSKAF2/shFBV5xh3jqAlKl8OXZYz4cXGFLA6djNuYrniuLAo7Ku97A== integrity sha512-sFWnLYVCn4zRfu45IcsIE9eXM0YpDV3S11vlM2/DVbIPAGoYo5ySpSof6aHcIvkeGsIsrHFpPHzNvDZ/efs7jA==
dependencies: dependencies:
"@intervolga/optimize-cssnano-plugin" "^1.0.5" "@intervolga/optimize-cssnano-plugin" "^1.0.5"
"@soda/friendly-errors-webpack-plugin" "^1.7.1" "@soda/friendly-errors-webpack-plugin" "^1.7.1"
@ -1320,10 +1320,10 @@
"@types/minimist" "^1.2.0" "@types/minimist" "^1.2.0"
"@types/webpack" "^4.0.0" "@types/webpack" "^4.0.0"
"@types/webpack-dev-server" "^3.11.0" "@types/webpack-dev-server" "^3.11.0"
"@vue/cli-overlay" "^4.5.13" "@vue/cli-overlay" "^4.5.15"
"@vue/cli-plugin-router" "^4.5.13" "@vue/cli-plugin-router" "^4.5.15"
"@vue/cli-plugin-vuex" "^4.5.13" "@vue/cli-plugin-vuex" "^4.5.15"
"@vue/cli-shared-utils" "^4.5.13" "@vue/cli-shared-utils" "^4.5.15"
"@vue/component-compiler-utils" "^3.1.2" "@vue/component-compiler-utils" "^3.1.2"
"@vue/preload-webpack-plugin" "^1.1.0" "@vue/preload-webpack-plugin" "^1.1.0"
"@vue/web-component-wrapper" "^1.2.0" "@vue/web-component-wrapper" "^1.2.0"
@ -1372,10 +1372,10 @@
optionalDependencies: optionalDependencies:
vue-loader-v16 "npm:vue-loader@^16.1.0" vue-loader-v16 "npm:vue-loader@^16.1.0"
"@vue/cli-shared-utils@^4.5.13": "@vue/cli-shared-utils@^4.5.15":
version "4.5.13" version "4.5.15"
resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.5.13.tgz#acd40f31b4790f1634292bdaa5fca95dc1e0ff50" resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.5.15.tgz#dba3858165dbe3465755f256a4890e69084532d6"
integrity sha512-HpnOrkLg42RFUsQGMJv26oTG3J3FmKtO2WSRhKIIL+1ok3w9OjGCtA3nMMXN27f9eX14TqO64M36DaiSZ1fSiw== integrity sha512-SKaej9hHzzjKSOw1NlFmc6BSE0vcqUQMQiv1cxQ2DhVyy4QxZXBmzmiLBUBe+hYZZs1neXW7n//udeN9bCAY+Q==
dependencies: dependencies:
"@hapi/joi" "^15.0.1" "@hapi/joi" "^15.0.1"
chalk "^2.4.2" chalk "^2.4.2"
@ -2339,9 +2339,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
version "1.0.30001245" version "1.0.30001311"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz#45b941bbd833cb0fa53861ff2bae746b3c6ca5d4" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001311.tgz"
integrity sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA== integrity sha512-mleTFtFKfykEeW34EyfhGIFjGCqzhh38Y0LhdQ9aWF+HorZTtdgKV/1hEE0NlFkG2ubvisPV6l400tlbPys98A==
case-sensitive-paths-webpack-plugin@^2.3.0: case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.4.0" version "2.4.0"
@ -2773,10 +2773,10 @@ core-js@^2.4.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.17.3: core-js@^3.21.1:
version "3.17.3" version "3.21.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.17.3.tgz#8e8bd20e91df9951e903cabe91f9af4a0895bc1e" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94"
integrity sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw== integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==
core-js@^3.6.5: core-js@^3.6.5:
version "3.15.2" version "3.15.2"
@ -3999,9 +3999,9 @@ flush-write-stream@^1.0.0:
readable-stream "^2.3.6" readable-stream "^2.3.6"
follow-redirects@^1.0.0: follow-redirects@^1.0.0:
version "1.14.1" version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
for-in@^1.0.2: for-in@^1.0.2:
version "1.0.2" version "1.0.2"
@ -5590,9 +5590,9 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.5: minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5" version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
minipass@^3.1.1: minipass@^3.1.1:
version "3.1.3" version "3.1.3"
@ -8227,9 +8227,9 @@ url-loader@^2.2.0:
schema-utils "^2.5.0" schema-utils "^2.5.0"
url-parse@^1.4.3, url-parse@^1.5.1: url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.3" version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies: dependencies:
querystringify "^2.1.1" querystringify "^2.1.1"
requires-port "^1.0.0" requires-port "^1.0.0"