Compare commits

...

387 Commits

Author SHA1 Message Date
a4c1f6a37d Added forwarder binding to Service.vue 2022-04-28 20:08:42 -04:00
76d30be8e3 Pass new forwarder config to services, give it a default value of empty 2022-04-19 20:37:38 -04:00
240e3f0e87 Changed config and header names as per @bastienwirtz feedback 2022-04-19 20:24:51 -04:00
33f75a798a Merge branch 'main' into proxy-api 2022-04-19 20:18:39 -04:00
9c370d3c5e Merge pull request #321 from Darkham42/feature/adguard-home-customservices-doc
doc: add doc for AdGuard Home
2022-04-07 22:33:58 +02:00
7341d7634b Merge branch 'main' into feature/adguard-home-customservices-doc 2022-04-07 22:33:20 +02:00
b2a4140054 Improve with @bastienwirtz comments 2022-04-07 22:06:26 +02:00
9e1e82b0f3 Merge pull request #402 from Zareix/feature/portainer-environments
Portainer service - Select environments
2022-04-07 21:45:32 +02:00
000a46ee88 Merge pull request #410 from espilioto/main
Emby integration
2022-04-07 21:27:37 +02:00
1275a8cce5 Update src/components/services/Emby.vue
Co-authored-by: Bastien Wirtz <bastien.wirtz@gmail.com>
2022-04-01 00:07:02 +03:00
cd1fc28f51 Removed await from api call 2022-03-31 23:55:32 +03:00
5c42d50d47 No authentication required for public endpoint 2022-03-31 23:54:26 +03:00
31027f4791 Merge pull request #409 from bastienwirtz/dependabot/npm_and_yarn/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6
2022-03-31 21:59:47 +02:00
abfe72b9cf Fixed yarn lint errors 2022-03-29 11:39:15 +03:00
6dc8fa2026 Update customservices.md 2022-03-28 20:42:06 +03:00
345dd6c194 Update customservices.md 2022-03-28 20:41:19 +03:00
585844394d Initial Emby service commit 2022-03-28 20:00:17 +03:00
a25f317bee Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-26 19:46:32 +00:00
a2dfffab68 Added url and apikey config options under proxy 2022-03-20 16:32:24 -04:00
775d0a8e86 FA note chages for new users (#404)
Update link to Font Awesome to reference v5 directly
2022-03-19 19:34:16 -04:00
6351bf973c Merge pull request #401 from Zareix/fix/portainer-service
Portainer service - Fix for down endpoints
2022-03-19 12:58:34 -04:00
4e953d7c81 prettier 2022-03-19 14:12:30 +01:00
e2ebf9973b select environments 2022-03-19 13:52:41 +01:00
a7cbcc7700 fix if endpoint not up 2022-03-19 12:59:05 +01:00
049610bc91 Merge pull request #311 from Aryess/main
Fix #121 - Change default theme and layout from config
2022-03-13 15:00:09 -04:00
f398006935 Add a get started link when no configuration is found 2022-03-11 22:47:26 +01:00
db2a2af3a4 Merge pull request #393 from jamesmacwhite/ping-endpoint-urls
Check if path has data before adding a trailing slash (/)
2022-03-06 22:30:10 +01:00
2ccadd578e Merge pull request #389 from bastienwirtz/dependabot/npm_and_yarn/url-parse-1.5.10
Bump url-parse from 1.5.7 to 1.5.10
2022-03-06 22:23:43 +01:00
120ee25bf5 Merge pull request #391 from Roundaround/portainer
Added Portainer custom service
2022-03-06 22:23:34 +01:00
1340a8e6d0 Check if path has data before adding / 2022-03-06 20:04:16 +00:00
edd2c9ce2d Removed number fudging used for testing 2022-03-01 21:05:14 -05:00
a1a70d4a3c Added Portainer custom service 2022-03-01 20:55:45 -05:00
1acdbe4920 Bump url-parse from 1.5.7 to 1.5.10
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-28 04:27:15 +00:00
ba2c7c5c57 Merge pull request #385 from saschabrockel/patch-1
Fix typos in troubleshooting
2022-02-23 22:09:34 +01:00
e4b077843c Fix typos in troubleshooting 2022-02-21 22:04:51 +01:00
5cd802d157 Merge pull request #380 from FinalDoom/main
Change Radarr/Sonarr v3 to use totalRecords
2022-02-19 09:10:19 +01:00
4a526f6e7f Merge pull request #371 from Wurzelmann/patch-2
fixed typos: "additionnal" -> "additional"
2022-02-19 09:08:40 +01:00
2c52f45048 Merge pull request #369 from Wurzelmann/patch-1
fixed typo: "additionnal" -> "additional"
2022-02-19 09:06:35 +01:00
7e81828b34 Merge pull request #377 from bastienwirtz/dependabot/npm_and_yarn/follow-redirects-1.14.8
Bump follow-redirects from 1.14.7 to 1.14.8
2022-02-19 09:04:15 +01:00
78d0fc5f1b Merge pull request #383 from bastienwirtz/dependabot/npm_and_yarn/url-parse-1.5.7
Bump url-parse from 1.5.3 to 1.5.7
2022-02-19 09:01:26 +01:00
dabcc0bae1 Bump url-parse from 1.5.3 to 1.5.7
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-19 02:57:57 +00:00
c6ec28f1c5 Change Radarr/Sonarr v3 to use totalRecords as counting paged records is
wrong number
2022-02-18 06:39:27 -07:00
9bfa95963d Bump follow-redirects from 1.14.7 to 1.14.8
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-15 13:19:33 +00:00
8b9ec8465f fixed typos: "Additionnal" -> "Additional"
Fixed all occurrences of typo "Additionnal" (including file name)
2022-02-11 11:44:32 +01:00
893690cf95 Lint fixes 2022-02-10 22:07:00 +01:00
dec7e466b9 Merge pull request #365 from nthduy-deevotech/fix/sonarr-radarr-api
Support for Radarr, Sonarr V3 API
2022-02-10 21:52:05 +01:00
096c7eda48 Merge branch 'main' into fix/sonarr-radarr-api 2022-02-10 21:50:53 +01:00
d92444ec19 Merge pull request #353 from spaceneb/patch-1
Fix Possible Typo (Heath -> Health)
2022-02-10 21:45:19 +01:00
5fdf790e2c Merge pull request #364 from Zareix/feature/add-prowlarr
Added Prowlarr custom services
2022-02-10 21:43:58 +01:00
5afd21a84c Merge pull request #354 from bastienwirtz/dependabot/npm_and_yarn/follow-redirects-1.14.7
Bump follow-redirects from 1.14.1 to 1.14.7
2022-02-10 21:38:00 +01:00
51829a85c4 fixed typo: "additionnal" -> "additional"
Fixed all occurrences of a typo (additionnal).
2022-02-05 20:50:46 +01:00
6c8f9f1c5b Fix radarr legacy api call 2022-02-01 23:01:09 +01:00
f7f4ebdf66 Parse new V3 api response 2022-02-01 18:32:25 +01:00
8ede30411e Radarr and Sonarr V3 api support optional 2022-02-01 17:05:23 +01:00
cb154a6818 Update Sonarr and Radarr API 2022-02-01 16:39:15 +01:00
50b3bddff1 Added Prowlarr doc 2022-01-31 13:14:40 +01:00
990606a38a Added Prowlarr custom service 2022-01-31 13:13:28 +01:00
0aa343d744 Bump follow-redirects from 1.14.1 to 1.14.7
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.1 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.1...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-14 10:47:08 +00:00
26dbed936a Fix Possible Typo (Heath -> Health) 2022-01-14 00:45:26 -05:00
68b10120c9 Merge pull request #342 from b-t-k/patch-1
Typo: should have "s" in alias
2021-12-23 07:41:59 -08:00
9f14de32fe Merge pull request #344 from michaelkrieger/patch-1
Warn about exposed apikeys
2021-12-22 07:34:07 -08:00
ad8efdc799 Warn about exposed apikeys 2021-12-20 16:11:15 -05:00
BTK
a9cdf57043 Typo: should have "s" in alias 2021-12-15 12:58:56 -07:00
8283b8da5a Make Lidaar component use the Generic base component 2021-12-13 21:07:30 +01:00
b1c8586441 Merge pull request #329 from Panzer1119/main
Add Lidarr service
2021-12-13 11:54:04 -08:00
9c77651692 Last batch of custom services refacto. 2021-12-12 22:20:10 +01:00
b4a2db6e37 Services refactoring 2021-12-12 16:28:20 +01:00
e6ba84d35a Upgrade dependancies 2021-12-12 09:45:54 +01:00
611fe797eb Merge pull request #338 from naioja/bump_alpine_version
Using a newer version of alpine
2021-12-08 12:32:28 -08:00
e961af8255 Using a newer version of alpine
Signed-off-by: Adrian Joian <ajoian@microsoft.com>
2021-12-04 01:35:55 +01:00
0121fa8036 docs: add Lidarr service to customservices.md 2021-11-02 18:28:47 +01:00
754372579e feat: add Lidarr service 2021-11-02 18:26:33 +01:00
6e6efc7d29 doc: add doc for AdGuard Home 2021-10-27 21:53:39 +02:00
b7480f632e Merge pull request #316 from robinschneider/apikey
Fixes apikey inconsistency
2021-10-25 13:17:30 -07:00
9fce0ce5a5 Update src/components/services/OpenWeather.vue
Co-authored-by: Bastien Wirtz <bastien.wirtz@gmail.com>
2021-10-25 14:08:31 +02:00
f2c901a1ec Merge branch 'bastienwirtz:main' into apikey 2021-10-25 14:06:41 +02:00
cf33747f42 Merge pull request #274 from robinschneider/hotkey
Added custom hotkey support
2021-10-25 04:46:58 -07:00
46c9a513e5 Merge pull request #319 from t-huyeng/fix-ping-doc
fixed Ping documentation yaml
2021-10-25 04:45:18 -07:00
400cdb8f6a fixed Ping documentation yaml 2021-10-21 16:52:06 +02:00
487f954a36 Fixes apikey inconsistency 2021-10-13 13:51:39 +02:00
446e78d2ab Merged main, fixed hotkey support 2021-10-12 14:36:22 +02:00
3668050ba3 Merge branch 'bastienwirtz:main' into hotkey 2021-10-12 14:16:13 +02:00
54c19bb5f0 Merge pull request #314 from AlexFullmoon/patch-1
Update customservices.md
2021-10-12 04:59:13 -07:00
168f157cf9 Update customservices.md
Added PaperlessNG configuration to be in line with other services.
2021-10-12 11:37:30 +03:00
5db2414d05 Fix #121 - Change default theme and layout from config 2021-10-12 11:37:24 +11:00
1c0bf7132a Added requested changes 2021-10-11 23:54:34 +02:00
2f19540400 Merge branch 'bastienwirtz:main' into hotkey 2021-10-11 23:48:05 +02:00
c72acd57d0 Merge pull request #310 from ArturBa/main
Add Prometheus custom component
2021-10-11 13:23:43 -07:00
80ba98cf66 Code review fixes 2021-10-11 18:41:28 +02:00
d31a9a79c2 Merge pull request #307 from robinschneider/icon-color
Added custom fontawesome icon color option with link property
2021-10-10 23:17:41 -07:00
277dafafa9 Update docs 2021-10-10 22:20:34 +02:00
8d9cfa98bd Add Prometheus custom component 2021-10-10 22:14:33 +02:00
7a4e78e8d0 Merge branch 'bastienwirtz:main' into icon-color 2021-10-10 21:41:23 +02:00
87aadbb6df Merge branch 'bastienwirtz:main' into hotkey 2021-10-10 21:40:42 +02:00
66a434e7db Implementation status warning 2021-10-10 20:57:04 +02:00
3acfb01d99 Custom services common options documentation 2021-10-10 10:47:23 +02:00
2fba043575 Allow non json reponse in fetch. 2021-10-10 10:37:20 +02:00
efc2bbb856 Allow any service to override the credentials option 2021-10-10 10:16:18 +02:00
fea0f09045 Proxy settings documentation 2021-10-10 09:46:51 +02:00
0a3be103dc Factorize fetch options 2021-10-10 09:26:02 +02:00
a25e1b1a70 Auto PR lint 2021-10-10 09:23:35 +02:00
cc26624f39 Merge pull request #308 from robinschneider/patch-1
Spelling fix
2021-10-08 12:39:32 -07:00
d7e17e6146 Spelling fix 2021-10-08 17:50:19 +02:00
3faeac7e9f Added custom fontawesome icon color option with link property 2021-10-08 17:42:23 +02:00
b64b17a4f9 Fixed spelling 2021-10-08 15:11:00 +02:00
270e522e0e Merge branch 'bastienwirtz:main' into hotkey 2021-10-07 00:15:26 +02:00
220c60cba0 Reduce docker healthcheck frequency 2021-10-06 22:55:53 +02:00
2ca4faad9c Extendable base service for easier development. 2021-10-06 22:55:09 +02:00
c7dc6bfd0d Merge pull request #304 from xconverge/patch-1
Update Ping entry in docs
2021-10-06 12:42:22 -07:00
b2f6da0382 Cleanup 2021-10-06 09:17:46 -07:00
e58461ffe3 Update customservices readme for ping
Fixes #249

Would be improved with https://github.com/bastienwirtz/homer/pull/255
2021-10-06 09:16:17 -07:00
451b1ac624 Merge pull request #277 from bastienwirtz/dependabot/npm_and_yarn/url-parse-1.5.3
Bump url-parse from 1.5.1 to 1.5.3
2021-09-25 04:37:00 -07:00
7129af3bda Add troubleshooting section 2021-09-25 12:19:32 +02:00
1d3287dcca Apply linters 2021-09-25 12:18:13 +02:00
6173d7df60 Merge pull request #260 from vosdev/main
Add healthcheck to Dockerfile
2021-09-25 01:19:52 -07:00
b2a31c0701 Merge pull request #291 from mcclurec/radarr-sonarr-sso-fix
Add credentials: "include" back to Radarr and Sonarr
2021-09-24 23:59:38 -07:00
d6d078132b Update Sonarr.vue 2021-09-22 10:47:10 -07:00
e32643056e Add credentials: "include" back to Sonarr 2021-09-22 10:37:52 -07:00
eecd0db92e Add credentials: "include" back to Radarr` 2021-09-22 10:23:24 -07:00
d489f6ef87 Merge pull request #243 from waschinski/patch-1
Improving Adguard Home service
2021-09-15 00:40:05 -07:00
b63add2f95 Status visibility no longer depending on subtitle 2021-09-14 22:44:42 +02:00
a6b72c97d0 Merge pull request #246 from waschinski/mealie-service
Adding Mealie service
2021-09-14 13:38:23 -07:00
4eeed6596b Merge branch 'main' into patch-1 2021-09-14 08:56:06 +02:00
f11b1c9dcf Weather service refactoring 2021-09-13 23:13:26 +02:00
fd18715085 Merge pull request #181 from dickwolff/main
Added OpenWeatherMap service
2021-09-13 14:08:08 -07:00
92d5b8d424 Merge branch 'main' into main 2021-09-13 13:09:40 -07:00
bcec0449ec Bump url-parse from 1.5.1 to 1.5.3
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-13 20:04:32 +00:00
55c3ea4d92 Deps updates & lint 2021-09-13 22:03:13 +02:00
33d60aa76a Merge pull request #271 from Sharknoon/main
Fixed the height of the Dashboard Title in case of a long string
2021-09-13 12:57:58 -07:00
afe6d34ced Merge pull request #258 from mcclurec/sso-compliant-fetch-calls
SSO Compliant Fetch Calls
2021-09-13 12:55:14 -07:00
1ed0a2f387 Merge pull request #213 from simonporte/main
Fix transparency level behind links
2021-09-13 12:45:58 -07:00
117fa7d3b9 Merge pull request #263 from jeremymeyers/patch-1
typo
2021-09-13 12:43:02 -07:00
584f2b4b32 Added custom hotkey support 2021-09-11 12:42:57 +02:00
6c834c24b6 Update app.scss
Fixed the height of the header in case of a longer dashboard title
2021-09-09 16:14:49 +02:00
cb325bd58a typo 2021-08-26 21:54:59 -04:00
bbe7149d58 Update PiHole.vue 2021-08-16 14:17:15 -07:00
7efcd282bb Update AdGuardHome.vue 2021-08-16 14:16:29 -07:00
addaf36c3d Fix transparency level behind links 2021-08-16 21:08:25 +02:00
6b54eedae7 Merge pull request #254 from rvankraaij/fix-cors-radarr-sonarr
Fix cors issue with radarr and sonarr services
2021-08-15 10:28:51 -07:00
6dd8342bf0 Add healthcheck to Dockerfile 2021-08-15 15:34:17 +02:00
4852ae6b85 Update AdGuardHome.vue 2021-08-11 15:41:33 -07:00
2f6d9e1b09 Update PaperlessNG.vue 2021-08-11 15:41:11 -07:00
0dc3cea15e Update PiHole.vue 2021-08-11 15:40:22 -07:00
76a46c3507 Update Ping.vue 2021-08-11 15:38:24 -07:00
cf2fb08dc7 Update PaperlessNG.vue 2021-08-11 15:37:17 -07:00
077be43473 Update AdGuardHome.vue 2021-08-11 15:35:41 -07:00
ecec695272 include credentials while making fetch calls 2021-08-11 15:25:51 -07:00
a74fa38302 Merge branch 'main' into patch-1 2021-08-08 11:18:46 +02:00
rvk
304362adfd Fix CORS issue for Radarr and Sonarr services 2021-08-04 21:26:15 +02:00
rvk
25f99adc6c Fix CORS issue for Radarr and Sonarr services 2021-08-04 21:19:28 +02:00
b2062fb60a Merge remote-tracking branch 'upstream/main' into mealie-service 2021-07-28 16:13:50 +02:00
4386cd094b Updating configuration 2021-07-28 16:12:19 +02:00
64ac4c48d5 Statistics now also need the token for authentication 2021-07-28 16:10:11 +02:00
35926e1e6e Version bump 2021-07-14 16:48:55 +02:00
f3b3b89b7c Simplify the connectivity checker 2021-07-14 16:41:00 +02:00
a6b7db5437 Merge pull request #186 from pdevq/connectivity_checker_status
Fix ConnectivityChecker to evaluate the response status codes
2021-07-14 07:37:53 -07:00
3a8fa151f4 Improve ping service 2021-07-14 15:49:19 +02:00
92d899bd48 regroup service documentation 2021-07-14 15:48:57 +02:00
c06c0cdf9b Lint & updates 2021-07-14 12:05:53 +02:00
73a102f3fa Merge branch 'main' into mealie-service 2021-07-14 11:48:54 +02:00
f9cc1d27cc Merge pull request #239 from azrikahar/patch-1
fix card border radius when it's the only child
2021-07-14 02:23:39 -07:00
f5b467f933 Merge pull request #245 from boerniee/paperlessng-integration
Added paperless service
2021-07-14 02:13:52 -07:00
ded5228972 Merge pull request #240 from stubbfel/stubbfel-add-ping-service
Add  Ping services
2021-07-14 02:07:35 -07:00
90d6bc67c0 Merge pull request #223 from bastienwirtz/dependabot/npm_and_yarn/browserslist-4.16.6
Bump browserslist from 4.16.4 to 4.16.6
2021-07-14 01:57:49 -07:00
5d642b3674 Merge pull request #225 from bastienwirtz/dependabot/npm_and_yarn/dns-packet-1.3.4
Bump dns-packet from 1.3.1 to 1.3.4
2021-07-14 01:57:40 -07:00
c98f88eaf2 Merge pull request #228 from bastienwirtz/dependabot/npm_and_yarn/ws-6.2.2
Bump ws from 6.2.1 to 6.2.2
2021-07-14 01:57:27 -07:00
b1ccd8d6b2 Merge pull request #233 from bastienwirtz/dependabot/npm_and_yarn/postcss-7.0.36
Bump postcss from 7.0.35 to 7.0.36
2021-07-14 01:57:14 -07:00
c4cae400d5 Merge pull request #219 from tpansino/support-message-icon-url
Support passing FA icon in message URL payload
2021-07-14 01:55:59 -07:00
9e1b1bd1d2 Merge pull request #178 from twolaw/healthcheck2
Create Radarr, Sonarr & Medusa services
2021-07-14 01:52:35 -07:00
c3878bca0b Adding Mealie service 2021-07-13 19:16:15 +02:00
bebb6953cb Adding status "unknown"
Changing code as per linter
2021-07-10 09:58:17 +02:00
3832025b0c Improving Adguard Home service
Showing "x.x% blocked" similar to how the PiHole service is doing it.
The status text will no longer be `true/false` but `enabled/disabled`.
Endpoints require to be logged in so fetch does now send cookies to prevent 403s.
2021-07-10 02:02:57 +02:00
68955dc1d3 Add Ping services
a  service (type) which check if the given url as available or not. if the service is  available then set the status to enable other to disable
2021-06-28 23:20:20 +02:00
f9ebff9311 fix card border radius when it's the only child 2021-06-24 09:05:13 +08:00
24229b5411 Added paperless service with documentation 2021-06-23 17:06:19 +02:00
adacb3c33f Bump postcss from 7.0.35 to 7.0.36
Bumps [postcss](https://github.com/postcss/postcss) from 7.0.35 to 7.0.36.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/7.0.35...7.0.36)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-17 04:22:49 +00:00
0178d73f66 Bump ws from 6.2.1 to 6.2.2
Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-06 05:50:18 +00:00
a2b59fb6c1 Bump dns-packet from 1.3.1 to 1.3.4
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-28 19:03:13 +00:00
764b470209 Bump browserslist from 4.16.4 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.4 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.4...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-26 11:32:52 +00:00
742ae4eb52 Support passing FA icon in message URL payload 2021-05-15 02:20:51 -07:00
996011956b Merge pull request #214 from pkoenig10/patch-1
Add crossorigin use-credentials attribute to manifest tag
2021-04-30 09:28:27 -07:00
2428998313 Add crossorigin use-credentials attribute to manifest tag 2021-04-25 14:38:41 -07:00
7596bc527f Fix null error releated to refreshInterval + cleanup. Fix #210 2021-04-21 22:10:51 -07:00
aadd8b49cc Make sure dependencies are compatible with vue-cli. Fix #206 2021-04-21 21:37:51 -07:00
64f189c9dc Update deps 2021-04-08 22:06:20 -07:00
4399f5fade pihole, medusa, radarr/sonarr services help 2021-03-11 16:12:17 +01:00
275a335cce tag moved to bottom 2021-03-11 16:12:16 +01:00
b1de1f9e08 medusa service 2021-03-11 16:12:15 +01:00
0211da26c8 radarr, sonarr services 2021-03-11 16:12:15 +01:00
b6c667a129 Merge pull request #197 from GaaH/add-auto-theme-mode
cycle between automatic-light-dark themes
2021-03-09 21:33:15 -08:00
edc336bba6 yarn lint 2021-03-09 18:32:38 +01:00
c0044cc765 title attribute that display current theme 2021-03-09 18:32:38 +01:00
4a1e8717e9 cycle between automatic-light-dark themes 2021-03-09 18:32:38 +01:00
66eace9e95 Merge pull request #196 from GaaH/use-logo-in-groups
Use logo in groups
2021-03-08 22:25:35 -08:00
ba07da6b10 Avoid full reload when swithcing page. 2021-03-06 22:50:58 -08:00
00b46a6dde Merge pull request #177 from luixal/multiple-pages
Adds multiple pages based on different config files
2021-03-06 22:09:58 -08:00
8e09331379 documenting logo in groups 2021-03-06 14:03:40 +01:00
42477020cf add option to use logo instead of icons in groups 2021-03-06 13:56:44 +01:00
cc7ff88552 Merge pull request #175 from luixal/message-remote-fields-mapping
Adds mapping remote field to Homer expected ones when loading message from URL
2021-03-04 18:40:07 -08:00
c6267296ec Changes relative paths to hash. Avoids problems with #8 2021-02-23 20:50:32 +01:00
9542de6eb2 Fixes identations and link docs. 2021-02-23 20:17:51 +01:00
7bcfce6bda Merge pull request #174 from GeorgeSG/main
Fix #167: align item name when subtitle is empty
2021-02-17 21:44:23 -08:00
2c0cb7ad55 Update dependencies 2021-02-15 22:23:54 -08:00
9ba0d54e22 Improved docker command. 2021-02-15 22:23:16 -08:00
a5fe53beb2 Fix ConnectivityChecker to evaluate the response status codes. 2021-01-30 00:17:24 -05:00
97f0c43ccc Added doc for locationID 2021-01-28 13:02:18 +01:00
551e32e203 Added location id 2021-01-28 12:58:29 +01:00
fb158d4767 Error handling and fixed link to city. 2021-01-13 22:19:17 +01:00
9e0ef05efe Removed URL and made fixed. 2021-01-13 21:27:19 +01:00
dfb0b14626 Fixed some typo's. 2021-01-13 21:25:07 +01:00
fd12de9ebd Improvements 2021-01-13 21:22:35 +01:00
b79561bc9c Format file. 2021-01-12 11:39:27 +01:00
593f8afc90 Added OpenWeather service component. 2021-01-12 11:35:57 +01:00
b6782c92b5 Removes forgotten console.log 2021-01-07 21:26:22 +01:00
1ddf394176 Refactors created function (splits logic for getting message) and adds timeout according to refreshInterval field 2021-01-07 21:23:13 +01:00
e3bd2ecc2c Adds multiple pages based on different config files 2021-01-07 09:39:58 +01:00
6d29bc27e7 Adds mapping remote field to Homer expected ones when loading message from url 2021-01-04 09:43:58 +01:00
3786f80dae Replace licence placeholder. Fix #142 2020-12-23 10:55:05 -08:00
6f2b141bf7 Fix #167: align item name when subtitle is empty 2020-12-23 16:11:47 +02:00
c1b5f6adab Fix #171: empty search opens first item 2020-12-19 15:59:01 -08:00
168810d76f Fix #173: invisible tag border. 2020-12-19 15:57:16 -08:00
59db9d2e12 Depenencies update 2020-12-19 15:56:22 -08:00
31bd77c81d Adding search url support 2020-12-11 11:30:59 -08:00
16a86df3e4 Merge pull request #169 from bramceulemans/extended-pihole-component
Added extended PiHole statistics
2020-12-09 15:16:19 -08:00
4ce53b68ea Fixed wrongly removed computed property 2020-12-10 00:07:53 +01:00
f81dc6f488 Fixed wrongly removed computed property 2020-12-10 00:05:30 +01:00
273a268ac4 Remove unused computed property 2020-12-09 22:57:35 +01:00
c5eab80d76 Added extended PiHole statistics 2020-12-08 13:26:18 +01:00
71a7d3cce4 Fix documentation link. 2020-12-07 22:57:44 -08:00
12063dafef Apply lint fix 2020-12-07 22:53:55 -08:00
2bd57d17fd Merge pull request #160 from simonporte/AdGuard-Home
Create AdGuard Home service
2020-12-07 22:13:50 -08:00
4c1f6c4329 Merge pull request #166 from jo-me/search-for-subtitle
Include subtitles in search
2020-12-07 21:42:43 -08:00
6fe21127f5 Merge branch 'main' into search-for-subtitle 2020-12-02 20:35:06 +01:00
a2481f7317 Merge pull request #164 from jo-me/fix-search-result-key
Fix item key for horizontal layout
2020-12-01 18:39:09 -08:00
e1ecf86ffd Change service iteration key to index 2020-11-29 20:43:08 +01:00
1c451e119e Include subtitles in search 2020-11-22 14:00:29 +01:00
1b41b46193 Fix item key for horizontal layout 2020-11-22 13:42:15 +01:00
a2e28229a9 Merge pull request #159 from simonporte/main
Minor edits on readme and documentation
2020-11-16 22:06:14 -08:00
33750a4cbc Create AdGuard Home service 2020-11-14 14:03:37 +01:00
86f4680a5b Minor edits on readme and documentation 2020-11-14 13:25:01 +01:00
ade77e4257 Merge pull request #153 from taigrr/appcolor
feat: Enables setting colors for individual cards
2020-11-10 21:52:16 -08:00
f8d0761d0c Merge pull request #156 from my-flow/main
Use gender-neutral terminology
2020-11-02 20:48:33 -08:00
c701075dfa Use gender-neutral terminology
Fix #155
2020-11-02 22:56:39 +01:00
be30825917 docs: document background property for cards 2020-10-28 18:44:55 -07:00
23eb5b3fb7 feat: Enables setting colors for individual cards 2020-10-28 18:42:29 -07:00
ff0b5150f1 Merge pull request #151 from taigrr/spellcheck
chore: applies spell-check against entire repo (aspell)
2020-10-26 14:04:15 -07:00
5a85322c92 Merge pull request #146 from taigrr/fixDuplicateKeys
bug: Fix duplicate URL keys #145
2020-10-26 14:02:54 -07:00
1f12c43da8 Fix duplicate URL keys #145 2020-10-26 03:19:40 -07:00
2662b17049 chore: applies spell-check against entire repo (aspell) 2020-10-25 21:27:09 -07:00
d1b29caaa6 Update README.md 2020-10-23 22:57:35 -07:00
e75945851c Fix screenshot url 2020-10-23 22:56:51 -07:00
13071ae3d1 Merge pull request #140 from Genymobile/dynamic-services
Custom service components
2020-10-23 18:29:14 -07:00
5b727eee02 Merge branch 'main' into dynamic-services 2020-10-23 18:29:06 -07:00
37dfd2a132 Merge pull request #139 from Genymobile/update-deps-and-fix-code-styles
Update deps & fix lint issues
2020-10-23 18:26:41 -07:00
9a14de007e Implement custom service component 2020-10-23 18:16:16 -07:00
ed8b17e0df Update deps & fix lint issues 2020-10-23 15:24:16 -07:00
c368290e32 Merge pull request #127 from NotWoods/pwa-icons
Reduce number of PWA icons, fix paths
2020-10-23 14:46:36 -07:00
00b069fc6f Merge pull request #136 from gabe565/message-icon
Add support for a message icon
2020-10-23 14:32:49 -07:00
40d3e8de76 Pull all values from manifest 2020-10-23 13:59:55 -07:00
1017cc9864 Update vue config 2020-10-23 13:56:51 -07:00
b04e718367 Update paths and style 2020-10-23 13:44:46 -07:00
ab8136bab7 Merge pull request #138 from taigrr/master
Fix config doc for sample application
2020-10-23 09:29:37 -07:00
488e4bbe7f docs: Fixes spelling error in filename 2020-10-18 22:43:23 -07:00
62ec6fa099 docs: Updates code-fork to code-branch 2020-10-18 22:42:43 -07:00
67fd101a38 Dark theme improvements 2020-10-16 21:58:37 -07:00
0e045b4c55 Add support for a message icon 2020-10-15 13:22:51 -05:00
e608701404 Minor css adjustement.
Fixes #114 & #117
2020-10-14 22:41:55 -07:00
2b5f88db18 Merge pull request #135 from AlexGustafsson/master
Update @fortawesome/fontawesome-free to 5.15.1
2020-10-14 22:04:28 -07:00
b8c81389dc Update @fortawesome/fontawesome-free to 5.15.1 2020-10-12 16:46:23 +02:00
5b05842512 Merge pull request #129 from Crayon2000/fix-typo
Fix typos
2020-09-13 17:52:19 -07:00
80f6a13140 Fix typos
Changed Optionnal to Optional
2020-09-11 17:14:57 -04:00
f2eacfcba1 Merge pull request #120 from tpansino/feature/custom-document-title
Feature/custom document title
2020-09-05 10:08:58 -07:00
a02961b70b Add documentTitle example to documentation 2020-09-04 16:09:43 -07:00
9d0ec9e348 Support custom document title 2020-09-04 16:07:20 -07:00
ffe3404a2a Simplify service structure 2020-09-04 15:52:34 -07:00
bcf0e1bec2 Merge pull request #119 from tpansino/feature/custom-card-colors
Feature/custom card colors
2020-09-04 15:30:01 -07:00
f70fc3ecae Reduce number of PWA icons, fix paths 2020-09-02 22:02:14 -07:00
e9afa4d7dd Rewrite custom card colors to use custom CSS classes instead of styles 2020-09-02 00:34:37 -07:00
fbe9338fd3 Merge remote-tracking branch 'upstream/master' into feature/custom-card-colors 2020-09-01 23:47:59 -07:00
83665e4f48 Merge pull request #113 from gabe565/custom-stylesheet
Add support for a custom stylesheet
2020-08-29 10:50:42 -07:00
8e5ee54a78 🔧 Make stylesheet config be an array of files 2020-08-29 02:43:02 -05:00
71cf63eb3b 📝 Document new stylesheet config 2020-08-29 02:21:09 -05:00
6777bc347b Add support for a custom stylesheet 2020-08-29 02:21:09 -05:00
2644101276 Merge pull request #118 from vosdev/patch-1
Add chrome extension for a custom new tab page
2020-08-28 23:36:41 -07:00
607fb898f8 Merge pull request #115 from gabe565/navbar-alignment
Fix alignment when navbar link name is empty
2020-08-28 23:35:08 -07:00
2e7eb41f8c Merge pull request #112 from gabe565/fa-fw-logo-alignment
Remove fa-fw from main logo to fix Font Awesome duotone issues
2020-08-28 23:15:00 -07:00
db738288fa Merge pull request #101 from simonporte/master
Added docker-compose.yml
2020-08-28 23:12:22 -07:00
7e5ad02248 Add example card colors to configuration.md 2020-08-10 23:31:34 -07:00
118d3e5ac0 Rename color: to colors: 2020-08-10 17:25:43 -07:00
687a9e4086 Add support for custom card colors 2020-08-09 23:03:00 -07:00
4f04feb2da Add chrome extension for a custom new tab page
I finally found a chrome extension that gives the same functionality as the firefox extension for the custom new tab page. I have updated the docs accordingly!
2020-08-03 20:00:52 +02:00
68c36d6c54 🐛 Fix alignment when navbar link name is empty 2020-07-28 17:18:41 -05:00
e4537f134b Added docker-compose.yml
Plus short explanation in readme
Changed the commit as pointed by @Shuro
Explained readme more as asked by @bastienwirtz
2020-07-24 14:38:42 +02:00
239ef1688d 🐛 Remove fa-fw from main logo to fix Font Awesome duotone issues 2020-07-22 15:06:26 -05:00
154e6efe80 Adding support for section backgrounf images 2020-07-15 14:26:01 -07:00
d05b8d3bf0 Uniformise dockerfile 2020-07-15 14:23:29 -07:00
0ae40f78f8 Auth redirection support 2020-07-13 09:16:47 -07:00
6de53c49b3 Merge pull request #108 from GlennToms/master
Added su-exec to ARM dockerfiles and removed USER line
2020-07-12 21:09:42 -07:00
ab40c4e007 Adding support for release build. 2020-07-12 20:12:53 -07:00
1f92e1746d Dependancies update 2020-07-12 20:12:31 -07:00
8ae1fe8a4e Lint clean 2020-07-12 20:11:54 -07:00
fd9237eb52 Added su-exec to ARM dockerfiles and removed USER line 2020-07-09 18:47:33 +01:00
4bfcc5bc95 Merge pull request #100 from fbartels/start_url
Explicitly define start_url
2020-07-04 18:11:08 -07:00
d3da4cfe93 Merge pull request #105 from fbartels/deduplicate
Removing duplicated dependencies
2020-07-04 18:10:22 -07:00
d1cc18761f deduplicate dependencies with https://github.com/atlassian/yarn-deduplicate
Signed-off-by: Felix Bartels <felix@host-consultants.de>
2020-07-01 11:56:54 +02:00
a503c5743e Explicitly define start_url
Relates to #99

Signed-off-by: Felix Bartels <felix@host-consultants.de>
2020-06-29 20:03:52 +02:00
a9aed9f9e2 Modernize readme 2020-06-28 21:42:07 -07:00
7ef65940ee Fix dynamic message override. 2020-06-28 18:31:42 -07:00
d1b9dea287 Donut's back! 2020-06-28 15:24:44 -07:00
ae73d7a5a0 Merge pull request #97 from gabe565/fa-fw
Add fa-fw to icons by default to fix variable width icons
2020-06-28 11:55:50 -07:00
25b6367aa1 Merge pull request #94 from bastienwirtz/avoid-file-volume
Regroup all editable files in one place
2020-06-28 11:07:37 -07:00
b102c9b2b3 Regroup all editable files in one place 2020-06-28 11:05:40 -07:00
da6e676d6e 💄 Add fa-fw to icons by default to fix variable width icons
Fixes #93
2020-06-25 17:56:26 -05:00
796a16c8da Fix Github action trigger syntax 2020-06-19 08:22:34 -07:00
8d7ca88c46 Merge branch 'master' of github.com-perso:bastienwirtz/homer 2020-06-19 08:03:55 -07:00
b242511101 Merge pull request #90 from JamiePhonic/patch-1
Add new trick: news as dynamic message
2020-06-19 08:02:50 -07:00
638dee7704 Merge pull request #87 from GlennToms/master
Added ENV PORT options and EXPORT PORT
2020-06-19 08:01:44 -07:00
fea05ea099 Release on tag event. 2020-06-19 07:56:17 -07:00
29d6b359ab Added entrypoint script and Dockerfile port&volume 2020-06-18 15:46:33 +01:00
5f71d1ea01 Merge branch 'master' of github.com:bastienwirtz/homer 2020-06-18 14:29:33 +01:00
af663d3376 Add news as dynamic message
Adds a method to dynamically replace the "message" in homer with a news item from a chosen RSS feed!
2020-06-14 21:05:51 +01:00
aac817970c Merge pull request #85 from bastienwirtz/splitted-documentation
Split documentation into separate files
2020-06-13 16:26:40 -07:00
b3635ea460 remove duplicated part 2020-06-13 16:24:26 -07:00
481ab9a01b Added ENV PORT options and EXPORT PORT 2020-06-12 17:50:07 +01:00
1bc75494ca Split documentation into separate files 2020-06-11 22:48:22 -07:00
cad54a560f Merge pull request #84 from calvinbui/master
Dockerfile Improvements
2020-06-11 21:50:00 -07:00
e6596ca6ee freeze-lockfile option on yarn 2020-06-11 11:42:56 +10:00
d10b219db5 copy artifacts later in the build step with permissions 2020-06-11 11:32:24 +10:00
ac1442c640 use env for USER instruction 2020-06-11 11:31:46 +10:00
a23914d573 specify all icons path to fix pwa issue, 2020-06-09 22:30:28 -07:00
9e4fe0d227 Customizable number of columns 2020-06-09 21:55:29 -07:00
e9113b48ce Optionnal connectivity check 2020-06-09 21:55:29 -07:00
1a42e30a17 Adding external config support 2020-06-09 21:55:29 -07:00
52ed5af607 Merge pull request #81 from timmillwood/patch-2
Make Homer more "web app" friendly
2020-06-09 18:14:15 -07:00
cd688ef12c Merge pull request #80 from timmillwood/patch-1
Update README.md
2020-06-08 08:35:36 -07:00
344c367ccd Added appleMobileWebApp definitions 2020-06-08 11:31:55 +01:00
9d25f392ec Reverted the apple web app meta tags 2020-06-08 11:26:17 +01:00
df66067c5e Make Homer more "web app" friendly 2020-06-08 10:07:17 +01:00
6c305c61ee Update README.md 2020-06-07 19:33:01 +01:00
897f67f0b6 Update docker documentation 2020-06-06 23:15:27 -07:00
97fa090e21 Adding gitter chat link 2020-06-06 17:35:36 -07:00
b0a0fdaa3b Allow subfolder usage 2020-06-06 17:05:58 -07:00
bd9109425a Display parsing error 2020-06-06 17:01:11 -07:00
10ea23a01d Update dependencies 2020-06-06 16:56:46 -07:00
e1470a8c42 Adding more documentation 2020-06-06 16:06:56 -07:00
51e02bed72 Merge pull request #79 from traeblain/bugfix-icons
Vue PWA Default Icon Paths are Incorrect
2020-06-06 15:33:44 -07:00
7012102454 Merge pull request #69 from sergeifilippov/patch-2
Typo
2020-06-06 15:31:09 -07:00
de0a746a27 Vue PWA Default Icon Paths are Incorrect
Corrected the icons due to the default iconPaths setting for Vue-PWA plugin is not where the icons actually are.  This corrects issue.
2020-06-05 23:13:31 -05:00
a747c58498 Typo 2020-06-02 14:25:25 +12:00
c712d7c780 Fix missing directory in artifact creation. 2020-05-31 00:08:18 -07:00
5fa6b6cfa6 Merge pull request #62 from bastienwirtz/dev/build-system
Build system integration using vue-cli.
2020-05-30 23:22:02 -07:00
9052ec59b7 Fix logo always shown issue. 2020-05-30 23:16:55 -07:00
2301d8919c Adding PWA icons for proper pwa support 2020-05-30 19:36:35 -07:00
1875c17aa5 Full offline support (local fonts) 2020-05-30 17:12:06 -07:00
3bf0edcf13 Adding missing rel attribute on links. 2020-05-30 15:01:09 -07:00
9814a037a5 Pure CSS font awesome icon 2020-05-29 18:21:32 -07:00
e11427508a Remove config.yml from versionning. 2020-05-27 20:58:20 -07:00
b9c5fcf085 Build system integration using vue-cli. 2020-05-25 15:07:03 -07:00
ab7ac44c19 Merge pull request #48 from issmirnov/patch-1
Add "Style Options" section for bulma CSS
2020-05-25 11:42:05 -07:00
554fc76d75 Merge pull request #60 from sylv-io/html-title
app.js: html title based on configuration
2020-05-25 11:40:14 -07:00
5530707ba2 app.js: html title based on configuration
Use the title and subtitle defined in the configuration file as
html title.

Signed-off-by: sylv <sylv@sylv.io>
2020-05-22 15:38:46 +02:00
0d6a177bae Docker: disable file listing on arm build 2020-05-21 09:29:20 -07:00
2b47d91464 Merge pull request #57 from Phill93/disable-directory-listing
disable directory listing
2020-05-20 21:08:52 -07:00
514b68eae3 Fix arg syntax 2020-05-19 23:05:37 -07:00
cf2df79869 Merge pull request #56 from nightah/fix-docker-multiarch-builds
Fix docker multiarch builds
2020-05-19 22:39:14 -07:00
94f44a4fe8 Merge branch 'master' into fix-docker-multiarch-builds 2020-05-20 15:32:19 +10:00
5a5412c5d1 Fixing multi-arch container builds 2020-05-20 15:31:02 +10:00
525120de61 Fix multiarch docker build. 2020-05-19 21:53:11 -07:00
c92844bd0e Fix multiarch docker build. 2020-05-19 21:32:55 -07:00
e6701f57cc Merge pull request #49 from nightah/docker-multiarch-builds
Build multi-arch container with manifest
2020-05-19 09:13:15 -07:00
694274f005 disable directory listing 2020-05-17 13:57:44 +02:00
55e0df5386 Merge pull request #53 from fbartels/readme
Lint Readme with markdownlint
2020-05-16 23:35:25 -07:00
7bb903e693 Lint Readme with markdownlint
Fix spelling

Signed-off-by: Felix Bartels <felix@host-consultants.de>
2020-05-16 08:41:47 +02:00
abe6df52b5 Build multi-arch container with manifest 2020-05-08 21:59:39 +10:00
fcf5f412a5 Add "Style Options" section for bulma CSS
This project uses bulma CSS, but this fact is not obvious without reading the code. This commit adds explicit pointers on how to use the `tagstyle` field.
2020-05-05 23:05:53 -07:00
b317857892 Merge pull request #44 from issmirnov/patch-1
Fix font icons typos
2020-05-03 22:14:08 -07:00
aabb7d538e Fix font icons typos
Font icons will not load with the appropriate `fa` `fas` prefixes. Sample config file is OK, but reamde is not.
2020-05-03 21:37:16 -07:00
c424ddba72 Merge pull request #34 from Sajfer/dockerignore
Add .dockerignore
2020-04-28 22:27:02 -07:00
45d38d1226 Fixed text-overflow 2020-04-28 21:48:30 -07:00
5861f0f899 Update demo link. 2020-04-28 21:47:43 -07:00
d2bf5e5f62 Merge pull request #37 from jozefs/master
Add keyboard shortcuts to navigate to the first search result.
2020-04-13 09:46:48 -07:00
a4de4a3a71 Add keyboard shortcuts to navigate to the first search result. 2020-04-04 10:59:10 +01:00
0503e77861 Change message box background according to the theme. 2020-03-25 23:03:36 -07:00
4aeaeb09ad Fix contributing link 2020-03-25 23:03:36 -07:00
990b6c3d46 Merge pull request #36 from jozefs/keyboard-shortcuts
Add keyboard shortcuts for searching
2020-03-25 23:03:25 -07:00
facaa346b7 Add .dockerignore
Add dockerignore file to cleanup the resulting docker image.
2020-03-23 16:00:45 +01:00
6eba37a370 Add keyboard shortcuts for searching.
/ starts searching. Escape clears the search terms.
2020-03-21 11:47:12 +00:00
115 changed files with 13189 additions and 10262 deletions

3
.browserslistrc Normal file
View File

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

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
assets/*
dockerfile
*.md
.git
screenshot.png
node_modules

15
.eslintrc.js Normal file
View File

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

View File

@ -6,15 +6,13 @@ Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
## Checklist:
- [ ] I read & comply with the [contributing guidelines](https://github.com/bastienwirtz/homer/blob/master/.github/CONTRIBUTING.md)
- [ ] I've read & comply with the [contributing guidelines](https://github.com/bastienwirtz/homer/blob/main/CONTRIBUTING.md)
- [ ] I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
- [ ] I have made corresponding changes the documentation (README.md).
- [ ] I've check my modifications for any breaking change, especially in the `config.yml` file
- [ ] I have made corresponding changes to the documentation (README.md).
- [ ] I've checked my modifications for any breaking changes, especially in the `config.yml` file

31
.github/workflows/integration.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Integration
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: yarn lint

40
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,40 @@
# Publish pre-build release
name: Upload Release Asset
on:
push:
tags: [v*]
jobs:
build:
name: Upload Release Asset
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build project
run: |
yarn install
yarn build
- name: Create artifact
working-directory: "dist"
run: zip -r ../homer.zip ./*
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
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

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# App configuration
config.yml
.drone.yml

View File

@ -6,27 +6,23 @@ First off, thank you for considering contributing to Homer!
### Project philosophy
Homer is meant to be a light and very simple dashboard that keeps all your usefull utilities at hands. The few features implemented in Homer focus on
Homer is meant to be a light and very simple dashboard that keeps all your useful utilities at hands. The few features implemented in Homer focus on
UX and usability. If you are looking for a full featured dashboard, there is tons of great stuff out there like https://heimdall.site/, https://github.com/rmountjoy92/DashMachine or https://organizr.app/.
- Configuration is stored in a simple config file, avoiding the need for a backend/database while making possible to use versionning or [config template](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html).
- 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
### Code of conduct and guidelines
First of all, we expect everyone (contributors and maintainers alike) to respect the [Code of conduct](https://github.com/bastienwirtz/homer/blob/master/CODE_OF_CONDUCT.md). It is not a recomandation, it is mandatory.
First of all, we expect everyone (contributors and maintainers alike) to respect the [Code of conduct](https://github.com/bastienwirtz/homer/blob/main/CODE_OF_CONDUCT.md). It is not a recommendation, it is mandatory.
For all contributions, please respect the following guidelines:
* Each pull request should implement ONE feature or bugfix. If you want to add or fix more than one thing, submit more than one pull request.
* Do not commit changes to files that are irrelevant to your feature or bugfix (eg: `.gitignore`).
* Do not commit changes to files that are irrelevant to your feature or bugfix (e.g. `.gitignore`).
* Do not add unnecessary dependencies.
* Be aware that the pull request review process is not immediate, and is generally proportional to the size of the pull request.
@ -34,14 +30,15 @@ For all contributions, please respect the following guidelines:
### Discuss about ideas
If you want to add a feature, it's often best to talk about it before starting working on it and submitting a pull request. It's not mandatory at all, but
If you want to add a feature, it's often best to talk about it before starting to work on it and submitting a pull request. It's not mandatory at all, but
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/master/.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:

View File

@ -1,15 +1,33 @@
FROM alpine:3.11
# build stage
FROM node:lts-alpine as build-stage
COPY ./ /www/
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
# production stage
FROM alpine:3.15
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 darkhttpd
apk add -U --no-cache su-exec darkhttpd
USER darkhttpd
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
ENTRYPOINT ["darkhttpd","/www/"]
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:${PORT}/ || exit 1
EXPOSE ${PORT}
VOLUME /www/assets
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]

43
Dockerfile.arm32v7 Normal file
View File

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

43
Dockerfile.arm64v8 Normal file
View File

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

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2018 Bastien Wirtz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

217
README.md
View File

@ -1,122 +1,137 @@
# Homer
A dead simple static **HOM**epage for your serv**ER** to keep your services on hand, from a simple `yaml` configuration file.
<h1 align="center">
<img
width="180"
alt="Homer's donut"
src="https://raw.githubusercontent.com//bastienwirtz/homer/main/public/logo.png">
<br/>
Homer
</h1>
**Check out the live demo [here](https://homer-demo.netlify.com/).**
<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>
If you need authentication support, you're on your own (it can be secured using a web server auth module or exposing it only through a VPN network / SSH tunnel, ...)
<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>
</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://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>
<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>
</p>
![screenshot](https://raw.github.com/bastienwirtz/homer/master/screenshot.png)
<p align="center">
<img src="https://raw.github.com/bastienwirtz/homer/main/docs/screenshot.png" width="100%">
</p>
## Roadmap
## Table of Contents
- [ ] Colors / theme customization
- [ ] Enable PWA support (making possible to "install" - add to homescreen - it)
- [ ] Improve maintenability (external library import & service workers cached file list.)
- [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)
## Installation
## 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.
## 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**.
See [documentation](docs/configuration.md) for information about the configuration (`assets/config.yml`) options.
### Using docker
To launch container:
```sh
sudo docker run -p 8080:8080 -v /your/local/config.yml:/www/config.yml -v /your/local/assets/:/www/assets b4bz/homer:latest
docker run -d \
-p 8080:8080 \
-v </your/local/assets/>:/www/assets \
--restart=always \
b4bz/homer:latest
```
### Manually
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" [...]`).
**How to build / install it?** There is no build system (😱), use it like that! It's meant to be stupid simple & zero maintenance required. Just copy the static files somewhere, and visit the `index.html`.
## Configuration
Title, icons, links, colors, and services can be configured in the `config.yml` file, using [yaml](http://yaml.org/) format.
### Using docker-compose
The `docker-compose.yml` file must be edited to match your needs.
Set the port and volume (equivalent to `-p` and `-v` arguments):
```yaml
---
# Homepage configuration
# See https://fontawesome.com/icons for icons options
title: "Simple homepage"
subtitle: "Homer"
logo: "assets/homer.png"
# Alternatively a fa icon can be provided:
# icon: "fas fa-skull-crossbones"
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.header:
# Optional message
message:
# url: "https://<my-api-endpoint>" # Can fetch information from an endpoint to override value below.
style: "is-warning"
title: "Optional message!"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque risus mi, tempus quis placerat ut, porta nec nulla. Vestibulum rhoncus ac ex sit amet fringilla. Nullam gravida purus diam, et dictum felis venenatis efficitur. Aenean ac eleifend lacus, in mollis lectus. Donec sodales, arcu et sollicitudin porttitor, tortor urna tempor ligula."
# Optional navbar
# links: [] # Allows for navbar (dark mode, layout, and search) without any links
links:
- name: "ansible"
icon: "fa-github"
url: "https://github.com/xxxxx/ansible/"
target: '_blank' # optionnal html a tag target attribute
- name: "Wiki"
icon: "fa-book"
url: "https://wiki.xxxxxx.com/"
# 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: "DevOps"
icon: "fa-code-fork"
items:
- name: "Jenkins"
logo: "/assets/tools/jenkins.png"
# Alternatively a fa icon can be provided:
# icon: "fab fa-jenkins"
subtitle: "Continuous integration server"
tag: "CI"
url: "#"
target: '_blank' # optionnal html a tag target attribute
- name: "RabbitMQ Management"
logo: "/assets/tools/rabbitmq.png"
subtitle: "Manage & monitor RabbitMQ server"
tag: "haproxy"
# Optional tagstyle
tagstyle: "is-success"
url: "#"
- name: "Monitoring"
icon: "fa-heartbeat"
items:
- name: "M/Monit"
logo: "/assets/tools/monit.png"
subtitle: "Monitor & manage all monit enabled hosts"
tag: "monit"
url: "#"
- name: "Grafana"
logo: "/assets/tools/grafana.png"
subtitle: "Metric analytics & dashboards"
url: "#"
- name: "Kibana"
logo: "/assets/tools/elastic.png"
subtitle: "Explore & visualize logs"
tag: "elk"
url: "#"
- name: "Website monitoring"
logo: "/assets/tools/pingdom.png"
subtitle: "Pingdom public reports overview"
tag: "CI"
url: "#"
volumes:
- /your/local/assets/:/www/assets
ports:
- 8080:8080
```
If you choose to fetch message information from an endpoint, the output format should be:
To launch container:
```json
{
"style": null,
"title": "Lorem ipsum 42",
"content": "LA LA LA Lorem ipsum dolor sit amet, ....."
}
```sh
cd /path/to/docker-compose.yml
docker-compose up -d
```
`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).
Default assets will be automatically installed in the `/www/assets` directory. Use `UID` and/or `GID` env var to change the assets owner, also in `docker-compose.yml`:
```yaml
environment:
- UID=1000
- GID=1000
```
### 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 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 ...
```
### Build manually
```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.

