Compare commits

..

21 Commits
main ... mkdocs

Author SHA1 Message Date
Evan Steinkerchner
36549c5a06 Added Tautulli service to ToC 2022-04-29 14:51:39 -04:00
Evan Steinkerchner
b19f6f228b Added new Tautulli documentation 2022-04-29 14:20:00 -04:00
Evan Steinkerchner
10fa40d1f6 Tweaked docs workflow according to official docs 2022-04-28 09:36:36 -04:00
Evan Steinkerchner
819f0d4052 Added 'Features' section from README to docs 2022-04-28 09:24:16 -04:00
Evan Steinkerchner
a01da0b8be Update docs workflow to install all requirements 2022-04-27 19:21:44 -04:00
Evan Steinkerchner
207762135e Updated README to point to new documentation 2022-04-27 19:20:36 -04:00
Evan Steinkerchner
d48fc7ca26 Revert "Added url and apikey config options under proxy"
This reverts commit a2dfffab68.
2022-04-27 19:15:18 -04:00
Evan Steinkerchner
2da071d0ea Fixed broken links in configuration.md 2022-04-27 19:10:24 -04:00
Evan Steinkerchner
084009be9d Add docs Github action workflow 2022-04-27 19:08:48 -04:00
Evan Steinkerchner
d390a72b24 Updated services list from main 2022-04-27 19:03:30 -04:00
Evan Steinkerchner
1d6d20e76e Prepped the rest of the documentation for launch 2022-04-27 19:02:02 -04:00
Evan Steinkerchner
a2f2e527fb Added some content to intro page, renamed files for consistency 2022-04-22 20:47:15 -04:00
Evan Steinkerchner
5e9a19861d Filled in ssome docker content 2022-04-22 20:22:34 -04:00
Evan Steinkerchner
b82626bc6d Added open props, changed lots of styles, scrapped home page 2022-04-22 18:56:25 -04:00
Evan Steinkerchner
2b53c1a82a Cleaned up configuration page a bit 2022-03-23 09:42:03 -04:00
Evan Steinkerchner
8285823517 Created new custom home page, moved Getting started to its own page, fixed requirements.txt, added docs/README.md 2022-03-22 18:22:16 -04:00
Evan Steinkerchner
6f9a74ddcf Added requirements.txt for docs development 2022-03-20 23:06:16 -04:00
Evan Steinkerchner
03f2e6bbb1 Changed repo icon to github 2022-03-20 22:40:22 -04:00
Evan Steinkerchner
d752f64f49 Changed badges to use centered class 2022-03-20 22:40:12 -04:00
Evan Steinkerchner
717894d8c5 Initial draft of new docs UI usuing MkDocs 2022-03-20 20:19:49 -04:00
Evan Steinkerchner
a2dfffab68 Added url and apikey config options under proxy 2022-03-20 16:32:24 -04:00
111 changed files with 8737 additions and 5938 deletions

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@ -1,17 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-prettier",
],
env: {
"vue/setup-compiler-macros": true,
},
rules: {
"vue/multi-word-component-names": "off",
},
};

15
.eslintrc.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
parserOptions: {
parser: "babel-eslint",
},
rules: {
"no-console": "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/require-v-for-key": "off",
},
};

3
.github/FUNDING.yml vendored
View File

@ -1,3 +0,0 @@
# These are supported funding model platforms
custom: ['https://www.buymeacoffee.com/bastien']

View File

@ -1,28 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs & errors**
Please include any usefull information:
- Errors in your browser console (`ctrl+shift+i` or `F12`)
- If applicable, your docker container logs.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Configuration**
If applicable, copy related homer yaml configuration here.
```yml
```

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

8
.github/release.yml vendored
View File

@ -1,8 +0,0 @@
changelog:
exclude:
authors:
- dependabot
categories:
- title: Main changes
labels:
- "*"

View File

@ -1,44 +0,0 @@
# Build & publish docker images
name: Dockerhub
on:
push:
tags: [v*]
branches: [ main ]
jobs:
dockerhub:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set tag name
run: |
if [[ ${{ github.ref_type }} == "tag" ]]; then
echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
else
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
fi
-
name: Build and push
uses: docker/build-push-action@v3
with:
push: true
tags: b4bz/homer:${{env.IMAGE_TAG}}
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64

16
.github/workflows/docs.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: docs
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
with:
python-version: '3.x'
cache: 'pip'
- run: pip install -r requirements.txt
- run: mkdocs gh-deploy --force

View File

@ -20,19 +20,12 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Use Node.js ${{ matrix.node-version }}
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
-
name: install dependencies
run: yarn install
-
name: Check code style & potentential issues
run: yarn lint
- run: yarn install
- run: yarn lint

View File

