Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
36549c5a06 | ||
|
b19f6f228b | ||
|
10fa40d1f6 | ||
|
819f0d4052 | ||
|
a01da0b8be | ||
|
207762135e | ||
|
d48fc7ca26 | ||
|
2da071d0ea | ||
|
084009be9d | ||
|
d390a72b24 | ||
|
1d6d20e76e | ||
|
a2f2e527fb | ||
|
5e9a19861d | ||
|
b82626bc6d | ||
|
2b53c1a82a | ||
|
8285823517 | ||
|
6f9a74ddcf | ||
|
03f2e6bbb1 | ||
|
d752f64f49 | ||
|
717894d8c5 | ||
|
a2dfffab68 |
3
.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
@ -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
@ -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
@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
custom: ['https://www.buymeacoffee.com/bastien']
|
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
|
||||
|
||||
```
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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
@ -1,8 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
authors:
|
||||
- dependabot
|
||||
categories:
|
||||
- title: Main changes
|
||||
labels:
|
||||
- "*"
|
44
.github/workflows/dockerhub.yml
vendored
@ -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
@ -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
|
15
.github/workflows/integration.yml
vendored
@ -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
|
||||
|
||||
|
37
.github/workflows/release.yml
vendored
@ -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
@ -23,4 +23,10 @@ yarn-error.log*
|
||||
# App configuration
|
||||
config.yml
|
||||
|
||||
.drone.yml
|
||||
.drone.yml
|
||||
|
||||
# Python venv
|
||||
venv
|
||||
|
||||
# MkDocs
|
||||
site
|
||||
|
25
Dockerfile
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
};
|
@ -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
@ -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/.
|
@ -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
@ -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"
|
||||
```
|
@ -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
|
||||
```
|
@ -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
@ -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
After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
34
docs/index.md
Normal 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.
|
||||
|
||||
|
1
docs/stylesheets/open-props.1.3.16.min.css
vendored
Normal file
71
docs/stylesheets/styles.css
Normal 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);
|
||||
}
|
@ -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.
|
||||
|
@ -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)
|
||||
```
|
@ -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
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}]
|
||||
}
|
@ -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
|
||||
}]
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -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
|
||||
}
|
@ -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>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<methodResponse>
|
||||
<params>
|
||||
<param><value><i8>149279</i8></value></param>
|
||||
</params>
|
||||
</methodResponse>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<methodResponse>
|
||||
<params>
|
||||
<param><value><i8>45616</i8></value></param>
|
||||
</params>
|
||||
</methodResponse>
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"download": 42.452234,
|
||||
"upload": 34.3948,
|
||||
"ping": 12.9873
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
@ -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
@ -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
|
15
index.html
@ -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>
|
@ -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
@ -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
|
44
package.json
@ -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"
|
||||
}
|
||||
|
@ -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"
|
@ -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"
|
||||
|
@ -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`
|
||||
|
||||
`
|
Before Width: | Height: | Size: 13 KiB |
BIN
public/assets/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 790 B |
BIN
public/assets/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
public/assets/icons/icon-any.png
Normal file
After Width: | Height: | Size: 75 KiB |
1
public/assets/icons/icon-any.svg
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
public/assets/icons/icon-maskable.png
Normal file
After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 38 KiB |
3
public/assets/icons/safari-pinned-tab.svg
Normal file
After Width: | Height: | Size: 6.7 KiB |
42
public/assets/manifest.json
Normal 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
@ -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>
|
BIN
public/logo.png
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 36 KiB |
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
23
requirements.txt
Normal 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
|
74
src/App.vue
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 () {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a
|
||||
@click="toggleTheme()"
|
||||
v-on:click="toggleTheme()"
|
||||
aria-label="Toggle dark mode"
|
||||
class="navbar-item is-inline-block-mobile"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ export default {
|
||||
this.$emit("input", value.toLowerCase());
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this._keyListener);
|
||||
},
|
||||
};
|
||||
|
@ -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`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|