diff --git a/README.md b/README.md index 33716f6..9441798 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ - [Features](#features) - [Getting started](#getting-started) - [Configuration](docs/configuration.md) +- [Custom services](docs/customservices.md) - [Tips & tricks](docs/tips-and-tricks.md) -- [Roadmap](#roadmap) - [Development](docs/development.md) @@ -73,7 +73,11 @@ See [documentation](docs/configuration.md) for information about the configurati To launch container: ```sh -docker run -p 8080:8080 -v /your/local/assets/:/www/assets b4bz/homer:latest +docker run -d \ + -p 8080:8080 \ + -v :/www/assets \ + --restart=always \ + b4bz/homer:latest ``` Default assets will be automatically installed in the `/www/assets` directory. Use `UID` and/or `GID` env var to change the assets owner (`docker run -e "UID=1000" -e "GID=1000" [...]`). @@ -130,9 +134,3 @@ npm run build ``` Then your dashboard is ready to use in the `/dist` directory. - - -## Roadmap - -- [ ] Add new themes. -- [ ] Add support for custom service card (add custom feature to some service / app link) diff --git a/docs/configuration.md b/docs/configuration.md index c88069e..a76ecb3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -66,6 +66,17 @@ colors: # Optional message message: # url: "https://" # Can fetch information from an endpoint to override value below. + # mapping: # allows to map fields from the remote format to the one expected by Homer + # title: 'id' # use value from field 'id' as title + # content: 'value' # value from field 'value' as content + # refreshInterval: 10000 # Optional: time interval to refresh message + # + # Real example using chucknorris.io for showing Chuck Norris facts as messages: + # url: https://api.chucknorris.io/jokes/random + # mapping: + # title: 'id' + # content: 'value' + # refreshInterval: 10000 style: "is-warning" title: "Optional message!" icon: "fa fa-exclamation-triangle" @@ -81,6 +92,11 @@ links: - name: "link 2" icon: "fas fa-book" url: "https://github.com/bastienwirtz/homer" + # this will link to a second homer page that will load config from page2.yml and keep default config values as in config.yml file + # see url field and assets/page.yml used in this example: + - name: "Second Page" + icon: "fas fa-file-alt" + url: "#page2" # Services # First level array represents a group. @@ -88,6 +104,8 @@ links: services: - name: "Application" icon: "fas fa-code-branch" + # A path to an image can also be provided. Note that icon take precedence if both icon and logo are set. + # logo: "path/to/logo" items: - name: "Awesome app" logo: "assets/tools/sample.png" @@ -118,9 +136,10 @@ services: # background: red # optional color for card to set color directly without custom stylesheet ``` + View [Custom Services](customservices.md) for details about all available custom services (like PiHole) and how to configure them. -If you choose to fetch message information from an endpoint, the output format should be: +If you choose to fetch message information from an endpoint, the output format should be as follows (or you can [custom map fields as shown in tips-and-tricks](./tips-and-tricks.md#mapping-fields)): ```json { diff --git a/docs/customservices.md b/docs/customservices.md index 40f2e3d..4f64ecc 100644 --- a/docs/customservices.md +++ b/docs/customservices.md @@ -1,6 +1,8 @@ # Custom Services -Here is an overview of all custom services that are available within Homer. +Some service can use a specific a component that provides some extra features by adding a `type` key to the service yaml +configuration. Available services are in `src/components/`. Here is an overview of all custom services that are available +within Homer. ## PiHole @@ -17,6 +19,7 @@ The following configuration is available for the PiHole service. type: "PiHole" ``` + ## OpenWeatherMap Using the OpenWeatherMap service you can display weather information about a given location. @@ -35,4 +38,38 @@ items: ``` **Remarks:** -If for some reason your city can't be found by entering the name in the `location` property, you could also try to configure the OWM city ID in the `locationId` property. To retrieve your specific City ID, go to the [OWM website](https://openweathermap.org), search for your city and retrieve the ID from the URL (for example, the City ID of Amsterdam is 2759794). \ No newline at end of file +If for some reason your city can't be found by entering the name in the `location` property, you could also try to configure the OWM city ID in the `locationId` property. To retrieve your specific City ID, go to the [OWM website](https://openweathermap.org), search for your city and retrieve the ID from the URL (for example, the City ID of Amsterdam is 2759794). + + +## Medusa + +This service displays News (grey), Warning (orange) or Error (red) notifications bubbles from the Medusa application. +Two lines are needed in the config.yml : +``` +type: "Medusa" +apikey: "01234deb70424befb1f4ef6a23456789" +``` +The url must be the root url of Medusa application. +The Medusa API key can be found in General configuration > Interface. It is needed to access Medusa API. + + +## Sonarr/Radarr + +This service displays Activity (blue), Warning (orange) or Error (red) notifications bubbles from the Radarr/Sonarr application. +Two lines are needed in the config.yml : +``` +type: "Radarr" or "Sonarr" +apikey: "01234deb70424befb1f4ef6a23456789" +``` +The url must be the root url of Radarr/Sonarr application. +The Radarr/Sonarr API key can be found in Settings > General. It is needed to access the API. + + +## PaperlessNG + +For Paperless you need an API-Key which you have to store at the item in the field `apikey`. + + +## Ping + +For Ping you need an API-Key which you have to store at the item in the field `apikey`. diff --git a/docs/tips-and-tricks.md b/docs/tips-and-tricks.md index 2719fc5..94167fb 100644 --- a/docs/tips-and-tricks.md +++ b/docs/tips-and-tricks.md @@ -113,6 +113,64 @@ docker create \ ## Get the news headlines in Homer + +### Mapping Fields +Most times, the url you're getting headlines from follows a different schema than the one expected by Homer. + +For example, if you would like to show jokes from ChuckNorris.io, you'll find that the url https://api.chucknorris.io/jokes/random is giving you info like this: + +```json +{ + "categories": [], + "created_at": "2020-01-05 13:42:22.089095", + "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png", + "id": "MR2-BnMBR667xSpQBIleUg", + "updated_at": "2020-01-05 13:42:22.089095", + "url": "https://api.chucknorris.io/jokes/MR2-BnMBR667xSpQBIleUg", + "value": "Chuck Norris can quitely sneak up on himself" +} +``` + +but... you need that info to be transformed to something like this: + +```json +{ + "title": "MR2-BnMBR667xSpQBIleUg", + "content": "Chuck Norris can quitely sneak up on himself" +} +``` + +Now, you can do that using the `mapping` field in your `message` configuration. This example would be something like this: + +```yml +message: + url: https://api.chucknorris.io/jokes/random + mapping: + title: 'id' + content: 'value' +``` + +As you would see, using the ID as a title doesn't seem nice, that's why when a field is empty it would keep the default values, like this: + +```yml +message: + url: https://api.chucknorris.io/jokes/random + mapping: + content: 'value' + title: "Chuck Norris Facts!" +``` + +and even an error message in case the `url` didn't respond or threw an error: + +```yml +message: + url: https://api.chucknorris.io/jokes/random + mapping: + content: 'value' + title: "Chuck Norris Facts!" + content: "Message could not be loaded" +``` + #### `by @JamiePhonic` Homer allows you to set a "message" that will appear at the top of the page, however, you can also supply a `url:`. diff --git a/package.json b/package.json index 65436fd..c5486bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homer", - "version": "20.06.1", + "version": "21.09.1", "license": "Apache-2.0", "scripts": { "serve": "vue-cli-service serve", @@ -8,28 +8,28 @@ "lint": "vue-cli-service lint" }, "dependencies": { - "@fortawesome/fontawesome-free": "^5.15.1", - "bulma": "^0.9.1", - "core-js": "^3.8.1", - "js-yaml": "^3.14.1", + "@fortawesome/fontawesome-free": "^5.15.4", + "bulma": "^0.9.3", + "core-js": "^3.17.3", + "js-yaml": "^4.1.0", "lodash.merge": "^4.6.2", "register-service-worker": "^1.7.2", - "vue": "^2.6.12" + "vue": "^2.6.14" }, "devDependencies": { - "@vue/cli-plugin-babel": "~4.5.9", - "@vue/cli-plugin-eslint": "~4.5.9", - "@vue/cli-plugin-pwa": "~4.5.9", - "@vue/cli-service": "~4.5.9", + "@vue/cli-plugin-babel": "~4.5.0", + "@vue/cli-plugin-eslint": "~4.5.0", + "@vue/cli-plugin-pwa": "~4.5.0", + "@vue/cli-service": "~4.5.0", "@vue/eslint-config-prettier": "^6.0.0", "babel-eslint": "^10.1.0", - "eslint": "^7.16.0", - "eslint-plugin-prettier": "^3.3.0", - "eslint-plugin-vue": "^7.3.0", + "eslint": "^6.7.2", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-vue": "^6.2.2", "prettier": "^2.2.1", "raw-loader": "^4.0.2", - "sass": "^1.30.0", - "sass-loader": "^10.1.0", + "sass": "^1.26.5", + "sass-loader": "^8.0.2", "vue-template-compiler": "^2.6.12" } } diff --git a/public/assets/additionnal-page.yml.dist b/public/assets/additionnal-page.yml.dist new file mode 100644 index 0000000..f918dc1 --- /dev/null +++ b/public/assets/additionnal-page.yml.dist @@ -0,0 +1,35 @@ +--- +# Additionnal page configuration + +# Additionnal configurations are loaded using its file name, minus the extension, as an anchor (https://#). +# `config.yml` is still used as a base configuration, and all values here will overwrite it, so you don't have to re-defined everything + + +subtitle: "this is another dashboard page" + +# This overwrites message config. Setting it to empty to remove message from this page and keep it only in the main one: +message: ~ + +# as we want to include a differente link here (so we can get back to home page), we need to replicate all links or they will be revome when overwriting the links field: +links: + - name: "Home" + icon: "fas fa-home" + url: "#" + - name: "Contribute" + icon: "fab fa-github" + url: "https://github.com/bastienwirtz/homer" + target: "_blank" # optional html a tag target attribute + - name: "Wiki" + icon: "fas fa-book" + url: "https://www.wikipedia.org/" + +services: + - name: "More applications on another page!" + icon: "fas fa-cloud" + items: + - name: "Awesome app on a second page!" + logo: "assets/tools/sample.png" + subtitle: "Bookmark example" + tag: "app" + url: "https://www.reddit.com/r/selfhosted/" + target: "_blank" diff --git a/public/assets/config.yml.dist b/public/assets/config.yml.dist index 85478ec..65c5098 100644 --- a/public/assets/config.yml.dist +++ b/public/assets/config.yml.dist @@ -56,6 +56,11 @@ links: - name: "Wiki" icon: "fas fa-book" url: "https://www.wikipedia.org/" + # this will link to a second homer page that will load config from additionnal-page.yml and keep default config values as in config.yml file + # see url field and assets/additionnal-page.yml.dist used in this example: + - name: "another page!" + icon: "fas fa-file-alt" + url: "#additionnal-page" # Services # First level array represent a group. diff --git a/src/App.vue b/src/App.vue index dc473ca..1f4f509 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,7 +13,9 @@
@@ -62,6 +64,11 @@ @@ -14,21 +18,55 @@ export default { data: function () { return { isDark: null, + faClasses: null, + titles: null, + mode: null, }; }, created: function () { - this.isDark = - "overrideDark" in localStorage - ? JSON.parse(localStorage.overrideDark) - : matchMedia("(prefers-color-scheme: dark)").matches; + this.faClasses = ["fas fa-adjust", "fas fa-circle", "far fa-circle"]; + this.titles = ["Auto-switch", "Light theme", "Dark theme"]; + this.mode = 0; + if ("overrideDark" in localStorage) { + // Light theme is 1 and Dark theme is 2 + this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1; + } + this.isDark = this.getIsDark(); this.$emit("updated", this.isDark); }, methods: { toggleTheme: function () { - this.isDark = !this.isDark; - localStorage.overrideDark = this.isDark; + this.mode = (this.mode + 1) % 3; + switch (this.mode) { + // Default behavior + case 0: + localStorage.removeItem("overrideDark"); + break; + // Force light theme + case 1: + localStorage.overrideDark = false; + break; + // Force dark theme + case 2: + localStorage.overrideDark = true; + break; + default: + // Should be unreachable + break; + } + + this.isDark = this.getIsDark(); this.$emit("updated", this.isDark); }, + + getIsDark: function () { + const values = [ + matchMedia("(prefers-color-scheme: dark)").matches, + false, + true, + ]; + return values[this.mode]; + }, }, }; diff --git a/src/components/Message.vue b/src/components/Message.vue index 5a1e0ea..00ce158 100644 --- a/src/components/Message.vue +++ b/src/components/Message.vue @@ -22,26 +22,52 @@ export default { }, data: function () { return { - show: false, message: {}, }; }, created: async function () { // Look for a new message if an endpoint is provided. this.message = Object.assign({}, this.item); - if (this.item && this.item.url) { - const fetchedMessage = await this.getMessage(this.item.url); - // keep the original config value if no value is provided by the endpoint - for (const prop of ["title", "style", "content"]) { - if (prop in fetchedMessage && fetchedMessage[prop] !== null) { - this.message[prop] = fetchedMessage[prop]; - } - } - } - this.show = this.message.title || this.message.content; + await this.getMessage(); + }, + computed: { + show: function () { + return this.message.title || this.message.content; + }, + }, + watch: { + item: function (item) { + this.message = Object.assign({}, item); + }, }, methods: { - getMessage: function (url) { + getMessage: async function () { + if (!this.item) { + return; + } + if (this.item.url) { + let fetchedMessage = await this.downloadMessage(this.item.url); + console.log("done"); + if (this.item.mapping) { + fetchedMessage = this.mapRemoteMessage(fetchedMessage); + } + + // keep the original config value if no value is provided by the endpoint + const message = this.message; + for (const prop of ["title", "style", "content", "icon"]) { + if (prop in fetchedMessage && fetchedMessage[prop] !== null) { + message[prop] = fetchedMessage[prop]; + } + } + this.message = { ...message }; // Force computed property to re-evaluate + } + + if (this.item.refreshInterval) { + setTimeout(this.getMessage, this.item.refreshInterval); + } + }, + + downloadMessage: function (url) { return fetch(url).then(function (response) { if (response.status != 200) { return; @@ -49,6 +75,15 @@ export default { return response.json(); }); }, + + mapRemoteMessage: function (message) { + let mapped = {}; + // map property from message into mapped according to mapping config (only if field has a value): + for (const prop in this.item.mapping) + if (message[this.item.mapping[prop]]) + mapped[prop] = message[this.item.mapping[prop]]; + return mapped; + }, }, }; diff --git a/src/components/services/AdGuardHome.vue b/src/components/services/AdGuardHome.vue index 6ef5302..19a2f7d 100644 --- a/src/components/services/AdGuardHome.vue +++ b/src/components/services/AdGuardHome.vue @@ -51,9 +51,9 @@ export default { }, methods: { fetchStatus: async function () { - this.status = await fetch( - `${this.item.url}/control/status` - ).then((response) => response.json()); + this.status = await fetch(`${this.item.url}/control/status`, { + credentials: "include", + }).then((response) => response.json()); }, }, }; diff --git a/src/components/services/Generic.vue b/src/components/services/Generic.vue index 3238ead..08bd3f6 100644 --- a/src/components/services/Generic.vue +++ b/src/components/services/Generic.vue @@ -1,16 +1,3 @@ - - - -*/ - - - - -