@ -1,5 +1,5 @@
# Publish pre-build release
name: Create Github release
name: Upload Release Asset
on:
push:
@ -10,24 +10,31 @@ jobs:
name: Upload Release Asset
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Build project
- uses: actions/checkout@v2
- name: Build project
run: |
yarn install
yarn build
-
name: Create artifact
- name: Create artifact
working-directory: "dist"
run: zip -r ../homer.zip ./*
-
name: Create Release
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
files: |
homer.zip
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./homer.zip
asset_name: homer.zip
asset_content_type: application/zip

8
.gitignore vendored
View File

@ -23,4 +23,10 @@ yarn-error.log*
# App configuration
config.yml
.drone.yml
.drone.yml
# Python venv
venv
# MkDocs
site

View File

@ -10,29 +10,24 @@ COPY . .
RUN yarn build
# production stage
FROM alpine:3.16
FROM alpine:3.15
ENV GID 1000
ENV UID 1000
ENV USER darkhttpd
ENV GROUP darkhttpd
ENV GID 911
ENV UID 911
ENV PORT 8080
ENV SUBFOLDER "/_"
ENV INIT_ASSETS 1
RUN addgroup -S lighttpd -g ${GID} && adduser -D -S -u ${UID} lighttpd lighttpd && \
apk add -U --no-cache lighttpd
RUN addgroup -S ${GROUP} -g ${GID} && adduser -D -S -u ${UID} ${USER} ${GROUP} && \
apk add -U --no-cache su-exec darkhttpd
WORKDIR /www
COPY lighttpd.conf /lighttpd.conf
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist /www/
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist/assets /www/default-assets
COPY entrypoint.sh /entrypoint.sh
COPY --from=build-stage --chown=${UID}:${GID} /app/dist /www/
COPY --from=build-stage --chown=${UID}:${GID} /app/dist/assets /www/default-assets
USER ${UID}:${GID}
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:${PORT}/ || exit 1
EXPOSE ${PORT}
VOLUME /www/assets
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]

43
Dockerfile.arm32v7 Normal file
View File

@ -0,0 +1,43 @@
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
# Multi arch build support
FROM alpine as qemu
ARG QEMU_VERSION="v4.2.0-7"
RUN wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-arm-static && chmod +x qemu-arm-static
# production stage
FROM arm32v7/alpine:3.11
COPY --from=qemu qemu-arm-static /usr/bin/
ENV USER darkhttpd
ENV GROUP darkhttpd
ENV GID 911
ENV UID 911
ENV PORT 8080
RUN addgroup -S ${GROUP} -g ${GID} && adduser -D -S -u ${UID} ${USER} ${GROUP} && \
apk add -U --no-cache darkhttpd su-exec && \
rm /usr/bin/qemu-arm-static
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist /www/
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist/assets /www/default-assets
COPY entrypoint.sh /entrypoint.sh
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:${PORT}/ || exit 1
EXPOSE ${PORT}
VOLUME /www/assets
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]

43
Dockerfile.arm64v8 Normal file
View File

@ -0,0 +1,43 @@
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
# Multi arch build support
FROM alpine as qemu
ARG QEMU_VERSION="v4.2.0-7"
RUN wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-aarch64-static && chmod +x qemu-aarch64-static
# production stage
FROM arm64v8/alpine:3.11
COPY --from=qemu qemu-aarch64-static /usr/bin/
ENV USER darkhttpd
ENV GROUP darkhttpd
ENV GID 911
ENV UID 911
ENV PORT 8080
RUN addgroup -S ${GROUP} -g ${GID} && adduser -D -S -u ${UID} ${USER} ${GROUP} && \
apk add -U --no-cache darkhttpd su-exec && \
rm /usr/bin/qemu-aarch64-static
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist /www/
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist/assets /www/default-assets
COPY entrypoint.sh /entrypoint.sh
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:${PORT}/ || exit 1
EXPOSE ${PORT}
VOLUME /www/assets
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]

136
README.md
View File

@ -2,7 +2,7 @@
<img
width="180"
alt="Homer's donut"
src="https://raw.githubusercontent.com//bastienwirtz/homer/main/public/logo.png">
src="public/logo.png">
<br/>
Homer
</h1>
@ -10,9 +10,7 @@
<h4 align="center">
A dead simple static <strong>HOM</strong>epage for your serv<strong>ER</strong> to keep your services on hand, from a simple <code>yaml</code> configuration file.
</h4>
<p align="center">
<a href="https://www.buymeacoffee.com/bastien" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-yellow.png" alt="Buy Me A Coffee" height="41" width="174"></a>
<p>
<p align="center">
<strong>
<a href="https://homer-demo.netlify.app">Demo</a>
@ -32,27 +30,24 @@
<a href="https://github.com/bastienwirtz/homer/releases/latest/download/homer.zip"><img
alt="Download homer static build"
src="https://img.shields.io/badge/Download-homer.zip-orange"></a>
<a href="https://twitter.com/acdlite/status/974390255393505280"><img
alt="speed-blazing"
src="https://img.shields.io/badge/speed-blazing%20%F0%9F%94%A5-red"></a>
<a href="https://github.com/awesome-selfhosted/awesome-selfhosted"><img
alt="Awesome"
src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"></a>
</p>
<p align="center">
<img src="https://raw.github.com/bastienwirtz/homer/main/docs/screenshot.png" width="100%">
<img src="docs/images/screenshot.png" width="100%">
</p>
## Table of Contents
- [Features](#features)
- [Getting started](#getting-started)
- [Configuration](docs/configuration.md)
- [Custom services](docs/customservices.md)
- [Tips & tricks](docs/tips-and-tricks.md)
- [Development](docs/development.md)
- [Troubleshooting](docs/troubleshooting.md)
- [Configuration](https://bastienwirtz.github.io/homer/configuration)
- [Custom services](https://bastienwirtz.github.io/homer/custom_services)
- [Tips & tricks](https://bastienwirtz.github.io/homer/tips_and_tricks)
- [Development](https://bastienwirtz.github.io/homer/development)
- [Troubleshooting](https://bastienwirtz.github.io/homer/troubleshooting)
## Features
@ -70,48 +65,92 @@
## Getting started
Homer is a full static html/js dashboard, based on a simple yaml configuration file. See [documentation](docs/configuration.md) for information about the configuration (`assets/config.yml`) options.
### Using Docker
It's meant to be served by an HTTP server, **it will not work if you open the index.html directly over file:// protocol**.
The fastest and recommended way to get your Homer instance up and running is
with Docker. The Docker image comes with a web server built-in so that all you
need to worry about is your config file.
### Using docker
Internally, the Docker image looks for the assets in the `/www/assets` directory
so you can bind a volume from your host machine to that directory in order to
modify and persist the configuration files. The web server serves the dashboard
on port 8080, but using a port binding will let you expose that to whatever
external port you like.
#### docker
To launch container:
```sh
docker run -d \
-p 8080:8080 \
-v </your/local/assets/>:/www/assets \
-v </your/local/assets>:/www/assets \
--restart=always \
b4bz/homer:latest
```
The container will run using a user uid and gid 1000. Add `--user <your-UID>:<your-GID>` to the docker command to adjust it. Make sure this match the ownership of your assets directory.
**Environment variables:**
* **`INIT_ASSETS`** (default: `1`)
Install example configuration file & assets (favicons, ...) to help you get started.
* **`SUBFOLDER`** (default: `null`)
If you would like to host Homer in a subfolder, (ex: *http://my-domain/**homer***), set this to the subfolder path (ex `/homer`).
* **`PORT`** (default: `8080`)
If you would like to change internal port of Homer from default `8080` to your port choice.
#### With docker-compose
A [`docker-compose.yml`](docker-compose.yml) file is available as an example. It must be edited to match your needs. You probably want to adjust the port mapping and volume binding (equivalent to `-p` and `-v` arguments).
Then launch the container:
Use `UID` and/or `GID` env var to change the assets owner:
```sh
cd /path/to/docker-compose.yml/
docker run -d \
-p 8080:8080 \
-v </your/local/assets>:/www/assets \
-e "UID=1000" -e "GID=1000" \
--restart=always \
b4bz/homer:latest
```
#### docker-compose
It is recommended to use docker-compose to manage your Docker containers, and
below you can find a simple compose yaml file. Copy the contents into a
`docker-compose.yaml` and modify the volume binding to your desired directory to
get started:
```yaml
version: '3.3'
services:
homer:
restart: always
volumes:
- /your/local/assets:/www/assets
ports:
- 8080:8080
image: b4bz/homer
```
To launch container:
```sh
cd /path/to/docker-compose.yml
docker-compose up -d
```
### Using the release tarball (prebuilt, ready to use)
Use `UID` and/or `GID` env var to change the assets owner:
Download and extract the latest release (`homer.zip`) from the [release page](https://github.com/bastienwirtz/homer/releases), rename the `assets/config.yml.dist` file to `assets/config.yml`, and put it behind a web server.
```yaml
version: '3.3'
services:
homer:
restart: always
volumes:
- /your/local/assets:/www/assets
ports:
- 8080:8080
environment:
- UID=1000
- GID=1000
image: b4bz/homer
```
### Shipping your own web server
#### Prebuilt release tarball
Download and extract the latest release (`homer.zip`) from the [release page]
(https://github.com/bastienwirtz/homer/releases), rename the
`assets/config.yml.dist` file to `assets/config.yml`, and put it behind a web
server.
```sh
wget https://github.com/bastienwirtz/homer/releases/latest/download/homer.zip
@ -121,24 +160,7 @@ cp assets/config.yml.dist assets/config.yml
npx serve # or python -m http.server 8010 or apache, nginx ...
```
### Using Helm
Thanks to [@djjudas21](https://github.com/djjudas21) [charts](https://github.com/djjudas21/charts/tree/main/charts/homer):
```sh
helm repo add djjudas21 https://djjudas21.github.io/charts/
helm repo update djjudas21
# install with all defaults
helm install homer djjudas21/homer
# install with customisations
wget https://raw.githubusercontent.com/djjudas21/charts/main/charts/homer/values.yaml
# edit values.yaml
helm install homer djjudas21/homer -f values.yaml
```
### Build manually
#### Building from source
```sh
# Using yarn (recommended)

3
babel.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

View File

@ -10,6 +10,7 @@ services:
- /your/local/assets/:/www/assets
ports:
- 8080:8080
user: 1000:1000 # default
environment:
- INIT_ASSETS=1 # default
#environment:
# - UID=1000
# - GID=1000
restart: unless-stopped

25
docs/README.md Normal file
View File

@ -0,0 +1,25 @@
# Homer docs
Live on github pages: [https://bastienwirtz.github.io/homer/](https://bastienwirtz.github.io/homer/)
## Local development
### Install Python dependencies
Homer's documentation is built using [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). To get started, you'll need Python 3 installed on your machine and set up your local environment.
```sh
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### Preview local copy
MkDocs comes with a command-line utility for building and serving the static documentation site every time you save a file. To launch it, run the `serve` command.
```sh
mkdocs serve
```
Your local version of the docs site will now be available at http://localhost:8000/.

View File

@ -1,6 +1,8 @@
# Configuration
Title, icons, links, colors, and services can be configured in the `config.yml` file (located in `/assets` directory once built, or in the `public/assets` directory in development mode), using [yaml](http://yaml.org/) format.
Title, icons, links, colors, and services can be configured in the `config.yml`
file (located in `/assets` directory once built, or in the `public/assets`
directory in development mode), using [yaml](http://yaml.org/) format.
```yaml
---
@ -13,41 +15,57 @@ Title, icons, links, colors, and services can be configured in the `config.yml`
title: "App dashboard"
subtitle: "Homer"
# documentTitle: "Welcome" # Customize the browser tab text
# Customize the browser tab text
# documentTitle: "Welcome"
logo: "assets/logo.png"
# Alternatively a fa icon can be provided:
# Alternatively a Font Awesome icon can be provided
# icon: "fas fa-skull-crossbones"
header: true # Set to false to hide the header
# Optional: Different hotkey for search, defaults to "/"
# Set to false to hide the header
header: true
# Set to false to hide the footer
footer: >
<p>Created with <span class="has-text-danger">❤️</span> with
<a href="https://bulma.io/">bulma</a>,
<a href="https://vuejs.org/">vuejs</a>, &
<a href="https://fontawesome.com/">font awesome</a>
// Fork me on <a href="https://github.com/bastienwirtz/homer">
<i class="fab fa-github-alt"></i></a></p>
# Use "auto" or number (must be a factor of 12: 1, 2, 3, 4, 6, 12)
columns: "3"
# Whether you want to display a message when the apps are not accessible
# anymore (VPN disconnected for example)
connectivityCheck: true
# Optional: Set a different hotkey for search, defaults to "/"
# hotkey:
# search: "Shift"
footer: '<p>Created with <span class="has-text-danger">❤️</span> with <a href="https://bulma.io/">bulma</a>, <a href="https://vuejs.org/">vuejs</a> & <a href="https://fontawesome.com/">font awesome</a> // Fork me on <a href="https://github.com/bastienwirtz/homer"><i class="fab fa-github-alt"></i></a></p>' # set false if you want to hide it.
columns: "3" # "auto" or number (must be a factor of 12: 1, 2, 3, 4, 6, 12)
connectivityCheck: true # 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.
# Optional: Proxy / hosting option
proxy:
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.
# 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
# Set the default layout and color scheme
defaults:
layout: columns # Either 'columns', or 'list'
colorTheme: auto # One of 'auto', 'light', or 'dark'
# Optional theming
theme: default # 'default' or one of the themes available in 'src/assets/themes'.
# Optional theming: 'default' or one of the themes available in
# 'src/assets/themes'.
theme: default
# Optional custom stylesheet
# Will load custom CSS files. Especially useful for custom icon sets.
# stylesheet:
# - "assets/custom.css"
# Here is the exhaustive list of customization parameters
# However all value are optional and will fallback to default if not set.
# if you want to change only some of the colors, feel free to remove all unused key.
# Here is the exhaustive list of customization parameters, however all values
# are optional and will fallback to default if not set. If you want to change
# only some of the colors, feel free to remove all unused keys.
colors:
light:
highlight-primary: "#3367d6"
@ -80,25 +98,30 @@ 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
# Uses Bulma. See https://bulma.io/documentation/components/message/#colors
# for styling options.
style: "is-warning"
title: "Optional message!"
icon: "fa fa-exclamation-triangle"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
# Can optionally fetch information from an endpoint to override value below.
# url: "https://<my-api-endpoint>"
# mapping: # Select the appropriate fields from the response object.
# title: 'id' # Use value from field 'id' as title
# content: 'value' # Use value from field 'value' as content
# refreshInterval: 10000 # Optional: time interval to refresh message
#
# Real example using chucknorris.io for showing Chuck Norris facts:
# url: https://api.chucknorris.io/jokes/random
# mapping:
# title: 'id'
# content: 'value'
# refreshInterval: 10000
# Optional navbar
# links: [] # Allows for navbar (dark mode, layout, and search) without any links
# Specify [] for navbar (dark mode, layout, and search) without any links
# links: []
links:
- name: "Link 1"
icon: "fab fa-github"
@ -107,19 +130,22 @@ 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:
# Urls starting with # will link to additional Homer pages. Passing "#page2"
# will load config from page2.yml as overrides on top of the default values
# set in this config.yml.
- name: "Second Page"
icon: "fas fa-file-alt"
url: "#page2"
# Services
# First level array represents a group.
# Leave only a "items" key if not using group (group name, icon & tagstyle are optional, section separation will not be displayed).
# First level array represents a group. Use only an "items" key if not using
# groups (name, icon, & tagstyle are optional; section separation will not be
# displayed).
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.
# A path to an image can also be provided. Note that icon will take
# precedence if both icon and logo are set.
# logo: "path/to/logo"
items:
- name: "Awesome app"
@ -128,9 +154,9 @@ services:
# icon: "fab fa-jenkins"
subtitle: "Bookmark example"
tag: "app"
keywords: "self hosted reddit" # optional keyword used for searching purpose
url: "https://www.reddit.com/r/selfhosted/"
target: "_blank" # optional html tag target attribute
# Optional: HTML <a> tag target attribute
target: "_blank"
- name: "Another one"
logo: "assets/tools/sample2.png"
subtitle: "Another application"
@ -143,18 +169,28 @@ services:
items:
- name: "Pi-hole"
logo: "assets/tools/sample.png"
# subtitle: "Network-wide Ad Blocking" # optional, if no subtitle is defined, PiHole statistics will be shown
# Optional: If no subtitle is defined, PiHole statistics will be shown.
# subtitle: "Network-wide Ad Blocking"
tag: "other"
url: "http://192.168.0.151/admin"
type: "PiHole" # optional, loads a specific component that provides extra features. MUST MATCH a file name (without file extension) available in `src/components/services`
target: "_blank" # optional html a tag target attribute
# class: "green" # optional custom CSS class for card, useful with custom stylesheet
# background: red # optional color for card to set color directly without custom stylesheet
# Optional: Loads a specific component that provides extra features.
# MUST MATCH a file name (without file extension) available in
# `src/components/services`
type: "PiHole"
# Optional: HTML <a> tag target attribute
target: "_blank"
# Optional: Custom CSS class for card, useful with custom stylesheet
# class: "green"
# Optional: Set background color directly without custom stylesheet
# background: red
```
View **[Custom Services](customservices.md)** for details about all available custom services (like `PiHole`) and how to configure them.
View [Custom Services](custom_services.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 as follows (or you can [custom map fields as shown in tips-and-tricks](./tips-and-tricks.md#mapping-fields)):
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#mapping-fields)):
```json
{
@ -164,33 +200,31 @@ If you choose to fetch message information from an endpoint, the output format s
}
```
`null` value or missing keys will be ignored and value from the `config.yml` will be used if available.
Empty values (either in `config.yml` or the endpoint data) will hide the element (ex: set `"title": ""` to hide the title bar).
`null` value or missing keys will be ignored and value from the `config.yml`
will be used if available. Empty values (either in `config.yml` or the endpoint
data) will hide the element (ex: set `"title": ""` to hide the title bar).
## Style Options
Homer uses [bulma CSS](https://bulma.io/), which provides a [modifiers syntax](https://bulma.io/documentation/modifiers/syntax/). You'll notice in the config there is a `tagstyle` option. It can be set to any of the bulma modifiers. You'll probably want to use one of these 4 main colors:
Homer uses [Bulma CSS](https://bulma.io/), which provides a
[modifiers syntax](https://bulma.io/documentation/modifiers/syntax/). You'll
notice in the config there is a `tagstyle` option. It can be set to any of the
bulma modifiers. You'll probably want to use one of these 4 main colors:
- `is-info` (blue)
- `is-success` (green)
- `is-warning` (yellow)
- `is-danger` (red)
You can read the [bulma modifiers page](https://bulma.io/documentation/modifiers/syntax/) for other options regarding size, style, or state.
## Theming & customization
See `colors` settings in the configuration example above.
Favicon et application icon (pwa) are located in the `assets/icons` directory and can be replaced by any image you want (just keep the same name & size).
The `/assets/manifest.json` can also be edited to change the app (pwa) name, description and other settings.
### Community theme
- [Dracula theme](https://draculatheme.com/homer) by [@Tuetenk0pp](https://github.com/Tuetenk0pp)
- [Homer Theme v2](https://github.com/walkxcode/homer-theme) by [walkxcode](https://github.com/walkxcode)
- [Catppuccin theme](https://github.com/mrpbennett/catppucin-homer) by [@mrpbenett](https://github.com/mrpbennett)
You can read the [bulma modifiers page](https://bulma.io/documentation/modifiers/syntax/)
for other options regarding size, style, or state.
## PWA Icons
See icons documentation [here](https://github.com/bastienwirtz/homer/blob/main/public/assets/icons/README.md).
In order to easily generate all required icon preset for the PWA to work, a tool
like [vue-pwa-asset-generator](https://www.npmjs.com/package/vue-pwa-asset-generator)
can be used:
```bash
npx vue-pwa-asset-generator -a {your_512x512_source_png} -o {your_output_folder}
```

225
docs/custom_services.md Normal file
View File

@ -0,0 +1,225 @@
# Custom Services
Some service can use a specific a component that provides some extra features by adding a `type` key to the service yaml
configuration and, where applicable, an apikey. Note that config.yml is exposed at /assets/config.yml via HTTP and any
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)
+ [Uptime Kuma](#uptime-kuma)
+ [Tautulli](#tautulli)
If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page.
## Common options
```yaml
- name: "My Service"
logo: "assets/tools/sample.png"
url: "http://my-service-link"
endpoint: "http://my-service-endpoint" # Optional: alternative base URL used to fetch service data is necessary.
useCredentials: false # Optional: Override global proxy.useCredentials configuration.
type: "<type>"
```
## PiHole
Using the PiHole service you can display info about your local PiHole instance right on your Homer dashboard.
The following configuration is available for the PiHole service.
```yaml
- name: "Pi-hole"
logo: "assets/tools/sample.png"
# subtitle: "Network-wide Ad Blocking" # optional, if no subtitle is defined, PiHole statistics will be shown
url: "http://192.168.0.151/admin"
type: "PiHole"
```
## OpenWeatherMap
Using the OpenWeatherMap service you can display weather information about a given location.
The following configuration is available for the OpenWeatherMap service:
```yaml
- name: "Weather"
location: "Amsterdam" # your location.
locationId: "2759794" # Optional: Specify OpenWeatherMap city ID for better accuracy
apikey: "<---insert-api-key-here--->" # insert your own API key here. Request one from https://openweathermap.org/api.
units: "metric" # units to display temperature. Can be one of: metric, imperial, kelvin. Defaults to kelvin.
background: "square" # choose which type of background you want behind the image. Can be one of: square, cicle, none. Defaults to none.
type: "OpenWeather"
```
**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).
## 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 :
```yaml
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.
## Lidarr, Prowlarr, Sonarr and Radarr
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 :
```yaml
type: "Lidarr", "Prowlarr", "Radarr" or "Sonarr"
apikey: "01234deb70424befb1f4ef6a23456789"
```
The url must be the root url of Lidarr, Prowlarr, Radarr or Sonarr application.
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
This service displays total number of documents stored. Two lines are required:
```yaml
type: "PaperlessNG"
apikey: "0123456789abcdef123456789abcdef"
```
API key can be generated in Settings > Administration > Auth Tokens
## Ping
For Ping you need to set the type to Ping and provide a url.
```yaml
- name: "Awesome app"
type: Ping
logo: "assets/tools/sample.png"
subtitle: "Bookmark example"
tag: "app"
url: "https://www.reddit.com/r/selfhosted/"
```
## Prometheus
For Prometheus you need to set the type to Prometheus and provide a url.
```yaml
- name: "Prometheus"
type: Prometheus
logo: "assets/tools/sample.png"
url: "http://192.168.0.151/"
# subtitle: "Monitor data server"
```
## AdGuard Home
For AdGuard Home you need to set the type to AdGuard, if you have somes issues as 403 responses on requests you need to provide authentification in headers for locations needed as below.
```yaml
- name: "Adguard"
logo: "assets/tools/adguardhome.png"
url: "https://adguard.exemple.com"
target: "_blank"
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.
```
## Uptime Kuma
Using the Uptime Kuma service you can display info about your instance uptime right on your Homer dashboard.
The following configuration is available for the UptimeKuma service. Needs v1.13.1 or later because of the change in APIs due to [multiple status pages support](https://github.com/louislam/uptime-kuma/releases/tag/1.13.1).
```yaml
- name: "Uptime Kuma"
logo: "assets/tools/sample.png"
# subtitle: "A fancy self-hosted monitoring tool" # optional, if no subtitle is defined, Uptime Kuma incidents, if any, will be shown
url: "http://192.168.0.151:3001"
slug: "myCustomDashboard" # Defaults to "default" if not provided.
type: "UptimeKuma"
```
## Tautulli
The Tautulli service can allow you to show the number of currently active
streams on you Plex instance. An API key is required, and can be obtained from
the "Web Interface" section of settings on the Tautulli web UI.
```yaml
- name: "Tautulli"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151:8181"
type: "Tautulli"
apikey: "MY-SUPER-SECRET-API-KEY"
```
Because the service type and link don't necessarily have to match, you could
even make the service type Tautulli on your Plex card and provide a separate
endpoint pointing to Tautulli!
```yaml
- name: "Plex"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151:32400/web" # Plex
endpoint: "http://192.168.0.151:8181" # Tautulli
type: "Tautulli"
apikey: "MY-SUPER-SECRET-API-KEY"
```

View File

@ -1,411 +0,0 @@
# Custom Services
Some service can use a specific a component that provides some extra features by adding a `type` key to the service yaml
configuration and, where applicable, an apikey. Note that config.yml is exposed at /assets/config.yml via HTTP and any
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:
- [Custom Services](#custom-services)
- [Common options](#common-options)
- [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 / Jellyfin](#emby--jellyfin)
- [Uptime Kuma](#uptime-kuma)
- [Tautulli](#tautulli)
- [Mealie](#mealie)
- [Healthchecks](#healthchecks)
- [Proxmox](#proxmox)
- [rTorrent](#rtorrent)
- [qBittorrent](#qbittorrent)
- [CopyToClipboard](#copy-to-clipboard)
- [Speedtest Tracker](#SpeedtestTracker)
- [What's Up Docker](#whats-up-docker)
- [SABnzbd](#sabnzbd)
- [OctoPrint](#sabnzbd)
- [Tdarr](#tdarr)
If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page.
## Common options
```yaml
- name: "My Service"
logo: "assets/tools/sample.png"
url: "http://my-service-link"
endpoint: "http://my-service-endpoint" # Optional: alternative base URL used to fetch service data is necessary.
useCredentials: false # Optional: Override global proxy.useCredentials configuration.
type: "<type>"
```
## PiHole
Using the PiHole service you can display info about your local PiHole instance right on your Homer dashboard.
The following configuration is available for the PiHole service.
```yaml
- name: "Pi-hole"
logo: "assets/tools/sample.png"
# subtitle: "Network-wide Ad Blocking" # optional, if no subtitle is defined, PiHole statistics will be shown
url: "http://192.168.0.151/admin"
apikey: "<---insert-api-key-here--->" # optional, needed if web interface is password protected
type: "PiHole"
```
**Remarks:**
If PiHole web interface is password protected, obtain the `apikey` from Settings > API/Web interface > Show API token.
## OpenWeatherMap
Using the OpenWeatherMap service you can display weather information about a given location.
The following configuration is available for the OpenWeatherMap service:
```yaml
- name: "Weather"
location: "Amsterdam" # your location.
locationId: "2759794" # Optional: Specify OpenWeatherMap city ID for better accuracy
apikey: "<---insert-api-key-here--->" # insert your own API key here. Request one from https://openweathermap.org/api.
units: "metric" # units to display temperature. Can be one of: metric, imperial, kelvin. Defaults to kelvin.
background: "square" # choose which type of background you want behind the image. Can be one of: square, circle, none. Defaults to none.
type: "OpenWeather"
```
**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).
## 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 :
```yaml
type: "Medusa"
apikey: "<---insert-api-key-here--->"
```
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.
## Lidarr, Prowlarr, Sonarr and Radarr
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 :
```yaml
type: "Lidarr", "Prowlarr", "Radarr" or "Sonarr"
apikey: "<---insert-api-key-here--->"
```
The url must be the root url of Lidarr, Prowlarr, Radarr or Sonarr application.
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: "<---insert-api-key-here--->"
target: "_blank"
legacyApi: true
```
## PaperlessNG
This service displays total number of documents stored. Two lines are required:
```yaml
type: "PaperlessNG"
apikey: "<---insert-api-key-here--->"
```
API key can be generated in Settings > Administration > Auth Tokens
## Ping
For Ping you need to set the type to Ping and provide a url. By default the HEAD method is used but it can be configured to use GET using the optional `method` property.
```yaml
- name: "Awesome app"
type: Ping
logo: "assets/tools/sample.png"
subtitle: "Bookmark example"
tag: "app"
url: "https://www.reddit.com/r/selfhosted/"
method: "head"
```
## Prometheus
For Prometheus you need to set the type to Prometheus and provide a url.
```yaml
- name: "Prometheus"
type: Prometheus
logo: "assets/tools/sample.png"
url: "http://192.168.0.151/"
# subtitle: "Monitor data server"
```
## AdGuard Home
For AdGuard Home you need to set the type to AdGuard, if you have somes issues as 403 responses on requests you need to provide authentification in headers for locations needed as below.
```yaml
- name: "Adguard"
logo: "assets/tools/adguardhome.png"
url: "https://adguard.exemple.com"
target: "_blank"
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: "<---insert-api-key-here--->"
# environments:
# - "raspberry"
# - "local"
```
## Emby / Jellyfin
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: "<---insert-api-key-here--->"
libraryType: "music" #Choose which stats to show. Can be one of: music, series or movies.
```
## Uptime Kuma
Using the Uptime Kuma service you can display info about your instance uptime right on your Homer dashboard.
The following configuration is available for the UptimeKuma service. Needs v1.13.1 or later because of the change in APIs due to [multiple status pages support](https://github.com/louislam/uptime-kuma/releases/tag/1.13.1).
```yaml
- name: "Uptime Kuma"
logo: "assets/tools/sample.png"
# subtitle: "A fancy self-hosted monitoring tool" # optional, if no subtitle is defined, Uptime Kuma incidents, if any, will be shown
url: "http://192.168.0.151:3001"
slug: "myCustomDashboard" # Defaults to "default" if not provided.
type: "UptimeKuma"
```
## Tautulli
The Tautulli service can allow you to show the number of currently active
streams on you Plex instance. An API key is required, and can be obtained from
the "Web Interface" section of settings on the Tautulli web UI.
```yaml
- name: "Tautulli"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151:8181"
type: "Tautulli"
apikey: "<---insert-api-key-here--->"
```
Because the service type and link don't necessarily have to match, you could
even make the service type Tautulli on your Plex card and provide a separate
endpoint pointing to Tautulli!
```yaml
- name: "Plex"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151:32400/web" # Plex
endpoint: "http://192.168.0.151:8181" # Tautulli
type: "Tautulli"
apikey: "<---insert-api-key-here--->"
```
## Mealie
First off make sure to remove an existing `subtitle` as it will take precedence if set.
Setting `type: "Mealie"` will then show the number of recipes Mealie is keeping organized or the planned meal for today if one is planned. You will have to set an API key in the field `apikey` which can be created in your Mealie installation.
## Healthchecks
This service displays information about the configured status checks from the Healthchecks application.
Two lines are needed in the config.yml :
```yaml
type: "Healthchecks"
apikey: "<---insert-api-key-here--->"
```
The url must be the root url of the Healthchecks application.
The Healthchecks API key can be found in Settings > API Access > API key (read-only). The key is needed to access Healthchecks API.
## rTorrent
This service displays the global upload and download rates, as well as the number of torrents
listed in rTorrent. The service communicates with the rTorrent XML-RPC interface which needs
to be accessible from the browser. Please consult
[the instructions](https://github.com/rakshasa/rtorrent-doc/blob/master/RPC-Setup-XMLRPC.md)
for setting up rTorrent and make sure the correct CORS-settings are applied. Examples for various
servers can be found at https://enable-cors.org/server.html.
```yaml
- name: "rTorrent"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151" # Your rTorrent web UI, f.e. ruTorrent or Flood.
xmlrpc: "http://192.168.0.151:8081" # Reverse proxy for rTorrent's XML-RPC.
type: "Rtorrent"
rateInterval: 5000 # Interval for updating the download and upload rates.
torrentInterval: 60000 # Interval for updating the torrent count.
username: "username" # Username for logging into rTorrent (if applicable).
password: "password" # Password for logging into rTorrent (if applicable).
```
## Proxmox
This service displays status information of a Proxmox node (VMs running and disk, memory and cpu used). It uses the proxmox API and [API Tokens](https://pve.proxmox.com/pve-docs/pveum-plain.html) for authorization so you need to generate one to set in the yaml config. You can set it up in Proxmox under Permissions > API Tokens. You also need to know the realm the user of the API Token is assigned to (by default pam).
The API Token (or the user asigned to that token if not separated permissions is checked) are this:
| Path | Permission | Comments |
|--------------------|------------|-------------------------------------------------------------------|
| /nodes/<your-node> | Sys.Audit | |
| /vms/<id-vm> | VM.Audit | You need to have this permission on any VM you want to be counted |
It is highly recommended that you create and API Token with only these permissions on a read-only mode.
If you get errors, they will be shown on browser's dev console. Main issues tend to be CORS related as Proxmox does not include CORS headers and you have to deploy it behind a reverse proxy and make the proxy add this headers.
Configuration example:
```yaml
- name: "Proxmox - Node"
logo: "https://www.google.com/url?sa=i&url=https%3A%2F%2Fgithub.com%2FandOTP%2FandOTP%2Fissues%2F337&psig=AOvVaw2YKVuEUIBeTUikr7kAjm8D&ust=1665323538747000&source=images&cd=vfe&ved=0CAkQjRxqFwoTCPCTruLj0PoCFQAAAAAdAAAAABAN"
type: "Proxmox"
url: "https://your.proxmox.server"
node: "your-node-name"
warning_value: 50
danger_value: 80
api_token: "PVEAPIToken=root@pam!your-api-token-name=your-api-token-key"
# values below this line are optional (default value are false/empty):
hide_decimals: true # removes decimals from stats values.
hide: [] # hides information. Possible values are "vms", "vms_total", "lxcs", "lxcs_total", "disk", "mem" and "cpu".
small_font_on_small_screens: true # uses small font on small screens (like mobile)
small_font_on_desktop: true # uses small font on desktops (just in case you're showing much info)
```
## qBittorrent
This service displays the global upload and download rates, as well as the number of torrents
listed. The service communicates with the qBittorrent API interface which needs
to be accessible from the browser. Please consult
[the instructions](https://github.com/qbittorrent/qBittorrent/pull/12579)
for setting up qBittorrent and make sure the correct CORS-settings are applied. Examples for various
servers can be found at [enable-cors.org](https://enable-cors.org/server.html).
```yaml
- name: "qBittorrent"
logo: "assets/tools/sample.png"
url: "http://192.168.1.2:8080" # Your rTorrent web UI, f.e. ruTorrent or Flood.
type: "qBittorrent"
rateInterval: 2000 # Interval for updating the download and upload rates.
torrentInterval: 5000 # Interval for updating the torrent count.
target: "_blank" # optional html a tag target attribute
```
## Copy to Clipboard
This service displays the same information of a generic one, but shows an icon button on the indicator place (right side) you can click to get the content of the `clipboard` field copied to your clipboard.
You can still provide an `url` that would be open when clicked anywhere but on the icon button.
Configuration example:
```yaml
- name: "Copy me!"
logo: "assets/tools/sample.png"
subtitle: "Subtitle text goes here"
url: "#"
type: "CopyToClipboard"
clipboard: "this text will be copied to your clipboard"
```
## SpeedtestTracker
For the SpeedtestTracker service you just need to define a entry with type `SpeedtestTracker`.
## What's up Docker
What's up Docker allow to display info about the number of container running and the number for which an update is available on your Homer dashboard.
The following configuration is available for the WUD service.
```yaml
- name: "What's Up Docker"
logo: "assets/tools/sample.png"
subtitle: "Docker image update notifier"
url: "http://192.168.1.12:3001"
type: "WUD"
```
## SABnzbd
The SABnzbd service can allow you to show the number of currently active
downloads on your SABnzbd instance. An API key is required, and can be obtained from
the "Config" > "General" section of the SABnzbd config in the SABnzbd web UI.
```yaml
- name: "SABnzbd"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151:8080"
type: "SABnzbd"
apikey: "MY-SUPER-SECRET-API-KEY"
downloadInterval: 5000 # (Optional) Interval (in ms) for updating the download count
```
## OctoPrint
The OctoPrint service only needs an `apikey` & `url` and optionally a `display` option.
```yaml
- name: "Octoprint"
logo: "https://cdn-icons-png.flaticon.com/512/3112/3112529.png"
apikey: "xxxxxxxxxxxx" # insert your own API key here. Request one from https://openweathermap.org/api.
url: "http://192.168.0.151:8080"
display: "text" # 'text' or 'bar'. Default to `text`.
type: "OctoPrint"
```
## Tdarr
The Tdarr service can allow you to show the number of currently queued items
for transcoding on your Tdarr instance as well as the number of errored items.
```yaml
- name: "Tdarr"
logo: "assets/tools/sample.png"
url: "http://192.168.0.151:8265"
type: "Tdarr"
checkInterval: 5000 # (Optional) Interval (in ms) for updating the queue & error counts
```

View File

@ -5,11 +5,11 @@ If you want to contribute to Homer, please read the [contributing guidelines](ht
```sh
# Using yarn (recommended)
yarn install
yarn dev
yarn serve
# **OR** Using npm
npm install
npm run dev
npm run serve
```
## Custom services
@ -72,3 +72,32 @@ body #app.theme-my-awesome-theme. { ... }
...
@import "./themes/my-awesome-theme.scss";
```
## Documentation
### Install Python dependencies
Homer's documentation is built using
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). To get
started, you'll need Python 3 installed on your machine and set up your local
environment.
```sh
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### Preview local copy
MkDocs comes with a command-line utility for building and serving the static
documentation site every time you save a file. To launch it, run the `serve`
command.
```sh
mkdocs serve
```
Your local version of the docs site will now be available at
http://localhost:8000/.

105
docs/getting_started.md Normal file
View File

@ -0,0 +1,105 @@
## Using Docker
The fastest and recommended way to get your Homer instance up and running is
with Docker. The Docker image comes with a web server built-in so that all you
need to worry about is your config file.
Internally, the Docker image looks for the assets in the `/www/assets` directory
so you can bind a volume from your host machine to that directory in order to
modify and persist the configuration files. The web server serves the dashboard
on port 8080, but using a port binding will let you expose that to whatever
external port you like.
### docker
To launch container:
```sh
docker run -d \
-p 8080:8080 \
-v </your/local/assets>:/www/assets \
--restart=always \
b4bz/homer:latest
```
Use `UID` and/or `GID` env var to change the assets owner:
```sh
docker run -d \
-p 8080:8080 \
-v </your/local/assets>:/www/assets \
-e "UID=1000" -e "GID=1000" \
--restart=always \
b4bz/homer:latest
```
### docker-compose
It is recommended to use docker-compose to manage your Docker containers, and
below you can find a simple compose yaml file. Copy the contents into a
`docker-compose.yaml` and modify the volume binding to your desired directory to
get started:
```yaml
version: '3.3'
services:
homer:
restart: always
volumes:
- /your/local/assets:/www/assets
ports:
- 8080:8080
image: b4bz/homer
```
To launch container:
```sh
cd /path/to/docker-compose.yml
docker-compose up -d
```
Use `UID` and/or `GID` env var to change the assets owner:
```yaml
version: '3.3'
services:
homer:
restart: always
volumes:
- /your/local/assets:/www/assets
ports:
- 8080:8080
environment:
- UID=1000
- GID=1000
image: b4bz/homer
```
## Shipping your own web server
### Prebuilt release tarball
Download and extract the latest release (`homer.zip`) from the [release page](https://github.com/bastienwirtz/homer/releases), rename the `assets/config.yml.dist` file to `assets/config.yml`, and put it behind a web server.
```sh
wget https://github.com/bastienwirtz/homer/releases/latest/download/homer.zip
unzip homer.zip
cd homer
cp assets/config.yml.dist assets/config.yml
npx serve # or python -m http.server 8010 or apache, nginx ...
```
### Building from source
```sh
# Using yarn (recommended)
yarn install
yarn build
# **OR** Using npm
npm install
npm run build
```
Then your dashboard is ready to use in the `/dist` directory.

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

34
docs/index.md Normal file
View File

@ -0,0 +1,34 @@
<img class="hero-image" src="images/screenshot.png" draggable="false" />
## High-level overview
Homer is web-based dashboard that simplifies your home page management. Designed
for home labs, corporate internal networks, and anyone looking to organize their
web-based work. It is built with [Vue](https://vuejs.org/) and uses `yaml`
configuration files to provide you with a home page that is dead simple to build
and serve!
## Ease of use & customizability
[Get up and running](/getting_started) in seconds using Docker and customize to
your hearts content using the long list of [configuration](/configuration)
options available! Add links to all your relevant services and web apps,
categorize them in groups, and in some cases,
[get live status data](/custom_services) right there on your home page.
Customize your home page with your own images and CSS to make it fit your style!
## Features
- [yaml](http://yaml.org/) file configuration
- Installable (pwa)
- Search
- Grouping
- Theme customization
- Offline health check
- keyboard shortcuts:
- `/` Start searching.
- `Escape` Stop searching.
- `Enter` Open the first matching result (respects the bookmark's `_target` property).
- `Alt`/`Option` + `Enter` Open the first matching result in a new tab.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,71 @@
/* Set some CSS variables for use throughout the page */
:root>* {
--md-hue: 208; /* blue-7 hue */
--md-footer-bg-color: var(--md-code-bg-color);
--md-footer-fg-color: var(--md-code-fg-color);
}
[data-md-color-scheme="default"] {
--md-primary-fg-color: var(--blue-7);
--md-accent-fg-color: var(--violet-6);
--md-code-bg-color: var(--gray-2);
--md-default-fg-color--lightest: var(--gray-4);
}
[data-md-color-scheme="slate"] {
--md-primary-fg-color: var(--blue-5);
--md-accent-fg-color: var(--violet-4);
}
/* Override some default material styles */
[dir=ltr] .md-header__title,
[dir=rtl] .md-header__title {
margin-inline: 0.3rem;
}
.md-nav__title {
display: none;
}
.md-footer__link {
display: inline-flex;
flex-direction: row;
align-items: center;
padding-top: 0.7rem;
}
.md-footer__link:is(:focus,:hover) {
opacity: unset;
}
.md-footer__title {
line-height: unset;
margin-top: 0.65rem;
padding-inline: 0.6rem;
}
.md-footer__direction {
padding-inline: 0.6rem;
}
.md-footer-meta {
background-color: hsla(var(--md-hue), 15%, 10%, 1);
}
html .md-footer .md-footer-meta a:focus,
html .md-footer .md-footer-meta a:hover,
html .md-footer a:focus,
html .md-footer a:hover {
color: var(--md-accent-fg-color);
}
/* Our custom styles */
.hero-image {
width: 65%;
margin-inline-start: 17.5%;
border-radius: var(--radius-2);
box-shadow: var(--shadow-3);
}

