Merge branch 'main' into linkding_support

# Conflicts:
#	docs/customservices.md
pull/895/head
Cees Bos 2025-04-01 20:28:18 +02:00
commit 2024297f61
62 changed files with 1318 additions and 666 deletions

8
.jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

359
.schema/config-schema.json Normal file
View File

@ -0,0 +1,359 @@
{
"$id": "https://raw.githubusercontent.com/bastienwirtz/homer/main/.schema/config-schema.json",
"$schema": "http://json-schema.org/draft-07/schema",
"description": "https://github.com/bastienwirtz/homer/blob/main/docs/configuration.md",
"examples": [],
"title": "Homer Dashboard configuration",
"type": "object",
"definitions": {
"Colors": {
"type": "object",
"additionalProperties": false,
"properties": {
"light": {
"$ref": "#/definitions/ColorSet"
},
"dark": {
"$ref": "#/definitions/ColorSet"
}
},
"title": "Colors"
},
"ColorSet": {
"type": "object",
"additionalProperties": false,
"properties": {
"highlight-primary": {
"type": "string"
},
"highlight-secondary": {
"type": "string"
},
"highlight-hover": {
"type": "string"
},
"background": {
"type": "string"
},
"card-background": {
"type": "string"
},
"text": {
"type": "string"
},
"text-header": {
"type": "string"
},
"text-title": {
"type": "string"
},
"text-subtitle": {
"type": "string"
},
"card-shadow": {
"type": "string"
},
"link": {
"type": "string"
},
"link-hover": {
"type": "string"
},
"background-image": {
"type": "string"
}
}
},
"Defaults": {
"type": "object",
"additionalProperties": false,
"properties": {
"layout": {
"enum": [
"columns",
"list"
],
"description": "Layout of the dashboard, either 'columns' or 'list'"
},
"colorTheme": {
"enum": [
"auto",
"light",
"dark"
],
"description": "One of 'auto', 'light', or 'dark'"
}
},
"title": "Defaults"
},
"Hotkey": {
"type": "object",
"additionalProperties": false,
"properties": {
"search": {
"type": "string",
"description": "hotkey for search, e.g. Shift"
}
},
"required": [
"search"
]
},
"Link": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "Name as seen in the navbar"
},
"icon": {
"type": "string",
"description": "Fontawesome icon"
},
"url": {
"type": "string",
"description": "Url of the link. When #filename is used, it is a link to another homer page, while 'filename' is the name of the config file"
},
"target": {
"type": "string",
"description": "html tag target attribute like _blank for a new page"
}
},
"required": [
"url"
],
"title": "Link"
},
"Message": {
"type": "object",
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"mapping": {
"$ref": "#/definitions/Mapping",
"description": "Mapping for the content loaded from the URL"
},
"refreshInterval": {
"type": "integer",
"description": "The refresh interval in milliseconds for reloading the message url"
},
"style": {
"type": "string",
"description": "See https://bulma.io/documentation/components/message/#colors for styling options"
},
"title": {
"type": "string",
"description": "Title of the message box"
},
"icon": {
"type": "string",
"description": "Fontawesome icon for the message box"
},
"content": {
"type": "string",
"description": "HTML content for the message box"
}
},
"title": "Messagebox"
},
"Mapping": {
"type": "object",
"additionalProperties": true,
"title": "Mapping"
},
"Proxy": {
"type": "object",
"additionalProperties": false,
"properties": {
"useCredentials": {
"type": "boolean",
"description": "# send cookies & authorization headers when fetching service specific data. Set to `true` if you use an authentication proxy. Can be overrided on service level. "
},
"headers": {
"$ref": "#/definitions/Headers",
"description": "send custom headers when fetching service specific data. Can also be set on a service level."
}
},
"title": "Proxy"
},
"Headers": {
"type": "object",
"additionalProperties": true,
"title": "Headers"
},
"Service": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "Service name"
},
"icon": {
"type": "string",
"description": "Fontawesome icon for the service"
},
"logo": {
"type": "string",
"description": "A path to an image can also be provided. Note that icon take precedence if both icon and logo are set."
},
"class": {
"type": "string",
"description": "Optional css class to add on the service group. Example 'highlight-purple'"
},
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/Item"
}
}
},
"required": [
"items"
],
"title": "Service"
},
"Item": {
"type": "object",
"additionalProperties": true,
"properties": {
"name": {
"type": "string"
},
"logo": {
"type": "string",
"description": "Path to a logo. Alternatively a fa icon can be provided"
},
"icon": {
"type": "string",
"description": "Fontawesome icon for the item, alternative for logo"
},
"subtitle": {
"type": "string"
},
"tag": {
"type": "string",
"description": "Show tag"
},
"keywords": {
"type": "string",
"description": "Optional keyword used for searching purpose"
},
"url": {
"type": "string",
"description": "Url of this item"
},
"target": {
"type": "string",
"description": "html tag target attribute like _blank for a new page"
},
"tagstyle": {
"type": "string",
"description": "Styleclass for the tag"
},
"type": {
"type": "string",
"description": "Optional, loads a specific component that provides extra features. MUST MATCH a file name (without file extension) available in `src/components/services`"
}
},
"title": "Item"
}
},
"properties": {
"externalConfig": {
"type": "string",
"description": "Use external configuration file. Using this will ignore remaining config in this file externalConfig: https://example.com/server-luci/config.yaml"
},
"title": {
"type": "string",
"description": "Title of the dashboard"
},
"subtitle": {
"type": "string",
"description": "Subtitle of the dashboard"
},
"documentTitle": {
"type": "string",
"description": "Title of the document. When not filled, title (and subtitle will be used)"
},
"logo": {
"type": "string",
"description": "Path to logo image"
},
"icon": {
"type": "string",
"description": "Dashboard icon"
},
"header": {
"type": "boolean",
"description": "Show header, default is true"
},
"hotkey": {
"$ref": "#/definitions/Hotkey",
"description": "Define hotkeys, for example for search"
},
"footer": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
}
],
"description": "footer Line content. HTML is supported. Set false if you want to hide it."
},
"columns": {
"type": "string",
"description": "'auto' or number (must be a factor of 12: 1, 2, 3, 4, 6, 12)",
"format": "integer"
},
"connectivityCheck": {
"type": "boolean",
"description": "# whether you want to display a message when the apps are not accessible anymore (VPN disconnected for example). You should set it to true when using an authentication proxy, it also reloads the page when a redirection is detected when checking connectivity."
},
"proxy": {
"$ref": "#/definitions/Proxy",
"description": "Optional: Proxy / hosting option"
},
"defaults": {
"$ref": "#/definitions/Defaults"
},
"theme": {
"type": "string",
"description": "'default' or one of the themes available in 'src/assets/themes'"
},
"stylesheet": {
"type": "array",
"items": {
"type": "string"
},
"description": "Will load custom CSS files. Especially useful for custom icon sets. Entries are paths to the stylesheets"
},
"colors": {
"$ref": "#/definitions/Colors"
},
"message": {
"$ref": "#/definitions/Message",
"description": "Messagebox"
},
"links": {
"description": "Links in the navigation bar",
"type": "array",
"items": {
"$ref": "#/definitions/Link"
}
},
"services": {
"description": "Services",
"type": "array",
"items": {
"$ref": "#/definitions/Service"
}
}
}
}