372
app.css
View File

@ -1,372 +0,0 @@
@charset "UTF-8";
/* raleway-regular - latin */
@font-face {
font-family: "Raleway";
font-style: normal;
font-weight: 400;
font-display: swap;
src: local("Raleway"), local("Raleway-Regular"), url("./webfonts/raleway/raleway-v14-latin-regular.woff2") format("woff2"), url("./webfonts/raleway/raleway-v14-latin-regular.woff") format("woff");
/* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* lato-regular - latin */
@font-face {
font-family: "Lato";
font-style: normal;
font-weight: 400;
font-display: swap;
src: local("Lato Regular"), local("Lato-Regular"), url("./webfonts/lato/lato-v16-latin-regular.woff2") format("woff2"), url("./webfonts/lato/lato-v16-latin-regular.woff") format("woff");
/* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
html {
height: 100%;
}
body {
font-family: "Raleway", sans-serif;
height: 100%;
}
body #app {
min-height: 100%;
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
background-color: #f5f5f5;
color: #363636;
}
body #app a:hover {
color: #363636;
}
body #app .title {
color: #303030;
}
body #app .subtitle {
color: #424242;
}
body #app .card {
background-color: #ffffff;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
}
body #app .card:hover {
background-color: #ffffff;
}
body #app .footer {
background-color: #ffffff;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
}
@media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) {
body #app {
background-color: #f5f5f5;
color: #363636;
}
body #app a:hover {
color: #363636;
}
body #app .title {
color: #303030;
}
body #app .subtitle {
color: #424242;
}
body #app .card {
background-color: #ffffff;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
}
body #app .card:hover {
background-color: #ffffff;
}
body #app .footer {
background-color: #ffffff;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
}
}
@media (prefers-color-scheme: dark) {
body #app {
background-color: #131313;
color: #eaeaea;
}
body #app a:hover {
color: #ffdd57;
}
body #app .title {
color: #fafafa;
}
body #app .subtitle {
color: #f5f5f5;
}
body #app .card {
background-color: #2b2b2b;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.4);
}
body #app .card:hover {
background-color: #2b2b2b;
}
body #app .footer {
background-color: #2b2b2b;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.4);
}
}
body #app.is-light {
background-color: #f5f5f5;
color: #363636;
}
body #app.is-light a:hover {
color: #363636;
}
body #app.is-light .title {
color: #303030;
}
body #app.is-light .subtitle {
color: #424242;
}
body #app.is-light .card {
background-color: #ffffff;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
}
body #app.is-light .card:hover {
background-color: #ffffff;
}
body #app.is-light .footer {
background-color: #ffffff;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
}
body #app.is-dark {
background-color: #131313;
color: #eaeaea;
}
body #app.is-dark a:hover {
color: #ffdd57;
}
body #app.is-dark .title {
color: #fafafa;
}
body #app.is-dark .subtitle {
color: #f5f5f5;
}
body #app.is-dark .card {
background-color: #2b2b2b;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.4);
}
body #app.is-dark .card:hover {
background-color: #2b2b2b;
}
body #app.is-dark .footer {
background-color: #2b2b2b;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.4);
}
body h1, body h2, body h3, body h4, body h5, body h6 {
font-family: "Lato", sans-serif;
}
body h1 {
font-size: 2rem;
}
body h2 {
font-size: 1.7rem;
margin-top: 2rem;
margin-bottom: 1rem;
}
body h2 .fas, body h2 .fab, body h2 .far {
margin-right: 10px;
}
body h2 span {
font-weight: bold;
color: #4285f4;
}
body [v-cloak] {
display: none;
}
body #bighead {
color: #ffffff;
}
body #bighead .dashboard-title {
padding: 6px 0 0 80px;
}
body #bighead .first-line {
height: 100px;
vertical-align: center;
background-color: #3367d6;
}
body #bighead .first-line h1 {
margin-top: -12px;
font-size: 2rem;
}
body #bighead .first-line .headline {
margin-top: 5px;
font-size: 0.9rem;
}
body #bighead .first-line .container {
height: 80px;
padding: 10px 0;
}
body #bighead .first-line .logo {
float: left;
}
body #bighead .first-line .logo i {
vertical-align: top;
padding: 8px 15px;
font-size: 50px;
}
body #bighead .first-line .logo img {
padding: 10px;
max-height: 70px;
max-width: 70px;
}
body #bighead .navbar, body #bighead .navbar-menu {
background-color: #4285f4;
}
body #bighead .navbar a, body #bighead .navbar-menu a {
color: #ffffff;
}
body #bighead .navbar a:hover, body #bighead .navbar a:focus, body #bighead .navbar-menu a:hover, body #bighead .navbar-menu a:focus {
color: #ffffff;
background-color: #5a95f5;
}
body #bighead .navbar-end {
text-align: right;
}
body #main-section {
margin-bottom: 2rem;
padding: 0;
}
body #main-section h2 {
border-bottom: 1px dashed #ccc;
padding-bottom: 10px;
}
body #main-section .title {
font-size: 1.1em;
}
body #main-section .subtitle {
font-size: 0.9em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
body #main-section .container {
padding: 1.2rem 0.75rem;
}
body #main-section .message {
margin-top: 45px;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
}
body #main-section .message .message-header {
font-weight: bold;
}
body #main-section .message .message-body {
border: none;
}
body .media-content {
overflow: inherit;
}
body .tag {
color: #4285f4;
background-color: #4285f4;
position: absolute;
top: 1rem;
right: -0.2rem;
width: 3px;
overflow: hidden;
transition: all 0.2s ease-out;
padding: 0;
}
body .tag .tag-text {
display: none;
}
body .card {
border-radius: 5px;
border: none;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
}
body .card a {
outline: none;
}
body .card:hover {
transform: translate(0, -3px);
}
body .card:hover .tag {
width: auto;
color: #ffffff;
padding: 0 0.75em;
}
body .card:hover .tag .tag-text {
display: block;
}
body .card-content {
height: 85px;
padding: 1.3rem;
}
body .layout-vertical .card {
border-radius: 0;
}
body .layout-vertical .column div:first-of-type .card {
border-radius: 5px 5px 0 0;
}
body .layout-vertical .column div:last-child .card {
border-radius: 0 0 5px 5px;
}
body .footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 0.5rem;
text-align: left;
color: #676767;
font-size: 0.85rem;
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
}
body .no-footer #main-section {
margin-bottom: 0;
}
body .no-footer .footer {
display: none;
}
body .search-bar {
position: relative;
display: inline-block;
}
body .search-bar #search {
border: none;
background-color: #5f98f6;
border-radius: 5px;
padding: 2px 12px 2px 30px;
margin: 0 0 0 12px;
transition: all 100ms linear;
color: #ffffff;
height: 30px;
width: 100px;
}
body .search-bar #search:focus {
color: #000000;
width: 250px;
background-color: #ffffff;
}
body .search-bar .search-label::before {
font-family: "Font Awesome 5 Free";
position: absolute;
top: 4px;
left: 16px;
content: "";
font-weight: 900;
width: 20px;
height: 20px;
color: #ffffff;
}
body .search-bar:focus-within .search-label::before {
color: #4a4a4a;
}
body .icon-button {
display: inline-block;
padding: 0 12px;
}
body .offline-message {
text-align: center;
margin: 35px 0;
}
body .offline-message i {
font-size: 2rem;
}
body .offline-message i.fa-redo-alt {
font-size: 1.3rem;
line-height: 1rem;
vertical-align: middle;
cursor: pointer;
color: #3273dc;
}

125
app.js
View File

@ -1,125 +0,0 @@
const app = new Vue({
el: '#app',
data: {
config: null,
offline: false,
filter: '',
vlayout: true,
isDark: null,
showMenu: false
},
created: async function () {
let that = this;
this.isDark = 'overrideDark' in localStorage ?
JSON.parse(localStorage.overrideDark) : matchMedia("(prefers-color-scheme: dark)").matches;
if ('vlayout' in localStorage) {
this.vlayout = JSON.parse(localStorage.vlayout)
}
this.checkOffline();
try {
this.config = await this.getConfig();
document.title = this.config.title + ' | Homer';
} catch (error) {
this.offline = true;
}
// Look for a new message if an endpoint is provided.
if (this.config.message && this.config.message.url) {
this.getMessage(this.config.message.url).then(function(message){
// keep the original config value if no value is provided by the endpoint
for (const prop of ['title','style','content']) {
if (prop in message && message[prop] !== null) {
that.config.message[prop] = message[prop];
}
}
});
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState == "visible") {
that.checkOffline();
}
}, false);
},
methods: {
checkOffline: function () {
let that = this;
return fetch(window.location.href + "?alive", {
method: 'HEAD',
cache: 'no-store'
}).then(function () {
that.offline = false;
}).catch(function () {
that.offline = true;
});
},
getConfig: function (event) {
return fetch('config.yml').then(function (response) {
if (response.status != 200) {
return
}
return response.text().then(function (body) {
return jsyaml.load(body);
});
});
},
getMessage: function (url) {
return fetch(url).then(function (response) {
if (response.status != 200) {
return;
}
return response.json();
});
},
toggleTheme: function() {
this.isDark = !this.isDark;
localStorage.overrideDark = this.isDark;
},
toggleLayout: function() {
this.vlayout = !this.vlayout;
localStorage.vlayout = this.vlayout;
},
toggleMenu: function() {
this.showMenu = !this.showMenu;
}
}
});
Vue.component('service', {
props: ['item'],
template: `<div>
<div class="card">
<a :href="item.url" :target="item.target">
<div class="card-content">
<div class="media">
<div v-if="item.logo" class="media-left">
<figure class="image is-48x48">
<img :src="item.logo" />
</figure>
</div>
<div v-if="item.icon" class="media-left">
<figure class="image is-48x48">
<i style="font-size: 35px" :class="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>
<div class="tag" :class="item.tagstyle" v-if="item.tag">
<strong class="tag-text">#{{ item.tag }}</strong>
</div>
</div>
</a>
</div></div>`
});
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('worker.js');
});
}

379
app.scss
View File

@ -1,379 +0,0 @@
$primary-color: #3367d6;
$secondary-color: #4285f4;
// /!\ Keep background colors sync with `theme-color` meta info
$theme-light: (
background: #f5f5f5,
card-background: #ffffff,
text: #363636,
text-title: #303030,
text-subtitle: #424242,
card-shadow: rgba(0, 0, 0, 0.1),
a-hover: #363636
);
$theme-dark: (
background: #131313,
card-background: #2b2b2b,
text: #eaeaea,
text-title: #fafafa,
text-subtitle: #f5f5f5,
card-shadow: rgba(0, 0, 0, 0.4),
a-hover: #ffdd57
);
/* raleway-regular - latin */
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Raleway'), local('Raleway-Regular'),
url('./webfonts/raleway/raleway-v14-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('./webfonts/raleway/raleway-v14-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* lato-regular - latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Lato Regular'), local('Lato-Regular'),
url('./webfonts/lato/lato-v16-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('./webfonts/lato/lato-v16-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
@mixin theme($theme) {
background-color: map-get($theme, "background");
color: map-get($theme, "text");
a {
&:hover {
color: map-get($theme, "a-hover");
}
}
.title {
color: map-get($theme, "text-title");
}
.subtitle {
color: map-get($theme, "text-subtitle");
}
.card {
background-color: map-get($theme, "card-background");
box-shadow: 0 2px 15px 0 map-get($theme, "card-shadow");
&:hover {
background-color: map-get($theme, "card-background");
}
}
.footer {
background-color: map-get($theme, "card-background");
box-shadow: 0 2px 15px 0 map-get($theme, "card-shadow");
}
}
html {
height: 100%;
}
body {
font-family: 'Raleway', sans-serif;
height: 100%;
#app {
min-height: 100%;
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
// Default theme
@include theme($theme-light);
// System pref theme
@media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) {
@include theme($theme-light);
}
@media (prefers-color-scheme: dark) {
@include theme($theme-dark);
}
// User override theme
&.is-light {
@include theme($theme-light);
}
&.is-dark {
@include theme($theme-dark);
}
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Lato', sans-serif;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.7rem;
margin-top: 2rem;
margin-bottom: 1rem;
.fas, .fab, .far {
margin-right: 10px;
}
span {
font-weight: bold;
color: $secondary-color;
}
}
[v-cloak] {
display: none
}
#bighead {
color: #ffffff;
.dashboard-title {
padding: 6px 0 0 80px;
}
.first-line {
height: 100px;
vertical-align: center;
background-color: $primary-color;
h1 {
margin-top: -12px;
font-size: 2rem;
}
.headline {
margin-top: 5px;
font-size: 0.9rem;
}
.container {
height: 80px;
padding: 10px 0;
}
.logo {
float: left;
i {
vertical-align: top;
padding: 8px 15px;
font-size: 50px
}
img {
padding: 10px;
max-height: 70px;
max-width: 70px;
}
}
}
.navbar, .navbar-menu {
background-color: $secondary-color;
a {
color: #ffffff;
&:hover, &:focus {
color: #ffffff;
background-color: lighten( $secondary-color, 5% );
}
}
}
.navbar-end {
text-align: right;
}
}
#main-section {
margin-bottom: 2rem;
padding: 0;
h2 {
border-bottom: 1px dashed #ccc;
padding-bottom: 10px;
}
.title {
font-size: 1.1em;
}
.subtitle {
font-size: .9em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.container {
padding: 1.2rem .75rem;
}
.message {
margin-top: 45px;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
.message-header {
font-weight: bold;
}
.message-body {
border: none;
}
}
}
.media-content {
overflow: inherit;
}
.tag {
color: $secondary-color;
background-color: $secondary-color;
position: absolute;
top: 1rem;
right: -0.2rem;
width: 3px;
overflow: hidden;
transition: all 0.2s ease-out;
padding: 0;
.tag-text {
display: none;
}
}
.card {
border-radius: 5px;
border: none;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
a {
outline: none;
}
}
.card:hover {
transform: translate(0, -3px);
.tag {
width: auto;
color: #ffffff;
padding: 0 0.75em;
.tag-text {
display: block;
}
}
}
.card-content {
height: 85px;
padding: 1.3rem;
}
.layout-vertical {
.card {
border-radius: 0;
}
.column div:first-of-type .card {
border-radius: 5px 5px 0 0;
}
.column div:last-child .card {
border-radius: 0 0 5px 5px;
}
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 0.5rem;
text-align: left;
color: #676767;
font-size: 0.85rem;
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
}
.no-footer {
#main-section {
margin-bottom: 0;
}
.footer {
display: none;
}
}
.search-bar {
position: relative;
display: inline-block;
#search {
border: none;
background-color: lighten( $secondary-color, 6% );
border-radius: 5px;
padding: 2px 12px 2px 30px;
margin: 0 0 0 12px;
transition: all 100ms linear;
color: #ffffff;
height: 30px;
width: 100px;
&:focus {
color: #000000;
width: 250px;
background-color: #ffffff;
}
}
.search-label::before {
font-family: 'Font Awesome 5 Free';
position: absolute;
top: 4px;
left: 16px;
content: "\f002";
font-weight: 900;
width: 20px;
height: 20px;
color: #ffffff;
}
&:focus-within .search-label::before {
color: #4a4a4a;
}
}
.icon-button {
display: inline-block;
padding: 0 12px;
}
.offline-message {
text-align: center;
margin: 35px 0;
i {
font-size: 2rem;
}
i.fa-redo-alt {
font-size: 1.3rem;
line-height: 1rem;
vertical-align: middle;
cursor: pointer;
color: #3273dc;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

3
babel.config.js Normal file
View File

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

16
docker-compose.yml Normal file
View File

@ -0,0 +1,16 @@
---
version: "2"
services:
homer:
image: b4bz/homer
#To build from source, comment previous line and uncomment below
#build: .
container_name: homer
volumes:
- /your/local/assets/:/www/assets
ports:
- 8080:8080
#environment:
# - UID=1000
# - GID=1000
restart: unless-stopped

204
docs/configuration.md Normal file
View File

@ -0,0 +1,204 @@
# 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.
```yaml
---
# Homepage configuration
# See https://fontawesome.com/v5/search for icons options
# Optional: Use external configuration file.
# Using this will ignore remaining config in this file
# externalConfig: https://example.com/server-luci/config.yaml
title: "App dashboard"
subtitle: "Homer"
# documentTitle: "Welcome" # Customize the browser tab text
logo: "assets/logo.png"
# Alternatively a fa 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 "/"
# 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)
# 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.
# 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 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.
colors:
light:
highlight-primary: "#3367d6"
highlight-secondary: "#4285f4"
highlight-hover: "#5a95f5"
background: "#f5f5f5"
card-background: "#ffffff"
text: "#363636"
text-header: "#424242"
text-title: "#303030"
text-subtitle: "#424242"
card-shadow: rgba(0, 0, 0, 0.1)
link: "#3273dc"
link-hover: "#363636"
background-image: "assets/your/light/bg.png"
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"
background-image: "assets/your/dark/bg.png"
# 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
style: "is-warning"
title: "Optional message!"
icon: "fa fa-exclamation-triangle"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
# Optional navbar
# links: [] # Allows for navbar (dark mode, layout, and search) without any links
links:
- name: "Link 1"
icon: "fab fa-github"
url: "https://github.com/bastienwirtz/homer"
target: "_blank" # optional html tag target attribute
- 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.
# Leave only a "items" key if not using group (group 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.
# logo: "path/to/logo"
items:
- name: "Awesome app"
logo: "assets/tools/sample.png"
# Alternatively a fa icon can be provided:
# icon: "fab fa-jenkins"
subtitle: "Bookmark example"
tag: "app"
url: "https://www.reddit.com/r/selfhosted/"
target: "_blank" # optional html tag target attribute
- name: "Another one"
logo: "assets/tools/sample2.png"
subtitle: "Another application"
tag: "app"
# Optional tagstyle
tagstyle: "is-success"
url: "#"
- name: "Other group"
icon: "fas fa-heartbeat"
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
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
```
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
{
"style": null,
"title": "Lorem ipsum 42",
"content": "LA LA LA Lorem ipsum dolor sit amet, ....."
}
```
`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:
- `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.
## 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.

181
docs/customservices.md Normal file
View File

@ -0,0 +1,181 @@
# 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)
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.
```

74
docs/development.md Normal file
View File

@ -0,0 +1,74 @@
# 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)
yarn install
yarn serve
# **OR** Using npm
npm install
npm run serve
```
## 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.
```scss
// `src/assets/themes/my-awesome-theme.scss`
body #app.theme-my-awesome-theme. { ... }
```
```scss
// `src/assets/app.scss`
// Themes import
@import "./themes/sui.scss";
...
@import "./themes/my-awesome-theme.scss";
```

BIN
docs/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

192
docs/tips-and-tricks.md Normal file
View File

@ -0,0 +1,192 @@
# Tips & Tricks
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.
The Firefox extension loads Homer in an iframe on your new tab page, meaning you have to add `target: '_top'` to each of your items.
```yaml
- name: "Reddit"
logo: "assets/daily/reddit.png"
url: "https://reddit.com"
target: '_top'
- name: "YouTube"
logo: "assets/daily/youtube.png"
url: "https://youtube.com"
target: '_top'
```
## 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.
Using Anchoring, you can define all your tags and their styles once like this: (for example)
```yaml
# Some pre-defined tag styles. reference these using <<: *{NAME} inside an item definition; For Example, <<: *Apps
tags:
Favourite: &Favourite
- tag: "Favourite"
tagstyle: "is-medium is-primary"
CI: &CI
- tag: "CI"
tagstyle: "is-medium is-success"
Apps: &Apps
- tag: "App"
tagstyle: "is-medium is-info"
```
and then simply reference these pre-defined (anchored) tags in each item like so:
```yaml
- name: "VS Code"
logo: "/assets/vscode.png"
subtitle: "Develop Code Anywhere, On Anything!"
<<: *Apps # Reference to the predefined "App" Tag
url: "https://vscode.example.com/"
target: "_blank" # optional html tag target attribute
````
Then when Homer reads your config, it will substitute your anchors automatically, the above example is equal to:
```yaml
- name: "VS Code"
logo: "/assets/vscode.png"
subtitle: "Develop Code Anywhere, On Anything!"
tag: "App"
tagstyle: "is-medium is-info"
url: "https://vscode.example.com/"
target: "_blank" # optional html tag target attribute
```
The end result is that if you want to update the name or style of any particular tag, just update it once, in the tags section!
Great if you have a lot of services or a lot of tags!
## 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!
You can setup and use [Code-Server](https://github.com/cdr/code-server) to edit your `config.yml` file from anywhere!
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
icon: fas fa-cog
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 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-e PASSWORD={YOUR_PASSWORD} `#optional` \
-e SUDO_PASSWORD={YOUR SUDO_PASSWORD} `#optional` \
-p 8443:8443 \
-v /path/to/appdata/config:/config \
-v /your/local/homer/config-dir/:/config/homer \
--restart unless-stopped \
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:
```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:`.
If the URL you specified returns a JSON object that defines a `title` and `content` item, homer will replace these values from your `config.yml` with the ones in the returned object.
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!
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!

19
docs/troubleshooting.md Normal file
View File

@ -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+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.
```
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.

15
entrypoint.sh Normal file
View File

@ -0,0 +1,15 @@
#!/bin/sh
# Ensure default assets are present.
while true; do echo n; done | cp -Ri /www/default-assets/* /www/assets/ &> /dev/null
# 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
# Install default config if no one is available.
yes n | cp -i /www/default-assets/config.yml.dist /www/assets/config.yml &> /dev/null
chown -R $UID:$GID /www/assets
exec su-exec $UID:$GID darkhttpd /www/ --no-listing --port "$PORT"

8
hooks/post_push Normal file
View File

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

8
hooks/pre_build Normal file
View File

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

View File

@ -1,132 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<link rel="icon" type="image/png" href="assets/favicon.png">
<title>Homer</title>
<link defer rel="stylesheet" href="vendors/font-awesone.min.css">
<link defer rel="stylesheet" href="vendors/bulma.min.css">
<link rel="stylesheet" href="app.css">
</head>
<body>
<div id="app" v-if="config" :class="[isDark ? 'is-dark' : 'is-light', !config.footer ? 'no-footer': '']">
<div id="bighead">
<section class="first-line">
<div v-cloak class="container">
<div class="logo">
<img v-if="config.logo" :src="config.logo" />
<i v-if="config.icon" :class="config.icon"></i>
</div>
<div class="dashboard-title">
<span class="headline">{{ config.subtitle }}</span>
<h1>{{ config.title }}</h1>
</div>
</div>
</section>
<div v-cloak v-if="config.links" class="container-fluid">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a role="button"
class="navbar-burger" :class="{ 'is-active': showMenu }"
aria-label="menu" aria-expanded="false"
v-on:click="toggleMenu()">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" :class="{ 'is-active': showMenu }">
<div class="navbar-start">
<a v-for="link in config.links" class="navbar-item" :href="link.url" :target="link.target">
<i v-if="link.icon" style="margin-right: 6px;" :class="link.icon"></i>
{{ link.name }}
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<a
v-on:click="toggleTheme()"
aria-label="Toggle dark mode"
class="icon-button"
><i class="fas fa-adjust"></i>
</a>
<a v-on:click="toggleLayout()" class="icon-button"><i
:class="['fas', vlayout ? 'fa-list' : 'fa-columns']"></i></a>
<div class="search-bar">
<label for="search" class="search-label"></label>
<input type="text" id="search" v-model="filter" />
</div>
</div>
</div>
</div>
</div>
</nav>
</div>
</div>
<section id="main-section" class="section">
<div v-cloak class="container">
<div v-if="offline" class="offline-message">
<i class="far fa-dizzy"></i>
<h1>You're offline bro. <i class="fas fa-redo-alt" v-on:click="checkOffline()"></i></h1>
</div>
<div v-else>
<!-- Optional messages -->
<article v-if="config && config.message" class="message" :class="config.message.style">
<div v-if="config.message.title" class="message-header">
<p>{{ config.message.title }}</p>
</div>
<div v-if="config.message.content" class="message-body">
{{ config.message.content }}
</div>
</article>
<h2 v-if="filter"><i class="fas fa-search"></i> Search</h2>
<!-- Horizontal layout -->
<div v-if="!vlayout || filter" class="columns is-multiline">
<template v-for="(group, index) in config.services">
<h2 v-if="!filter && group.name" class="column is-full"><i v-if="group.icon" :class="group.icon"></i><span
v-else>#</span>
{{ group.name }}</h2>
<service v-for="item in group.items" v-bind:item="item" class="column is-one-third-widescreen"
v-if="!filter || (item && (item.name.toLowerCase().includes(filter.toLowerCase()) || (item.tag && item.tag.toLowerCase().includes(filter.toLowerCase()))))">
</service>
</template>
</div>
<!-- Vertical layout -->
<div v-if="!filter && vlayout" class="columns is-multiline layout-vertical">
<div class="column is-one-third-widescreen" v-for="(group, index) in config.services">
<h2 v-if="!filter && group.name"><i v-if="group.icon" :class="group.icon"></i><span v-else>#</span>
{{ group.name }}</h2>
<service v-for="item in group.items" v-bind:item="item"
v-if="!filter || (item && (item.name.toLowerCase().includes(filter.toLowerCase()) || (item.tag && item.tag.toLowerCase().includes(filter.toLowerCase()))))">
</service>
</div>
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="content has-text-centered" v-if="config.footer" v-html="config.footer">
</div>
</div>
</footer>
</div>
<script src="vendors/vue.min.js"></script>
<script src="vendors/js-yaml.min.js"></script>
<script src="app.js"></script>
</body>
</html>

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "homer",
"version": "21.09.1",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@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",
"register-service-worker": "^1.7.2",
"vue": "^2.6.14"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.15",
"@vue/cli-plugin-eslint": "~4.5.15",
"@vue/cli-plugin-pwa": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"raw-loader": "^4.0.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.12"
},
"license": "Apache-2.0"
}

View File

@ -0,0 +1,35 @@
---
# Additional page configuration
# Additional configurations are loaded using its file name, minus the extension, as an anchor (https://<mydashboad>#<config>).
# `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"

View File

@ -0,0 +1,84 @@
---
# 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/">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.
# 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:
#url: https://b4bz.io
style: "is-dark" # See https://bulma.io/documentation/components/message/#colors for styling options.
title: "Demo !"
icon: "fa fa-grin"
content: "This is a dummy homepage demo. <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: "Wiki"
icon: "fas fa-book"
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"
# 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: "Applications"
icon: "fas fa-cloud"
items:
- name: "Awesome app"
logo: "assets/tools/sample.png"
subtitle: "Bookmark example"
tag: "app"
url: "https://www.reddit.com/r/selfhosted/"
target: "_blank" # optional html a tag target attribute
- name: "Another one"
logo: "assets/tools/sample2.png"
subtitle: "Another application"
tag: "app"
url: "#"

View File

@ -1,59 +1,53 @@
---
# Homepage configuration
# See https://fontawesome.com/icons for icons options
# See https://fontawesome.com/v5/search for icons options
title: "Demo dashboard"
subtitle: "Homer"
logo: "assets/logo.png"
title: "Hello beautiful!"
subtitle: "App dashboard"
logo: false
# icon: "fas fa-skull-crossbones" Optional icon
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.
# Optional message
message:
# url: https://....
style: "is-dark" # See https://bulma.io/documentation/components/message/#colors for styling options.
title: "Demo !"
content: "This is a dummy homepage demo. Find more information on github.com/bastienwirtz/homer"
header: true
# Optional theme customization
theme: sui
colors:
light:
highlight-primary: transparent
highlight-secondary: transparent
highlight-hover: "#4a4a4a"
text-subtitle: "#424242"
dark:
background: "#2B2C56"
highlight-primary: transparent
highlight-secondary: transparent
highlight-hover: "#200b35"
text-subtitle: "#6375e8"
# Optional navbar
# links: [] # Allows for navbar (dark mode, layout, and search) without any links
links:
- name: "ansible"
icon: "fab fa-github"
url: "https://github.com/bastienwirtz/homer"
target: '_blank' # optionnal html a tag target attribute
- name: "Wiki"
icon: "fas fa-book"
url: "https://www.wikipedia.org/"
links: []
# 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: "DevOps"
icon: "fas fa-code-branch"
- name: "APPLICATIONS"
items:
- name: "Jenkins"
logo: "assets/tools/jenkins.png"
subtitle: "Continuous integration server"
tag: "CI"
url: "https://jenkins.io/"
target: '_blank' # optionnal html a tag target attribute
- name: "RabbitMQ Management"
logo: "assets/tools/rabbitmq.png"
subtitle: "Manage & monitor RabbitMQ server"
tag: "haproxy"
# Optional tagstyle
# Same styling options as the optional message.
tagstyle: "is-success"
url: "https://www.rabbitmq.com/"
- name: "Monitoring"
icon: "fas fa-heartbeat"
items:
- name: "M/Monit"
logo: "assets/tools/monit.png"
subtitle: "Monitor & manage all monit enabled hosts"
tag: "monit"
url: "https://mmonit.com/monit/"
- name: "Grafana"
logo: "assets/tools/grafana.png"
@ -62,7 +56,6 @@ services:
- name: "Kibana"
logo: "assets/tools/elastic.png"
subtitle: "Explore & visualize logs"
tag: "elk"
url: "https://www.elastic.co/products/kibana"
- name: "Website monitoring"
logo: "assets/tools/pingdom.png"

View File

@ -0,0 +1,8 @@
@charset "UTF-8";
/* Custom card colors */
/* Use with `class:` property of services in config.yml */
body #app .card.green {
background-color: #006600;
color: #00ff00;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

18
public/index.html Normal file
View File

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

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

2
public/robots.txt Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

303
src/App.vue Normal file
View File

@ -0,0 +1,303 @@
<template>
<div
id="app"
v-if="config"
:class="[
`theme-${config.theme}`,
isDark ? 'is-dark' : 'is-light',
!config.footer ? 'no-footer' : '',
]"
>
<DynamicTheme :themes="config.colors" />
<div id="bighead">
<section v-if="config.header" class="first-line">
<div v-cloak class="container">
<div class="logo">
<a href="#">
<img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
</a>
<i v-if="config.icon" :class="config.icon"></i>
</div>
<div class="dashboard-title">
<span class="headline">{{ config.subtitle }}</span>
<h1>{{ config.title }}</h1>
</div>
</div>
</section>
<Navbar
:open="showMenu"
:links="config.links"
@navbar-toggle="showMenu = !showMenu"
>
<DarkMode
@updated="isDark = $event"
:defaultValue="this.config.defaults.colorTheme"
/>
<SettingToggle
@updated="vlayout = $event"
name="vlayout"
icon="fa-list"
iconAlt="fa-columns"
:defaultValue="this.config.defaults.layout == 'columns'"
/>
<SearchInput
class="navbar-item is-inline-block-mobile"
:hotkey="searchHotkey()"
@input="filterServices"
@search-focus="showMenu = true"
@search-open="navigateToFirstService"
@search-cancel="filterServices"
/>
</Navbar>
</div>
<section id="main-section" class="section">
<div v-cloak class="container">
<ConnectivityChecker
v-if="config.connectivityCheck"
@network-status-update="offline = $event"
/>
<GetStarted v-if="loaded && !services" />
<div v-if="!offline">
<!-- Optional messages -->
<Message :item="config.message" />
<!-- Horizontal layout -->
<div v-if="!vlayout || filter" class="columns is-multiline">
<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">
<img :src="group.logo" :alt="`${group.name} logo`" />
</figure>
</div>
{{ group.name }}
</h2>
<Service
v-for="(item, index) in group.items"
:key="index"
:item="item"
:proxy="config.proxy"
:forwarder="config.forwarder"
:class="['column', `is-${12 / config.columns}`]"
/>
</template>
</div>
<!-- Vertical layout -->
<div
v-if="!filter && vlayout"
class="columns is-multiline layout-vertical"
>
<div
:class="['column', `is-${12 / config.columns}`]"
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>
<div v-else-if="group.logo" class="group-logo media-left">
<figure class="image is-48x48">
<img :src="group.logo" :alt="`${group.name} logo`" />
</figure>
</div>
{{ group.name }}
</h2>
<Service
v-for="(item, index) in group.items"
:key="index"
:item="item"
:proxy="config.proxy"
:forwarder="config.forwarder"
/>
</div>
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div
class="content has-text-centered"
v-if="config.footer"
v-html="config.footer"
></div>
</div>
</footer>
</div>
</template>
<script>
const jsyaml = require("js-yaml");
const merge = require("lodash.merge");
import Navbar from "./components/Navbar.vue";
import GetStarted from "./components/GetStarted.vue";
import ConnectivityChecker from "./components/ConnectivityChecker.vue";
import Service from "./components/Service.vue";
import Message from "./components/Message.vue";
import SearchInput from "./components/SearchInput.vue";
import SettingToggle from "./components/SettingToggle.vue";
import DarkMode from "./components/DarkMode.vue";
import DynamicTheme from "./components/DynamicTheme.vue";
import defaultConfig from "./assets/defaults.yml";
export default {
name: "App",
components: {
Navbar,
GetStarted,
ConnectivityChecker,
Service,
Message,
SearchInput,
SettingToggle,
DarkMode,
DynamicTheme,
},
data: function () {
return {
loaded: false,
config: null,
services: null,
offline: false,
filter: "",
vlayout: true,
isDark: null,
showMenu: false,
};
},
created: async function () {
this.buildDashboard();
window.onhashchange = this.buildDashboard;
this.loaded = true;
},
methods: {
searchHotkey() {
if (this.config.hotkey && this.config.hotkey.search) {
return this.config.hotkey.search;
}
},
buildDashboard: async function () {
const defaults = jsyaml.load(defaultConfig);
let config;
try {
config = await this.getConfig();
const path =
window.location.hash.substring(1) != ""
? window.location.hash.substring(1)
: null;
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);
config = this.handleErrors("⚠️ Error loading configuration", error);
}
this.config = merge(defaults, config);
this.services = this.config.services;
document.title =
this.config.documentTitle ||
`${this.config.title} | ${this.config.subtitle}`;
if (this.config.stylesheet) {
let stylesheet = "";
for (const file of this.config.stylesheet) {
stylesheet += `@import "${file}";`;
}
this.createStylesheet(stylesheet);
}
},
getConfig: function (path = "assets/config.yml") {
return fetch(path).then((response) => {
if (response.redirected) {
// This allows to work with authentication proxies.
window.location.href = response.url;
return;
}
if (!response.ok) {
throw Error(`${response.statusText}: ${response.body}`);
}
const that = this;
return response
.text()
.then((body) => {
return jsyaml.load(body);
})
.then(function (config) {
if (config.externalConfig) {
return that.getConfig(config.externalConfig);
}
return config;
});
});
},
matchesFilter: function (item) {
return (
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) {
try {
const service = this.services[0].items[0];
window.open(service.url, target || service.target || "_self");
} catch (error) {
console.warning("fail to open service");
}
},
filterServices: function (filter) {
this.filter = filter;
if (!filter) {
this.services = this.config.services;
return;
}
const searchResultItems = [];
for (const group of this.config.services) {
for (const item of group.items) {
if (this.matchesFilter(item)) {
searchResultItems.push(item);
}
}
}
this.services = [
{
name: filter,
icon: "fas fa-search",
items: searchResultItems,
},
];
},
handleErrors: function (title, content) {
return {
message: {
title: title,
style: "is-danger",
content: content,
},
};
},
createStylesheet: function (css) {
let style = document.createElement("style");
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
},
},
};
</script>

355
src/assets/app.scss Normal file
View File

@ -0,0 +1,355 @@
@charset "utf-8";
@import "./webfonts/webfonts.scss";
@import "bulma";
// Themes import
@import "./themes/sui.scss";
@mixin ellipsis() {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
html, body, body #app {
height: 100%;
background-color: var(--background);
}
body {
font-family: "Raleway", sans-serif;
#app {
height: auto;
min-height: 100%;
background-image: var(--background-image);
background-size: cover;
background-position: center;
color: var(--text);
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
a {
color: var(--link);
&:hover {
color: var(--link-hover);
}
}
.title {
color: var(--text-title);
}
.subtitle {
color: var(--text-subtitle);
}
.card {
background-color: var(--card-background);
box-shadow: 0 2px 15px 0 var(--card-shadow);
&:hover {
background-color: var(--card-background);
}
}
.message {
background-color: var(--card-background);
.message-body {
color: var(--text);
}
}
.footer {
background-color: var(--card-background);
box-shadow: 0 2px 15px 0 var(--card-shadow);
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Lato", sans-serif;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.7rem;
margin-top: 2rem;
margin-bottom: 1rem;
.fas,
.fab,
.far {
margin-right: 10px;
}
span {
font-weight: bold;
color: var(--highlight-secondary);
}
}
[v-cloak] {
display: none;
}
#bighead {
color: var(--text-header);
.dashboard-title {
padding: 6px 0 0 80px;
}
.first-line {
min-height: 100px;
vertical-align: center;
background-color: var(--highlight-primary);
h1 {
margin-top: -12px;
font-size: 2rem;
}
.headline {
margin-top: 5px;
font-size: 0.9rem;
}
.container {
min-height: 80px;
padding: 10px 0;
}
.logo {
float: left;
i {
vertical-align: top;
padding: 8px 15px;
font-size: 50px;
}
img {
padding: 10px;
max-height: 70px;
max-width: 70px;
}
}
}
.navbar {
background-color: var(--highlight-secondary);
a {
color: var(--text-header);
padding: 8px 12px;
&:hover,
&:focus {
color: var(--text-header);
background-color: var(--highlight-hover);
}
}
.navbar-menu {
background-color: inherit;
}
}
.navbar-end {
text-align: right;
}
}
#main-section {
margin-bottom: 2rem;
padding: 0;
h2 {
padding-bottom: 0px;
@include ellipsis();
}
.title {
font-size: 1.1em;
line-height: 1.2em;
@include ellipsis();
}
.subtitle {
font-size: 0.9em;
@include ellipsis();
}
.container {
padding: 1.2rem 0.75rem;
}
.message {
margin-top: 45px;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
.message-header {
font-weight: bold;
}
.message-body {
border: none;
}
}
}
.media.no-subtitle {
display: flex;
align-items: center;
}
.media-content {
overflow: hidden;
text-overflow: inherit;
}
.tag {
color: var(--highlight-secondary);
background-color: var(--highlight-secondary);
position: absolute;
bottom: 1rem;
right: -0.2rem;
width: 3px;
overflow: hidden;
transition: all 0.2s ease-out;
padding: 0;
.tag-text {
display: none;
}
}
.card {
border: none;
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
overflow: visible;
a {
outline: none;
}
}
.card:hover {
transform: translate(0, -3px);
.tag {
width: auto;
color: #ffffff;
padding: 0 0.75em;
.tag-text {
display: block;
}
}
}
.card-content {
height: 85px;
padding: 1.3rem;
}
.layout-vertical {
.card {
border-radius: 0;
}
.column div:first-of-type .card {
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
.column div:last-child .card {
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 0.5rem;
text-align: left;
color: #676767;
font-size: 0.85rem;
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
}
.no-footer {
#main-section {
margin-bottom: 0;
}
.footer {
display: none;
}
}
.search-bar {
position: relative;
display: inline-block;
input {
border: none;
background-color: var(--highlight-hover);
border-radius: 5px;
margin-top: 2px;
padding: 2px 12px 2px 30px;
transition: all 100ms linear;
color: #ffffff;
height: 30px;
width: 100px;
&:focus {
color: #000000;
width: 250px;
background-color: #ffffff;
}
}
.search-label::before {
font-family: "Font Awesome 5 Free";
position: absolute;
top: 14px;
left: 16px;
content: "\f002";
font-weight: 900;
width: 20px;
height: 20px;
color: #ffffff;
}
&:focus-within .search-label::before {
color: #6e6e6e;
}
}
.offline-message {
text-align: center;
margin: 35px 0;
i {
font-size: 2rem;
}
i.fa-redo-alt {
font-size: 1.3rem;
line-height: 1rem;
vertical-align: middle;
cursor: pointer;
color: #3273dc;
}
}
}
.group-logo {
float: left;
}

56
src/assets/defaults.yml Normal file
View File

@ -0,0 +1,56 @@
---
# Default configuration
title: "Dashboard"
subtitle: "Homer"
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/">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
connectivityCheck: true
defaults:
# columns, list
layout: columns
# auto, light, dark
colorTheme: auto
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"
background-image: ""
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"
background-image: ""
message: ~
links: []
services: []
proxy: ~
forwarder: ~

View File

@ -0,0 +1,34 @@
/*
* SUI theme
* Inspired by the great https://github.com/jeroenpardon/sui start page
* Author: @bastienwirtz
*/
body #app.theme-sui {
#bighead .dashboard-title {
padding: 65px 0 0 12px;
h1 {
margin-top: 0;
font-weight: bold;
font-size: 2.2rem;
}
}
.navbar .navbar-item:hover {
background-color: transparent;
}
.card,
.card:hover {
background-color: transparent;
box-shadow: none;
.title {
font-weight: bold;
}
.card-content {
padding: 0;
}
}
}