View File

@ -1,58 +1,30 @@
# Troubleshooting
## My docker container refuse to start / is stuck at restarting.
You might be facing a permission issue. First of all, check your container logs (adjust the container name if necessary):
```sh
$ docker logs homer
[...]
Assets directory not writable. Check assets directory permissions & docker user or skip default assets install by setting the INIT_ASSETS env var to 0
```
In this case you need to make sure your mounted assets directory have the same GID / UID the container user have (default 1000:1000), and that the read and write permission is granted for the user or the group.
You can either:
- Update your assets directory permissions (ex: `chown -R 1000:1000 /your/assets/folder/`, `chmod -R u+rw /your/assets/folder/`)
- Change the docker user by using the `--user` arguments with docker cli or `user: 1000:1000` with docker compose.
⚠️ Notes:
- **Do not** use env var to set the GID / UID of the user running container. Use the Docker `user` option.
- **Do not** use 0:0 as a user value, it would be a security risk, and it's not guaranty to work.
Check this [thread](https://github.com/bastienwirtz/homer/issues/459) for more information about debugging
permission issues.
## My custom service card doesn't work, nothing appears or offline status is displayed (pi-hole, sonarr, ping, ...)
You might be facing a [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (Cross Origin Request Sharing) issue.
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
must include a special `Access-Control-Allow-Origin: *` HTTP headers).
If this happens your web console (`ctrl+shift+i` or `F12`) will be filled with this kind of errors:
You might by facing a [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
(Cross Origin Request Sharing) issue. 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 must include a special `Access-Control-Allow-Origin: *` HTTP headers).
If this happens your web console (`ctrl+shift+i` or `F12`) will be filled with
this kind of errors:
```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.
```
To resolve this, you can either:
* Host all your target service under the same domain & port.
* 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 server like [`cors-container`](https://github.com/imjacobclark/cors-container), [`cors-anywhere`](https://github.com/Rob--W/cors-anywhere) or many others.
## I am using an authentication proxy and homer says I am offline
This should be a configuration issue.
* Make sure the option `connectivityCheck` is set to `true` in configuration.
* Check your proxy configuration, the expected behavior is to redirect user using a 302 to the login page when user is not authenticated.
## I put my API key into the OpenWeather service and it still isn't working
If you have just made an OpenWeatherMap account and/or a newly-made API key, there is a high chance that you need to wait for it to be activated (often a few hours). If after waiting it still doesn't work, make sure to check the location you have provided since it may be an invalid location.
For some basic debugging steps, you can:
* Check with a large city such as Amsterdam as the specified location within your configuration.
* Make sure your web browser is running the latest version of the homer configuration after updating the location (Ctrl + Shift + R).
* Check for errors within the browser console (Ctrl + Shift + I) relating to api.openweathermap.org
* 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 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,22 +0,0 @@
# Dummy data
This directory content makes possible to test custom services cards or create a demo without actually running the service.
The principle is simple: save a sample output of the API used in the service in a static file in this directory. The path must be identical as the service endpoint to be used seamlessly.
## Start the mock server to expose dummy data
```sh
yarn mock
```
## How to add a new services sample
- create a directory for your service, and any sub-folder existing in the service api path.
- save the api output in a file named after the service endpoint.
Example:
```sh
mkdir pihole
curl http://my-pihole.me/admin/api.php -o pihole/api.php # /admin is omitted because for PiHole, the implementation expect it to be in the base url (`url` or `endpoint` property)
```

View File

@ -1,5 +0,0 @@
/*
Content-Type: application/json
Access-Control-Allow-Origin: https://homer-demo.netlify.app
Access-Control-Allow-Headers: Authorization
Access-Control-Allow-Methods: GET, OPTIONS

View File

@ -1,30 +0,0 @@
{
"job": {
"averagePrintTime": 669.3131185749999,
"estimatedPrintTime": 314.87566979223726,
"filament": {
"tool0": {
"length": 134.81171000000032,
"volume": 0.0
}
},
"file": {
"date": 1665547748,
"display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"origin": "local",
"path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
"size": 129581
},
"lastPrintTime": 669.3131185749999,
"user": "friendlyngeeks"
},
"progress": {
"completion": 27.456185706237797,
"filepos": 35578,
"printTime": 460,
"printTimeLeft": 4612,
"printTimeLeftOrigin": "linear"
},
"state": "Printing"
}

View File

@ -1,26 +0,0 @@
{
"error": "SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)",
"job": {
"averagePrintTime": null,
"estimatedPrintTime": null,
"filament": null,
"file": {
"date": null,
"display": null,
"name": null,
"origin": null,
"path": null,
"size": null
},
"lastPrintTime": null,
"user": null
},
"progress": {
"completion": null,
"filepos": null,
"printTime": null,
"printTimeLeft": null,
"printTimeLeftOrigin": null
},
"state": "Offline after error"
}

View File

@ -1,26 +0,0 @@
{
"job": {
"estimatedPrintTime": null,
"filament": {
"length": null,
"volume": null
},
"file": {
"date": null,
"name": null,
"origin": null,
"path": null,
"size": null
},
"lastPrintTime": null,
"user": null
},
"progress": {
"completion": null,
"filepos": null,
"printTime": null,
"printTimeLeft": null,
"printTimeOrigin": null
},
"state": "Offline"
}

View File

@ -1,26 +0,0 @@
{
"job": {
"estimatedPrintTime": null,
"filament": {
"length": null,
"volume": null
},
"file": {
"date": null,
"name": null,
"origin": null,
"path": null,
"size": null
},
"lastPrintTime": null,
"user": null
},
"progress": {
"completion": null,
"filepos": null,
"printTime": null,
"printTimeLeft": null,
"printTimeOrigin": null
},
"state": "Operational"
}

View File

@ -1,30 +0,0 @@
{
"job": {
"averagePrintTime": 669.3131185749999,
"estimatedPrintTime": 314.87566979223726,
"filament": {
"tool0": {
"length": 134.81171000000032,
"volume": 0.0
}
},
"file": {
"date": 1665547748,
"display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"origin": "local",
"path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
"size": 129581
},
"lastPrintTime": 669.3131185749999,
"user": "friendlyngeeks"
},
"progress": {
"completion": 0.1551153332664511,
"filepos": 201,
"printTime": 0,
"printTimeLeft": 668,
"printTimeLeftOrigin": "average"
},
"state": "Printing"
}

View File

@ -1,30 +0,0 @@
{
"job": {
"averagePrintTime": 669.3131185749999,
"estimatedPrintTime": 314.87566979223726,
"filament": {
"tool0": {
"length": 134.81171000000032,
"volume": 0.0
}
},
"file": {
"date": 1665547748,
"display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"origin": "local",
"path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
"size": 129581
},
"lastPrintTime": 669.3131185749999,
"user": "friendlyngeeks"
},
"progress": {
"completion": 0.1551153332664511,
"filepos": 201,
"printTime": 0,
"printTimeLeft": 668,
"printTimeLeftOrigin": "average"
},
"state": "Printing"
}

View File

@ -1,30 +0,0 @@
{
"job": {
"averagePrintTime": 669.3131185749999,
"estimatedPrintTime": 314.87566979223726,
"filament": {
"tool0": {
"length": 134.81171000000032,
"volume": 0.0
}
},
"file": {
"date": 1665547748,
"display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"origin": "local",
"path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
"size": 129581
},
"lastPrintTime": 669.3131185749999,
"user": "friendlyngeeks"
},
"progress": {
"completion": 27.456185706237797,
"filepos": 35578,
"printTime": 476,
"printTimeLeft": 1612,
"printTimeLeftOrigin": "linear"
},
"state": "Printing"
}

View File

@ -1,30 +0,0 @@
{
"job": {
"averagePrintTime": 698.814525153,
"estimatedPrintTime": 314.87566979223726,
"filament": {
"tool0": {
"length": 134.81171000000032,
"volume": 0.0
}
},
"file": {
"date": 1665547748,
"display": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"name": "CE3PRO_3mmX3mm Brass insert V2.gcode",
"origin": "local",
"path": "MISC/CE3PRO_3mmX3mm Brass insert V2.gcode",
"size": 129581
},
"lastPrintTime": 728.315931731,
"user": "friendlyngeeks"
},
"progress": {
"completion": 100.0,
"filepos": 129581,
"printTime": 728,
"printTimeLeft": 0,
"printTimeLeftOrigin": null
},
"state": "Operational"
}

View File

@ -1,46 +0,0 @@
{
"coord": {
"lon": 4.5833,
"lat": 45.75
},
"weather": [
{
"id": 804,
"main": "Clouds",
"description": "overcast clouds",
"icon": "04d"
}
],
"base": "stations",
"main": {
"temp": 23,
"feels_like": 22.3,
"temp_min": 21.75,
"temp_max": 25.03,
"pressure": 1019,
"humidity": 36,
"sea_level": 1019,
"grnd_level": 957
},
"visibility": 10000,
"wind": {
"speed": 2.29,
"deg": 174,
"gust": 6.22
},
"clouds": {
"all": 97
},
"dt": 1667136939,
"sys": {
"type": 2,
"id": 2005747,
"country": "FR",
"sunrise": 1667110705,
"sunset": 1667147524
},
"timezone": 3600,
"id": 2996943,
"name": "Lyon",
"cod": 200
}

View File

@ -1,38 +0,0 @@
{
"domains_being_blocked": 152588,
"dns_queries_today": 0,
"ads_blocked_today": 0,
"ads_percentage_today": 42,
"unique_domains": 0,
"queries_forwarded": 0,
"queries_cached": 0,
"clients_ever_seen": 0,
"unique_clients": 0,
"dns_queries_all_types": 0,
"reply_UNKNOWN": 0,
"reply_NODATA": 0,
"reply_NXDOMAIN": 0,
"reply_CNAME": 0,
"reply_IP": 0,
"reply_DOMAIN": 0,
"reply_RRNAME": 0,
"reply_SERVFAIL": 0,
"reply_REFUSED": 0,
"reply_NOTIMP": 0,
"reply_OTHER": 0,
"reply_DNSSEC": 0,
"reply_NONE": 0,
"reply_BLOB": 0,
"dns_queries_all_replies": 0,
"privacy_level": 0,
"status": "enabled",
"gravity_last_updated": {
"file_exists": true,
"absolute": 1665486627,
"relative": {
"days": 0,
"hours": 0,
"minutes": 22
}
}
}

View File

@ -1,35 +0,0 @@
{
"data": [{
"disk": 0,
"mem": 983848043,
"cpus": 2,
"pid": 1218,
"maxdisk": 107374182400,
"netin": 43863882954,
"diskread": 0,
"diskwrite": 0,
"name": "HAOS",
"netout": 10426448652,
"cpu": 0.00879886290177172,
"uptime": 3390069,
"status": "running",
"maxmem": 3221225472,
"vmid": 100
}, {
"cpu": 0.00219971572544293,
"name": "debian1",
"netout": 919020028,
"vmid": 101,
"maxmem": 4294967296,
"uptime": 3390064,
"status": "running",
"maxdisk": 107374182400,
"pid": 1295,
"cpus": 2,
"disk": 0,
"mem": 2755160795,
"diskread": 0,
"diskwrite": 0,
"netin": 5105600872
}]
}

View File

@ -1,35 +0,0 @@
{
"data": [{
"disk": 0,
"mem": 983848043,
"cpus": 2,
"pid": 1218,
"maxdisk": 107374182400,
"netin": 43863882954,
"diskread": 0,
"diskwrite": 0,
"name": "HAOS",
"netout": 10426448652,
"cpu": 0.00879886290177172,
"uptime": 3390069,
"status": "running",
"maxmem": 3221225472,
"vmid": 100
}, {
"cpu": 0.00219971572544293,
"name": "debian1",
"netout": 919020028,
"vmid": 101,
"maxmem": 4294967296,
"uptime": 3390064,
"status": "running",
"maxdisk": 107374182400,
"pid": 1295,
"cpus": 2,
"disk": 0,
"mem": 2755160795,
"diskread": 0,
"diskwrite": 0,
"netin": 5105600872
}]
}

View File

@ -1,44 +0,0 @@
{
"data": {
"swap": {
"free": 8589930496,
"total": 8589930496,
"used": 0
},
"cpuinfo": {
"model": "Intel(R) Core(TM) i7-4790 CPU @3.60GHz",
"hvm": "1",
"user_hz": 100,
"sockets": 1,
"cpus": 8,
"flags": "fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm xsaveopt dtherm ida arat pln pts",
"cores": 4,
"mhz": "4000.000"
},
"idle": 0,
"memory": {
"used": 6283382784,
"total": 12419133440,
"free": 6135750656
},
"rootfs": {
"avail": 22670036992,
"free": 24176627712,
"total": 29148368896,
"used": 4971741184
},
"uptime": 3390081,
"ksm": {
"shared": 1079975936
},
"cpu": 0.00440286186020914,
"kversion": "Linux 5.15.30-2-pve #1 SMP PVE 5.15.30-3 (Fri, 22 Apr 2022 18: 08: 27+0200)",
"loadavg": [
"0.00",
"0.01",
"0.04"
],
"pveversion": "pve-manager/7.2-3/c743d6c1",
"wait": 0.00330214639515685
}
}

View File

@ -1,53 +0,0 @@
[
{
"added_on": 1666985518,
"amount_left": 0,
"auto_tmm": false,
"availability": -1,
"category": "",
"completed": 1474873344,
"completion_on": 1666985584,
"content_path": "/downloads/ubuntu-22.04.1-live-server-amd64.iso",
"dl_limit": -1,
"dlspeed": 0,
"download_path": "",
"downloaded": 1513976240,
"downloaded_session": 0,
"eta": 8640000,
"f_l_piece_prio": false,
"force_start": false,
"hash": "cf3ea75e2ebbd30e0da6e6e215e2226bf35f2e33",
"infohash_v1": "cf3ea75e2ebbd30e0da6e6e215e2226bf35f2e33",
"infohash_v2": "",
"last_activity": 1666985588,
"magnet_uri": "magnet:?xt=urn:btih:cf3ea75e2ebbd30e0da6e6e215e2226bf35f2e33&dn=ubuntu-22.04.1-live-server-amd64.iso&tr=https%3a%2f%2ftorrent.ubuntu.com%2fannounce&tr=https%3a%2f%2fipv6.torrent.ubuntu.com%2fannounce",
"max_ratio": 0,
"max_seeding_time": -1,
"name": "ubuntu-22.04.1-live-server-amd64.iso",
"num_complete": 0,
"num_incomplete": 583,
"num_leechs": 0,
"num_seeds": 0,
"priority": 0,
"progress": 1,
"ratio": 1.7163413343924075e-05,
"ratio_limit": -2,
"save_path": "/downloads/",
"seeding_time": 4,
"seeding_time_limit": -2,
"seen_complete": 1666985584,
"seq_dl": false,
"size": 1474873344,
"state": "pausedUP",
"super_seeding": false,
"tags": "",
"time_active": 69,
"total_size": 1474873344,
"tracker": "",
"trackers_count": 2,
"up_limit": -1,
"uploaded": 25985,
"uploaded_session": 0,
"upspeed": 0
}
]

View File

@ -1,10 +0,0 @@
{
"connection_status": "connected",
"dht_nodes": 318,
"dl_info_data": 23481469329,
"dl_info_speed": 1234567,
"dl_rate_limit": 40960000,
"up_info_data": 1788370216,
"up_info_speed": 765432,
"up_rate_limit": 10547200
}

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param><value><array><data>
<value><string>2BAC78C9E10D82415142E57D24601F2FD8927816</string></value>
<value><string>8BB10DB9EA239106D4907601C342ABBA29BE4391</string></value>
<value><string>2790CE71493BE7083929D5A1CE9CFD6B8394F224</string></value>
</data></array></value></param>
</params>
</methodResponse>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param><value><i8>149279</i8></value></param>
</params>
</methodResponse>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param><value><i8>45616</i8></value></param>
</params>
</methodResponse>

View File

@ -1,7 +0,0 @@
{
"data": {
"download": 42.452234,
"upload": 34.3948,
"ping": 12.9873
}
}

View File

@ -1,470 +0,0 @@
{
"totalFileCount": 3245,
"totalTranscodeCount": 3148,
"totalHealthCheckCount": 7278,
"sizeDiff": 5265.423687950708,
"_id": "statistics",
"tdarrScore": "99.97",
"healthCheckScore": "99.97",
"table0Count": 0,
"table2Count": 3244,
"table3Count": 0,
"table4Count": 1,
"table5Count": 3244,
"table6Count": 0,
"table1Count": 1,
"pies": [
[
"All",
"all",
3245,
3148,
5265.423687950708,
7278,
[
{
"name": "Transcode success",
"value": 1995
},
{
"name": "Not required",
"value": 1249
},
{
"name": "Queued",
"value": 1
}
],
[
{
"name": "Success",
"value": 3244
},
{
"name": "Queued",
"value": 1
}
],
[
{
"name": "hevc",
"value": 3172
},
{
"name": "vp9",
"value": 48
},
{
"name": "h264",
"value": 24
}
],
[
{
"name": "mkv",
"value": 3115
},
{
"name": "webm",
"value": 48
},
{
"name": "mp4",
"value": 81
}
],
[
{
"name": "1080p",
"value": 2582
},
{
"name": "480p",
"value": 406
},
{
"name": "720p",
"value": 224
},
{
"name": "4KUHD",
"value": 29
},
{
"name": "576p",
"value": 3
}
],
[],
[]
],
[
"Type1",
"t7_0knr-z",
3,
0,
0,
3,
[
{
"name": "Not required",
"value": 3
}
],
[
{
"name": "Success",
"value": 3
}
],
[
{
"name": "hevc",
"value": 3
}
],
[
{
"name": "mkv",
"value": 3
}
],
[
{
"name": "480p",
"value": 3
}
],
[],
[]
],
[
"Type2",
"ekyBRmWbD",
9,
13,
10.722183834761381,
65,
[
{
"name": "Transcode success",
"value": 9
}
],
[
{
"name": "Success",
"value": 9
}
],
[
{
"name": "hevc",
"value": 9
}
],
[
{
"name": "mkv",
"value": 9
}
],
[
{
"name": "480p",
"value": 1
},
{
"name": "576p",
"value": 1
},
{
"name": "720p",
"value": 4
},
{
"name": "1080p",
"value": 3
}
],
[],
[]
],
[
"Type3",
"-dy1H5yNz",
2619,
2641,
2710.185842271894,
5837,
[
{
"name": "Transcode success",
"value": 1586
},
{
"name": "Not required",
"value": 1033
}
],
[
{
"name": "Success",
"value": 2619
}
],
[
{
"name": "hevc",
"value": 2571
},
{
"name": "vp9",
"value": 48
}
],
[
{
"name": "mkv",
"value": 2510
},
{
"name": "webm",
"value": 48
},
{
"name": "mp4",
"value": 61
}
],
[
{
"name": "1080p",
"value": 2050
},
{
"name": "720p",
"value": 186
},
{
"name": "480p",
"value": 383
}
],
[],
[]
],
[
"Type4",
"ASRD2TAeP",
1,
11,
83.31165281962603,
32,
[
{
"name": "Queued",
"value": 1
}
],
[
{
"name": "Queued",
"value": 1
}
],
[
{
"name": "h264",
"value": 1
}
],
[
{
"name": "mp4",
"value": 1
}
],
[
{
"name": "1080p",
"value": 1
}
],
[],
[]
],
[
"Type5",
"KQ03rLWIw",
11,
14,
17.225701110437512,
43,
[
{
"name": "Not required",
"value": 11
}
],
[
{
"name": "Success",
"value": 11
}
],
[
{
"name": "hevc",
"value": 11
}
],
[
{
"name": "mkv",
"value": 11
}
],
[
{
"name": "720p",
"value": 6
},
{
"name": "480p",
"value": 4
},
{
"name": "1080p",
"value": 1
}
],
[],
[]
],
[
"Type6",
"RQhHe9OCl",
602,
473,
2420.9242209186777,
1300,
[
{
"name": "Not required",
"value": 202
},
{
"name": "Transcode success",
"value": 400
}
],
[
{
"name": "Success",
"value": 602
}
],
[
{
"name": "hevc",
"value": 578
},
{
"name": "h264",
"value": 23
}
],
[
{
"name": "mkv",
"value": 582
},
{
"name": "mp4",
"value": 19
}
],
[
{
"name": "480p",
"value": 15
},
{
"name": "1080p",
"value": 527
},
{
"name": "4KUHD",
"value": 29
},
{
"name": "720p",
"value": 28
},
{
"name": "576p",
"value": 2
}
],
[],
[]
]
],
"streamStats": {
"duration": {
"average": 3127,
"highest": 8548,
"total": 253273
},
"bit_rate": {
"average": 2242894,
"highest": 20149278,
"total": 181674395
},
"nb_frames": {
"average": 75320,
"highest": 204941,
"total": 6100852
}
},
"avgNumberOfStreamsInVideo": 5.049321824907522,
"languages": {
"ara": {
"count": 181
},
"est": {
"count": 62
},
"lav": {
"count": 62
},
"may": {
"count": 131
},
"nor": {
"count": 110
},
"chi": {
"count": 384
},
"ind": {
"count": 63
},
"rum": {
"count": 138
},
"nob": {
"count": 18
},
"srp": {
"count": 3
}
},
"DBPollPeriod": "1s",
"DBFetchTime": "1s",
"DBLoadStatus": "Stable",
"DBQueue": 0,
"processWarning": "",
"processWarningQueues": true
}

View File

@ -1,18 +1,15 @@
#!/bin/sh
PERMISSION_ERROR="Check assets directory permissions & docker user or skip default assets install by setting the INIT_ASSETS env var to 0"
# Ensure default assets are present.
while true; do echo n; done | cp -Ri /www/default-assets/* /www/assets/ &> /dev/null
# Default assets & exemple configuration installation if possible.
if [[ "${INIT_ASSETS}" == "1" ]] && [[ ! -f "/www/assets/config.yml" ]]; then
echo "No configuration found, installing default config & assets"
if [[ ! -w "/www/assets/" ]]; then echo "Assets directory not writable. $PERMISSION_ERROR" && exit 1; fi
while true; do echo n; done | cp -Ri /www/default-assets/* /www/assets/ &> /dev/null
if [[ $? -ne 0 ]]; then echo "Fail to copy default assets. $PERMISSION_ERROR" && exit 1; fi
yes n | cp -i /www/default-assets/config.yml.dist /www/assets/config.yml &> /dev/null
if [[ $? -ne 0 ]]; then echo "Fail to copy default config file. $PERMISSION_ERROR" && exit 1; fi
# Ensure compatibility with previous version (config.yml was in the root directory)
if [ -f "/www/config.yml" ]; then
yes n | cp -i /www/config.yml /www/assets/ &> /dev/null
fi
echo "Starting webserver"
exec lighttpd -D -f /lighttpd.conf
# Install default config if no one is available.
yes n | cp -i /www/default-assets/config.yml.dist /www/assets/config.yml &> /dev/null
chown -R $UID:$GID /www/assets
exec su-exec $UID:$GID darkhttpd /www/ --no-listing --port "$PORT"

8
hooks/post_push Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
IFS='-' read -r TAG string <<< "$DOCKER_TAG"
docker manifest create b4bz/homer:$TAG b4bz/homer:$TAG-amd64 b4bz/homer:$TAG-arm32v7 b4bz/homer:$TAG-arm64v8
docker manifest annotate b4bz/homer:$TAG b4bz/homer:$TAG-arm32v7 --os linux --arch arm
docker manifest annotate b4bz/homer:$TAG b4bz/homer:$TAG-arm64v8 --os linux --arch arm64 --variant v8
docker manifest push --purge b4bz/homer:$TAG

8
hooks/pre_build Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
# Update to docker-ee 18.x for manifests
apt-get -y update
apt-get -y --only-upgrade install docker-ee
# Register qemu-*-static for all supported processors except the
# current one, but also remove all registered binfmt_misc before
docker run --rm --privileged multiarch/qemu-user-static:register --reset

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="assets/icons/favicon.ico" />
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png" sizes="180x180">
<link rel="mask-icon" href="assets/icons/logo.svg">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
<title>Homer</title>
</head>
<body>
<div id="app-mount"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -1,11 +0,0 @@
include "/etc/lighttpd/mime-types.conf"
server.port = env.PORT
server.modules = ( "mod_alias" )
server.username = "lighttpd"
server.groupname = "lighttpd"
server.document-root = "/www"
alias.url = ( env.SUBFOLDER => "/www" )
server.indexfiles = ("index.html")
server.follow-symlink = "enable"
server.feature-flags += ( "server.clock-jump-restart" => 0 )

87
mkdocs.yml Normal file
View File

@ -0,0 +1,87 @@
# Project information
site_name: Homer
site_url: https://bastienwirtz.github.io/
# Repository
repo_name: bastienwirtz/homer
repo_url: https://github.com/bastienwirtz/homer
edit_uri: ""
# Custom CSS
extra_css:
- stylesheets/open-props.1.3.16.min.css
- stylesheets/styles.css
# Theme
theme:
favicon: images/logo.png
logo: images/logo.png
name: material
# custom_dir: docs/overrides/
icon:
repo: fontawesome/brands/github
language: en
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: fontawesome/solid/sun
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: fontawesome/solid/moon
name: Switch to light mode
features:
- content.code.annotate
- navigation.indexes
- navigation.sections
- navigation.top
- navigation.tracking
- search.highlight
- search.share
- search.suggest
- toc.follow
# Don't include MkDocs' JavaScript
include_search_page: false
search_index_only: true
# Copyright - name for footer text
copyright: Homer
# Socials
extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/bastienwirtz/homer
- icon: fontawesome/brands/gitter
link: https://gitter.im/homer-dashboard/community
- icon: fontawesome/brands/docker
link: https://hub.docker.com/r/b4bz/homer
# Extensions
markdown_extensions:
- abbr
- admonition
- attr_list
- def_list
- footnotes
- meta
- md_in_html
- toc:
permalink: true
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
# Page tree
nav:
- What is Homer: index.md
- Getting started: getting_started.md
- Configuration: configuration.md
- Custom services: custom_services.md
- Tips & tricks: tips_and_tricks.md
- Development: development.md
- Troubleshooting: troubleshooting.md

View File

@ -1,31 +1,35 @@
{
"name": "homer",
"version": "22.07.2",
"version": "21.09.1",
"scripts": {
"dev": "vite",
"mock": "http-server dummy-data/ --cors",
"build": "vite build",
"preview": "vite preview --port 5050",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.2.0",
"bulma": "^0.9.4",
"@fortawesome/fontawesome-free": "^5.15.4",
"bulma": "^0.9.3",
"core-js": "^3.21.1",
"js-yaml": "^4.1.0",
"lodash.merge": "^4.6.2",
"vue": "^3.2.41",
"yaml": "^2.1.3"
"register-service-worker": "^1.7.2",
"vue": "^2.6.14"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@vitejs/plugin-vue": "^3.2.0",
"@vue/eslint-config-prettier": "^7.0.0",
"eslint": "^8.26.0",
"eslint-plugin-vue": "^9.6.0",
"http-server": "^14.1.1",
"prettier": "^2.7.1",
"sass": "^1.55.0",
"vite": "^3.2.1",
"vite-plugin-pwa": "^0.13.1"
"@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-pwa": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.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.26.5",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.12"
},
"license": "Apache-2.0"
}

View File

@ -1,131 +0,0 @@
---
# Homepage configuration
# See https://fontawesome.com/v5/search for icons options
title: "Demo dashboard"
subtitle: "Homer"
logo: "logo.png"
# icon: "fas fa-skull-crossbones" # Optional icon
header: true
footer: '<p>Created with <span class="has-text-danger">❤️</span> with <a href="https://bulma.io/">Bulma</a>, <a href="https://vuejs.org/">Vue.js</a> & <a href="https://fontawesome.com/">font awesome</a> // Fork me on <a href="https://github.com/bastienwirtz/homer"><i class="fab fa-github-alt"></i></a></p>' # set false if you want to hide it.
# Optional theme customization
theme: default
colors:
light:
highlight-primary: "#3367d6"
highlight-secondary: "#4285f4"
highlight-hover: "#5a95f5"
background: "#f5f5f5"
card-background: "#ffffff"
text: "#363636"
text-header: "#ffffff"
text-title: "#303030"
text-subtitle: "#424242"
card-shadow: rgba(0, 0, 0, 0.1)
link: "#3273dc"
link-hover: "#363636"
dark:
highlight-primary: "#3367d6"
highlight-secondary: "#4285f4"
highlight-hover: "#5a95f5"
background: "#131313"
card-background: "#2b2b2b"
text: "#eaeaea"
text-header: "#ffffff"
text-title: "#fafafa"
text-subtitle: "#f5f5f5"
card-shadow: rgba(0, 0, 0, 0.4)
link: "#3273dc"
link-hover: "#ffdd57"
# Optional message
message:
style: "is-dark" # See https://bulma.io/documentation/components/message/#colors for styling options.
title: "👋 Welcome !"
content: "This demo page shows an overview of Homer possibilities. Feel free to click anywhere!<br /> Find more information on <a href='https://github.com/bastienwirtz/homer'>github.com/bastienwirtz/homer</a>"
# Optional navbar
# links: [] # Allows for navbar (dark mode, layout, and search) without any links
links:
- name: "Contribute"
icon: "fab fa-github"
url: "https://github.com/bastienwirtz/homer"
target: "_blank" # optional html a tag target attribute
- name: "Documentation"
icon: "fas fa-book"
url: "https://github.com/bastienwirtz/homer/blob/main/README.md#table-of-contents"
# 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/additional-page.yml.dist used in this example:
- name: "another page!"
icon: "fas fa-file-alt"
url: "#additional-page"
# Services
# First level array represent a group.
# Leave only a "items" key if not using group (group name, icon & tagstyle are optional, section separation will not be displayed).
services:
- name: "My apps"
icon: "fas fa-cloud"
items:
- name: "Pi-hole"
logo: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/pihole.png"
url: "https://pi-hole.net/"
endpoint: "https://homer-demo-content.netlify.app/pihole"
type: "PiHole"
- name: "Proxmox - Node1"
logo: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/proxmox.png"
type: "Proxmox"
tag: "sys"
url: "https://www.proxmox.com/en/"
endpoint: "https://homer-demo-content.netlify.app/proxmox"
node: "node1"
warning_value: 50
danger_value: 80
api_token: "xxxxxxxxxxxx"
- name: "An awesome app"
logo: "assets/tools/sample.png"
subtitle: "Bookmark example"
tag: "app"
keywords: "self hosted reddit"
url: "https://www.reddit.com/r/selfhosted/"
- name: "Tools"
icon: "fa-solid fa-screwdriver-wrench"
items:
- name: "Octoprint"
logo: "https://cdn-icons-png.flaticon.com/512/3112/3112529.png"
apikey: "xxxxxxxxxxxx"
endpoint: "https://homer-demo-content.netlify.app/octoprint"
type: "OctoPrint"
- name: "Example item"
logo: "assets/tools/sample.png"
subtitle: "This another example"
tag: "app"
keywords: "demo"
url: "#"
target: "_blank"
- name: "Weather"
location: "Lyon"
apikey: "xxxxxxxxxxxx" # insert your own API key here. Request one from https://openweathermap.org/api.
units: "metric"
endpoint: "https://homer-demo-content.netlify.app/openweather/weather"
type: "OpenWeather"
- name: "interesting links"
icon: "fas fa-solid fa-arrow-up-right-from-square"
items:
- name: "Buy me a coffee !"
subtitle: "If you want to support me, buy me a coffee "
logo: "https://www.buymeacoffee.com/assets/img/guidelines/logo-mark-1.svg"
url: "https://www.buymeacoffee.com/bastien"
- name: "r/selfhosted"
icon: "fa-brands fa-reddit-alien"
subtitle: "Bookmark example"
tag: "reddit"
url: "https://www.reddit.com/r/selfhosted/"
target: "_blank"
- name: "Awesome selfhosted"
icon: "fa-brands fa-github-alt"
subtitle: "Another application"
tag: "awesome-list"
url: "https://github.com/awesome-selfhosted/awesome-selfhosted"

View File

@ -60,9 +60,9 @@ links:
url: "https://www.wikipedia.org/"
# 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/additional-page.yml.dist used in this example:
#- name: "another page!"
# icon: "fas fa-file-alt"
# url: "#additional-page"
- name: "another page!"
icon: "fas fa-file-alt"
url: "#additional-page"
# Services
# First level array represent a group.
@ -75,7 +75,6 @@ services:
logo: "assets/tools/sample.png"
subtitle: "Bookmark example"
tag: "app"
keywords: "self hosted reddit"
url: "https://www.reddit.com/r/selfhosted/"
target: "_blank" # optional html a tag target attribute
- name: "Another one"

View File

@ -1,12 +0,0 @@
# PWA Icons / Images
We suggest you to create a svg or png icon (if it is a png icon, with the maximum resolution possible) for your application and use it to generate a favicon package in [Favicon Generator](https://realfavicongenerator.net/).
Once generated, download the ZIP and use android-* icons for pwa-*:
- use `android-chrome-192x192.png` for `pwa-192x192.png`
- use `android-chrome-512x512.png` for `pwa-512x512.png`
- `apple-touch-icon.png` is `apple-touch-icon.png`
- `favicon.ico` is `favicon.ico`
`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,42 @@
{
"name": "Homer Dashboard",
"short_name": "Homer",
"theme_color": "#3367D6",
"start_url": "../",
"icons": [
{
"src": "./icons/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "./icons/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "./icons/icon-any.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./icons/icon-any.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "./icons/icon-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./icons/safari-pinned-tab.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "monochrome"
}
]
}

18
public/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
<meta name="robots" content="noindex">
<link rel="icon" href="<%= BASE_URL %>favicon.png">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 36 KiB

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

23
requirements.txt Normal file
View File

@ -0,0 +1,23 @@
bracex==2.2.1
click==8.0.4
ghp-import==2.0.2
importlib-metadata==4.11.3
Jinja2==3.0.3
Markdown==3.3.6
MarkupSafe==2.1.1
mergedeep==1.3.4
mkdocs==1.2.3
mkdocs-awesome-pages-plugin==2.7.0
mkdocs-material==8.2.5
mkdocs-material-extensions==1.0.3
packaging==21.3
Pygments==2.11.2
pymdown-extensions==9.3
pyparsing==3.0.7
python-dateutil==2.8.2
PyYAML==6.0
pyyaml-env-tag==0.1
six==1.16.0
watchdog==2.1.6
wcmatch==8.3
zipp==3.7.0

View File

@ -4,7 +4,6 @@
v-if="config"
:class="[
`theme-${config.theme}`,
`page-${currentPage}`,
isDark ? 'is-dark' : 'is-light',
!config.footer ? 'no-footer' : '',
]"
@ -19,10 +18,7 @@
</a>
<i v-if="config.icon" :class="config.icon"></i>
</div>
<div
class="dashboard-title"
:class="{ 'no-logo': !config.icon || !config.logo }"
>
<div class="dashboard-title">
<span class="headline">{{ config.subtitle }}</span>
<h1>{{ config.title }}</h1>
</div>
@ -50,10 +46,10 @@
<SearchInput
class="navbar-item is-inline-block-mobile"
:hotkey="searchHotkey()"
@input="filterServices($event.target?.value)"
@input="filterServices"
@search-focus="showMenu = true"
@search-open="navigateToFirstService($event?.target?.value)"
@search-cancel="filterServices()"
@search-open="navigateToFirstService"
@search-cancel="filterServices"
/>
</Navbar>
</div>
@ -65,7 +61,7 @@
@network-status-update="offline = $event"
/>
<GetStarted v-if="configurationNeeded" />
<GetStarted v-if="loaded && !services" />
<div v-if="!offline">
<!-- Optional messages -->
@ -73,12 +69,8 @@
<!-- Horizontal layout -->
<div v-if="!vlayout || filter" class="columns is-multiline">
<template v-for="(group, groupIndex) in services">
<h2
v-if="group.name"
class="column is-full group-title"
:key="`header-${groupIndex}`"
>
<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">
@ -89,7 +81,7 @@
</h2>
<Service
v-for="(item, index) in group.items"
:key="`service-${groupIndex}-${index}`"
:key="index"
:item="item"
:proxy="config.proxy"
:class="['column', `is-${12 / config.columns}`]"
@ -104,8 +96,8 @@
>
<div
:class="['column', `is-${12 / config.columns}`]"
v-for="(group, groupIndex) in services"
:key="groupIndex"
v-for="group in services"
:key="group.name"
>
<h2 v-if="group.name" class="group-title">
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
@ -141,8 +133,8 @@
</template>
<script>
import { parse } from "yaml";
import merge from "lodash.merge";
const jsyaml = require("js-yaml");
const merge = require("lodash.merge");
import Navbar from "./components/Navbar.vue";
import GetStarted from "./components/GetStarted.vue";
@ -154,7 +146,7 @@ import SettingToggle from "./components/SettingToggle.vue";
import DarkMode from "./components/DarkMode.vue";
import DynamicTheme from "./components/DynamicTheme.vue";
import defaultConfig from "./assets/defaults.yml?raw";
import defaultConfig from "./assets/defaults.yml";
export default {
name: "App",
@ -172,8 +164,6 @@ export default {
data: function () {
return {
loaded: false,
currentPage: null,
configNotFound: false,
config: null,
services: null,
offline: false,
@ -183,11 +173,6 @@ export default {
showMenu: false,
};
},
computed: {
configurationNeeded: function () {
return (this.loaded && !this.services) || this.configNotFound;
},
},
created: async function () {
this.buildDashboard();
window.onhashchange = this.buildDashboard;
@ -200,17 +185,18 @@ export default {
}
},
buildDashboard: async function () {
const defaults = parse(defaultConfig);
const defaults = jsyaml.load(defaultConfig);
let config;
try {
config = await this.getConfig();
this.currentPage = window.location.hash.substring(1) || "default";
const path =
window.location.hash.substring(1) != ""
? window.location.hash.substring(1)
: null;
if (this.currentPage !== "default") {
let pageConfig = await this.getConfig(
`assets/${this.currentPage}.yml`
);
config = Object.assign(config, pageConfig);
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);
@ -232,9 +218,10 @@ export default {
},
getConfig: function (path = "assets/config.yml") {
return fetch(path).then((response) => {
if (response.status == 404 || response.redirected) {
this.configNotFound = true;
return {};
if (response.redirected) {
// This allows to work with authentication proxies.
window.location.href = response.url;
return;
}
if (!response.ok) {
@ -245,7 +232,7 @@ export default {
return response
.text()
.then((body) => {
return parse(body);
return jsyaml.load(body);
})
.then(function (config) {
if (config.externalConfig) {
@ -256,12 +243,10 @@ export default {
});
},
matchesFilter: function (item) {
const needle = this.filter?.toLowerCase();
return (
item.name.toLowerCase().includes(needle) ||
(item.subtitle && item.subtitle.toLowerCase().includes(needle)) ||
(item.tag && item.tag.toLowerCase().includes(needle)) ||
(item.keywords && item.keywords.toLowerCase().includes(needle))
item.name.toLowerCase().includes(this.filter) ||
(item.subtitle && item.subtitle.toLowerCase().includes(this.filter)) ||
(item.tag && item.tag.toLowerCase().includes(this.filter))
);
},
navigateToFirstService: function (target) {
@ -273,7 +258,6 @@ export default {
}
},
filterServices: function (filter) {
console.log(filter);
this.filter = filter;
if (!filter) {

View File

@ -1,8 +1,8 @@
@charset "utf-8";
@import "./webfonts/webfonts.scss";
@import "../../node_modules/bulma/bulma";
@import "./components/status.scss";
@import "bulma";
// Themes import
@import "./themes/sui.scss";
@ -13,7 +13,7 @@
text-overflow: ellipsis;
}
html, body, body #app-mount, body #app {
html, body, body #app {
height: 100%;
background-color: var(--background);
}
@ -104,10 +104,6 @@ body {
.dashboard-title {
padding: 6px 0 0 80px;
&.no-logo {
padding-left: 0;
}
}
.first-line {
@ -167,7 +163,8 @@ body {
}
#main-section {
padding: 0 0 2.5rem 0;
margin-bottom: 2rem;
padding: 0;
h2 {
padding-bottom: 0px;
@ -289,7 +286,7 @@ body {
.no-footer {
#main-section {
padding-bottom: 0;
margin-bottom: 0;
}
.footer {

View File

@ -1,48 +0,0 @@
.status {
font-size: 0.8rem;
color: var(--text-title);
&.offline:before, &.error:before {
background-color: #d65c68;
box-shadow: 0 0 5px 1px #d65c68;
color: #d65c68;
}
&.pending:before {
background-color: #e8bb7d;
box-shadow: 0 0 5px 1px #e8bb7d;
}
&.online:before, &.ready:before {
background-color: #94e185;
box-shadow: 0 0 5px 1px #94e185;
}
&.in-progress:before {
background-color: #8fe87d;
box-shadow: 0 0 5px 1px #8fe87d;
animation: pulse 1s alternate infinite;
}
@keyframes pulse {
0% {
background: rgba(255, 255, 255, 0.2);
box-shadow: inset 0px 0px 10px 2px rgba(0, 255, 182, 0.3),
0px 0px 5px 2px rgba(0, 255, 135, 0.2);
}
100% {
background: rgba(255, 255, 255, 1);
box-shadow: inset 0px 0px 10px 2px rgba(0, 255, 182, 0.5),
0px 0px 15px 2px rgba(0, 255, 135, 1);
}
}
&:before {
content: " ";
display: inline-block;
width: 8px;
height: 8px;
margin-right: 10px;
border-radius: 8px;
}
}

View File

@ -17,9 +17,6 @@ export default {
};
},
created: function () {
if (/t=\d+/.test(window.location.href)) {
window.history.replaceState({}, document.title, window.location.pathname);
}
let that = this;
this.checkOffline();
@ -32,45 +29,15 @@ export default {
},
false
);
window.addEventListener(
"online",
function () {
that.checkOffline();
},
false
);
window.addEventListener(
"offline",
function () {
this.offline = true;
},
false
);
},
methods: {
checkOffline: function () {
if (!navigator.onLine) {
this.offline = true;
return;
}
// extra check to make sure we're not offline
let that = this;
const urlPath = window.location.pathname.replace(/\/+$/, "");
const aliveCheckUrl = `${window.location.origin}${urlPath}/index.html?t=${new Date().valueOf()}`;
return fetch(aliveCheckUrl, {
return fetch(window.location.href + "?alive", {
method: "HEAD",
cache: "no-store",
redirect: "manual",
})
.then(function (response) {
// opaqueredirect means request has been redirected, to auth provider probably
if (
(response.type === "opaqueredirect" && !response.ok) ||
[401, 403].indexOf(response.status) != -1
) {
window.location.href = aliveCheckUrl;
}
that.offline = !response.ok;
})
.catch(function () {

View File

@ -1,6 +1,6 @@
<template>
<a
@click="toggleTheme()"
v-on:click="toggleTheme()"
aria-label="Toggle dark mode"
class="navbar-item is-inline-block-mobile"
>

View File

@ -6,7 +6,7 @@
<p>
<a
class="button is-primary mt-5 has-text-weight-bold"
href="https://github.com/bastienwirtz/homer/blob/main/docs/configuration.md#configuration"
href="https://github.com/bastienwirtz/homer/blob/main/README.md#getting-started"
target="_blank"
>
Get started

View File

@ -47,6 +47,7 @@ export default {
}
if (this.item.url) {
let fetchedMessage = await this.downloadMessage(this.item.url);
console.log("done");
if (this.item.mapping) {
fetchedMessage = this.mapRemoteMessage(fetchedMessage);
}

View File

@ -75,7 +75,7 @@ export default {
this.$emit("input", value.toLowerCase());
},
},
beforeUnmount() {
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener);
},
};

View File

@ -1,9 +1,8 @@
<template>
<component :is="component" :item="item" :proxy="proxy"></component>
<component v-bind:is="component" :item="item" :proxy="proxy"></component>
</template>
<script>
import { defineAsyncComponent } from "vue";
import Generic from "./services/Generic.vue";
export default {
@ -18,7 +17,7 @@ export default {
if (type === "Generic") {
return Generic;
}
return defineAsyncComponent(() => import(`./services/${type}.vue`));
return () => import(`./services/${type}.vue`);
},
},
};

View File

@ -1,8 +1,5 @@
<template>
<a
@click.prevent="toggleSetting()"
class="navbar-item is-inline-block-mobile"
>
<a v-on:click="toggleSetting()" class="navbar-item is-inline-block-mobile">
<span><i :class="['fas', 'fa-fw', value ? icon : secondaryIcon]"></i></span>
<slot></slot>
</a>

View File

@ -1,85 +0,0 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="status">
<i
class="fa-regular fa-copy fa-xl"
:class="{ scale: animate }"
@click="copy()"
@animationend="animate = false"
></i>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "CopyToClipboard",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
animate: false,
}),
methods: {
copy() {
navigator.clipboard.writeText(this.item.clipboard);
this.animate = true;
},
},
};
</script>
<style scoped lang="scss">
.scale {
-webkit-animation: scale-up 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
animation: scale-up 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.is-light i {
color: black;
}
.is-dark i {
color: white;
}
/**
* ----------------------------------------
* animation scale-down-center
* ----------------------------------------
*/
@-webkit-keyframes scale-up {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(1.25);
transform: scale(1.25);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes scale-up {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(1.25);
transform: scale(1.25);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>

View File

@ -1,118 +0,0 @@
<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

@ -1,115 +0,0 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="up > 0" class="notif up" title="Up">
{{ up }}
</strong>
<strong v-if="down > 0" class="notif down" title="Down">
{{ down }}
</strong>
<strong v-if="grace > 0" class="notif grace" title="Grace">
{{ grace }}
</strong>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Healthchecks",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
api: null,
}),
computed: {
up: function () {
if (!this.api) {
return "";
}
return this.api.checks?.filter((check) => {
return check.status.toLowerCase() === "up";
}).length;
},
down: function () {
if (!this.api) {
return "";
}
return this.api.checks?.filter((check) => {
return check.status.toLowerCase() === "down";
}).length;
},
grace: function () {
if (!this.api) {
return "";
}
return this.api.checks?.filter((check) => {
return check.status.toLowerCase() === "grace";
}).length;
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
const apikey = this.item.apikey;
if (!apikey) {
console.error(
"apikey is not present in config.yml for the Healthchecks entry!"
);
return;
}
const headers = {
"X-Api-Key": this.item.apikey,
};
this.api = await this.fetch("/api/v1/checks/", { headers }).catch((e) => {
console.error(e);
});
},
},
};
</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;
&.up {
background-color: #4fd671;
}
&.down {
background-color: #e51111;
}
&.grace {
background-color: #cdd02e;
}
}
}
</style>

