]> git.immae.eu Git - github/bastienwirtz/homer.git/commitdiff
Merge branch 'bastienwirtz:main' into hotkey
authorRobin Schneider <45321827+robinschneider@users.noreply.github.com>
Wed, 6 Oct 2021 22:15:26 +0000 (00:15 +0200)
committerGitHub <noreply@github.com>
Wed, 6 Oct 2021 22:15:26 +0000 (00:15 +0200)
23 files changed:
CONTRIBUTING.md
Dockerfile
Dockerfile.arm32v7
Dockerfile.arm64v8
README.md
docs/configuration.md
docs/customservices.md
docs/development.md
docs/tips-and-tricks.md
docs/troubleshooting.md [new file with mode: 0644]
package.json
src/assets/app.scss
src/components/Service.vue
src/components/services/AdGuardHome.vue
src/components/services/Generic.vue
src/components/services/Mealie.vue [new file with mode: 0644]
src/components/services/OpenWeather.vue [new file with mode: 0644]
src/components/services/PaperlessNG.vue
src/components/services/PiHole.vue
src/components/services/Ping.vue
src/components/services/Radarr.vue
src/components/services/Sonarr.vue
yarn.lock

index f29b7d83a537538606c21aea29e2608b33404ef9..de893b6c7e512649b13ef44e1613e76e4b8f0059 100644 (file)
@@ -12,10 +12,6 @@ UX and usability. If you are looking for a full featured dashboard, there is ton
 - Configuration is stored in a simple config file, avoiding the need for a backend/database while making possible to use versioning or [config template](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html).
 - Only modern browsers are supported, feel free to use any JS features without any polyfill as soon as the latest version of the major browsers supports them.
 