View File

@ -0,0 +1,23 @@
/* raleway-regular - latin */
@font-face {
font-family: "Raleway";
font-style: normal;
font-weight: 400;
font-display: swap;
src: local("Raleway"), local("Raleway-Regular"),
url("./webfonts/raleway/raleway-v14-latin-regular.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./webfonts/raleway/raleway-v14-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* lato-regular - latin */
@font-face {
font-family: "Lato";
font-style: normal;
font-weight: 400;
font-display: swap;
src: local("Lato Regular"), local("Lato-Regular"),
url("./webfonts/lato/lato-v16-latin-regular.woff2") format("woff2"),
/* Chrome 26+, Opera 23+, Firefox 39+ */
url("./webfonts/lato/lato-v16-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

View File

@ -0,0 +1,52 @@
<template>
<div v-if="offline" class="offline-message">
<i class="far fa-dizzy"></i>
<h1>
You're offline friend.
<span @click="checkOffline"> <i class="fas fa-redo-alt"></i></span>
</h1>
</div>
</template>
<script>
export default {
name: "ConnectivityChecker",
data: function () {
return {
offline: false,
};
},
created: function () {
let that = this;
this.checkOffline();
document.addEventListener(
"visibilitychange",
function () {
if (document.visibilityState == "visible") {
that.checkOffline();
}
},
false
);
},
methods: {
checkOffline: function () {
let that = this;
return fetch(window.location.href + "?alive", {
method: "HEAD",
cache: "no-store",
})
.then(function (response) {
that.offline = !response.ok;
})
.catch(function () {
that.offline = true;
})
.finally(function () {
that.$emit("network-status-update", that.offline);
});
},
},
};
</script>

View File

@ -0,0 +1,86 @@
<template>
<a
v-on:click="toggleTheme()"
aria-label="Toggle dark mode"
class="navbar-item is-inline-block-mobile"
>
<i
:class="`${faClasses[mode]}`"
class="fa-fw"
:title="`${titles[mode]}`"
></i>
</a>
</template>
<script>
export default {
name: "Darkmode",
props: {
defaultValue: String,
},
data: function () {
return {
isDark: null,
faClasses: null,
titles: null,
mode: null,
};
},
created: function () {
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;
} else {
switch (this.defaultValue) {
case "light":
this.mode = 1;
break;
case "dark":
this.mode = 2;
break;
default:
this.mode = 0;
}
}
this.isDark = this.getIsDark();
this.$emit("updated", this.isDark);
},
methods: {
toggleTheme: function () {
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];
},
},
};
</script>

View File

@ -0,0 +1,38 @@
<template>
<DynamicStyle>
:root, body #app.is-light {
{{ getVars(themes.light) }}
} @media (prefers-color-scheme: light), (prefers-color-scheme:
no-preference) { :root, body #app {
{{ getVars(themes.light) }}
} } body #app.is-dark {
{{ getVars(themes.dark) }}
} @media (prefers-color-scheme: dark) { :root, body #app {
{{ getVars(themes.dark) }}
} }
</DynamicStyle>
</template>
<script>
export default {
name: "DynamicTheme",
props: {
themes: Object,
},
methods: {
getVars: function (theme) {
let vars = [];
for (const themeVars in theme) {
let value = `${theme[themeVars]}`;
if (!value) {
value = "initial";
} else if (themeVars == "background-image") {
value = `url(${theme[themeVars]})`;
}
vars.push(`--${themeVars}: ${value}`);
}
return vars.join(";");
},
},
};
</script>