View File

@ -1,108 +0,0 @@
<template>
<Generic :item="item" :title="state">
<template #content>
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">
<template v-if="item.subtitle && !state">
{{ item.subtitle }}
</template>
<template v-if="!error && display == 'text'">
<i class="fa-solid fa-gear mr-1"></i>
<b v-if="completion">{{ completion.toFixed() }}%</b>
<span class="separator mx-1"> | </span>
<span v-if="printTime" :title="`${toTime(printTimeLeft)} left`">
<i class="fa-solid fa-stopwatch mr-1"></i>
{{ toTime(printTime) }}
</span>
</template>
<template v-if="!error && display == 'bar'">
<progress
v-if="completion"
class="progress is-primary"
:value="completion"
max="100"
:title="`${state} - ${completion.toFixed()}%, ${toTime(
printTimeLeft
)} left`"
>
{{ completion }}%
</progress>
</template>
<span v-if="error" :title="error">{{ error }}</span>
</p>
</template>
<template #indicator>
<i :class="['status', statusClass]" :title="state"></i>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "OctoPrint",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
printTime: null,
printTimeLeft: null,
completion: null,
state: null,
error: null,
}),
computed: {
statusClass: function () {
switch (this.state) {
case "Operational":
return "ready";
case "Offline":
return "offline";
case "Printing":
return "in-progress";
default:
return "pending";
}
},
},
created() {
this.display = this.item.display == "bar" ? this.item.display : "text";
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
try {
const response = await this.fetch(`api/job?apikey=${this.item.apikey}`);
this.printTime = response.progress.printTime;
this.printTimeLeft = response.progress.printTimeLeft;
this.completion = response.progress.completion;
this.state = response.state;
this.error = response.error;
} catch (e) {
this.error = `Fail to fetch octoprint data (${e.message})`;
console.error(e);
}
},
toTime: function (timastamp) {
return new Date(timastamp * 1000).toTimeString().substring(0, 5);
},
},
};
</script>
<style scoped lang="scss">
.fa-triangle-exclamation::before {
color: #d65c68;
}
.progress {
height: 8px;
width: 90%;
}
</style>

View File

@ -22,12 +22,7 @@
<div v-else>
<p class="title is-4">{{ name }}</p>
<p class="subtitle is-6">
<span>
{{ temperature }}
</span>
<span class="location-time">
{{ locationTime }}
</span>
{{ temp | tempSuffix(this.item.units) }}
</p>
</div>
</div>
@ -54,24 +49,7 @@ export default {
temp: null,
conditions: null,
error: false,
timezoneOffset: 0,
}),
computed: {
temperature: function () {
if (!this.temp) return "";
let unit = "K";
if (this.item.units === "metric") {
unit = "°C";
} else if (this.item.units === "imperial") {
unit = "°F";
}
return `${this.temp} ${unit}`;
},
locationTime: function () {
return this.calcTime(this.timezoneOffset);
},
},
created() {
this.fetchWeather();
},
@ -87,11 +65,7 @@ export default {
}
const apiKey = this.item.apikey || this.item.apiKey;
let url = `https://api.openweathermap.org/data/2.5/weather?${locationQuery}&appid=${apiKey}&units=${this.item.units}`;
if (this.item.endpoint) {
url = this.item.endpoint;
}
const url = `https://api.openweathermap.org/data/2.5/weather?${locationQuery}&appid=${apiKey}&units=${this.item.units}`;
fetch(url)
.then((response) => {
if (!response.ok) {
@ -105,22 +79,24 @@ export default {
this.temp = parseInt(weather.main.temp).toFixed(1);
this.icon = weather.weather[0].icon;
this.conditions = weather.weather[0].description;
this.timezoneOffset = weather.timezone;
})
.catch((e) => {
console.log(e);
this.error = true;
});
},
calcTime: (offset) => {
const localTime = new Date();
const utcTime =
localTime.getTime() + localTime.getTimezoneOffset() * 60000;
const calculatedTime = new Date(utcTime + 1000 * offset);
return calculatedTime.toLocaleString([], {
hour: "2-digit",
minute: "2-digit",
});
},
filters: {
tempSuffix: function (value, type) {
if (!value) return "";
let unit = "K";
if (type === "metric") {
unit = "°C";
} else if (type === "imperial") {
unit = "°F";
}
return `${value} ${unit}`;
},
},
};
@ -157,9 +133,4 @@ export default {
}
}
}
//Location Time
.location-time {
margin-left: 20px;
}
</style>

