mirror of
https://github.com/bastienwirtz/homer.git
synced 2025-01-28 16:58:44 +01:00
Merge branch 'main' into main
This commit is contained in:
commit
92d5b8d424
14
README.md
14
README.md
@ -43,8 +43,8 @@
|
||||
- [Features](#features)
|
||||
- [Getting started](#getting-started)
|
||||
- [Configuration](docs/configuration.md)
|
||||
- [Custom services](docs/customservices.md)
|
||||
- [Tips & tricks](docs/tips-and-tricks.md)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Development](docs/development.md)
|
||||
|
||||
|
||||
@ -73,7 +73,11 @@ See [documentation](docs/configuration.md) for information about the configurati
|
||||
To launch container:
|
||||
|
||||
```sh
|
||||
docker run -p 8080:8080 -v /your/local/assets/:/www/assets b4bz/homer:latest
|
||||
docker run -d \
|
||||
-p 8080:8080 \
|
||||
-v </your/local/assets/>:/www/assets \
|
||||
--restart=always \
|
||||
b4bz/homer:latest
|
||||
```
|
||||
|
||||
Default assets will be automatically installed in the `/www/assets` directory. Use `UID` and/or `GID` env var to change the assets owner (`docker run -e "UID=1000" -e "GID=1000" [...]`).
|
||||
@ -130,9 +134,3 @@ npm run build
|
||||
```
|
||||
|
||||
Then your dashboard is ready to use in the `/dist` directory.
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Add new themes.
|
||||
- [ ] Add support for custom service card (add custom feature to some service / app link)
|
||||
|
@ -66,6 +66,17 @@ colors:
|
||||
# Optional message
|
||||
message:
|
||||
# url: "https://<my-api-endpoint>" # Can fetch information from an endpoint to override value below.
|
||||
# mapping: # allows to map fields from the remote format to the one expected by Homer
|
||||
# title: 'id' # use value from field 'id' as title
|
||||
# content: 'value' # value from field 'value' as content
|
||||
# refreshInterval: 10000 # Optional: time interval to refresh message
|
||||
#
|
||||
# Real example using chucknorris.io for showing Chuck Norris facts as messages:
|
||||
# url: https://api.chucknorris.io/jokes/random
|
||||
# mapping:
|
||||
# title: 'id'
|
||||
# content: 'value'
|
||||
# refreshInterval: 10000
|
||||
style: "is-warning"
|
||||
title: "Optional message!"
|
||||
icon: "fa fa-exclamation-triangle"
|
||||
@ -81,6 +92,11 @@ links:
|
||||
- name: "link 2"
|
||||
icon: "fas fa-book"
|
||||
url: "https://github.com/bastienwirtz/homer"
|
||||
# this will link to a second homer page that will load config from page2.yml and keep default config values as in config.yml file
|
||||
# see url field and assets/page.yml used in this example:
|
||||
- name: "Second Page"
|
||||
icon: "fas fa-file-alt"
|
||||
url: "#page2"
|
||||
|
||||
# Services
|
||||
# First level array represents a group.
|
||||
@ -88,6 +104,8 @@ links:
|
||||
services:
|
||||
- name: "Application"
|
||||
icon: "fas fa-code-branch"
|
||||
# A path to an image can also be provided. Note that icon take precedence if both icon and logo are set.
|
||||
# logo: "path/to/logo"
|
||||
items:
|
||||
- name: "Awesome app"
|
||||
logo: "assets/tools/sample.png"
|
||||
@ -118,9 +136,10 @@ services:
|
||||
# background: red # optional color for card to set color directly without custom stylesheet
|
||||
```
|
||||
|
||||
|
||||
View [Custom Services](customservices.md) for details about all available custom services (like PiHole) and how to configure them.
|
||||
|
||||
If you choose to fetch message information from an endpoint, the output format should be:
|
||||
If you choose to fetch message information from an endpoint, the output format should be as follows (or you can [custom map fields as shown in tips-and-tricks](./tips-and-tricks.md#mapping-fields)):
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -1,6 +1,8 @@
|
||||
# Custom Services
|
||||
|
||||
Here is an overview of all custom services that are available within Homer.
|
||||
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
|
||||
within Homer.
|
||||
|
||||
## PiHole
|
||||
|
||||
@ -17,6 +19,7 @@ The following configuration is available for the PiHole service.
|
||||
type: "PiHole"
|
||||
```
|
||||
|
||||
|
||||
## OpenWeatherMap
|
||||
|
||||
Using the OpenWeatherMap service you can display weather information about a given location.
|
||||
@ -35,4 +38,38 @@ items:
|
||||
```
|
||||
|
||||
**Remarks:**
|
||||
If for some reason your city can't be found by entering the name in the `location` property, you could also try to configure the OWM city ID in the `locationId` property. To retrieve your specific City ID, go to the [OWM website](https://openweathermap.org), search for your city and retrieve the ID from the URL (for example, the City ID of Amsterdam is 2759794).
|
||||
If for some reason your city can't be found by entering the name in the `location` property, you could also try to configure the OWM city ID in the `locationId` property. To retrieve your specific City ID, go to the [OWM website](https://openweathermap.org), search for your city and retrieve the ID from the URL (for example, the City ID of Amsterdam is 2759794).
|
||||
|
||||
|
||||
## Medusa
|
||||
|
||||
This service displays News (grey), Warning (orange) or Error (red) notifications bubbles from the Medusa application.
|
||||
Two lines are needed in the config.yml :
|
||||
```
|
||||
type: "Medusa"
|
||||
apikey: "01234deb70424befb1f4ef6a23456789"
|
||||
```
|
||||
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.
|
||||
|
||||
|
||||
## Sonarr/Radarr
|
||||
|
||||
This service displays Activity (blue), Warning (orange) or Error (red) notifications bubbles from the Radarr/Sonarr application.
|
||||
Two lines are needed in the config.yml :
|
||||
```
|
||||
type: "Radarr" or "Sonarr"
|
||||
apikey: "01234deb70424befb1f4ef6a23456789"
|
||||
```
|
||||
The url must be the root url of Radarr/Sonarr application.
|
||||
The Radarr/Sonarr API key can be found in Settings > General. It is needed to access the API.
|
||||
|
||||
|
||||
## PaperlessNG
|
||||
|
||||
For Paperless you need an API-Key which you have to store at the item in the field `apikey`.
|
||||
|
||||
|
||||
## Ping
|
||||
|
||||
For Ping you need an API-Key which you have to store at the item in the field `apikey`.
|
||||
|
@ -113,6 +113,64 @@ docker create \
|
||||
|
||||
|
||||
## Get the news headlines in Homer
|
||||
|
||||
### Mapping Fields
|
||||
Most times, the url you're getting headlines from follows a different schema than the one expected by Homer.
|
||||
|
||||
For example, if you would like to show jokes from ChuckNorris.io, you'll find that the url https://api.chucknorris.io/jokes/random is giving you info like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"categories": [],
|
||||
"created_at": "2020-01-05 13:42:22.089095",
|
||||
"icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
|
||||
"id": "MR2-BnMBR667xSpQBIleUg",
|
||||
"updated_at": "2020-01-05 13:42:22.089095",
|
||||
"url": "https://api.chucknorris.io/jokes/MR2-BnMBR667xSpQBIleUg",
|
||||
"value": "Chuck Norris can quitely sneak up on himself"
|
||||
}
|
||||
```
|
||||
|
||||
but... you need that info to be transformed to something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "MR2-BnMBR667xSpQBIleUg",
|
||||
"content": "Chuck Norris can quitely sneak up on himself"
|
||||
}
|
||||
```
|
||||
|
||||
Now, you can do that using the `mapping` field in your `message` configuration. This example would be something like this:
|
||||
|
||||
```yml
|
||||
message:
|
||||
url: https://api.chucknorris.io/jokes/random
|
||||
mapping:
|
||||
title: 'id'
|
||||
content: 'value'
|
||||
```
|
||||
|
||||
As you would see, using the ID as a title doesn't seem nice, that's why when a field is empty it would keep the default values, like this:
|
||||
|
||||
```yml
|
||||
message:
|
||||
url: https://api.chucknorris.io/jokes/random
|
||||
mapping:
|
||||
content: 'value'
|
||||
title: "Chuck Norris Facts!"
|
||||
```
|
||||
|
||||
and even an error message in case the `url` didn't respond or threw an error:
|
||||
|
||||
```yml
|
||||
message:
|
||||
url: https://api.chucknorris.io/jokes/random
|
||||
mapping:
|
||||
content: 'value'
|
||||
title: "Chuck Norris Facts!"
|
||||
content: "Message could not be loaded"
|
||||
```
|
||||
|
||||
#### `by @JamiePhonic`
|
||||
|
||||
Homer allows you to set a "message" that will appear at the top of the page, however, you can also supply a `url:`.
|
||||
|
30
package.json
30
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homer",
|
||||
"version": "20.06.1",
|
||||
"version": "21.09.1",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
@ -8,28 +8,28 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"bulma": "^0.9.1",
|
||||
"core-js": "^3.8.1",
|
||||
"js-yaml": "^3.14.1",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"bulma": "^0.9.3",
|
||||
"core-js": "^3.17.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"vue": "^2.6.12"
|
||||
"vue": "^2.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.9",
|
||||
"@vue/cli-plugin-eslint": "~4.5.9",
|
||||
"@vue/cli-plugin-pwa": "~4.5.9",
|
||||
"@vue/cli-service": "~4.5.9",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-pwa": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.16.0",
|
||||
"eslint-plugin-prettier": "^3.3.0",
|
||||
"eslint-plugin-vue": "^7.3.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"prettier": "^2.2.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass": "^1.30.0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
}
|
||||
}
|
||||
|
35
public/assets/additionnal-page.yml.dist
Normal file
35
public/assets/additionnal-page.yml.dist
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
# Additionnal page configuration
|
||||
|
||||
# Additionnal 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
|
||||
|
||||
|
||||
subtitle: "this is another dashboard page"
|
||||
|
||||
# This overwrites message config. Setting it to empty to remove message from this page and keep it only in the main one:
|
||||
message: ~
|
||||
|
||||
# as we want to include a differente link here (so we can get back to home page), we need to replicate all links or they will be revome when overwriting the links field:
|
||||
links:
|
||||
- name: "Home"
|
||||
icon: "fas fa-home"
|
||||
url: "#"
|
||||
- name: "Contribute"
|
||||
icon: "fab fa-github"
|
||||
url: "https://github.com/bastienwirtz/homer"
|
||||
target: "_blank" # optional html a tag target attribute
|
||||
- name: "Wiki"
|
||||
icon: "fas fa-book"
|
||||
url: "https://www.wikipedia.org/"
|
||||
|
||||
services:
|
||||
- name: "More applications on another page!"
|
||||
icon: "fas fa-cloud"
|
||||
items:
|
||||
- name: "Awesome app on a second page!"
|
||||
logo: "assets/tools/sample.png"
|
||||
subtitle: "Bookmark example"
|
||||
tag: "app"
|
||||
url: "https://www.reddit.com/r/selfhosted/"
|
||||
target: "_blank"
|
@ -56,6 +56,11 @@ links:
|
||||
- name: "Wiki"
|
||||
icon: "fas fa-book"
|
||||
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
|
||||
# see url field and assets/additionnal-page.yml.dist used in this example:
|
||||
- name: "another page!"
|
||||
icon: "fas fa-file-alt"
|
||||
url: "#additionnal-page"
|
||||
|
||||
# Services
|
||||
# First level array represent a group.
|
||||
|
67
src/App.vue
67
src/App.vue
@ -13,7 +13,9 @@
|
||||
<section v-if="config.header" class="first-line">
|
||||
<div v-cloak class="container">
|
||||
<div class="logo">
|
||||
<img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
|
||||
<a href="#">
|
||||
<img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
|
||||
</a>
|
||||
<i v-if="config.icon" :class="config.icon"></i>
|
||||
</div>
|
||||
<div class="dashboard-title">
|
||||
@ -62,6 +64,11 @@
|
||||
<template v-for="group in services">
|
||||
<h2 v-if="group.name" class="column is-full group-title">
|
||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
||||
<div v-else-if="group.logo" class="group-logo media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="group.logo" :alt="`${group.name} logo`" />
|
||||
</figure>
|
||||
</div>
|
||||
{{ group.name }}
|
||||
</h2>
|
||||
<Service
|
||||
@ -85,6 +92,11 @@
|
||||
>
|
||||
<h2 v-if="group.name" class="group-title">
|
||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
||||
<div v-else-if="group.logo" class="group-logo media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="group.logo" :alt="`${group.name} logo`" />
|
||||
</figure>
|
||||
</div>
|
||||
{{ group.name }}
|
||||
</h2>
|
||||
<Service
|
||||
@ -149,28 +161,41 @@ export default {
|
||||
};
|
||||
},
|
||||
created: async function () {
|
||||
const defaults = jsyaml.load(defaultConfig);
|
||||
let config;
|
||||
try {
|
||||
config = await this.getConfig();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
config = this.handleErrors("⚠️ Error loading configuration", error);
|
||||
}
|
||||
this.config = merge(defaults, config);
|
||||
this.services = this.config.services;
|
||||
document.title =
|
||||
this.config.documentTitle ||
|
||||
`${this.config.title} | ${this.config.subtitle}`;
|
||||
if (this.config.stylesheet) {
|
||||
let stylesheet = "";
|
||||
for (const file of this.config.stylesheet) {
|
||||
stylesheet += `@import "${file}";`;
|
||||
}
|
||||
this.createStylesheet(stylesheet);
|
||||
}
|
||||
this.buildDashboard();
|
||||
window.onhashchange = this.buildDashboard;
|
||||
},
|
||||
methods: {
|
||||
buildDashboard: async function () {
|
||||
const defaults = jsyaml.load(defaultConfig);
|
||||
let config;
|
||||
try {
|
||||
config = await this.getConfig();
|
||||
const path =
|
||||
window.location.hash.substring(1) != ""
|
||||
? window.location.hash.substring(1)
|
||||
: null;
|
||||
|
||||
if (path) {
|
||||
let pathConfig = await this.getConfig(`assets/${path}.yml`); // the slash (/) is included in the pathname
|
||||
config = Object.assign(config, pathConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
config = this.handleErrors("⚠️ Error loading configuration", error);
|
||||
}
|
||||
this.config = merge(defaults, config);
|
||||
this.services = this.config.services;
|
||||
document.title =
|
||||
this.config.documentTitle ||
|
||||
`${this.config.title} | ${this.config.subtitle}`;
|
||||
if (this.config.stylesheet) {
|
||||
let stylesheet = "";
|
||||
for (const file of this.config.stylesheet) {
|
||||
stylesheet += `@import "${file}";`;
|
||||
}
|
||||
this.createStylesheet(stylesheet);
|
||||
}
|
||||
},
|
||||
getConfig: function (path = "assets/config.yml") {
|
||||
return fetch(path).then((response) => {
|
||||
if (response.redirected) {
|
||||
|
@ -106,7 +106,7 @@ body {
|
||||
}
|
||||
|
||||
.first-line {
|
||||
height: 100px;
|
||||
min-height: 100px;
|
||||
vertical-align: center;
|
||||
background-color: var(--highlight-primary);
|
||||
|
||||
@ -121,7 +121,7 @@ body {
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 80px;
|
||||
min-height: 80px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
@ -140,8 +140,7 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navbar,
|
||||
.navbar-menu {
|
||||
.navbar {
|
||||
background-color: var(--highlight-secondary);
|
||||
|
||||
a {
|
||||
@ -153,6 +152,9 @@ body {
|
||||
background-color: var(--highlight-hover);
|
||||
}
|
||||
}
|
||||
.navbar-menu {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
.navbar-end {
|
||||
text-align: right;
|
||||
@ -197,6 +199,11 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.media.no-subtitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-content {
|
||||
overflow: hidden;
|
||||
text-overflow: inherit;
|
||||
@ -206,7 +213,7 @@ body {
|
||||
color: var(--highlight-secondary);
|
||||
background-color: var(--highlight-secondary);
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
bottom: 1rem;
|
||||
right: -0.2rem;
|
||||
width: 3px;
|
||||
overflow: hidden;
|
||||
@ -219,7 +226,6 @@ body {
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
|
||||
@ -255,11 +261,13 @@ body {
|
||||
}
|
||||
|
||||
.column div:first-of-type .card {
|
||||
border-radius: 5px 5px 0 0;
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.column div:last-child .card {
|
||||
border-radius: 0 0 5px 5px;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -340,3 +348,7 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-logo {
|
||||
float: left;
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ export default {
|
||||
method: "HEAD",
|
||||
cache: "no-store",
|
||||
})
|
||||
.then(function () {
|
||||
that.offline = false;
|
||||
.then(function (response) {
|
||||
that.offline = !response.ok;
|
||||
})
|
||||
.catch(function () {
|
||||
that.offline = true;
|
||||
|
@ -4,7 +4,11 @@
|
||||
aria-label="Toggle dark mode"
|
||||
class="navbar-item is-inline-block-mobile"
|
||||
>
|
||||
<i class="fas fa-fw fa-adjust"></i>
|
||||
<i
|
||||
:class="`${faClasses[mode]}`"
|
||||
class="fa-fw"
|
||||
:title="`${titles[mode]}`"
|
||||
></i>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -14,21 +18,55 @@ export default {
|
||||
data: function () {
|
||||
return {
|
||||
isDark: null,
|
||||
faClasses: null,
|
||||
titles: null,
|
||||
mode: null,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.isDark =
|
||||
"overrideDark" in localStorage
|
||||
? JSON.parse(localStorage.overrideDark)
|
||||
: matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
this.faClasses = ["fas fa-adjust", "fas fa-circle", "far fa-circle"];
|
||||
this.titles = ["Auto-switch", "Light theme", "Dark theme"];
|
||||
this.mode = 0;
|
||||
if ("overrideDark" in localStorage) {
|
||||
// Light theme is 1 and Dark theme is 2
|
||||
this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1;
|
||||
}
|
||||
this.isDark = this.getIsDark();
|
||||
this.$emit("updated", this.isDark);
|
||||
},
|
||||
methods: {
|
||||
toggleTheme: function () {
|
||||
this.isDark = !this.isDark;
|
||||
localStorage.overrideDark = this.isDark;
|
||||
this.mode = (this.mode + 1) % 3;
|
||||
switch (this.mode) {
|
||||
// Default behavior
|
||||
case 0:
|
||||
localStorage.removeItem("overrideDark");
|
||||
break;
|
||||
// Force light theme
|
||||
case 1:
|
||||
localStorage.overrideDark = false;
|
||||
break;
|
||||
// Force dark theme
|
||||
case 2:
|
||||
localStorage.overrideDark = true;
|
||||
break;
|
||||
default:
|
||||
// Should be unreachable
|
||||
break;
|
||||
}
|
||||
|
||||
this.isDark = this.getIsDark();
|
||||
this.$emit("updated", this.isDark);
|
||||
},
|
||||
|
||||
getIsDark: function () {
|
||||
const values = [
|
||||
matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
false,
|
||||
true,
|
||||
];
|
||||
return values[this.mode];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -22,26 +22,52 @@ export default {
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
show: false,
|
||||
message: {},
|
||||
};
|
||||
},
|
||||
created: async function () {
|
||||
// Look for a new message if an endpoint is provided.
|
||||
this.message = Object.assign({}, this.item);
|
||||
if (this.item && this.item.url) {
|
||||
const fetchedMessage = await this.getMessage(this.item.url);
|
||||
// keep the original config value if no value is provided by the endpoint
|
||||
for (const prop of ["title", "style", "content"]) {
|
||||
if (prop in fetchedMessage && fetchedMessage[prop] !== null) {
|
||||
this.message[prop] = fetchedMessage[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.show = this.message.title || this.message.content;
|
||||
await this.getMessage();
|
||||
},
|
||||
computed: {
|
||||
show: function () {
|
||||
return this.message.title || this.message.content;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: function (item) {
|
||||
this.message = Object.assign({}, item);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getMessage: function (url) {
|
||||
getMessage: async function () {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
if (this.item.url) {
|
||||
let fetchedMessage = await this.downloadMessage(this.item.url);
|
||||
console.log("done");
|
||||
if (this.item.mapping) {
|
||||
fetchedMessage = this.mapRemoteMessage(fetchedMessage);
|
||||
}
|
||||
|
||||
// keep the original config value if no value is provided by the endpoint
|
||||
const message = this.message;
|
||||
for (const prop of ["title", "style", "content", "icon"]) {
|
||||
if (prop in fetchedMessage && fetchedMessage[prop] !== null) {
|
||||
message[prop] = fetchedMessage[prop];
|
||||
}
|
||||
}
|
||||
this.message = { ...message }; // Force computed property to re-evaluate
|
||||
}
|
||||
|
||||
if (this.item.refreshInterval) {
|
||||
setTimeout(this.getMessage, this.item.refreshInterval);
|
||||
}
|
||||
},
|
||||
|
||||
downloadMessage: function (url) {
|
||||
return fetch(url).then(function (response) {
|
||||
if (response.status != 200) {
|
||||
return;
|
||||
@ -49,6 +75,15 @@ export default {
|
||||
return response.json();
|
||||
});
|
||||
},
|
||||
|
||||
mapRemoteMessage: function (message) {
|
||||
let mapped = {};
|
||||
// map property from message into mapped according to mapping config (only if field has a value):
|
||||
for (const prop in this.item.mapping)
|
||||
if (message[this.item.mapping[prop]])
|
||||
mapped[prop] = message[this.item.mapping[prop]];
|
||||
return mapped;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -51,9 +51,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
fetchStatus: async function () {
|
||||
this.status = await fetch(
|
||||
`${this.item.url}/control/status`
|
||||
).then((response) => response.json());
|
||||
this.status = await fetch(`${this.item.url}/control/status`, {
|
||||
credentials: "include",
|
||||
}).then((response) => response.json());
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,16 +1,3 @@
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
*/
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
@ -20,7 +7,7 @@ export default {};
|
||||
>
|
||||
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div :class="mediaClass">
|
||||
<div v-if="item.logo" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||
@ -33,7 +20,9 @@ export default {};
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ item.name }}</p>
|
||||
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||
<p class="subtitle is-6" v-if="item.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||
@ -51,11 +40,23 @@ export default {
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
computed: {
|
||||
mediaClass: function () {
|
||||
return { media: true, "no-subtitle": !this.item.subtitle };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
.media-left {
|
||||
.image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
128
src/components/services/Medusa.vue
Normal file
128
src/components/services/Medusa.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card" :class="item.class">
|
||||
<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">
|
||||
<strong
|
||||
v-if="config !== null && config.system.news.unread > 0"
|
||||
class="notif news"
|
||||
title="News"
|
||||
>{{ config.system.news.unread }}</strong
|
||||
>
|
||||
<strong
|
||||
v-if="config !== null && config.main.logs.numWarnings > 0"
|
||||
class="notif warnings"
|
||||
title="Warning"
|
||||
>{{ config.main.logs.numWarnings }}</strong
|
||||
>
|
||||
<strong
|
||||
v-if="config !== null && config.main.logs.numErrors > 0"
|
||||
class="notif errors"
|
||||
title="Error"
|
||||
>{{ config.main.logs.numErrors }}</strong
|
||||
>
|
||||
<strong
|
||||
v-if="serverError"
|
||||
class="notif errors"
|
||||
title="Connection error to Medusa API, check url and apikey in config.yml"
|
||||
>?</strong
|
||||
>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Medusa",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
config: null,
|
||||
serverError: false,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.fetchConfig();
|
||||
},
|
||||
methods: {
|
||||
fetchConfig: function () {
|
||||
fetch(`${this.item.url}/api/v2/config`, {
|
||||
credentials: "include",
|
||||
headers: { "X-Api-Key": `${this.item.apikey}` },
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status != 200) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((conf) => {
|
||||
this.config = conf;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
this.serverError = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
}
|
||||
.notifs {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
top: 0.3em;
|
||||
right: 0.5em;
|
||||
}
|
||||
.notif {
|
||||
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;
|
||||
}
|
||||
.news {
|
||||
background-color: #777777;
|
||||
}
|
||||
|
||||
.warnings {
|
||||
background-color: #d08d2e;
|
||||
}
|
||||
|
||||
.errors {
|
||||
background-color: #e51111;
|
||||
}
|
||||
</style>
|
84
src/components/services/PaperlessNG.vue
Normal file
84
src/components/services/PaperlessNG.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card" :class="item.class">
|
||||
<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">
|
||||
<template v-if="item.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</template>
|
||||
<template v-else-if="api">
|
||||
happily storing {{ api.count }} documents
|
||||
</template>
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Paperless",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
data: () => ({
|
||||
api: null,
|
||||
}),
|
||||
created() {
|
||||
this.fetchStatus();
|
||||
},
|
||||
methods: {
|
||||
fetchStatus: async function () {
|
||||
if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing
|
||||
var apikey = this.item.apikey;
|
||||
if (!apikey) {
|
||||
console.error(
|
||||
"apikey is not present in config.yml for the paperless entry!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const url = `${this.item.url}/api/documents/`;
|
||||
this.api = await fetch(url, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Authorization: "Token " + this.item.apikey,
|
||||
},
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("Not 2xx response");
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
@ -64,7 +64,9 @@ export default {
|
||||
methods: {
|
||||
fetchStatus: async function () {
|
||||
const url = `${this.item.url}/api.php`;
|
||||
this.api = await fetch(url)
|
||||
this.api = await fetch(url, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.catch((e) => console.log(e));
|
||||
},
|
||||
@ -83,13 +85,13 @@ export default {
|
||||
&.enabled:before {
|
||||
background-color: #94e185;
|
||||
border-color: #78d965;
|
||||
box-shadow: 0 0 4px 1px #94e185;
|
||||
box-shadow: 0 0 5px 1px #94e185;
|
||||
}
|
||||
|
||||
&.disabled:before {
|
||||
background-color: #c9404d;
|
||||
border-color: #c42c3b;
|
||||
box-shadow: 0 0 4px 1px #c9404d;
|
||||
box-shadow: 0 0 5px 1px #c9404d;
|
||||
}
|
||||
|
||||
&:before {
|
||||
|
102
src/components/services/Ping.vue
Normal file
102
src/components/services/Ping.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card" :class="item.class">
|
||||
<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">
|
||||
<template v-if="item.subtitle">
|
||||
{{ item.subtitle }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="status" class="status" :class="status">
|
||||
{{ status }}
|
||||
</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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Ping",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
data: () => ({
|
||||
status: null,
|
||||
}),
|
||||
created() {
|
||||
this.fetchStatus();
|
||||
},
|
||||
methods: {
|
||||
fetchStatus: async function () {
|
||||
const url = `${this.item.url}`;
|
||||
fetch(url, {
|
||||
method: "HEAD",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
this.status = "online";
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = "offline";
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
}
|
||||
.status {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-title);
|
||||
|
||||
&.online:before {
|
||||
background-color: #94e185;
|
||||
border-color: #78d965;
|
||||
box-shadow: 0 0 5px 1px #94e185;
|
||||
}
|
||||
|
||||
&.offline: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>
|
151
src/components/services/Radarr.vue
Normal file
151
src/components/services/Radarr.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card" :class="item.class">
|
||||
<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">
|
||||
<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 Radarr API, check url and apikey in config.yml"
|
||||
>?</strong
|
||||
>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Radarr",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
activity: null,
|
||||
warnings: null,
|
||||
errors: null,
|
||||
serverError: false,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.fetchConfig();
|
||||
},
|
||||
methods: {
|
||||
fetchConfig: function () {
|
||||
fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
|
||||
.then((response) => {
|
||||
if (response.status != 200) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.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;
|
||||
});
|
||||
fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
|
||||
.then((response) => {
|
||||
if (response.status != 200) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((queue) => {
|
||||
this.activity = 0;
|
||||
for (var i = 0; i < queue.length; i++) {
|
||||
if (queue[i].movie) {
|
||||
this.activity++;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.serverError = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
}
|
||||
.notifs {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
top: 0.3em;
|
||||
right: 0.5em;
|
||||
}
|
||||
.notif {
|
||||
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>
|
151
src/components/services/Sonarr.vue
Normal file
151
src/components/services/Sonarr.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card" :class="item.class">
|
||||
<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">
|
||||
<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 Sonarr API, check url and apikey in config.yml"
|
||||
>?</strong
|
||||
>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Sonarr",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
activity: null,
|
||||
warnings: null,
|
||||
errors: null,
|
||||
serverError: false,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.fetchConfig();
|
||||
},
|
||||
methods: {
|
||||
fetchConfig: function () {
|
||||
fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
|
||||
.then((response) => {
|
||||
if (response.status != 200) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.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;
|
||||
});
|
||||
fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
|
||||
.then((response) => {
|
||||
if (response.status != 200) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((queue) => {
|
||||
this.activity = 0;
|
||||
for (var i = 0; i < queue.length; i++) {
|
||||
if (queue[i].series) {
|
||||
this.activity++;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.serverError = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
}
|
||||
.notifs {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
top: 0.3em;
|
||||
right: 0.5em;
|
||||
}
|
||||
.notif {
|
||||
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>
|
@ -12,6 +12,7 @@ module.exports = {
|
||||
publicPath: "",
|
||||
pwa: {
|
||||
manifestPath: "assets/manifest.json",
|
||||
manifestCrossorigin: "use-credentials",
|
||||
appleMobileWebAppStatusBarStyle: "black",
|
||||
appleMobileWebAppCapable: "yes",
|
||||
name: manifestOptions.name,
|
||||
|
Loading…
Reference in New Issue
Block a user