aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/DarkMode.vue14
-rw-r--r--src/components/GetStarted.vue35
-rw-r--r--src/components/SearchInput.vue4
-rw-r--r--src/components/SettingToggle.vue3
-rw-r--r--src/components/services/AdGuardHome.vue3
-rw-r--r--src/components/services/Emby.vue118
-rw-r--r--src/components/services/Lidarr.vue110
-rw-r--r--src/components/services/Mealie.vue106
-rw-r--r--src/components/services/Medusa.vue142
-rw-r--r--src/components/services/PaperlessNG.vue79
-rw-r--r--src/components/services/PiHole.vue85
-rw-r--r--src/components/services/Portainer.vue139
-rw-r--r--src/components/services/Prowlarr.vue94
-rw-r--r--src/components/services/Radarr.vue156
-rw-r--r--src/components/services/Sonarr.vue158
15 files changed, 793 insertions, 453 deletions
diff --git a/src/components/DarkMode.vue b/src/components/DarkMode.vue
index 80491fa..677238a 100644
--- a/src/components/DarkMode.vue
+++ b/src/components/DarkMode.vue
@@ -15,6 +15,9 @@
15<script> 15<script>
16export default { 16export default {
17 name: "Darkmode", 17 name: "Darkmode",
18 props: {
19 defaultValue: String,
20 },
18 data: function () { 21 data: function () {
19 return { 22 return {
20 isDark: null, 23 isDark: null,
@@ -30,6 +33,17 @@ export default {
30 if ("overrideDark" in localStorage) { 33 if ("overrideDark" in localStorage) {
31 // Light theme is 1 and Dark theme is 2 34 // Light theme is 1 and Dark theme is 2
32 this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1; 35 this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1;
36 } else {
37 switch (this.defaultValue) {
38 case "light":
39 this.mode = 1;
40 break;
41 case "dark":
42 this.mode = 2;
43 break;
44 default:
45 this.mode = 0;
46 }
33 } 47 }
34 this.isDark = this.getIsDark(); 48 this.isDark = this.getIsDark();
35 this.$emit("updated", this.isDark); 49 this.$emit("updated", this.isDark);
diff --git a/src/components/GetStarted.vue b/src/components/GetStarted.vue
new file mode 100644
index 0000000..dcabc02
--- /dev/null
+++ b/src/components/GetStarted.vue
@@ -0,0 +1,35 @@
1<template>
2 <article>
3 <div class="m-6 has-text-centered py-6">
4 <p class="is-size-5 mb-0">No configured service found!</p>
5 <p>Check out the documentation to start building your Homer dashboard.</p>
6 <p>
7 <a
8 class="button is-primary mt-5 has-text-weight-bold"
9 href="https://github.com/bastienwirtz/homer/blob/main/README.md#getting-started"
10 target="_blank"
11 >
12 Get started
13 </a>
14 </p>
15 </div>
16 </article>
17</template>
18
19<script>
20export default {
21 name: "GetStarted",
22};
23</script>
24
25<style lang="scss" scoped>
26p {
27 color: #4a4a4a;
28}
29
30body #app a {
31 font-weight: 900;
32 color: #ffffff;
33 font-family: "Lato", sans-serif;
34}
35</style>
diff --git a/src/components/SearchInput.vue b/src/components/SearchInput.vue
index 586ff71..165c992 100644
--- a/src/components/SearchInput.vue
+++ b/src/components/SearchInput.vue
@@ -19,8 +19,8 @@ export default {
19 value: String, 19 value: String,
20 hotkey: { 20 hotkey: {
21 type: String, 21 type: String,
22 default: "/" 22 default: "/",
23 } 23 },
24 }, 24 },
25 mounted() { 25 mounted() {
26 this._keyListener = function (event) { 26 this._keyListener = function (event) {
diff --git a/src/components/SettingToggle.vue b/src/components/SettingToggle.vue
index 985ca84..6c8a10f 100644
--- a/src/components/SettingToggle.vue
+++ b/src/components/SettingToggle.vue
@@ -12,6 +12,7 @@ export default {
12 name: String, 12 name: String,
13 icon: String, 13 icon: String,
14 iconAlt: String, 14 iconAlt: String,
15 defaultValue: Boolean,
15 }, 16 },
16 data: function () { 17 data: function () {
17 return { 18 return {
@@ -24,6 +25,8 @@ export default {
24 25
25 if (this.name in localStorage) { 26 if (this.name in localStorage) {
26 this.value = JSON.parse(localStorage[this.name]); 27 this.value = JSON.parse(localStorage[this.name]);
28 } else {
29 this.value = this.defaultValue;
27 } 30 }
28 31
29 this.$emit("updated", this.value); 32 this.$emit("updated", this.value);
diff --git a/src/components/services/AdGuardHome.vue b/src/components/services/AdGuardHome.vue
index b01f0f4..4c53398 100644
--- a/src/components/services/AdGuardHome.vue
+++ b/src/components/services/AdGuardHome.vue
@@ -76,9 +76,6 @@ export default {
76</script> 76</script>
77 77
78<style scoped lang="scss"> 78<style scoped lang="scss">
79.media-left img {
80 max-height: 100%;
81}
82.status { 79.status {
83 font-size: 0.8rem; 80 font-size: 0.8rem;
84 color: var(--text-title); 81 color: var(--text-title);
diff --git a/src/components/services/Emby.vue b/src/components/services/Emby.vue
new file mode 100644
index 0000000..25a2612
--- /dev/null
+++ b/src/components/services/Emby.vue
@@ -0,0 +1,118 @@
1<template>
2 <Generic :item="item">
3 <template #content>
4 <p class="title is-4">{{ item.name }}</p>
5 <p class="subtitle is-6">
6 <template v-if="item.subtitle">
7 {{ item.subtitle }}
8 </template>
9 <template v-else>
10 {{ embyCount }}
11 </template>
12 </p>
13 </template>
14 <template #indicator>
15 <div v-if="status" class="status" :class="status">
16 {{ status }}
17 </div>
18 </template>
19 </Generic>
20</template>
21
22<script>
23import service from "@/mixins/service.js";
24import Generic from "./Generic.vue";
25
26export default {
27 name: "Emby",
28 mixins: [service],
29 props: {
30 item: Object,
31 },
32 components: {
33 Generic,
34 },
35 data: () => ({
36 status: "",
37 albumCount: 0,
38 songCount: 0,
39 movieCount: 0,
40 seriesCount: 0,
41 episodeCount: 0,
42 }),
43 computed: {
44 embyCount: function () {
45 if (this.item.libraryType === "music")
46 return `${this.songCount} songs, ${this.albumCount} albums`;
47 else if (this.item.libraryType === "movies")
48 return `${this.movieCount} movies`;
49 else if (this.item.libraryType === "series")
50 return `${this.episodeCount} eps, ${this.seriesCount} series`;
51 else return `wrong library type 💀`;
52 },
53 },
54 created() {
55 this.fetchServerStatus();
56
57 if (!this.item.subtitle && this.status !== "dead")
58 this.fetchServerMediaStats();
59 },
60 methods: {
61 fetchServerStatus: async function () {
62 this.fetch("/System/info/public")
63 .then((response) => {
64 if (response.Id) this.status = "running";
65 else throw new Error();
66 })
67 .catch((e) => {
68 console.log(e);
69 this.status = "dead";
70 });
71 },
72 fetchServerMediaStats: async function () {
73 const headers = {
74 "X-Emby-Token": this.item.apikey,
75 };
76
77 var data = await this.fetch("/items/counts", { headers }).catch((e) => {
78 console.log(e);
79 });
80
81 this.albumCount = data.AlbumCount;
82 this.songCount = data.SongCount;
83 this.movieCount = data.MovieCount;
84 this.seriesCount = data.SeriesCount;
85 this.episodeCount = data.EpisodeCount;
86 },
87 },
88};
89</script>
90
91<style scoped lang="scss">
92.status {
93 font-size: 0.8rem;
94 color: var(--text-title);
95
96 &.running:before {
97 background-color: #94e185;
98 border-color: #78d965;
99 box-shadow: 0 0 5px 1px #94e185;
100 }
101
102 &.dead:before {
103 background-color: #c9404d;
104 border-color: #c42c3b;
105 box-shadow: 0 0 5px 1px #c9404d;
106 }
107
108 &:before {
109 content: " ";
110 display: inline-block;
111 width: 7px;
112 height: 7px;
113 margin-right: 10px;
114 border: 1px solid #000;
115 border-radius: 7px;
116 }
117}
118</style>
diff --git a/src/components/services/Lidarr.vue b/src/components/services/Lidarr.vue
new file mode 100644
index 0000000..cbe5516
--- /dev/null
+++ b/src/components/services/Lidarr.vue
@@ -0,0 +1,110 @@
1<template>
2 <Generic :item="item">
3 <template #indicator>
4 <div class="notifs">
5 <strong v-if="activity > 0" class="notif activity" title="Activity">
6 {{ activity }}
7 </strong>
8 <strong v-if="warnings > 0" class="notif warnings" title="Warning">
9 {{ warnings }}
10 </strong>
11 <strong v-if="errors > 0" class="notif errors" title="Error">
12 {{ errors }}
13 </strong>
14 <strong
15 v-if="serverError"
16 class="notif errors"
17 title="Connection error to Lidarr API, check url and apikey in config.yml"
18 >?</strong
19 >
20 </div>
21 </template>
22 </Generic>
23</template>
24
25<script>
26import service from "@/mixins/service.js";
27import Generic from "./Generic.vue";
28
29export default {
30 name: "Lidarr",
31 mixins: [service],
32 props: {
33 item: Object,
34 },
35 components: {
36 Generic,
37 },
38 data: () => {
39 return {
40 activity: null,
41 warnings: null,
42 errors: null,
43 serverError: false,
44 };
45 },
46 created: function () {
47 this.fetchConfig();
48 },
49 methods: {
50 fetchConfig: function () {
51 this.fetch(`/api/v1/health?apikey=${this.item.apikey}`)
52 .then((health) => {
53 this.warnings = 0;
54 this.errors = 0;
55 for (var i = 0; i < health.length; i++) {
56 if (health[i].type == "warning") {
57 this.warnings++;
58 } else if (health[i].type == "error") {
59 this.errors++;
60 }
61 }
62 })
63 .catch((e) => {
64 console.error(e);
65 this.serverError = true;
66 });
67 this.fetch(`/api/v1/queue/status?apikey=${this.item.apikey}`)
68 .then((queue) => {
69 this.activity = queue.totalCount;
70 })
71 .catch((e) => {
72 console.error(e);
73 this.serverError = true;
74 });
75 },
76 },
77};
78</script>
79
80<style scoped lang="scss">
81.notifs {
82 position: absolute;
83 color: white;
84 font-family: sans-serif;
85 top: 0.3em;
86 right: 0.5em;
87 .notif {
88 display: inline-block;
89 padding-right: 0.35em;
90 padding-left: 0.35em;
91 padding-top: 0.2em;
92 padding-bottom: 0.2em;
93 border-radius: 0.25em;
94 position: relative;
95 margin-left: 0.3em;
96 font-size: 0.8em;
97 &.activity {
98 background-color: #4fb5d6;
99 }
100
101 &.warnings {
102 background-color: #d08d2e;
103 }
104
105 &.errors {
106 background-color: #e51111;
107 }
108 }
109}
110</style>
diff --git a/src/components/services/Mealie.vue b/src/components/services/Mealie.vue
index 790a9b1..b5b2255 100644
--- a/src/components/services/Mealie.vue
+++ b/src/components/services/Mealie.vue
@@ -1,47 +1,33 @@
1<template> 1<template>
2 <div> 2 <Generic :item="item">
3 <div class="card" :class="item.class"> 3 <template #content>
4 <a :href="item.url" :target="item.target" rel="noreferrer"> 4 <p class="title is-4">{{ item.name }}</p>
5 <div class="card-content"> 5 <p class="subtitle is-6">
6 <div class="media"> 6 <template v-if="item.subtitle">
7 <div v-if="item.logo" class="media-left"> 7 {{ item.subtitle }}
8 <figure class="image is-48x48"> 8 </template>
9 <img :src="item.logo" :alt="`${item.name} logo`" /> 9 <template v-else-if="meal"> Today: {{ meal.name }} </template>
10 </figure> 10 <template v-else-if="stats">
11 </div> 11 happily keeping {{ stats.totalRecipes }} recipes organized
12 <div v-if="item.icon" class="media-left"> 12 </template>
13 <figure class="image is-48x48"> 13 </p>
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> 14 </template>
15 </figure> 15 </Generic>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">
20 <template v-if="item.subtitle">
21 {{ item.subtitle }}
22 </template>
23 <template v-else-if="meal"> Today: {{ meal.name }} </template>
24 <template v-else-if="stats">
25 happily keeping {{ stats.totalRecipes }} recipes organized
26 </template>
27 </p>
28 </div>
29 </div>
30 <div class="tag" :class="item.tagstyle" v-if="item.tag">
31 <strong class="tag-text">#{{ item.tag }}</strong>
32 </div>
33 </div>
34 </a>
35 </div>
36 </div>
37</template> 16</template>
38 17
39<script> 18<script>
19import service from "@/mixins/service.js";
20import Generic from "./Generic.vue";
21
40export default { 22export default {
41 name: "Mealie", 23 name: "Mealie",
24 mixins: [service],
42 props: { 25 props: {
43 item: Object, 26 item: Object,
44 }, 27 },
28 components: {
29 Generic,
30 },
45 data: () => ({ 31 data: () => ({
46 stats: null, 32 stats: null,
47 meal: null, 33 meal: null,
@@ -51,44 +37,20 @@ export default {
51 }, 37 },
52 methods: { 38 methods: {
53 fetchStatus: async function () { 39 fetchStatus: async function () {
54 if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing 40 const headers = {
55 this.meal = await fetch(`${this.item.url}/api/meal-plans/today/`, { 41 Authorization: "Bearer " + this.item.apikey,
56 headers: { 42 Accept: "application/json",
57 Authorization: "Bearer " + this.item.apikey, 43 };
58 Accept: "application/json", 44
59 }, 45 if (this.item.subtitle != null) return;
60 }) 46
61 .then(function (response) { 47 this.meal = await this.fetch("/api/meal-plans/today/", { headers }).catch(
62 if (!response.ok) { 48 (e) => console.log(e)
63 throw new Error("Not 2xx response"); 49 );
64 } else { 50 this.stats = await this.fetch("/api/debug/statistics/", {
65 if (response != null) { 51 headers,
66 return response.json(); 52 }).catch((e) => console.log(e));
67 }
68 }
69 })
70 .catch((e) => console.log(e));
71 this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, {
72 headers: {
73 Authorization: "Bearer " + this.item.apikey,
74 Accept: "application/json",
75 },
76 })
77 .then(function (response) {
78 if (!response.ok) {
79 throw new Error("Not 2xx response");
80 } else {
81 return response.json();
82 }
83 })
84 .catch((e) => console.log(e));
85 }, 53 },
86 }, 54 },
87}; 55};
88</script> 56</script>
89
90<style scoped lang="scss">
91.media-left img {
92 max-height: 100%;
93}
94</style>
diff --git a/src/components/services/Medusa.vue b/src/components/services/Medusa.vue
index 5720649..43b5651 100644
--- a/src/components/services/Medusa.vue
+++ b/src/components/services/Medusa.vue
@@ -1,65 +1,49 @@
1<template> 1<template>
2 <div> 2 <Generic :item="item">
3 <div class="card" :class="item.class"> 3 <template #indicator>
4 <a :href="item.url" :target="item.target" rel="noreferrer"> 4 <div class="notifs">
5 <div class="card-content"> 5 <strong
6 <div class="media"> 6 v-if="config !== null && config.system.news.unread > 0"
7 <div v-if="item.logo" class="media-left"> 7 class="notif news"
8 <figure class="image is-48x48"> 8 title="News"
9 <img :src="item.logo" :alt="`${item.name} logo`" /> 9 >{{ config.system.news.unread }}</strong
10 </figure> 10 >
11 </div> 11 <strong
12 <div v-if="item.icon" class="media-left"> 12 v-if="config !== null && config.main.logs.numWarnings > 0"
13 <figure class="image is-48x48"> 13 class="notif warnings"
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> 14 title="Warning"
15 </figure> 15 >{{ config.main.logs.numWarnings }}</strong
16 </div> 16 >
17 <div class="media-content"> 17 <strong
18 <p class="title is-4">{{ item.name }}</p> 18 v-if="config !== null && config.main.logs.numErrors > 0"
19 <p class="subtitle is-6">{{ item.subtitle }}</p> 19 class="notif errors"
20 </div> 20 title="Error"
21 <div class="notifs"> 21 >{{ config.main.logs.numErrors }}</strong
22 <strong 22 >
23 v-if="config !== null && config.system.news.unread > 0" 23 <strong
24 class="notif news" 24 v-if="serverError"
25 title="News" 25 class="notif errors"
26 >{{ config.system.news.unread }}</strong 26 title="Connection error to Medusa API, check url and apikey in config.yml"
27 > 27 >?</strong
28 <strong 28 >
29 v-if="config !== null && config.main.logs.numWarnings > 0" 29 </div>
30 class="notif warnings" 30 </template>
31 title="Warning" 31 </Generic>
32 >{{ config.main.logs.numWarnings }}</strong
33 >
34 <strong
35 v-if="config !== null && config.main.logs.numErrors > 0"
36 class="notif errors"
37 title="Error"
38 >{{ config.main.logs.numErrors }}</strong
39 >
40 <strong
41 v-if="serverError"
42 class="notif errors"
43 title="Connection error to Medusa API, check url and apikey in config.yml"
44 >?</strong
45 >
46 </div>
47 </div>
48 <div class="tag" :class="item.tagstyle" v-if="item.tag">
49 <strong class="tag-text">#{{ item.tag }}</strong>
50 </div>
51 </div>
52 </a>
53 </div>
54 </div>
55</template> 32</template>
56 33
57<script> 34<script>
35import service from "@/mixins/service.js";
36import Generic from "./Generic.vue";
37
58export default { 38export default {
59 name: "Medusa", 39 name: "Medusa",
40 mixins: [service],
60 props: { 41 props: {
61 item: Object, 42 item: Object,
62 }, 43 },
44 components: {
45 Generic,
46 },
63 data: () => { 47 data: () => {
64 return { 48 return {
65 config: null, 49 config: null,
@@ -71,16 +55,9 @@ export default {
71 }, 55 },
72 methods: { 56 methods: {
73 fetchConfig: function () { 57 fetchConfig: function () {
74 fetch(`${this.item.url}/api/v2/config`, { 58 this.fetch("/api/v2/config", {
75 credentials: "include", 59 headers: { "X-Api-Key": this.item.apikey },
76 headers: { "X-Api-Key": `${this.item.apikey}` },
77 }) 60 })
78 .then((response) => {
79 if (response.status != 200) {
80 throw new Error(response.statusText);
81 }
82 return response.json();
83 })
84 .then((conf) => { 61 .then((conf) => {
85 this.config = conf; 62 this.config = conf;
86 }) 63 })
@@ -94,35 +71,32 @@ export default {
94</script> 71</script>
95 72
96<style scoped lang="scss"> 73<style scoped lang="scss">
97.media-left img {
98 max-height: 100%;
99}
100.notifs { 74.notifs {
101 position: absolute; 75 position: absolute;
102 color: white; 76 color: white;
103 font-family: sans-serif; 77 font-family: sans-serif;
104 top: 0.3em; 78 top: 0.3em;
105 right: 0.5em; 79 right: 0.5em;
106} 80 .notif {
107.notif { 81 padding-right: 0.35em;
108 padding-right: 0.35em; 82 padding-left: 0.35em;
109 padding-left: 0.35em; 83 padding-top: 0.2em;
110 padding-top: 0.2em; 84 padding-bottom: 0.2em;
111 padding-bottom: 0.2em; 85 border-radius: 0.25em;
112 border-radius: 0.25em; 86 position: relative;
113 position: relative; 87 margin-left: 0.3em;
114 margin-left: 0.3em; 88 font-size: 0.8em;
115 font-size: 0.8em; 89 &.news {
116} 90 background-color: #777777;
117.news { 91 }
118 background-color: #777777;
119}
120 92
121.warnings { 93 &.warnings {
122 background-color: #d08d2e; 94 background-color: #d08d2e;
123} 95 }
124 96
125.errors { 97 &.errors {
126 background-color: #e51111; 98 background-color: #e51111;
99 }
100 }
127} 101}
128</style> 102</style>
diff --git a/src/components/services/PaperlessNG.vue b/src/components/services/PaperlessNG.vue
index af13317..69f2437 100644
--- a/src/components/services/PaperlessNG.vue
+++ b/src/components/services/PaperlessNG.vue
@@ -1,46 +1,32 @@
1<template> 1<template>
2 <div> 2 <Generic :item="item">
3 <div class="card" :class="item.class"> 3 <template #content>
4 <a :href="item.url" :target="item.target" rel="noreferrer"> 4 <p class="title is-4">{{ item.name }}</p>
5 <div class="card-content"> 5 <p class="subtitle is-6">
6 <div class="media"> 6 <template v-if="item.subtitle">
7 <div v-if="item.logo" class="media-left"> 7 {{ item.subtitle }}
8 <figure class="image is-48x48"> 8 </template>
9 <img :src="item.logo" :alt="`${item.name} logo`" /> 9 <template v-else-if="api">
10 </figure> 10 happily storing {{ api.count }} documents
11 </div> 11 </template>
12 <div v-if="item.icon" class="media-left"> 12 </p>
13 <figure class="image is-48x48"> 13 </template>
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> 14 </Generic>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">
20 <template v-if="item.subtitle">
21 {{ item.subtitle }}
22 </template>
23 <template v-else-if="api">
24 happily storing {{ api.count }} documents
25 </template>
26 </p>
27 </div>
28 </div>
29 <div class="tag" :class="item.tagstyle" v-if="item.tag">
30 <strong class="tag-text">#{{ item.tag }}</strong>
31 </div>
32 </div>
33 </a>
34 </div>
35 </div>
36</template> 15</template>
37 16
38<script> 17<script>
18import service from "@/mixins/service.js";
19import Generic from "./Generic.vue";
20
39export default { 21export default {
40 name: "Paperless", 22 name: "Paperless",
23 mixins: [service],
41 props: { 24 props: {
42 item: Object, 25 item: Object,
43 }, 26 },
27 components: {
28 Generic,
29 },
44 data: () => ({ 30 data: () => ({
45 api: null, 31 api: null,
46 }), 32 }),
@@ -49,36 +35,21 @@ export default {
49 }, 35 },
50 methods: { 36 methods: {
51 fetchStatus: async function () { 37 fetchStatus: async function () {
52 if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing 38 if (this.item.subtitle != null) return;
53 var apikey = this.item.apikey; 39
40 const apikey = this.item.apikey;
54 if (!apikey) { 41 if (!apikey) {
55 console.error( 42 console.error(
56 "apikey is not present in config.yml for the paperless entry!" 43 "apikey is not present in config.yml for the paperless entry!"
57 ); 44 );
58 return; 45 return;
59 } 46 }
60 const url = `${this.item.url}/api/documents/`; 47 this.api = await this.fetch("/api/documents/", {
61 this.api = await fetch(url, {
62 credentials: "include",
63 headers: { 48 headers: {
64 Authorization: "Token " + this.item.apikey, 49 Authorization: "Token " + this.item.apikey,
65 }, 50 },
66 }) 51 }).catch((e) => console.log(e));
67 .then(function (response) {
68 if (!response.ok) {
69 throw new Error("Not 2xx response");
70 } else {
71 return response.json();
72 }
73 })
74 .catch((e) => console.log(e));
75 }, 52 },
76 }, 53 },
77}; 54};
78</script> 55</script>
79
80<style scoped lang="scss">
81.media-left img {
82 max-height: 100%;
83}
84</style>
diff --git a/src/components/services/PiHole.vue b/src/components/services/PiHole.vue
index 87f7090..9aac016 100644
--- a/src/components/services/PiHole.vue
+++ b/src/components/services/PiHole.vue
@@ -1,59 +1,45 @@
1<template> 1<template>
2 <div> 2 <Generic :item="item">
3 <div class="card" :class="item.class"> 3 <template #content>
4 <a :href="item.url" :target="item.target" rel="noreferrer"> 4 <p class="title is-4">{{ item.name }}</p>
5 <div class="card-content"> 5 <p class="subtitle is-6">
6 <div class="media"> 6 <template v-if="item.subtitle">
7 <div v-if="item.logo" class="media-left"> 7 {{ item.subtitle }}
8 <figure class="image is-48x48"> 8 </template>
9 <img :src="item.logo" :alt="`${item.name} logo`" /> 9 <template v-else-if="percentage">
10 </figure> 10 {{ percentage }}&percnt; blocked
11 </div> 11 </template>
12 <div v-if="item.icon" class="media-left"> 12 </p>
13 <figure class="image is-48x48"> 13 </template>
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> 14 <template #indicator>
15 </figure> 15 <div v-if="status" class="status" :class="status">
16 </div> 16 {{ status }}
17 <div class="media-content"> 17 </div>
18 <p class="title is-4">{{ item.name }}</p> 18 </template>
19 <p class="subtitle is-6"> 19 </Generic>
20 <template v-if="item.subtitle">
21 {{ item.subtitle }}
22 </template>
23 <template v-else-if="api">
24 {{ percentage }}&percnt; blocked
25 </template>
26 </p>
27 </div>
28 <div v-if="api" class="status" :class="api.status">
29 {{ api.status }}
30 </div>
31 </div>
32 <div class="tag" :class="item.tagstyle" v-if="item.tag">
33 <strong class="tag-text">#{{ item.tag }}</strong>
34 </div>
35 </div>
36 </a>
37 </div>
38 </div>
39</template> 20</template>
40 21
41<script> 22<script>
23import service from "@/mixins/service.js";
24import Generic from "./Generic.vue";
25
42export default { 26export default {
43 name: "PiHole", 27 name: "PiHole",
28 mixins: [service],
44 props: { 29 props: {
45 item: Object, 30 item: Object,
46 }, 31 },
32 components: {
33 Generic,
34 },
47 data: () => ({ 35 data: () => ({
48 api: { 36 status: "",
49 status: "", 37 ads_percentage_today: 0,
50 ads_percentage_today: 0,
51 },
52 }), 38 }),
53 computed: { 39 computed: {
54 percentage: function () { 40 percentage: function () {
55 if (this.api) { 41 if (this.ads_percentage_today) {
56 return this.api.ads_percentage_today.toFixed(1); 42 return this.ads_percentage_today.toFixed(1);
57 } 43 }
58 return ""; 44 return "";
59 }, 45 },
@@ -63,21 +49,16 @@ export default {
63 }, 49 },
64 methods: { 50 methods: {
65 fetchStatus: async function () { 51 fetchStatus: async function () {
66 const url = `${this.item.url}/api.php`; 52 const result = await this.fetch("/api.php").catch((e) => console.log(e));
67 this.api = await fetch(url, { 53
68 credentials: "include", 54 this.status = result.status;
69 }) 55 this.ads_percentage_today = result.ads_percentage_today;
70 .then((response) => response.json())
71 .catch((e) => console.log(e));
72 }, 56 },
73 }, 57 },
74}; 58};
75</script> 59</script>
76 60
77<style scoped lang="scss"> 61<style scoped lang="scss">
78.media-left img {
79 max-height: 100%;
80}
81.status { 62.status {
82 font-size: 0.8rem; 63 font-size: 0.8rem;
83 color: var(--text-title); 64 color: var(--text-title);
diff --git a/src/components/services/Portainer.vue b/src/components/services/Portainer.vue
new file mode 100644
index 0000000..d101ecc
--- /dev/null
+++ b/src/components/services/Portainer.vue
@@ -0,0 +1,139 @@
1<template>
2 <Generic :item="item">
3 <template #indicator>
4 <div class="notifs">
5 <strong v-if="running > 0" class="notif running" title="Running">
6 {{ running }}
7 </strong>
8 <strong v-if="dead > 0" class="notif dead" title="Dead">
9 {{ dead }}
10 </strong>
11 <strong
12 v-if="misc > 0"
13 class="notif misc"
14 title="Other (creating, paused, exited, etc.)"
15 >
16 {{ misc }}
17 </strong>
18 </div>
19 </template>
20 </Generic>
21</template>
22
23<script>
24import service from "@/mixins/service.js";
25import Generic from "./Generic.vue";
26
27export default {
28 name: "Portainer",
29 mixins: [service],
30 props: {
31 item: Object,
32 },
33 components: {
34 Generic,
35 },
36 data: () => ({
37 endpoints: null,
38 containers: null,
39 }),
40 computed: {
41 running: function () {
42 if (!this.containers) {
43 return "";
44 }
45 return this.containers.filter((container) => {
46 return container.State.toLowerCase() === "running";
47 }).length;
48 },
49 dead: function () {
50 if (!this.containers) {
51 return "";
52 }
53 return this.containers.filter((container) => {
54 return container.State.toLowerCase() === "dead";
55 }).length;
56 },
57 misc: function () {
58 if (!this.containers) {
59 return "";
60 }
61 return this.containers.filter((container) => {
62 return (
63 container.State.toLowerCase() !== "running" &&
64 container.State.toLowerCase() !== "dead"
65 );
66 }).length;
67 },
68 },
69 created() {
70 this.fetchStatus();
71 },
72 methods: {
73 fetchStatus: async function () {
74 const headers = {
75 "X-Api-Key": this.item.apikey,
76 };
77
78 this.endpoints = await this.fetch("/api/endpoints", { headers }).catch(
79 (e) => {
80 console.error(e);
81 }
82 );
83
84 let containers = [];
85 for (let endpoint of this.endpoints) {
86 if (
87 this.item.environments &&
88 !this.item.environments.includes(endpoint.Name)
89 ) {
90 continue;
91 }
92 const uri = `/api/endpoints/${endpoint.Id}/docker/containers/json?all=1`;
93 const endpointContainers = await this.fetch(uri, { headers }).catch(
94 (e) => {
95 console.error(e);
96 }
97 );
98
99 if (endpointContainers) {
100 containers = containers.concat(endpointContainers);
101 }
102 }
103
104 this.containers = containers;
105 },
106 },
107};
108</script>
109
110<style scoped lang="scss">
111.notifs {
112 position: absolute;
113 color: white;
114 font-family: sans-serif;
115 top: 0.3em;
116 right: 0.5em;
117
118 .notif {
119 display: inline-block;
120 padding: 0.2em 0.35em;
121 border-radius: 0.25em;
122 position: relative;
123 margin-left: 0.3em;
124 font-size: 0.8em;
125
126 &.running {
127 background-color: #4fd671;
128 }
129
130 &.dead {
131 background-color: #e51111;
132 }
133
134 &.misc {
135 background-color: #2ed0c8;
136 }
137 }
138}
139</style>
diff --git a/src/components/services/Prowlarr.vue b/src/components/services/Prowlarr.vue
new file mode 100644
index 0000000..abaa0f0
--- /dev/null
+++ b/src/components/services/Prowlarr.vue
@@ -0,0 +1,94 @@
1<template>
2 <Generic :item="item">
3 <template #indicator>
4 <div class="notifs">
5 <strong v-if="warnings > 0" class="notif warnings" title="Warning">
6 {{ warnings }}
7 </strong>
8 <strong v-if="errors > 0" class="notif errors" title="Error">
9 {{ errors }}
10 </strong>
11 <strong
12 v-if="serverError"
13 class="notif errors"
14 title="Connection error to Prowlarr API, check url and apikey in config.yml"
15 >
16 ?
17 </strong>
18 </div>
19 </template>
20 </Generic>
21</template>
22
23<script>
24import service from "@/mixins/service.js";
25import Generic from "./Generic.vue";
26
27export default {
28 name: "Prowlarr",
29 mixins: [service],
30 props: {
31 item: Object,
32 },
33 components: {
34 Generic,
35 },
36 data: () => {
37 return {
38 warnings: null,
39 errors: null,
40 serverError: false,
41 };
42 },
43 created: function () {
44 this.fetchConfig();
45 },
46 methods: {
47 fetchConfig: function () {
48 this.fetch(`/api/v1/health?apikey=${this.item.apikey}`)
49 .then((health) => {
50 this.warnings = 0;
51 this.errors = 0;
52 for (var i = 0; i < health.length; i++) {
53 if (health[i].type == "warning") {
54 this.warnings++;
55 } else if (health[i].type == "error") {
56 this.errors++;
57 }
58 }
59 })
60 .catch((e) => {
61 console.error(e);
62 this.serverError = true;
63 });
64 },
65 },
66};
67</script>
68
69<style scoped lang="scss">
70.notifs {
71 position: absolute;
72 color: white;
73 font-family: sans-serif;
74 top: 0.3em;
75 right: 0.5em;
76
77 .notif {
78 display: inline-block;
79 padding: 0.2em 0.35em;
80 border-radius: 0.25em;
81 position: relative;
82 margin-left: 0.3em;
83 font-size: 0.8em;
84
85 &.warnings {
86 background-color: #d08d2e;
87 }
88
89 &.errors {
90 background-color: #e51111;
91 }
92 }
93}
94</style>
diff --git a/src/components/services/Radarr.vue b/src/components/services/Radarr.vue
index a9cdedf..5c40aaa 100644
--- a/src/components/services/Radarr.vue
+++ b/src/components/services/Radarr.vue
@@ -1,62 +1,43 @@
1<template> 1<template>
2 <div> 2 <Generic :item="item">
3 <div class="card" :class="item.class"> 3 <template #indicator>
4 <a :href="item.url" :target="item.target" rel="noreferrer"> 4 <div class="notifs">
5 <div class="card-content"> 5 <strong v-if="activity > 0" class="notif activity" title="Activity">
6 <div class="media"> 6 {{ activity }}
7 <div v-if="item.logo" class="media-left"> 7 </strong>
8 <figure class="image is-48x48"> 8 <strong v-if="warnings > 0" class="notif warnings" title="Warning">
9 <img :src="item.logo" :alt="`${item.name} logo`" /> 9 {{ warnings }}
10 </figure> 10 </strong>
11 </div> 11 <strong v-if="errors > 0" class="notif errors" title="Error">
12 <div v-if="item.icon" class="media-left"> 12 {{ errors }}
13 <figure class="image is-48x48"> 13 </strong>
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> 14 <strong
15 </figure> 15 v-if="serverError"
16 </div> 16 class="notif errors"
17 <div class="media-content"> 17 title="Connection error to Radarr API, check url and apikey in config.yml"
18 <p class="title is-4">{{ item.name }}</p> 18 >?</strong
19 <p class="subtitle is-6">{{ item.subtitle }}</p> 19 >
20 </div> 20 </div>
21 <div class="notifs"> 21 </template>
22 <strong 22 </Generic>
23 v-if="activity > 0"
24 class="notif activity"
25 title="Activity"
26 >{{ activity }}</strong
27 >
28 <strong
29 v-if="warnings > 0"
30 class="notif warnings"
31 title="Warning"
32 >{{ warnings }}</strong
33 >
34 <strong v-if="errors > 0" class="notif errors" title="Error">{{
35 errors
36 }}</strong>
37 <strong
38 v-if="serverError"
39 class="notif errors"
40 title="Connection error to Radarr API, check url and apikey in config.yml"
41 >?</strong
42 >
43 </div>
44 </div>
45 <div class="tag" :class="item.tagstyle" v-if="item.tag">
46 <strong class="tag-text">#{{ item.tag }}</strong>
47 </div>
48 </div>
49 </a>
50 </div>
51 </div>
52</template> 23</template>
53 24
54<script> 25<script>
26import service from "@/mixins/service.js";
27import Generic from "./Generic.vue";
28
29const V3_API = "/api/v3";
30const LEGACY_API = "/api";
31
55export default { 32export default {
56 name: "Radarr", 33 name: "Radarr",
34 mixins: [service],
57 props: { 35 props: {
58 item: Object, 36 item: Object,
59 }, 37 },
38 components: {
39 Generic,
40 },
60 data: () => { 41 data: () => {
61 return { 42 return {
62 activity: null, 43 activity: null,
@@ -68,17 +49,14 @@ export default {
68 created: function () { 49 created: function () {
69 this.fetchConfig(); 50 this.fetchConfig();
70 }, 51 },
52 computed: {
53 apiPath() {
54 return this.item.legacyApi ? LEGACY_API : V3_API;
55 },
56 },
71 methods: { 57 methods: {
72 fetchConfig: function () { 58 fetchConfig: function () {
73 fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { 59 this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`)
74 credentials: "include",
75 })
76 .then((response) => {
77 if (response.status != 200) {
78 throw new Error(response.statusText);
79 }
80 return response.json();
81 })
82 .then((health) => { 60 .then((health) => {
83 this.warnings = 0; 61 this.warnings = 0;
84 this.errors = 0; 62 this.errors = 0;
@@ -94,21 +72,18 @@ export default {
94 console.error(e); 72 console.error(e);
95 this.serverError = true; 73 this.serverError = true;
96 }); 74 });
97 fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { 75 this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`)
98 credentials: "include",
99 })
100 .then((response) => {
101 if (response.status != 200) {
102 throw new Error(response.statusText);
103 }
104 return response.json();
105 })
106 .then((queue) => { 76 .then((queue) => {
107 this.activity = 0; 77 this.activity = 0;
108 for (var i = 0; i < queue.length; i++) { 78
109 if (queue[i].movie) { 79 if (this.item.legacyApi) {
110 this.activity++; 80 for (var i = 0; i < queue.length; i++) {
81 if (queue[i].movie) {
82 this.activity++;
83 }
111 } 84 }
85 } else {
86 this.activity = queue.totalRecords;
112 } 87 }
113 }) 88 })
114 .catch((e) => { 89 .catch((e) => {
@@ -121,35 +96,30 @@ export default {
121</script> 96</script>
122 97
123<style scoped lang="scss"> 98<style scoped lang="scss">
124.media-left img {
125 max-height: 100%;
126}
127.notifs { 99.notifs {
128 position: absolute; 100 position: absolute;
129 color: white; 101 color: white;
130 font-family: sans-serif; 102 font-family: sans-serif;
131 top: 0.3em; 103 top: 0.3em;
132 right: 0.5em; 104 right: 0.5em;
133} 105 .notif {
134.notif { 106 display: inline-block;
135 padding-right: 0.35em; 107 padding: 0.2em 0.35em;
136 padding-left: 0.35em; 108 border-radius: 0.25em;
137 padding-top: 0.2em; 109 position: relative;
138 padding-bottom: 0.2em; 110 margin-left: 0.3em;
139 border-radius: 0.25em; 111 font-size: 0.8em;
140 position: relative; 112 &.activity {
141 margin-left: 0.3em; 113 background-color: #4fb5d6;
142 font-size: 0.8em; 114 }
143}
144.activity {
145 background-color: #4fb5d6;
146}
147 115
148.warnings { 116 &.warnings {
149 background-color: #d08d2e; 117 background-color: #d08d2e;
150} 118 }
151 119
152.errors { 120 &.errors {
153 background-color: #e51111; 121 background-color: #e51111;
122 }
123 }
154} 124}
155</style> 125</style>
diff --git a/src/components/services/Sonarr.vue b/src/components/services/Sonarr.vue
index 0270255..bb83b6b 100644
--- a/src/components/services/Sonarr.vue
+++ b/src/components/services/Sonarr.vue
@@ -1,62 +1,49 @@
1<template> 1<template>
2 <div> 2 <Generic :item="item">
3 <div class="card" :class="item.class"> 3 <template #indicator>
4 <a :href="item.url" :target="item.target" rel="noreferrer"> 4 <div class="notifs">
5 <div class="card-content"> 5 <strong v-if="activity > 0" class="notif activity" title="Activity">
6 <div class="media"> 6 {{ activity }}
7 <div v-if="item.logo" class="media-left"> 7 </strong>
8 <figure class="image is-48x48"> 8 <strong v-if="warnings > 0" class="notif warnings" title="Warning">
9 <img :src="item.logo" :alt="`${item.name} logo`" /> 9 {{ warnings }}
10 </figure> 10 </strong>
11 </div> 11 <strong v-if="errors > 0" class="notif errors" title="Error">
12 <div v-if="item.icon" class="media-left"> 12 {{ errors }}
13 <figure class="image is-48x48"> 13 </strong>
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i> 14 <strong
15 </figure> 15 v-if="serverError"
16 </div> 16 class="notif errors"
17 <div class="media-content"> 17 title="Connection error to Sonarr API, check url and apikey in config.yml"
18 <p class="title is-4">{{ item.name }}</p> 18 >
19 <p class="subtitle is-6">{{ item.subtitle }}</p> 19 ?
20 </div> 20 </strong>
21 <div class="notifs"> 21 </div>
22 <strong 22 </template>
23 v-if="activity > 0" 23 </Generic>
24 class="notif activity"
25 title="Activity"
26 >{{ activity }}</strong
27 >
28 <strong
29 v-if="warnings > 0"
30 class="notif warnings"
31 title="Warning"
32 >{{ warnings }}</strong
33 >
34 <strong v-if="errors > 0" class="notif errors" title="Error">{{
35 errors
36 }}</strong>
37 <strong
38 v-if="serverError"
39 class="notif errors"
40 title="Connection error to Sonarr API, check url and apikey in config.yml"
41 >?</strong
42 >
43 </div>
44 </div>
45 <div class="tag" :class="item.tagstyle" v-if="item.tag">
46 <strong class="tag-text">#{{ item.tag }}</strong>
47 </div>
48 </div>
49 </a>
50 </div>
51 </div>
52</template> 24</template>
53 25
54<script> 26<script>
27import service from "@/mixins/service.js";
28import Generic from "./Generic.vue";
29
30const V3_API = "/api/v3";
31const LEGACY_API = "/api";
32
55export default { 33export default {
56 name: "Sonarr", 34 name: "Sonarr",
35 mixins: [service],
57 props: { 36 props: {
58 item: Object, 37 item: Object,
59 }, 38 },
39 components: {
40 Generic,
41 },
42 computed: {
43 apiPath() {
44 return this.item.legacyApi ? LEGACY_API : V3_API;
45 },
46 },
60 data: () => { 47 data: () => {
61 return { 48 return {
62 activity: null, 49 activity: null,
@@ -70,15 +57,7 @@ export default {
70 }, 57 },
71 methods: { 58 methods: {
72 fetchConfig: function () { 59 fetchConfig: function () {
73 fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { 60 this.fetch(`${this.apiPath}/health?apikey=${this.item.apikey}`)
74 credentials: "include",
75 })
76 .then((response) => {
77 if (response.status != 200) {
78 throw new Error(response.statusText);
79 }
80 return response.json();
81 })
82 .then((health) => { 61 .then((health) => {
83 this.warnings = 0; 62 this.warnings = 0;
84 this.errors = 0; 63 this.errors = 0;
@@ -94,21 +73,17 @@ export default {
94 console.error(e); 73 console.error(e);
95 this.serverError = true; 74 this.serverError = true;
96 }); 75 });
97 fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { 76 this.fetch(`${this.apiPath}/queue?apikey=${this.item.apikey}`)
98 credentials: "include",
99 })
100 .then((response) => {
101 if (response.status != 200) {
102 throw new Error(response.statusText);
103 }
104 return response.json();
105 })
106 .then((queue) => { 77 .then((queue) => {
107 this.activity = 0; 78 this.activity = 0;
108 for (var i = 0; i < queue.length; i++) { 79 if (this.item.legacyApi) {
109 if (queue[i].series) { 80 for (var i = 0; i < queue.length; i++) {
110 this.activity++; 81 if (queue[i].series) {
82 this.activity++;
83 }
111 } 84 }
85 } else {
86 this.activity = queue.totalRecords;
112 } 87 }
113 }) 88 })
114 .catch((e) => { 89 .catch((e) => {
@@ -121,35 +96,32 @@ export default {
121</script> 96</script>
122 97
123<style scoped lang="scss"> 98<style scoped lang="scss">
124.media-left img {
125 max-height: 100%;
126}
127.notifs { 99.notifs {
128 position: absolute; 100 position: absolute;
129 color: white; 101 color: white;
130 font-family: sans-serif; 102 font-family: sans-serif;
131 top: 0.3em; 103 top: 0.3em;
132 right: 0.5em; 104 right: 0.5em;
133}
134.notif {
135 padding-right: 0.35em;
136 padding-left: 0.35em;
137 padding-top: 0.2em;
138 padding-bottom: 0.2em;
139 border-radius: 0.25em;
140 position: relative;
141 margin-left: 0.3em;
142 font-size: 0.8em;
143}
144.activity {
145 background-color: #4fb5d6;
146}
147 105
148.warnings { 106 .notif {
149 background-color: #d08d2e; 107 display: inline-block;
150} 108 padding: 0.2em 0.35em;
109 border-radius: 0.25em;
110 position: relative;
111 margin-left: 0.3em;
112 font-size: 0.8em;
113
114 &.activity {
115 background-color: #4fb5d6;
116 }
117
118 &.warnings {
119 background-color: #d08d2e;
120 }
151 121
152.errors { 122 &.errors {
153 background-color: #e51111; 123 background-color: #e51111;
124 }
125 }
154} 126}
155</style> 127</style>