View File

@ -49,11 +49,7 @@ export default {
},
methods: {
fetchStatus: async function () {
const authQueryParams = this.item.apikey
? `?summaryRaw&auth=${this.item.apikey}`
: "";
const result = await this.fetch(`/api.php${authQueryParams}`)
.catch((e) => console.log(e));
const result = await this.fetch("/api.php").catch((e) => console.log(e));
this.status = result.status;
this.ads_percentage_today = result.ads_percentage_today;

View File

@ -29,17 +29,7 @@ export default {
},
methods: {
fetchStatus: async function () {
const method =
typeof this.item.method === "string"
? this.item.method.toUpperCase()
: "HEAD";
if (!["GET", "HEAD", "OPTION"].includes(method)) {
console.error(`Ping: ${method} is not a supported HTTP method`);
return;
}
this.fetch("/", { method, cache: "no-cache" }, false)
this.fetch("/", { method: "HEAD", cache: "no-cache" }, false)
.then(() => {
this.status = "online";
})
@ -55,8 +45,6 @@ export default {
.status {
font-size: 0.8rem;
color: var(--text-title);
white-space: nowrap;
margin-left: 0.25rem;
&.online:before {
background-color: #94e185;

View File

@ -83,12 +83,6 @@ export default {
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) => {

View File

@ -1,193 +0,0 @@
<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="vms">
<div v-if="loading">
<strong>Loading...</strong>
</div>
<div v-else-if="error">
<strong class="danger">Error loading info</strong>
</div>
<div
v-else
class="metrics"
:class="{
'is-size-7-mobile': item.small_font_on_small_screens,
'is-small': item.small_font_on_desktop,
}"
>
<span v-if="isValueShown('vms')" class="margined"
>VMs:
<span class="is-number"
><span class="has-text-weight-bold">{{ vms.running }}</span
><span v-if="isValueShown('vms_total')"
>/{{ vms.total }}</span
></span
></span
>
<span v-if="isValueShown('lxcs')" class="margined"
>LXCs:
<span class="is-number"
><span class="has-text-weight-bold">{{ lxcs.running }}</span
><span v-if="isValueShown('lxcs_total')"
>/{{ lxcs.total }}</span
></span
></span
>
<span v-if="isValueShown('disk')" class="margined"
>Disk:
<span
class="has-text-weight-bold is-number"
:class="statusClass(diskUsed)"
>{{ diskUsed }}%</span
></span
>
<span v-if="isValueShown('mem')" class="margined"
>Mem:
<span
class="has-text-weight-bold is-number"
:class="statusClass(memoryUsed)"
>{{ memoryUsed }}%</span
></span
>
<span v-if="isValueShown('cpu')" class="margined"
>CPU:
<span
class="has-text-weight-bold is-number"
:class="statusClass(cpuUsed)"
>{{ cpuUsed }}%</span
></span
>
</div>
</template>
</p>
</template>
<template #indicator>
<i v-if="loading" class="fa fa-circle-notch fa-spin fa-2xl"></i>
<i v-if="error" class="fa fa-exclamation-circle fa-2xl danger"></i>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Proxmox",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
vms: {
total: 0,
running: 0,
},
lxcs: {
total: 0,
running: 0,
},
memoryUsed: 0,
diskUsed: 0,
cpuUsed: 0,
hide: [],
error: false,
loading: true,
}),
created() {
if (this.item.hide) this.hide = this.item.hide;
this.fetchStatus();
},
methods: {
statusClass(value) {
if (value > this.item.danger_value) return "danger";
if (value > this.item.warning_value) return "warning";
return "healthy";
},
fetchStatus: async function () {
try {
const options = {
headers: {
Authorization: this.item.api_token,
},
};
const status = await this.fetch(
`/api2/json/nodes/${this.item.node}/status`,
options
);
// main metrics:
const decimalsToShow = this.item.hide_decimals ? 0 : 1;
this.memoryUsed = (
(status.data.memory.used * 100) /
status.data.memory.total
).toFixed(decimalsToShow);
this.diskUsed = (
(status.data.rootfs.used * 100) /
status.data.rootfs.total
).toFixed(decimalsToShow);
this.cpuUsed = (status.data.cpu * 100).toFixed(decimalsToShow);
// vms:
if (this.isValueShown("vms")) {
const vms = await this.fetch(
`/api2/json/nodes/${this.item.node}/qemu`,
options
);
this.parseVMsAndLXCs(vms, this.vms);
}
// lxc containers:
if (this.isValueShown("lxcs")) {
const lxcs = await this.fetch(
`/api2/json/nodes/${this.item.node}/lxc`,
options
);
this.parseVMsAndLXCs(lxcs, this.lxcs);
}
this.error = false;
} catch (err) {
console.log(err);
this.error = true;
}
this.loading = false;
},
parseVMsAndLXCs(items, value) {
value.total += items.data.length;
value.running += items.data.filter((i) => i.status === "running").length;
// if no vms, hide this value:
if (value.total == 0) this.hide.push("lxcs");
},
isValueShown(value) {
return this.hide.indexOf(value) == -1;
},
},
};
</script>
<style scoped lang="scss">
.is-number {
font-family: "Lato";
}
.healthy {
color: green;
}
.warning {
color: orange;
}
.danger {
color: red;
}
.metrics .margined:not(:first-child) {
margin-left: 0.3rem;
}
.is-small {
font-size: small;
}
</style>

View File

@ -1,164 +0,0 @@
<template>
<Generic :item="item">
<template #content>
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">
<span v-if="error" class="error">An error has occurred.</span>
<template v-else>
<span class="down">
<i class="fas fa-download"></i> {{ downRate }}
</span>
<span class="up"> <i class="fas fa-upload"></i> {{ upRate }} </span>
</template>
</p>
</template>
<template #indicator>
<span v-if="!error" class="count"
>{{ count }}
<template v-if="count === 1">torrent</template>
<template v-else>torrents</template>
</span>
</template>
</Generic>
</template>
<script>
import Generic from "./Generic.vue";
// Units to add to download and upload rates.
const units = ["B", "kiB", "MiB", "GiB"];
// Take the rate in bytes and keep dividing it by 1k until the lowest
// value for which we have a unit is determined. Return the value with
// up to two decimals as a string and unit/s appended.
const displayRate = (rate) => {
let i = 0;
while (rate > 1000 && i < units.length) {
rate /= 1000;
i++;
}
return (
Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(
rate || 0
) + ` ${units[i]}/s`
);
};
export default {
name: "rTorrent",
props: { item: Object },
components: { Generic },
// Properties for download, upload, torrent count and errors.
data: () => ({ dl: null, ul: null, count: null, error: null }),
// Computed properties for the rate labels.
computed: {
downRate: function () {
return displayRate(this.dl);
},
upRate: function () {
return displayRate(this.ul);
},
},
created() {
// Set intervals if configured so the rates and/or torrent count
// will be updated.
const rateInterval = parseInt(this.item.rateInterval, 10) || 0;
const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0;
if (rateInterval > 0) {
setInterval(() => this.fetchRates(), rateInterval);
}
if (torrentInterval > 0) {
setInterval(() => this.fetchCount(), torrentInterval);
}
// Fetch the initial values.
this.fetchRates();
this.fetchCount();
},
methods: {
// Perform two calls to the XML-RPC service and fetch download
// and upload rates. Values are saved to the `ul` and `dl`
// properties.
fetchRates: async function () {
this.getRate("throttle.global_up.rate")
.then((ul) => (this.ul = ul))
.catch(() => (this.error = true));
this.getRate("throttle.global_down.rate")
.then((dl) => (this.dl = dl))
.catch(() => (this.error = true));
},
// Perform a call to the XML-RPC service to fetch the number of
// torrents.
fetchCount: async function () {
this.getCount().catch(() => (this.error = true));
},
// Fetch a numeric value from the XML-RPC service by requesting
// the specified method name and parsing the XML. The response
// is expected to adhere to the structure of a single numeric
// value.
getRate: async function (methodName) {
return this.getXml(methodName).then((xml) =>
parseInt(
xml.getElementsByTagName("value")[0].firstChild.textContent,
10
)
);
},
// Fetch the numer of torrents by requesting the download list
// and counting the number of entries therein.
getCount: async function () {
return this.getXml("download_list").then((xml) => {
const arrayEl = xml.getElementsByTagName("array");
this.count = arrayEl
? arrayEl[0].getElementsByTagName("value").length
: 0;
});
},
// Perform a call to the XML-RPC service and parse the response
// as XML, which is then returned.
getXml: async function (methodName) {
const headers = { "Content-Type": "text/xml" };
if (this.item.username && this.item.password) {
headers[
"Authorization"
] = `${this.item.username}:${this.item.password}`;
}
return fetch(`${this.item.xmlrpc.replace(/\/$/, "")}/RPC2`, {
method: "POST",
headers,
body: `<methodCall><methodName>${methodName}</methodName></methodCall>`,
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.text();
})
.then((text) =>
Promise.resolve(new DOMParser().parseFromString(text, "text/xml"))
);
},
},
};
</script>
<style scoped lang="scss">
.error {
color: #e51111 !important;
}
.down {
margin-right: 1em;
}
.count {
color: var(--text);
font-size: 0.8em;
}
</style>

View File

@ -1,99 +0,0 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong
v-if="downloads > 0"
class="notif downloading"
:title="`${downloads} active download${downloads > 1 ? 's' : ''}`"
>
{{ downloads }}
</strong>
<i
v-if="error"
class="notif error fa-solid fa-triangle-exclamation"
title="Unable to fetch current status"
></i>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "SABnzbd",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
stats: null,
error: false,
}),
computed: {
downloads: function () {
if (!this.stats) {
return "";
}
return this.stats.noofslots;
},
},
created() {
const downloadInterval = parseInt(this.item.downloadInterval, 10) || 0;
if (downloadInterval > 0) {
setInterval(() => this.fetchStatus(), downloadInterval);
}
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
try {
const response = await this.fetch(
`/api?output=json&apikey=${this.item.apikey}&mode=queue`
);
this.error = false;
this.stats = response.queue;
} catch (e) {
this.error = true;
console.error(e);
}
},
},
};
</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;
&.downloading {
background-color: #4fb5d6;
}
&.error {
border-radius: 50%;
aspect-ratio: 1;
background-color: #e51111;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More