View File

@ -0,0 +1,35 @@
<template>
<article>
<div class="m-6 has-text-centered py-6">
<p class="is-size-5 mb-0">No configured service found!</p>
<p>Check out the documentation to start building your Homer dashboard.</p>
<p>
<a
class="button is-primary mt-5 has-text-weight-bold"
href="https://github.com/bastienwirtz/homer/blob/main/README.md#getting-started"
target="_blank"
>
Get started
</a>
</p>
</div>
</article>
</template>
<script>
export default {
name: "GetStarted",
};
</script>
<style lang="scss" scoped>
p {
color: #4a4a4a;
}
body #app a {
font-weight: 900;
color: #ffffff;
font-family: "Lato", sans-serif;
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<article v-if="show" class="message" :class="message.style">
<div v-if="message.title || message.icon" class="message-header">
<p>
<i v-if="message.icon" :class="`fa-fw ${message.icon}`"></i>
{{ message.title }}
</p>
</div>
<div
v-if="message.content"
class="message-body"
v-html="message.content"
></div>
</article>
</template>
<script>
export default {
name: "Message",
props: {
item: Object,
},
data: function () {
return {
message: {},
};
},
created: async function () {
// Look for a new message if an endpoint is provided.
this.message = Object.assign({}, this.item);
await this.getMessage();
},
computed: {
show: function () {
return this.message.title || this.message.content;
},
},
watch: {
item: function (item) {
this.message = Object.assign({}, item);
},
},
methods: {
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;
}
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;
},
},
};
</script>