View File

@ -17,6 +17,8 @@ RUN pnpm build
# production stage
FROM alpine:3.21
ARG VERSION_TAG=latest
LABEL \
org.label-schema.schema-version="1.0" \
org.label-schema.version="$VERSION_TAG" \

View File

@ -26,6 +26,7 @@ within Homer:
- [Jellystat](#jellystat)
- [Lidarr, Prowlarr, Sonarr, Readarr and Radarr](#lidarr-prowlarr-sonarr-readarr-and-radarr)
- [Linkding](#linkding)
- [Matrix](#matrix)
- [Mealie](#mealie)
- [Medusa](#medusa)
- [Nextcloud](#nextcloud)
@ -49,7 +50,9 @@ within Homer:
- [Tautulli](#tautulli)
- [Tdarr](#tdarr)
- [Traefik](#traefik)
- [TrueNas Scale](#truenas-scale)
- [Uptime Kuma](#uptime-kuma)
- [Vaultwarden](#vaultwarden)
- [Wallabag](#wallabag)
- [What's Up Docker](#whats-up-docker)
@ -298,6 +301,18 @@ This integration supports at max 15 results from Linkding. But you can add it mu
query: "#ToDo #Homer"
```
## Matrix
This service displays a version string instead of a subtitle. The indicator
shows if Matrix Server is online, offline
```yaml
- name: "Matrix - Server"
type: "Matrix"
logo: "assets/tools/sample.png"
url: "http://matrix.example.com"
```
## Mealie
First off make sure to remove an existing `subtitle` as it will take precedence if set.
@ -470,6 +485,9 @@ This service displays info about the total number of containers managed by your
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.
### New features:
Displays the Portainer version from /api/status
Shows online/offline status depending on API reachability
See <https://docs.portainer.io/api/access#creating-an-access-token>
@ -662,6 +680,18 @@ This service displays a version string instead of a subtitle. Example configurat
url: http://traefik.example.com
```
## Truenas Scale
This service displays a version string instead of a subtitle. Example configuration:
```yaml
- name: "Truenas"
type: "TruenasScale"
logo: "assets/tools/sample.png"
url: "http://truenas.example.com"
api_token: "your_api_token"
```
## Uptime Kuma
Using the Uptime Kuma service you can display info about your instance uptime right on your Homer dashboard.
@ -676,6 +706,17 @@ The following configuration is available for the UptimeKuma service. Needs v1.13
slug: "myCustomDashboard" # Defaults to "default" if not provided.
type: "UptimeKuma"
```
## Vaultwarden
This service displays a version string instead of a subtitle. The indicator
shows if Vaultwarden is online, offline
```yaml
- name: "Vaultwarden - Server"
type: "Vaultwarden"
logo: "assets/tools/sample.png"
url: "http://vaultwarden.example.com"
```
## Wallabag

View File

@ -78,6 +78,21 @@ Then when Homer reads your config, it will substitute your anchors automatically
The end result is that if you want to update the name or style of any particular tag, just update it once, in the tags section!
Great if you have a lot of services or a lot of tags!
## YAML auto complete with a YAML schema
A lot of editor support auto completion, see <https://www.schemastore.org/json/>
The homer schema is available here: <https://raw.githubusercontent.com/bastienwirtz/homer/main/.schema/config-schema.json>
For example with IntelliJ you can define:
```yaml
# $schema: https://raw.githubusercontent.com/bastienwirtz/homer/main/.schema/config-schema.json
```
With VSCode you can define it like this:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/bastienwirtz/homer/main/.schema/config-schema.json
```
## Remotely edit your config with Code Server
#### `by @JamiePhonic`

View File

@ -14,4 +14,5 @@ if [[ "${INIT_ASSETS}" == "1" ]] && [[ ! -f "/www/assets/config.yml" ]]; then
fi
echo "Starting webserver"
exec 3>&1
exec lighttpd -D -f /lighttpd.conf

View File

@ -18,6 +18,6 @@ export default [
},
},
{
ignores: ["*.d.ts", "**/coverage", "**/dist"],
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
},
];

View File

@ -2,7 +2,7 @@ include "/etc/lighttpd/mime-types.conf"
include_shell "/etc/lighttpd/ipv6.sh"
server.port = env.PORT
server.modules = ( "mod_alias" )
server.modules = ( "mod_alias", "mod_accesslog" )
server.username = "lighttpd"
server.groupname = "lighttpd"
server.document-root = "/www"
@ -10,3 +10,9 @@ alias.url = ( env.SUBFOLDER => "/www" )
server.indexfiles = ("index.html")
server.follow-symlink = "enable"
server.feature-flags += ( "server.clock-jump-restart" => 0 )
server.max-request-field-size = 65535
accesslog.filename = "/dev/fd/3"
# Avoid logging docker healthcheck request
$HTTP["remote-ip"] == "127.0.0.1" { accesslog.filename = "" }
$HTTP["remote-ip"] == "[::1]" { accesslog.filename = "" }

View File

@ -1,6 +1,6 @@
{
"name": "homer",
"version": "25.02.2",
"version": "25.03.3",
"type": "module",
"scripts": {
"dev": "vite",
@ -26,7 +26,7 @@
"http-server": "^14.1.1",
"prettier": "^3.5.2",
"sass-embedded": "^1.85.0",
"vite": "^6.1.1",
"vite": "^6.1.2",
"vite-plugin-pwa": "^0.21.1"
},
"license": "Apache-2.0",

File diff suppressed because it is too large Load Diff

View File

@ -171,9 +171,9 @@
.title {
font-size: 1.1em;
line-height: 1.2em;
line-height: 1.3em;
font-weight: 500;
margin-bottom: 4px;
margin-bottom: 3px;
@include ellipsis();
}

View File

@ -26,10 +26,7 @@
:href="link.url"
:target="link.target"
>
<i
v-if="link.icon"
:class="['fa-fw', link.icon, { 'mr-2': link.name }]"
></i>
<i v-if="link.icon" :class="['fa-fw', link.icon]"></i>
{{ link.name }}
</a>
</div>
@ -65,3 +62,11 @@ export default {
},
};
</script>
<style lang="scss" scoped>
@media (min-width: 1023px) {
i.fa-fw {
width: 0.8em;
}
}
</style>

View File

@ -64,7 +64,7 @@ export default {
});
},
hasFocus: function () {
return document.activeElement == this.$refs.search
return document.activeElement == this.$refs.search;
},
setSearchURL: function (value) {
const url = new URL(window.location);

View File

@ -1,10 +1,12 @@
<template>
<component :is="component" :item="item" :proxy="proxy"></component>
<Generic v-if="isGeneric" :item="item"></Generic>
<component :is="component" v-else :item="item" :proxy="proxy"></component>
</template>
<script>
import { defineAsyncComponent } from "vue";
import Generic from "./services/Generic.vue";
const defaultService = "Generic";
export default {
name: "Service",
@ -13,12 +15,13 @@ export default {
proxy: Object,
},
computed: {
isGeneric() {
return defaultService === (this.item.type || defaultService);
},
component() {
const type = this.item.type || "Generic";
if (type === "Generic") {
return Generic;
}
return defineAsyncComponent(() => import(`./services/${type}.vue`));
return defineAsyncComponent(
() => import(`./services/${this.item.type}.vue`),
);
},
},
};

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "AdGuardHome",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -15,13 +15,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "CopyToClipboard",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Docuseal",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Emby",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -25,13 +25,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "FreshRSS",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Gitea",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -17,13 +17,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Glances",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -19,13 +19,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Gotify",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -18,13 +18,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Healthchecks",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "HomeAssistant",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -27,13 +27,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Immich",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -20,13 +20,9 @@
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Jellyfin",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -27,13 +27,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Lidarr",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -0,0 +1,88 @@
<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-if="versionstring">
Version {{ versionstring }}
</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";
export default {
name: "Matrix",
mixins: [service],
props: {
item: Object,
},
data: () => ({
fetchOk: null,
versionstring: null,
}),
computed: {
status: function () {
return this.fetchOk ? "online" : "offline";
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
this.fetch("_matrix/federation/v1/version")
.then((response) => {
this.fetchOk = true;
this.versionstring = response.server.version;
})
.catch((e) => {
this.fetchOk = false;
console.log(e);
});
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
white-space: nowrap;
margin-left: 0.25rem;
&.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>

View File

@ -19,13 +19,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Mealie",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -33,13 +33,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Medusa",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -22,13 +22,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Mylar",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -16,13 +16,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Nextcloud",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -51,13 +51,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "OctoPrint",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Olivetin",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "OpenHAB",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -16,13 +16,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Paperless",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -19,13 +19,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "PeaNUT",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -27,13 +27,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "PiAlert",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "PiHole",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Ping",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -1,5 +1,16 @@
<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-if="versionstring">
Version {{ versionstring }}
</template>
</p>
</template>
<template #indicator>
<div class="notifs">
<strong v-if="running > 0" class="notif running" title="Running">
@ -16,19 +27,18 @@
{{ misc }}
</strong>
</div>
<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: "Portainer",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,
@ -36,6 +46,8 @@ export default {
data: () => ({
endpoints: null,
containers: null,
fetchOk: null,
versionstring: null,
}),
computed: {
running: function () {
@ -65,9 +77,13 @@ export default {
);
}).length;
},
status: function () {
return this.fetchOk ? "online" : "offline";
},
},
created() {
this.fetchStatus();
this.fetchVersion();
},
methods: {
fetchStatus: async function () {
@ -103,11 +119,54 @@ export default {
this.containers = containers;
},
fetchVersion: async function () {
const headers = {
"X-Api-Key": this.item.apikey,
};
this.fetch("/api/status", { headers })
.then((response) => {
this.fetchOk = true;
this.versionstring = response.Version;
})
.catch((e) => {
this.fetchOk = false;
console.error(e);
});
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
white-space: nowrap;
margin-left: 0.25rem;
&.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;
}
}
.notifs {
position: absolute;
color: white;

View File

@ -19,7 +19,6 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const AlertsStatus = Object.freeze({
firing: "firing",
@ -29,9 +28,6 @@ const AlertsStatus = Object.freeze({
export default {
name: "Prometheus",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -22,13 +22,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Prowlarr",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -76,13 +76,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Proxmox",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -27,16 +27,12 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const V3_API = "/api/v3";
const LEGACY_API = "/api";
export default {
name: "Radarr",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -27,15 +27,11 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const API = "/api/v1";
export default {
name: "Readarr",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -23,8 +23,6 @@
</template>
<script>
import Generic from "./Generic.vue";
// Units to add to download and upload rates.
const units = ["B", "kiB", "MiB", "GiB"];
@ -48,7 +46,6 @@ const displayRate = (rate) => {
export default {
name: "RTorrent",
components: { Generic },
props: { item: Object },
// Properties for download, upload, torrent count and errors.
data: () => ({ dl: null, ul: null, count: null, error: null }),

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "SABnzbd",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -24,13 +24,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Scrutiny",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -28,16 +28,12 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const V3_API = "/api/v3";
const LEGACY_API = "/api";
export default {
name: "Sonarr",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -15,13 +15,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "SpeedtestTracker",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Tautulli",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -28,13 +28,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Tdarr",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -20,16 +20,11 @@
</template>
<script>
import Generic from "./Generic.vue";
let currentTheme;
const app = document.getElementById("app");
export default {
name: "ThemeChooser",
components: {
Generic,
},
props: {
item: Object,
},

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Traefik",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -0,0 +1,95 @@
<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-if="versionstring">
<span class="is-hidden-touch">Version {{ versionstring }}</span>
<span class="is-hidden-desktop"
>Version {{ versionstring.split("-").pop() }}</span
>
</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";
export default {
name: "TruenasScale",
mixins: [service],
props: {
item: Object,
},
data: () => ({
fetchOk: null,
versionstring: null,
}),
computed: {
status: function () {
return this.fetchOk ? "online" : "offline";
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
let headers = {};
if (this.item.api_token) {
headers["Authorization"] = `Bearer ${this.item.api_token}`;
}
this.fetch("/api/v2.0/system/version", { headers })
.then((response) => {
this.fetchOk = true;
this.versionstring = response;
})
.catch((e) => {
this.fetchOk = false;
console.log(e);
});
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
white-space: nowrap;
margin-left: 0.25rem;
&.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>

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "UptimeKuma",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -0,0 +1,88 @@
<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-if="versionstring">
Version {{ versionstring }}
</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";
export default {
name: "Vaultwarden",
mixins: [service],
props: {
item: Object,
},
data: () => ({
fetchOk: null,
versionstring: null,
}),
computed: {
status: function () {
return this.fetchOk ? "online" : "offline";
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
this.fetch("api/version")
.then((response) => {
this.fetchOk = true;
this.versionstring = response;
})
.catch((e) => {
this.fetchOk = false;
console.log(e);
});
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
white-space: nowrap;
margin-left: 0.25rem;
&.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>

View File

@ -22,13 +22,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "WUD",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -21,13 +21,9 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Wallabag",
components: {
Generic,
},
mixins: [service],
props: {
item: Object,

View File

@ -28,7 +28,6 @@
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const units = ["B", "KB", "MB", "GB"];
// Take the rate in bytes and keep dividing it by 1k until the lowest
@ -50,7 +49,6 @@ const displayRate = (rate) => {
export default {
name: "QBittorrent",
components: { Generic },
mixins: [service],
props: { item: Object },
data: () => ({ dl: null, ul: null, count: null, error: null }),

View File

@ -3,8 +3,11 @@ import { createApp, h } from "vue";
import App from "./App.vue";
const app = createApp(App);
import Generic from "./components/services/Generic.vue";
app.component("DynamicStyle", (_props, context) => {
app
.component("Generic", Generic)
.component("DynamicStyle", (_props, context) => {
return h("style", {}, context.slots);
});