-### Roadmap
-
-If you want to know more about the project direction or looking for something to work on, checkout the [roadmap](https://github.com/bastienwirtz/homer#Roadmap)!
-Feel free to open an issue if you have any question.
 
 # Ground Rules
 
@@ -40,8 +36,9 @@ feel free to open an issue to present your idea.
 ### How to submit a contribution
 
 The general process to submit a contribution is as follow:
-1. Create your own fork of the code
-2. Do the changes in your fork
-3. Make sure to fill the [pull request description](https://github.com/bastienwirtz/homer/blob/main/.github/PULL_REQUEST_TEMPLATE.md) properly.
+1. Take a look to the [development guideline](https://github.com/bastienwirtz/homer/blob/main/docs/development.md).
+2. Create your own fork of the code
+3. Do the changes in your fork
+4. Make sure to fill the [pull request description](https://github.com/bastienwirtz/homer/blob/main/.github/PULL_REQUEST_TEMPLATE.md) properly.
 
 ### Happy coding :metal:
index 19d01f810aab9ac58277190be5ead17ea4ed9f0e..e31d97d4af318a4c854f6d51d213c73aeae0fc34 100644 (file)
@@ -25,6 +25,9 @@ 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"]
index 3efb5aa109d825556e8c175abe94a163ba629ebb..95a2db1dc3ea5102ed4db569fd35af50fcf5c77e 100644 (file)
@@ -35,6 +35,9 @@ 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"]
index e6d0a511d453d71b091683a5711631f21b302007..cd15e4a0ddc08de149acc830d9c2b28c870e8ac2 100644 (file)
@@ -35,6 +35,9 @@ 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"]
index 944179877705d1d6f026eb02fad74385e9c5f416..2dd2b42b50dd6bba697c50b7404ec55b01a4d176 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,67 +1,68 @@
 <h1 align="center">
      <img
-               width="180"
-               alt="Homer's donut"
-               src="https://raw.githubusercontent.com//bastienwirtz/homer/main/public/logo.png">
+ <img
+  width="180"
+  alt="Homer's donut"
+  src="https://raw.githubusercontent.com//bastienwirtz/homer/main/public/logo.png">
     <br/>
     Homer
 </h1>
 
 <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.
+ 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">
      <strong>
-       <a href="https://homer-demo.netlify.app">Demo</a>
-               
-               <a href="https://gitter.im/homer-dashboard/community">Chat</a>
-               
-               <a href="#getting-started">Getting started</a>
      </strong>
+ <strong>
+   <a href="https://homer-demo.netlify.app">Demo</a>
+  •
+  <a href="https://gitter.im/homer-dashboard/community">Chat</a>
+  •
+  <a href="#getting-started">Getting started</a>
+ </strong>
 </p>
 <p align="center">
      <a href="https://opensource.org/licenses/Apache-2.0"><img
-               alt="License: Apache 2"
-               src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>
+ <a href="https://opensource.org/licenses/Apache-2.0"><img
+  alt="License: Apache 2"
+  src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>
   <a href="https://gitter.im/homer-dashboard/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge"><img
-               alt="Gitter chat"
-               src="https://badges.gitter.im/homer-dashboard/community.svg"></a>
+  alt="Gitter chat"
+  src="https://badges.gitter.im/homer-dashboard/community.svg"></a>
   <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://github.com/awesome-selfhosted/awesome-selfhosted"><img
-               alt="Awesome"
-               src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"></a>
+  alt="Download homer static build"
+  src="https://img.shields.io/badge/Download-homer.zip-orange"></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="https://raw.github.com/bastienwirtz/homer/main/docs/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)
 
 ## Features
+
 - [yaml](http://yaml.org/) file configuration
 - Installable (pwa)
 - Search
 - Grouping
 - Theme customization
-- Offline heathcheck
+- Offline heath 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.
 
-
 ## Getting started
 
 Homer is a full static html/js dashboard, generated from the source in `/src` using webpack. It's meant to be served by an HTTP server, **it will not work if you open dist/index.html directly over file:// protocol**.
@@ -111,7 +112,7 @@ environment:
 
 ### Using the release tarball (prebuilt, ready to use)
 
-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 webserver.
+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
index 93abadbc4608eef1e03952dba161a060c9f580de..807fa688559d7251b35fb8d34f39e3e219986300 100644 (file)
@@ -1,4 +1,4 @@
-## Configuration
+# 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.
 
@@ -7,7 +7,7 @@ Title, icons, links, colors, and services can be configured in the `config.yml`
 # Homepage configuration
 # See https://fontawesome.com/icons for icons options
 
-# Optional: Use external configuration file. 
+# Optional: Use external configuration file.
 # Using this will ignore remaining config in this file
 # externalConfig: https://example.com/server-luci/config.yaml
 
@@ -138,6 +138,8 @@ services:
         # background: red # optional color for card to set color directly without custom stylesheet
 ```
 
+View [Custom Services](customservices.md) for details about all available custom services (like PiHole) and how to configure them.
+
 If you choose to fetch message information from an endpoint, the output format should be as follows (or you can [custom map fields as shown in tips-and-tricks](./tips-and-tricks.md#mapping-fields)):
 
 ```json
@@ -151,7 +153,7 @@ 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).
 
-### Style Options
+## 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:
 
@@ -162,10 +164,29 @@ Homer uses [bulma CSS](https://bulma.io/), which provides a [modifiers syntax](h
 
 You can read the [bulma modifiers page](https://bulma.io/documentation/modifiers/syntax/) for other options regarding size, style, or state.
 
-### PWA Icons
+## PWA Icons
 
 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}
 ```
+
+## Supported services
+
+Currently the following services are supported for showing quick infos on the card. They can be used by setting the type to one of the following values at the item.
+
+- PiHole
+- AdGuardHome
+- PaperlessNG
+- Mealie
+
+## Additional configuration
+
+### Paperless
+
+For Paperless you need an API-Key which you have to store at the item in the field `apikey`.
+
+### 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.
index 43f45f480734519719b87815c0afd293585243d4..f79428fc7afa6898f1aee579ecf2982a39583622 100644 (file)
@@ -1,16 +1,18 @@
 # 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. Available services are in `src/components/`. Here is an overview of all custom services that are available 
+configuration. Available services are in `src/components/`. Here is an overview of all custom services that are available
 within Homer.
 
+If you experiencing any issue, please have a look to the [troubleshooting](troubleshooting.md) page.
+
 ## 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
  items:
       - name: "Pi-hole"
         logo: "assets/tools/sample.png"
@@ -19,35 +21,64 @@ The following configuration is available for the PiHole service.
         type: "PiHole"
 ```
 
+## OpenWeatherMap
+
+Using the OpenWeatherMap service you can display weather information about a given location.
+The following configuration is available for the OpenWeatherMap service:
+
+```yaml
+items:
+     - 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.
 
-
 ## Sonarr/Radarr
 
 This service displays Activity (blue), Warning (orange) or Error (red) notifications bubbles from the Radarr/Sonarr application.
 Two lines are needed in the config.yml :
-```
+
+```yaml
 type: "Radarr" or "Sonarr"
 apikey: "01234deb70424befb1f4ef6a23456789"
 ```
+
 The url must be the root url of Radarr/Sonarr application.
 The Radarr/Sonarr API key can be found in Settings > General. It is needed to access the API.
 
-
 ## PaperlessNG
 
 For Paperless you need an API-Key which you have to store at the item in the field `apikey`.
 
-
 ## Ping
 
-For Paperless you need an API-Key which you have to store at the item in the field `apikey`.
+For Ping you need to set the type to Ping and provide a url.
+
+```yaml
+items:
+   - name: "Awesome app"
+     type: Ping
+     logo: "assets/tools/sample.png"
+     subtitle: "Bookmark example" tag: "app" 
+     url: "https://www.reddit.com/r/selfhosted/" 
+```
index f2e90201f9c86853cb5ddd374c337271f525866b..a22ae0b2b6b648a25a5dd8a08ff2ed327d025c04 100644 (file)
@@ -1,4 +1,6 @@
-## Development
+# Development
+
+If you want to contribute to Homer, please read the [contributing guidelines](https://github.com/bastienwirtz/homer/blob/main/CONTRIBUTING.md) first. 
 
 ```sh
 # Using yarn (recommended)
@@ -10,7 +12,50 @@ npm install
 npm run serve
 ```
 
-### Themes
+## Custom services
+
+Custom services are small VueJs component (see `src/components/services/`) that add little features to a classic, "static", dashboard item. It should be very simple.
+A dashboard can contain a lot of items, so performance is very important. 
+
+The [`Generic`](https://github.com/bastienwirtz/homer/blob/main/src/components/services/Generic.vue) service provides a typical card layout which
+you can extend to add specific features. Unless you want a completely different design, extended the generic service is the recommended way. It gives you 3 [slots](https://vuejs.org/v2/guide/components-slots.html#Named-Slots) to extend: `icon`, `content` and `indicator`. 
+Each one is **optional**, and will display the usual information if omitted.
+
+Each service must implement the `item` [property](https://vuejs.org/v2/guide/components-props.html) and bind it the Generic component if used.
+
+### Skeleton
+```Vue
+<template>
+  <Generic :item="item">
+    <template #icon>
+      <!-- left area containing the icon -->
+    </template>
+    <template #content>
+      <!-- main area containing the title, subtitle, ... -->
+    </template>
+    <template #indicator>
+      <!-- top right area, empty by default -->
+    </template>
+  </Generic>
+</template>
+
+<script>
+import Generic from "./Generic.vue";
+
+export default {
+  name: "MyNewService",
+  props: {
+    item: Object,
+  },
+  components: {
+    Generic,
+  }
+};
+</script>
+```
+
+
+## Themes
 
 Themes are meant to be simple customization (written in [scss](https://sass-lang.com/documentation/syntax)).
 To add a new theme, just add a file in the theme directory, and put all style in the `body #app.theme-<name>` scope. Then import it in the main style file.
index 94167fbad4b8e6aaed078ab12a44ab012f059b9e..17bba9afd8905c8b77f21db2c0c6a8ed56342549 100644 (file)
@@ -3,6 +3,7 @@
 Here is a collection of neat tips and tricks that Homer users have come up with!
 
 ## Use Homer as a custom "new tab" page
+
 #### `by @vosdev`
 
 These extensions for [Firefox](https://addons.mozilla.org/firefox/addon/custom-new-tab-page) and [Chrome & Friends](https://chrome.google.com/webstore/detail/new-tab-changer/occbjkhimchkolibngmcefpjlbknggfh) allow you to have your homer dashboard in your new tab page, while leaving focus on the address bar meaning you can still type right away if you want to search or go to a page that is not on your homer dash.
@@ -22,11 +23,12 @@ The Firefox extension loads Homer in an iframe on your new tab page, meaning you
 ```
 
 ## YAML Anchors
+
 #### `by @JamiePhonic`
 
 Since Homer is configured using YAML, it supports all of YAML's helpful features, such as anchoring!
 
-For example, you can define tags and tag styles for each "item" in a service. 
+For example, you can define tags and tag styles for each "item" in a service.
 Using Anchoring, you can define all your tags and their styles once like this: (for example)
 
 ```yaml
@@ -70,6 +72,7 @@ The end result is that if you want to update the name or style of any particular
 Great if you have a lot of services or a lot of tags!  
 
 ## Remotely edit your config with Code Server
+
 #### `by @JamiePhonic`
 
 Homer doesn't yet provide a way to edit your configuration from inside Homer itself, but that doesn't mean it can't be done!
@@ -78,14 +81,17 @@ You can setup and use [Code-Server](https://github.com/cdr/code-server) to edit
 
 If you're running Homer in docker, you can setup a Code-Server container and pass your homer config directory into it.
 Simply pass your homer config directory as an extra -v parameter to your code-server container:
-```
+
+```sh
 -v '/your/local/homer/config-dir/':'/config/homer':'rw'
 ```
+
 This will map your homer config directory (For example, /docker/appdata/homer/) into code-server's `/config/` directory, in a sub folder called `homer`
 
 As a bonus, Code-Server puts the "current folder" as a parameter in the URL bar, so you could add a `links:` entry in Homer that points to your code-server instance with the directory pre-filled for essentially 1 click editing!
 
 For example:
+
 ```yml
 links:
   - name: Edit config
@@ -93,9 +99,11 @@ links:
     url: https://vscode.example.net/?folder=/config/homer
     target: "_blank" # optional html tag target attribute
 ```
+
 where the path after `?folder=` is the path to the folder where you mounted your homer config INSIDE the Code-Server container.
 
 ### Example Code-Server docker create command
+
 ```sh
 docker create \
   --name=code-server \
@@ -111,13 +119,13 @@ docker create \
   linuxserver/code-server
 ```
 
-
 ## Get the news headlines in Homer
 
 ### Mapping Fields
+
 Most times, the url you're getting headlines from follows a different schema than the one expected by Homer.
 
-For example, if you would like to show jokes from ChuckNorris.io, you'll find that the url https://api.chucknorris.io/jokes/random is giving you info like this:
+For example, if you would like to show jokes from ChuckNorris.io, you'll find that the url <https://api.chucknorris.io/jokes/random> is giving you info like this:
 
 ```json
 {
@@ -179,6 +187,6 @@ If the URL you specified returns a JSON object that defines a `title` and `conte
 
 So, using [Node-Red](https://nodered.org/docs/getting-started/) and a quick flow, you can process an RSS feed to replace the message with a news item!
 
-To get started, simply import [this flow](https://flows.nodered.org/flow/4b6406c9a684c26ace0430dd1826e95d) into your Node-Red instance and change the RSS feed in the "Get News RSS Feed" node to one of your choosing! 
+To get started, simply import [this flow](https://flows.nodered.org/flow/4b6406c9a684c26ace0430dd1826e95d) into your Node-Red instance and change the RSS feed in the "Get News RSS Feed" node to one of your choosing!
 
 So far, the flow has been tested with BBC News and Sky News, however it should be easy to modify the flow to work with other RSS feeds if they don't work out of the box!
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644 (file)
index 0000000..ed1f85d
--- /dev/null
@@ -0,0 +1,19 @@
+# Troubleshooting
+
+## My custom service card doesn't work, nothing appears or offline status is displayed (pi-hole, sonarr, ping, ...)
+
+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+shit+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.
+```
+
+To resolve this, you can either:
+
+* Host all your target service under the same domain & port.
+* Modify the target sever configuration so that the response of the server included following header- `Access-Control-Allow-Origin: *` (<https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests>). It might be an option in the targeted service, otherwise depending on how the service is hosted, the proxy or web server can seamlessly add it.
+* Use a cors proxy sever like [`cors-container`](https://github.com/imjacobclark/cors-container), [`cors-anywhere`](https://github.com/Rob--W/cors-anywhere) or many others.
index 61b3073c3e76f866597a87b1729f692a84ad0bb7..c5486bbc9721fcaf484e41fd448371e90f873f31 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "homer",
-  "version": "21.07.1",
+  "version": "21.09.1",
   "license": "Apache-2.0",
   "scripts": {
     "serve": "vue-cli-service serve",
@@ -8,9 +8,9 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "@fortawesome/fontawesome-free": "^5.15.3",
+    "@fortawesome/fontawesome-free": "^5.15.4",
     "bulma": "^0.9.3",
-    "core-js": "^3.15.2",
+    "core-js": "^3.17.3",
     "js-yaml": "^4.1.0",
     "lodash.merge": "^4.6.2",
     "register-service-worker": "^1.7.2",
index 6bb50686259aa06ab029156b3af72abcde01dae7..f2dfb37e4f8f9798c3e6c92889a0f0edae4c83d3 100644 (file)
@@ -106,7 +106,7 @@ body {
     }
 
     .first-line {
-      height: 100px;
+      min-height: 100px;
       vertical-align: center;
       background-color: var(--highlight-primary);
 
@@ -121,7 +121,7 @@ body {
       }
 
       .container {
-        height: 80px;
+        min-height: 80px;
         padding: 10px 0;
       }
 
@@ -140,8 +140,7 @@ body {
         }
       }
     }
-    .navbar,
-    .navbar-menu {
+    .navbar {
       background-color: var(--highlight-secondary);
 
       a {
@@ -153,6 +152,9 @@ body {
           background-color: var(--highlight-hover);
         }
       }
+       .navbar-menu {
+      background-color: inherit;
+      }
     }
     .navbar-end {
       text-align: right;
index 868675957a201d59f24578e666cf2ecccf354469..39a9ac41e0df81855e4970f012d775eb2b09a398 100644 (file)
@@ -7,16 +7,13 @@ import Generic from "./services/Generic.vue";
 
 export default {
   name: "Service",
-  components: {
-    Generic,
-  },
   props: {
     item: Object,
   },
   computed: {
     component() {
       const type = this.item.type || "Generic";
-      if (type == "Generic") {
+      if (type === "Generic") {
         return Generic;
       }
       return () => import(`./services/${type}.vue`);
index d4a2b89119a8969e55d0fbb5d125272827ead010..16881fa59ea6d93ceb43bd4573e785fb1107cce1 100644 (file)
@@ -1,59 +1,77 @@
 <template>
-  <div>
-    <div class="card" :class="item.class">
-      <a :href="item.url" :target="item.target" rel="noreferrer">
-        <div class="card-content">
-          <div class="media">
-            <div v-if="item.logo" class="media-left">
-              <figure class="image is-48x48">
-                <img :src="item.logo" :alt="`${item.name} logo`" />
-              </figure>
-            </div>
-            <div v-if="item.icon" class="media-left">
-              <figure class="image is-48x48">
-                <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
-              </figure>
-            </div>
-            <div class="media-content">
-              <p class="title is-4">{{ item.name }}</p>
-              <p class="subtitle is-6">{{ item.subtitle }}</p>
-            </div>
-            <div
-              v-if="status"
-              class="status"
-              v-bind:class="status.protection_enabled ? 'enabled' : 'disabled'"
-            >
-              {{ status.protection_enabled }}
-            </div>
-          </div>
-          <div class="tag" :class="item.tagstyle" v-if="item.tag">
-            <strong class="tag-text">#{{ item.tag }}</strong>
-          </div>
-        </div>
-      </a>
-    </div>
-  </div>
+  <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="stats">
+          {{ percentage }}&percnt; blocked
+        </template>
+      </p>
+    </template>
+    <template #indicator>
+      <div class="status" :class="protection">
+        {{ protection }}
+      </div>
+    </template>
+  </Generic>
 </template>
 
 <script>
+import Generic from "./Generic.vue";
+
 export default {
   name: "AdGuardHome",
   props: {
     item: Object,
   },
+  components: {
+    Generic,
+  },
   data: () => {
     return {
       status: null,
+      stats: null,
     };
   },
+  computed: {
+    percentage: function () {
+      if (this.stats) {
+        return (
+          (this.stats.num_blocked_filtering * 100) /
+          this.stats.num_dns_queries
+        ).toFixed(2);
+      }
+      return "";
+    },
+    protection: function () {
+      if (this.status) {
+        return this.status.protection_enabled ? "enabled" : "disabled";
+      } else return "unknown";
+    },
+  },
   created: function () {
     this.fetchStatus();
+    if (!this.item.subtitle) {
+      this.fetchStats();
+    }
   },
   methods: {
     fetchStatus: async function () {
-      this.status = await fetch(`${this.item.url}/control/status`).then(
-        (response) => response.json()
-      );
+      this.status = await fetch(`${this.item.url}/control/status`, {
+        credentials: "include",
+      })
+        .then((response) => response.json())
+        .catch((e) => console.log(e));
+    },
+    fetchStats: async function () {
+      this.stats = await fetch(`${this.item.url}/control/stats`, {
+        credentials: "include",
+      })
+        .then((response) => response.json())
+        .catch((e) => console.log(e));
     },
   },
 };
@@ -79,6 +97,12 @@ export default {
     box-shadow: 0px 0px 4px 1px #c9404d;
   }
 
+  &.unknown:before {
+    background-color: #c9c740;
+    border-color: #ccc935;
+    box-shadow: 0px 0px 4px 1px #c9c740;
+  }
+
   &:before {
     content: " ";
     display: inline-block;
index 08bd3f6bf0ab3db2f1851bcfaaf949763f49387b..af65a8c6ad8580f496906b53891e47035b58afca 100644 (file)
@@ -8,22 +8,27 @@
       <a :href="item.url" :target="item.target" rel="noreferrer">
         <div class="card-content">
           <div :class="mediaClass">
-            <div v-if="item.logo" class="media-left">
-              <figure class="image is-48x48">
-                <img :src="item.logo" :alt="`${item.name} logo`" />
-              </figure>
-            </div>
-            <div v-if="item.icon" class="media-left">
-              <figure class="image is-48x48">
-                <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
-              </figure>
-            </div>
+            <slot name="icon">
+              <div v-if="item.logo" class="media-left">
+                <figure class="image is-48x48">
+                  <img :src="item.logo" :alt="`${item.name} logo`" />
+                </figure>
+              </div>
+              <div v-if="item.icon" class="media-left">
+                <figure class="image is-48x48">
+                  <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
+                </figure>
+              </div>
+            </slot>
             <div class="media-content">
-              <p class="title is-4">{{ item.name }}</p>
-              <p class="subtitle is-6" v-if="item.subtitle">
-                {{ item.subtitle }}
-              </p>
+              <slot name="content">
+                <p class="title is-4">{{ item.name }}</p>
+                <p class="subtitle is-6" v-if="item.subtitle">
+                  {{ item.subtitle }}
+                </p>
+              </slot>
             </div>
+            <slot name="indicator" class="indicator"></slot>
           </div>
           <div class="tag" :class="item.tagstyle" v-if="item.tag">
             <strong class="tag-text">#{{ item.tag }}</strong>
diff --git a/src/components/services/Mealie.vue b/src/components/services/Mealie.vue
new file mode 100644 (file)
index 0000000..790a9b1
--- /dev/null
@@ -0,0 +1,94 @@
+<template>
+  <div>
+    <div class="card" :class="item.class">
+      <a :href="item.url" :target="item.target" rel="noreferrer">
+        <div class="card-content">
+          <div class="media">
+            <div v-if="item.logo" class="media-left">
+              <figure class="image is-48x48">
+                <img :src="item.logo" :alt="`${item.name} logo`" />
+              </figure>
+            </div>
+            <div v-if="item.icon" class="media-left">
+              <figure class="image is-48x48">
+                <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
+              </figure>
+            </div>
+            <div class="media-content">
+              <p class="title is-4">{{ item.name }}</p>
+              <p class="subtitle is-6">
+                <template v-if="item.subtitle">
+                  {{ item.subtitle }}
+                </template>
+                <template v-else-if="meal"> Today: {{ meal.name }} </template>
+                <template v-else-if="stats">
+                  happily keeping {{ stats.totalRecipes }} recipes organized
+                </template>
+              </p>
+            </div>
+          </div>
+          <div class="tag" :class="item.tagstyle" v-if="item.tag">
+            <strong class="tag-text">#{{ item.tag }}</strong>
+          </div>
+        </div>
+      </a>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "Mealie",
+  props: {
+    item: Object,
+  },
+  data: () => ({
+    stats: null,
+    meal: null,
+  }),
+  created() {
+    this.fetchStatus();
+  },
+  methods: {
+    fetchStatus: async function () {
+      if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing
+      this.meal = await fetch(`${this.item.url}/api/meal-plans/today/`, {
+        headers: {
+          Authorization: "Bearer " + this.item.apikey,
+          Accept: "application/json",
+        },
+      })
+        .then(function (response) {
+          if (!response.ok) {
+            throw new Error("Not 2xx response");
+          } else {
+            if (response != null) {
+              return response.json();
+            }
+          }
+        })
+        .catch((e) => console.log(e));
+      this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, {
+        headers: {
+          Authorization: "Bearer " + this.item.apikey,
+          Accept: "application/json",
+        },
+      })
+        .then(function (response) {
+          if (!response.ok) {
+            throw new Error("Not 2xx response");
+          } else {
+            return response.json();
+          }
+        })
+        .catch((e) => console.log(e));
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+.media-left img {
+  max-height: 100%;
+}
+</style>
diff --git a/src/components/services/OpenWeather.vue b/src/components/services/OpenWeather.vue
new file mode 100644 (file)
index 0000000..09ff76a
--- /dev/null
@@ -0,0 +1,135 @@
+<template>
+  <div>
+    <div class="card" :class="item.class">
+      <a
+        :href="`https://openweathermap.org/city/${id}`"
+        :target="item.target"
+        rel="noreferrer"
+      >
+        <div class="card-content">
+          <div class="media">
+            <div v-if="icon" class="media-left" :class="item.background">
+              <figure class="image is-48x48">
+                <img
+                  :src="`https://openweathermap.org/img/wn/${icon}@2x.png`"
+                  :alt="conditions"
+                  :title="conditions"
+                />
+              </figure>
+            </div>
+            <div class="media-content">
+              <p v-if="error" class="error">Data could not be retrieved</p>
+              <div v-else>
+                <p class="title is-4">{{ name }}</p>
+                <p class="subtitle is-6">
+                  {{ temp | tempSuffix(this.item.units) }}
+                </p>
+              </div>
+            </div>
+          </div>
+          <div class="tag" :class="item.tagstyle" v-if="item.tag">
+            <strong class="tag-text">#{{ item.tag }}</strong>
+          </div>
+        </div>
+      </a>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "OpenWeather",
+  props: {
+    item: Object,
+  },
+  data: () => ({
+    id: null,
+    icon: null,
+    name: null,
+    temp: null,
+    conditions: null,
+    error: false,
+  }),
+  created() {
+    this.fetchWeather();
+  },
+  methods: {
+    fetchWeather: async function () {
+      let locationQuery;
+
+      // Use location ID if specified, otherwise retrieve value from location (name).
+      if (this.item.locationId) {
+        locationQuery = `id=${this.item.locationId}`;
+      } else {
+        locationQuery = `q=${this.item.location}`;
+      }
+
+      const url = `https://api.openweathermap.org/data/2.5/weather?${locationQuery}&appid=${this.item.apiKey}&units=${this.item.units}`;
+      fetch(url)
+        .then((response) => {
+          if (!response.ok) {
+            throw Error(response.statusText);
+          }
+          return response.json();
+        })
+        .then((weather) => {
+          this.id = weather.id;
+          this.name = weather.name;
+          this.temp = parseInt(weather.main.temp).toFixed(1);
+          this.icon = weather.weather[0].icon;
+          this.conditions = weather.weather[0].description;
+        })
+        .catch((e) => {
+          console.log(e);
+          this.error = true;
+        });
+    },
+  },
+  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}`;
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+// Add a border around the weather image.
+// Otherwise the image is not always distinguishable.
+.media-left {
+  &.circle,
+  &.square {
+    background-color: #e4e4e4;
+  }
+
+  &.circle {
+    border-radius: 90%;
+  }
+
+  img {
+    max-height: 100%;
+  }
+}
+
+.error {
+  color: #de0000;
+}
+
+// Change background color in dark mode.
+.is-dark {
+  .media-left {
+    &.circle,
+    &.square {
+      background-color: #909090;
+    }
+  }
+}
+</style>
index 4fb31f852661cc31d633b4d9567e87d2275af87c..af13317859e1f7e1117cf29bcf9ec02adf9b9ce6 100644 (file)
@@ -59,6 +59,7 @@ export default {
       }
       const url = `${this.item.url}/api/documents/`;
       this.api = await fetch(url, {
+        credentials: "include",
         headers: {
           Authorization: "Token " + this.item.apikey,
         },
index 7042a7b2a5055662e7434bead008af4036a6e934..87f7090d4ad8b16f47b5a2285db872f8102074ba 100644 (file)
@@ -64,7 +64,9 @@ export default {
   methods: {
     fetchStatus: async function () {
       const url = `${this.item.url}/api.php`;
-      this.api = await fetch(url)
+      this.api = await fetch(url, {
+        credentials: "include",
+      })
         .then((response) => response.json())
         .catch((e) => console.log(e));
     },
index 8a9b7a4e0579dbea2e04daf99fd21b733bdc9ed0..6fd3ec57f987e34719e7b169f5adaa16ab604303 100644 (file)
@@ -1,46 +1,24 @@
 <template>
-  <div>
-    <div class="card" :class="item.class">
-      <a :href="item.url" :target="item.target" rel="noreferrer">
-        <div class="card-content">
-          <div class="media">
-            <div v-if="item.logo" class="media-left">
-              <figure class="image is-48x48">
-                <img :src="item.logo" :alt="`${item.name} logo`" />
-              </figure>
-            </div>
-            <div v-if="item.icon" class="media-left">
-              <figure class="image is-48x48">
-                <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
-              </figure>
-            </div>
-            <div class="media-content">
-              <p class="title is-4">{{ item.name }}</p>
-              <p class="subtitle is-6">
-                <template v-if="item.subtitle">
-                  {{ item.subtitle }}
-                </template>
-              </p>
-            </div>
-            <div v-if="status" class="status" :class="status">
-              {{ status }}
-            </div>
-          </div>
-          <div class="tag" :class="item.tagstyle" v-if="item.tag">
-            <strong class="tag-text">#{{ item.tag }}</strong>
-          </div>
-        </div>
-      </a>
-    </div>
-  </div>
+  <Generic :item="item">
+    <template #indicator>
+      <div v-if="status" class="status" :class="status">
+        {{ status }}
+      </div>
+    </template>
+  </Generic>
 </template>
 
 <script>
+import Generic from "./Generic.vue";
+
 export default {
   name: "Ping",
   props: {
     item: Object,
   },
+  components: {
+    Generic,
+  },
   data: () => ({
     status: null,
   }),
@@ -50,7 +28,11 @@ export default {
   methods: {
     fetchStatus: async function () {
       const url = `${this.item.url}`;
-      fetch(url, { method: "HEAD", cache: "no-cache" })
+      fetch(url, {
+        method: "HEAD",
+        cache: "no-cache",
+        credentials: "include",
+      })
         .then((response) => {
           if (!response.ok) {
             throw Error(response.statusText);
@@ -66,9 +48,6 @@ export default {
 </script>
 
 <style scoped lang="scss">
-.media-left img {
-  max-height: 100%;
-}
 .status {
   font-size: 0.8rem;
   color: var(--text-title);
index 9d3829279ca55f5ee48cf829561609ce3af2c095..a9cdedf0defb4ee336f829d539395e902f2febde 100644 (file)
@@ -70,7 +70,9 @@ export default {
   },
   methods: {
     fetchConfig: function () {
-      fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
+      fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, {
+        credentials: "include",
+      })
         .then((response) => {
           if (response.status != 200) {
             throw new Error(response.statusText);
@@ -92,7 +94,9 @@ export default {
           console.error(e);
           this.serverError = true;
         });
-      fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
+      fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, {
+        credentials: "include",
+      })
         .then((response) => {
           if (response.status != 200) {
             throw new Error(response.statusText);
index 7851b6b6b1dbc484e2da5218a2fa7926c239f746..0270255995bfe9e35625d95c0aef81daf39c4053 100644 (file)
@@ -70,7 +70,9 @@ export default {
   },
   methods: {
     fetchConfig: function () {
-      fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
+      fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, {
+        credentials: "include",
+      })
         .then((response) => {
           if (response.status != 200) {
             throw new Error(response.statusText);
@@ -92,7 +94,9 @@ export default {
           console.error(e);
           this.serverError = true;
         });
-      fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
+      fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, {
+        credentials: "include",
+      })
         .then((response) => {
           if (response.status != 200) {
             throw new Error(response.statusText);
index c79c2fd2166323090e934f4bb38deb67bd010661..18906e18338c1c2dd848f4d136588d25e20aa4f0 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@babel/helper-validator-identifier" "^7.14.5"
     to-fast-properties "^2.0.0"
 
-"@fortawesome/fontawesome-free@^5.15.3":
-  version "5.15.3"
-  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz#c36ffa64a2a239bf948541a97b6ae8d729e09a9a"
-  integrity sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==
+"@fortawesome/fontawesome-free@^5.15.4":
+  version "5.15.4"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5"
+  integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==
 
 "@hapi/address@2.x.x":
   version "2.1.4"
@@ -2773,7 +2773,12 @@ core-js@^2.4.0:
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
   integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
 
-core-js@^3.15.2, core-js@^3.6.5:
+core-js@^3.17.3:
+  version "3.17.3"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.17.3.tgz#8e8bd20e91df9951e903cabe91f9af4a0895bc1e"
+  integrity sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==
+
+core-js@^3.6.5:
   version "3.15.2"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.2.tgz#740660d2ff55ef34ce664d7e2455119c5bdd3d61"
   integrity sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==
@@ -8222,9 +8227,9 @@ url-loader@^2.2.0:
     schema-utils "^2.5.0"
 
 url-parse@^1.4.3, url-parse@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b"
-  integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==
+  version "1.5.3"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
+  integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
   dependencies:
     querystringify "^2.1.1"
     requires-port "^1.0.0"