66
src/components/Navbar.vue Normal file
View File

@ -0,0 +1,66 @@
<template>
<div v-cloak v-if="links" class="container-fluid">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a
role="button"
aria-label="menu"
aria-expanded="false"
class="navbar-burger"
:class="{ 'is-active': showMenu }"
v-on:click="$emit('navbar-toggle')"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" :class="{ 'is-active': showMenu }">
<div class="navbar-start">
<a
class="navbar-item"
rel="noreferrer"
v-for="(link, key) in links"
:key="key"
:href="link.url"
:target="link.target"
>
<i
v-if="link.icon"
:class="['fa-fw', link.icon, { 'mr-2': link.name }]"
></i>
{{ link.name }}
</a>
</div>
<div class="navbar-end">
<slot></slot>
</div>
</div>
</div>
</nav>
</div>
</template>
<script>
export default {
name: "Navbar",
props: {
open: {
type: Boolean,
default: false,
},
links: Array,
},
computed: {
showMenu: function () {
return this.open && this.isSmallScreen();
},
},
methods: {
isSmallScreen: function () {
return window.matchMedia("screen and (max-width: 1023px)").matches;
},
},
};
</script>

View File

@ -0,0 +1,84 @@
<template>
<div class="search-bar">
<label for="search" class="search-label"></label>
<input
type="text"
ref="search"
:value="value"
@input="search($event.target.value)"
@keyup.enter.exact="open()"
@keyup.alt.enter="open('_blank')"
/>
</div>
</template>
<script>
export default {
name: "SearchInput",
props: {
value: String,
hotkey: {
type: String,
default: "/",
},
},
mounted() {
this._keyListener = function (event) {
if (event.key === this.hotkey) {
event.preventDefault();
this.focus();
}
if (event.key === "Escape") {
this.cancel();
}
};
document.addEventListener("keydown", this._keyListener.bind(this));
// fill search from get parameter.
const search = new URLSearchParams(window.location.search).get("search");
if (search) {
this.$refs.search.value = search;
this.search(search);
this.focus();
}
},
methods: {
open: function (target = null) {
if (!this.$refs.search.value) {
return;
}
this.$emit("search-open", target);
},
focus: function () {
this.$emit("search-focus");
this.$nextTick(() => {
this.$refs.search.focus();
});
},
setSearchURL: function (value) {
const url = new URL(window.location);
if (value === "") {
url.searchParams.delete("search");
} else {
url.searchParams.set("search", value);
}
window.history.replaceState("search", null, url);
},
cancel: function () {
this.setSearchURL("");
this.$refs.search.value = "";
this.$refs.search.blur();
this.$emit("search-cancel");
},
search: function (value) {
this.setSearchURL(value);
this.$emit("input", value.toLowerCase());
},
},
beforeDestroy() {
document.removeEventListener("keydown", this._keyListener);
},
};
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,30 @@
<template>
<component
v-bind:is="component"
:item="item"
:proxy="proxy"
:forwarder="forwarder"
></component>
</template>
<script>
import Generic from "./services/Generic.vue";
export default {
name: "Service",
props: {
item: Object,
proxy: Object,
forwarder: Object,
},
computed: {
component() {
const type = this.item.type || "Generic";
if (type === "Generic") {
return Generic;
}
return () => import(`./services/${type}.vue`);
},
},
};
</script>

View File

@ -0,0 +1,42 @@
<template>
<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>
</template>
<script>
export default {
name: "SettingToggle",
props: {
name: String,
icon: String,
iconAlt: String,
defaultValue: Boolean,
},
data: function () {
return {
secondaryIcon: null,
value: true,
};
},
created: function () {
this.secondaryIcon = this.iconAlt || this.icon;
if (this.name in localStorage) {
this.value = JSON.parse(localStorage[this.name]);
} else {
this.value = this.defaultValue;
}
this.$emit("updated", this.value);
},
methods: {
toggleSetting: function () {
this.value = !this.value;
localStorage[this.name] = this.value;
this.$emit("updated", this.value);
},
},
};
</script>

View File

@ -0,0 +1,111 @@
<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="stats">
{{ percentage }}&percnt; blocked
</template>
</p>
</template>
<template #indicator>
<div class="status" :class="protection">
{{ protection }}
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "AdGuardHome",
mixins: [service],
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 this.fetch("/control/status").catch((e) =>
console.log(e)
);
},
fetchStats: async function () {
this.stats = await this.fetch("/control/stats").catch((e) =>
console.log(e)
);
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
&.enabled:before {
background-color: #94e185;
border-color: #78d965;
box-shadow: 0px 0px 4px 1px #94e185;
}
&.disabled:before {
background-color: #c9404d;
border-color: #c42c3b;
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;
width: 7px;
height: 7px;
margin-right: 10px;
border: 1px solid #000;
border-radius: 7px;
}
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<Generic :item="item">
<template #content>
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">
<template v-if="item.subtitle">
{{ item.subtitle }}
</template>
<template v-else>
{{ embyCount }}
</template>
</p>
</template>
<template #indicator>
<div v-if="status" class="status" :class="status">
{{ status }}
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Emby",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
status: "",
albumCount: 0,
songCount: 0,
movieCount: 0,
seriesCount: 0,
episodeCount: 0,
}),
computed: {
embyCount: function () {
if (this.item.libraryType === "music")
return `${this.songCount} songs, ${this.albumCount} albums`;
else if (this.item.libraryType === "movies")
return `${this.movieCount} movies`;
else if (this.item.libraryType === "series")
return `${this.episodeCount} eps, ${this.seriesCount} series`;
else return `wrong library type 💀`;
},
},
created() {
this.fetchServerStatus();
if (!this.item.subtitle && this.status !== "dead")
this.fetchServerMediaStats();
},
methods: {
fetchServerStatus: async function () {
this.fetch("/System/info/public")
.then((response) => {
if (response.Id) this.status = "running";
else throw new Error();
})
.catch((e) => {
console.log(e);
this.status = "dead";
});
},
fetchServerMediaStats: async function () {
const headers = {
"X-Emby-Token": this.item.apikey,
};
var data = await this.fetch("/items/counts", { headers }).catch((e) => {
console.log(e);
});
this.albumCount = data.AlbumCount;
this.songCount = data.SongCount;
this.movieCount = data.MovieCount;
this.seriesCount = data.SeriesCount;
this.episodeCount = data.EpisodeCount;
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
&.running:before {
background-color: #94e185;
border-color: #78d965;
box-shadow: 0 0 5px 1px #94e185;
}
&.dead:before {
background-color: #c9404d;
border-color: #c42c3b;
box-shadow: 0 0 5px 1px #c9404d;
}
&:before {
content: " ";
display: inline-block;
width: 7px;
height: 7px;
margin-right: 10px;
border: 1px solid #000;
border-radius: 7px;
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div>
<div
class="card"
:style="`background-color:${item.background};`"
:class="item.class"
>
<a :href="item.url" :target="item.target" rel="noreferrer">
<div class="card-content">
<div :class="mediaClass">
<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">
<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>
</div>
</div>
</a>
</div>
</div>
</template>
<script>
export default {
name: "Generic",
props: {
item: Object,
},
computed: {
mediaClass: function () {
return { media: true, "no-subtitle": !this.item.subtitle };
},
},
};
</script>
<style scoped lang="scss">
.media-left {
.image {
display: flex;
align-items: center;
}
img {
max-height: 100%;
}
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="activity > 0" class="notif activity" title="Activity">
{{ activity }}
</strong>
<strong v-if="warnings > 0" class="notif warnings" title="Warning">
{{ warnings }}
</strong>
<strong v-if="errors > 0" class="notif errors" title="Error">
{{ errors }}
</strong>
<strong
v-if="serverError"
class="notif errors"
title="Connection error to Lidarr API, check url and apikey in config.yml"
>?</strong
>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Lidarr",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => {
return {
activity: null,
warnings: null,
errors: null,
serverError: false,
};
},
created: function () {
this.fetchConfig();
},
methods: {
fetchConfig: function () {
this.fetch(`/api/v1/health?apikey=${this.item.apikey}`)
.then((health) => {
this.warnings = 0;
this.errors = 0;
for (var i = 0; i < health.length; i++) {
if (health[i].type == "warning") {
this.warnings++;
} else if (health[i].type == "error") {
this.errors++;
}
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
this.fetch(`/api/v1/queue/status?apikey=${this.item.apikey}`)
.then((queue) => {
this.activity = queue.totalCount;
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
},
},
};
</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-right: 0.35em;
padding-left: 0.35em;
padding-top: 0.2em;
padding-bottom: 0.2em;
border-radius: 0.25em;
position: relative;
margin-left: 0.3em;
font-size: 0.8em;
&.activity {
background-color: #4fb5d6;
}
&.warnings {
background-color: #d08d2e;
}
&.errors {
background-color: #e51111;
}
}
}
</style>

View File

@ -0,0 +1,56 @@
<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="meal"> Today: {{ meal.name }} </template>
<template v-else-if="stats">
happily keeping {{ stats.totalRecipes }} recipes organized
</template>
</p>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Mealie",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
stats: null,
meal: null,
}),
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
const headers = {
Authorization: "Bearer " + this.item.apikey,
Accept: "application/json",
};
if (this.item.subtitle != null) return;
this.meal = await this.fetch("/api/meal-plans/today/", { headers }).catch(
(e) => console.log(e)
);
this.stats = await this.fetch("/api/debug/statistics/", {
headers,
}).catch((e) => console.log(e));
},
},
};
</script>

View File

@ -0,0 +1,102 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong
v-if="config !== null && config.system.news.unread > 0"
class="notif news"
title="News"
>{{ config.system.news.unread }}</strong
>
<strong
v-if="config !== null && config.main.logs.numWarnings > 0"
class="notif warnings"
title="Warning"
>{{ config.main.logs.numWarnings }}</strong
>
<strong
v-if="config !== null && config.main.logs.numErrors > 0"
class="notif errors"
title="Error"
>{{ config.main.logs.numErrors }}</strong
>
<strong
v-if="serverError"
class="notif errors"
title="Connection error to Medusa API, check url and apikey in config.yml"
>?</strong
>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Medusa",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => {
return {
config: null,
serverError: false,
};
},
created: function () {
this.fetchConfig();
},
methods: {
fetchConfig: function () {
this.fetch("/api/v2/config", {
headers: { "X-Api-Key": this.item.apikey },
})
.then((conf) => {
this.config = conf;
})
.catch((e) => {
console.log(e);
this.serverError = true;
});
},
},
};
</script>
<style scoped lang="scss">
.notifs {
position: absolute;
color: white;
font-family: sans-serif;
top: 0.3em;
right: 0.5em;
.notif {
padding-right: 0.35em;
padding-left: 0.35em;
padding-top: 0.2em;
padding-bottom: 0.2em;
border-radius: 0.25em;
position: relative;
margin-left: 0.3em;
font-size: 0.8em;
&.news {
background-color: #777777;
}
&.warnings {
background-color: #d08d2e;
}
&.errors {
background-color: #e51111;
}
}
}
</style>

View File

@ -0,0 +1,136 @@
<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 apiKey = this.item.apikey || this.item.apiKey;
const url = `https://api.openweathermap.org/data/2.5/weather?${locationQuery}&appid=${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>

View File

@ -0,0 +1,55 @@
<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="api">
happily storing {{ api.count }} documents
</template>
</p>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Paperless",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
api: null,
}),
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
if (this.item.subtitle != null) return;
const apikey = this.item.apikey;
if (!apikey) {
console.error(
"apikey is not present in config.yml for the paperless entry!"
);
return;
}
this.api = await this.fetch("/api/documents/", {
headers: {
Authorization: "Token " + this.item.apikey,
},
}).catch((e) => console.log(e));
},
},
};
</script>

View File

@ -0,0 +1,88 @@
<template>
<Generic :item="item">
<template #content>
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">
<template v-if="item.subtitle">
{{ item.subtitle }}
</template>
<template v-else-if="percentage">
{{ percentage }}&percnt; blocked
</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: "PiHole",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
status: "",
ads_percentage_today: 0,
}),
computed: {
percentage: function () {
if (this.ads_percentage_today) {
return this.ads_percentage_today.toFixed(1);
}
return "";
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
const result = await this.fetch("/api.php").catch((e) => console.log(e));
this.status = result.status;
this.ads_percentage_today = result.ads_percentage_today;
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
&.enabled:before {
background-color: #94e185;
border-color: #78d965;
box-shadow: 0 0 5px 1px #94e185;
}
&.disabled:before {
background-color: #c9404d;
border-color: #c42c3b;
box-shadow: 0 0 5px 1px #c9404d;
}
&:before {
content: " ";
display: inline-block;
width: 7px;
height: 7px;
margin-right: 10px;
border: 1px solid #000;
border-radius: 7px;
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<Generic :item="item">
<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: "Ping",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
status: null,
}),
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
this.fetch("/", { method: "HEAD", cache: "no-cache" }, false)
.then(() => {
this.status = "online";
})
.catch(() => {
this.status = "offline";
});
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
&.online:before {
background-color: #94e185;
border-color: #78d965;
box-shadow: 0 0 5px 1px #94e185;
}
&.offline:before {
background-color: #c9404d;
border-color: #c42c3b;
box-shadow: 0 0 5px 1px #c9404d;
}
&:before {
content: " ";
display: inline-block;
width: 7px;
height: 7px;
margin-right: 10px;
border: 1px solid #000;
border-radius: 7px;
}
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="running > 0" class="notif running" title="Running">
{{ running }}
</strong>
<strong v-if="dead > 0" class="notif dead" title="Dead">
{{ dead }}
</strong>
<strong
v-if="misc > 0"
class="notif misc"
title="Other (creating, paused, exited, etc.)"
>
{{ misc }}
</strong>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Portainer",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
endpoints: null,
containers: null,
}),
computed: {
running: function () {
if (!this.containers) {
return "";
}
return this.containers.filter((container) => {
return container.State.toLowerCase() === "running";
}).length;
},
dead: function () {
if (!this.containers) {
return "";
}
return this.containers.filter((container) => {
return container.State.toLowerCase() === "dead";
}).length;
},
misc: function () {
if (!this.containers) {
return "";
}
return this.containers.filter((container) => {
return (
container.State.toLowerCase() !== "running" &&
container.State.toLowerCase() !== "dead"
);
}).length;
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
const headers = {
"X-Api-Key": this.item.apikey,
};
this.endpoints = await this.fetch("/api/endpoints", { headers }).catch(
(e) => {
console.error(e);
}
);
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) => {
console.error(e);
}
);
if (endpointContainers) {
containers = containers.concat(endpointContainers);
}
}
this.containers = containers;
},
},
};
</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;
&.running {
background-color: #4fd671;
}
&.dead {
background-color: #e51111;
}
&.misc {
background-color: #2ed0c8;
}
}
}
</style>

View File

@ -0,0 +1,143 @@
<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="api"> {{ count }} {{ level }} alerts </template>
</p>
</template>
<template #indicator>
<div v-if="api" class="status" :class="level">
{{ count }}
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const AlertsStatus = Object.freeze({
firing: "firing",
pending: "pending",
inactive: "inactive",
});
export default {
name: "Prometheus",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => ({
api: {
status: "",
count: 0,
alerts: {
firing: 0,
inactive: 0,
pending: 0,
},
},
}),
computed: {
count: function () {
return (
this.countFiring() || this.countPending() || this.countInactive() || 0
);
},
level: function () {
if (this.countFiring()) {
return AlertsStatus.firing;
} else if (this.countPending()) {
return AlertsStatus.pending;
}
return AlertsStatus.inactive;
},
},
created() {
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
this.api = await this.fetch("api/v1/alerts").catch((e) => console.log(e));
},
countFiring: function () {
if (this.api) {
return this.api.data?.alerts?.filter(
(alert) => alert.state === AlertsStatus.firing
).length;
}
return 0;
},
countPending: function () {
if (this.api) {
return this.api.data?.alerts?.filter(
(alert) => alert.state === AlertsStatus.pending
).length;
}
return 0;
},
countInactive: function () {
if (this.api) {
return this.api.data?.alerts?.filter(
(alert) => alert.state === AlertsStatus.pending
).length;
}
return 0;
},
},
};
</script>
<style scoped lang="scss">
.media-left {
.image {
display: flex;
align-items: center;
}
img {
max-height: 100%;
}
}
.status {
font-size: 0.8rem;
color: var(--text-title);
&.firing:before {
background-color: #d65c68;
border-color: #e87d88;
box-shadow: 0 0 5px 1px #d65c68;
}
&.pending:before {
background-color: #e8bb7d;
border-color: #d6a35c;
box-shadow: 0 0 5px 1px #e8bb7d;
}
&.inactive:before {
background-color: #8fe87d;
border-color: #70d65c;
box-shadow: 0 0 5px 1px #8fe87d;
}
&:before {
content: " ";
display: inline-block;
width: 7px;
height: 7px;
margin-right: 10px;
border: 1px solid #000;
border-radius: 7px;
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="warnings > 0" class="notif warnings" title="Warning">
{{ warnings }}
</strong>
<strong v-if="errors > 0" class="notif errors" title="Error">
{{ errors }}
</strong>
<strong
v-if="serverError"
class="notif errors"
title="Connection error to Prowlarr API, check url and apikey in config.yml"
>
?
</strong>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
export default {
name: "Prowlarr",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => {
return {
warnings: null,
errors: null,
serverError: false,
};
},
created: function () {
this.fetchConfig();
},
methods: {
fetchConfig: function () {
this.fetch(`/api/v1/health?apikey=${this.item.apikey}`)
.then((health) => {
this.warnings = 0;
this.errors = 0;
for (var i = 0; i < health.length; i++) {
if (health[i].type == "warning") {
this.warnings++;
} else if (health[i].type == "error") {
this.errors++;
}
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
},
},
};
</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;
&.warnings {
background-color: #d08d2e;
}
&.errors {
background-color: #e51111;
}
}
}
</style>

View File

@ -0,0 +1,125 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="activity > 0" class="notif activity" title="Activity">
{{ activity }}
</strong>
<strong v-if="warnings > 0" class="notif warnings" title="Warning">
{{ warnings }}
</strong>
<strong v-if="errors > 0" class="notif errors" title="Error">
{{ errors }}
</strong>
<strong
v-if="serverError"
class="notif errors"
title="Connection error to Radarr API, check url and apikey in config.yml"
>?</strong
>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const V3_API = "/api/v3";
const LEGACY_API = "/api";
export default {
name: "Radarr",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
data: () => {
return {
activity: null,
warnings: null,
errors: null,
serverError: false,
};
},
created: function () {
this.fetchConfig();
},
computed: {
apiPath() {
return this.item.legacyApi ? LEGACY_API : V3_API;
},
},
methods: {
fetchConfig: function () {
this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`)
.then((health) => {
this.warnings = 0;
this.errors = 0;
for (var i = 0; i < health.length; i++) {
if (health[i].type == "warning") {
this.warnings++;
} else if (health[i].type == "error") {
this.errors++;
}
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`)
.then((queue) => {
this.activity = 0;
if (this.item.legacyApi) {
for (var i = 0; i < queue.length; i++) {
if (queue[i].movie) {
this.activity++;
}
}
} else {
this.activity = queue.totalRecords;
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
},
},
};
</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;
&.activity {
background-color: #4fb5d6;
}
&.warnings {
background-color: #d08d2e;
}
&.errors {
background-color: #e51111;
}
}
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<Generic :item="item">
<template #indicator>
<div class="notifs">
<strong v-if="activity > 0" class="notif activity" title="Activity">
{{ activity }}
</strong>
<strong v-if="warnings > 0" class="notif warnings" title="Warning">
{{ warnings }}
</strong>
<strong v-if="errors > 0" class="notif errors" title="Error">
{{ errors }}
</strong>
<strong
v-if="serverError"
class="notif errors"
title="Connection error to Sonarr API, check url and apikey in config.yml"
>
?
</strong>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
import Generic from "./Generic.vue";
const V3_API = "/api/v3";
const LEGACY_API = "/api";
export default {
name: "Sonarr",
mixins: [service],
props: {
item: Object,
},
components: {
Generic,
},
computed: {
apiPath() {
return this.item.legacyApi ? LEGACY_API : V3_API;
},
},
data: () => {
return {
activity: null,
warnings: null,
errors: null,
serverError: false,
};
},
created: function () {
this.fetchConfig();
},
methods: {
fetchConfig: function () {
this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`)
.then((health) => {
this.warnings = 0;
this.errors = 0;
for (var i = 0; i < health.length; i++) {
if (health[i].type == "warning") {
this.warnings++;
} else if (health[i].type == "error") {
this.errors++;
}
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`)
.then((queue) => {
this.activity = 0;
if (this.item.legacyApi) {
for (var i = 0; i < queue.length; i++) {
if (queue[i].series) {
this.activity++;
}
}
} else {
this.activity = queue.totalRecords;
}
})
.catch((e) => {
console.error(e);
this.serverError = true;
});
},
},
};
</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;
&.activity {
background-color: #4fb5d6;
}
&.warnings {
background-color: #d08d2e;
}
&.errors {
background-color: #e51111;
}
}
}
</style>

19
src/main.js Normal file
View File

@ -0,0 +1,19 @@
import Vue from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import "@fortawesome/fontawesome-free/css/all.css";
import "./assets/app.scss";
Vue.config.productionTip = false;
Vue.component("DynamicStyle", {
render: function (createElement) {
return createElement("style", this.$slots.default);
},
});
new Vue({
render: (h) => h(App),
}).$mount("#app");

62
src/mixins/service.js Normal file
View File

@ -0,0 +1,62 @@
const merge = require("lodash.merge");
export default {
props: {
proxy: Object,
forwarder: Object,
},
created: function () {
// custom service often consume info from an API using the item link (url) as a base url,
// but sometimes the base url is different. An optional alternative URL can be provided with the "endpoint" key.
this.endpoint = this.item.endpoint || this.item.url;
if (this.endpoint.endsWith("/")) {
this.endpoint = this.endpoint.slice(0, -1);
}
},
methods: {
fetch: function (path, init, json = true) {
let options = {};
if (this.proxy?.useCredentials) {
options.credentials = "include";
}
// Each item can override the credential settings
if (this.item.useCredentials !== undefined) {
options.credentials =
this.item.useCredentials === true ? "include" : "omit";
}
if (this.forwarder?.apikey) {
options.headers = {
"X-Homer-Forwarder-Api-Key": this.forwarder.apikey,
};
}
if (path.startsWith("/")) {
path = path.slice(1);
}
let url = path ? `${this.endpoint}/${path}` : this.endpoint;
if (this.forwarder?.url) {
options.headers = {
...(options.headers || {}),
"X-Homer-Forwarder-Url": url,
};
url = this.forwarder.url;
}
options = merge(options, init);
return fetch(url, options).then((response) => {
if (!response.ok) {
throw new Error("Not 2xx response");
}
return json ? response.json() : response;
});
},
},
};

View File

@ -0,0 +1,34 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB"
);
},
registered() {
console.log("Service worker has been registered.");
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated() {
console.log("New content is available; please refresh.");
},
offline() {
console.log(
"No internet connection found. App is running in offline mode."
);
},
error(error) {
console.error("Error during service worker registration:", error);
},
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
vendors/vue.min.js vendored

File diff suppressed because one or more lines are too long

29
vue.config.js Normal file
View File

@ -0,0 +1,29 @@
const manifestOptions = require("./public/assets/manifest.json");
module.exports = {
chainWebpack: (config) => {
config.module
.rule("yaml")
.test(/\.ya?ml$/)
.use("raw-loader")
.loader("raw-loader")
.end();
},
publicPath: "",
pwa: {
manifestPath: "assets/manifest.json",
manifestCrossorigin: "use-credentials",
appleMobileWebAppStatusBarStyle: "black",
appleMobileWebAppCapable: "yes",
name: manifestOptions.name,
themeColor: manifestOptions.theme_color,
manifestOptions,
iconPaths: {
favicon32: "assets/icons/favicon-32x32.png",
favicon16: "assets/icons/favicon-16x16.png",
appleTouchIcon: "assets/icons/icon-maskable.png",
maskIcon: "assets/icons/safari-pinned-tab.svg",
msTileImage: "assets/icons/icon-any.png",
},
},
};

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 692 KiB

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