aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue67
-rw-r--r--src/assets/app.scss28
-rw-r--r--src/components/ConnectivityChecker.vue4
-rw-r--r--src/components/DarkMode.vue52
-rw-r--r--src/components/Message.vue59
-rw-r--r--src/components/services/AdGuardHome.vue6
-rw-r--r--src/components/services/Generic.vue35
-rw-r--r--src/components/services/Medusa.vue128
-rw-r--r--src/components/services/PaperlessNG.vue84
-rw-r--r--src/components/services/PiHole.vue8
-rw-r--r--src/components/services/Ping.vue102
-rw-r--r--src/components/services/Radarr.vue151
-rw-r--r--src/components/services/Sonarr.vue151
13 files changed, 802 insertions, 73 deletions
diff --git a/src/App.vue b/src/App.vue
index dc473ca..1f4f509 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -13,7 +13,9 @@
13 <section v-if="config.header" class="first-line"> 13 <section v-if="config.header" class="first-line">
14 <div v-cloak class="container"> 14 <div v-cloak class="container">
15 <div class="logo"> 15 <div class="logo">
16 <img v-if="config.logo" :src="config.logo" alt="dashboard logo" /> 16 <a href="#">
17 <img v-if="config.logo" :src="config.logo" alt="dashboard logo" />
18 </a>
17 <i v-if="config.icon" :class="config.icon"></i> 19 <i v-if="config.icon" :class="config.icon"></i>
18 </div> 20 </div>
19 <div class="dashboard-title"> 21 <div class="dashboard-title">
@@ -62,6 +64,11 @@
62 <template v-for="group in services"> 64 <template v-for="group in services">
63 <h2 v-if="group.name" class="column is-full group-title"> 65 <h2 v-if="group.name" class="column is-full group-title">
64 <i v-if="group.icon" :class="['fa-fw', group.icon]"></i> 66 <i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
67 <div v-else-if="group.logo" class="group-logo media-left">
68 <figure class="image is-48x48">
69 <img :src="group.logo" :alt="`${group.name} logo`" />
70 </figure>
71 </div>
65 {{ group.name }} 72 {{ group.name }}
66 </h2> 73 </h2>
67 <Service 74 <Service
@@ -85,6 +92,11 @@
85 > 92 >
86 <h2 v-if="group.name" class="group-title"> 93 <h2 v-if="group.name" class="group-title">
87 <i v-if="group.icon" :class="['fa-fw', group.icon]"></i> 94 <i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
95 <div v-else-if="group.logo" class="group-logo media-left">
96 <figure class="image is-48x48">
97 <img :src="group.logo" :alt="`${group.name} logo`" />
98 </figure>
99 </div>
88 {{ group.name }} 100 {{ group.name }}
89 </h2> 101 </h2>
90 <Service 102 <Service
@@ -149,28 +161,41 @@ export default {
149 }; 161 };
150 }, 162 },
151 created: async function () { 163 created: async function () {
152 const defaults = jsyaml.load(defaultConfig); 164 this.buildDashboard();
153 let config; 165 window.onhashchange = this.buildDashboard;
154 try {
155 config = await this.getConfig();
156 } catch (error) {
157 console.log(error);
158 config = this.handleErrors("⚠️ Error loading configuration", error);
159 }
160 this.config = merge(defaults, config);
161 this.services = this.config.services;
162 document.title =
163 this.config.documentTitle ||
164 `${this.config.title} | ${this.config.subtitle}`;
165 if (this.config.stylesheet) {
166 let stylesheet = "";
167 for (const file of this.config.stylesheet) {
168 stylesheet += `@import "${file}";`;
169 }
170 this.createStylesheet(stylesheet);
171 }
172 }, 166 },
173 methods: { 167 methods: {
168 buildDashboard: async function () {
169 const defaults = jsyaml.load(defaultConfig);
170 let config;
171 try {
172 config = await this.getConfig();
173 const path =
174 window.location.hash.substring(1) != ""
175 ? window.location.hash.substring(1)
176 : null;
177
178 if (path) {
179 let pathConfig = await this.getConfig(`assets/${path}.yml`); // the slash (/) is included in the pathname
180 config = Object.assign(config, pathConfig);
181 }
182 } catch (error) {
183 console.log(error);
184 config = this.handleErrors("⚠️ Error loading configuration", error);
185 }
186 this.config = merge(defaults, config);
187 this.services = this.config.services;
188 document.title =
189 this.config.documentTitle ||
190 `${this.config.title} | ${this.config.subtitle}`;
191 if (this.config.stylesheet) {
192 let stylesheet = "";
193 for (const file of this.config.stylesheet) {
194 stylesheet += `@import "${file}";`;
195 }
196 this.createStylesheet(stylesheet);
197 }
198 },
174 getConfig: function (path = "assets/config.yml") { 199 getConfig: function (path = "assets/config.yml") {
175 return fetch(path).then((response) => { 200 return fetch(path).then((response) => {
176 if (response.redirected) { 201 if (response.redirected) {
diff --git a/src/assets/app.scss b/src/assets/app.scss
index c246500..f2dfb37 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -106,7 +106,7 @@ body {
106 } 106 }
107 107
108 .first-line { 108 .first-line {
109 height: 100px; 109 min-height: 100px;
110 vertical-align: center; 110 vertical-align: center;
111 background-color: var(--highlight-primary); 111 background-color: var(--highlight-primary);
112 112
@@ -121,7 +121,7 @@ body {
121 } 121 }
122 122
123 .container { 123 .container {
124 height: 80px; 124 min-height: 80px;
125 padding: 10px 0; 125 padding: 10px 0;
126 } 126 }
127 127
@@ -140,8 +140,7 @@ body {
140 } 140 }
141 } 141 }
142 } 142 }
143 .navbar, 143 .navbar {
144 .navbar-menu {
145 background-color: var(--highlight-secondary); 144 background-color: var(--highlight-secondary);
146 145
147 a { 146 a {
@@ -153,6 +152,9 @@ body {
153 background-color: var(--highlight-hover); 152 background-color: var(--highlight-hover);
154 } 153 }
155 } 154 }
155 .navbar-menu {
156 background-color: inherit;
157 }
156 } 158 }
157 .navbar-end { 159 .navbar-end {
158 text-align: right; 160 text-align: right;
@@ -197,6 +199,11 @@ body {
197 } 199 }
198 } 200 }
199 201
202 .media.no-subtitle {
203 display: flex;
204 align-items: center;
205 }
206
200 .media-content { 207 .media-content {
201 overflow: hidden; 208 overflow: hidden;
202 text-overflow: inherit; 209 text-overflow: inherit;
@@ -206,7 +213,7 @@ body {
206 color: var(--highlight-secondary); 213 color: var(--highlight-secondary);
207 background-color: var(--highlight-secondary); 214 background-color: var(--highlight-secondary);
208 position: absolute; 215 position: absolute;
209 top: 1rem; 216 bottom: 1rem;
210 right: -0.2rem; 217 right: -0.2rem;
211 width: 3px; 218 width: 3px;
212 overflow: hidden; 219 overflow: hidden;
@@ -219,7 +226,6 @@ body {
219 } 226 }
220 227
221 .card { 228 .card {
222 border-radius: 5px;
223 border: none; 229 border: none;
224 box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1); 230 box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1);
225 transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms; 231 transition: cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
@@ -255,11 +261,13 @@ body {
255 } 261 }
256 262
257 .column div:first-of-type .card { 263 .column div:first-of-type .card {
258 border-radius: 5px 5px 0 0; 264 border-top-left-radius: 0.25rem;
265 border-top-right-radius: 0.25rem;
259 } 266 }
260 267
261 .column div:last-child .card { 268 .column div:last-child .card {
262 border-radius: 0 0 5px 5px; 269 border-bottom-left-radius: 0.25rem;
270 border-bottom-right-radius: 0.25rem;
263 } 271 }
264 } 272 }
265 273
@@ -340,3 +348,7 @@ body {
340 } 348 }
341 } 349 }
342} 350}
351
352.group-logo {
353 float: left;
354}
diff --git a/src/components/ConnectivityChecker.vue b/src/components/ConnectivityChecker.vue
index d41c443..02cbd7f 100644
--- a/src/components/ConnectivityChecker.vue
+++ b/src/components/ConnectivityChecker.vue
@@ -37,8 +37,8 @@ export default {
37 method: "HEAD", 37 method: "HEAD",
38 cache: "no-store", 38 cache: "no-store",
39 }) 39 })
40 .then(function () { 40 .then(function (response) {
41 that.offline = false; 41 that.offline = !response.ok;
42 }) 42 })
43 .catch(function () { 43 .catch(function () {
44 that.offline = true; 44 that.offline = true;
diff --git a/src/components/DarkMode.vue b/src/components/DarkMode.vue
index a5aae41..80491fa 100644
--- a/src/components/DarkMode.vue
+++ b/src/components/DarkMode.vue
@@ -4,7 +4,11 @@
4 aria-label="Toggle dark mode" 4 aria-label="Toggle dark mode"
5 class="navbar-item is-inline-block-mobile" 5 class="navbar-item is-inline-block-mobile"
6 > 6 >
7 <i class="fas fa-fw fa-adjust"></i> 7 <i
8 :class="`${faClasses[mode]}`"
9 class="fa-fw"
10 :title="`${titles[mode]}`"
11 ></i>
8 </a> 12 </a>
9</template> 13</template>
10 14
@@ -14,21 +18,55 @@ export default {
14 data: function () { 18 data: function () {
15 return { 19 return {
16 isDark: null, 20 isDark: null,
21 faClasses: null,
22 titles: null,
23 mode: null,
17 }; 24 };
18 }, 25 },
19 created: function () { 26 created: function () {
20 this.isDark = 27 this.faClasses = ["fas fa-adjust", "fas fa-circle", "far fa-circle"];
21 "overrideDark" in localStorage 28 this.titles = ["Auto-switch", "Light theme", "Dark theme"];
22 ? JSON.parse(localStorage.overrideDark) 29 this.mode = 0;
23 : matchMedia("(prefers-color-scheme: dark)").matches; 30 if ("overrideDark" in localStorage) {
31 // Light theme is 1 and Dark theme is 2
32 this.mode = JSON.parse(localStorage.overrideDark) ? 2 : 1;
33 }
34 this.isDark = this.getIsDark();
24 this.$emit("updated", this.isDark); 35 this.$emit("updated", this.isDark);
25 }, 36 },
26 methods: { 37 methods: {
27 toggleTheme: function () { 38 toggleTheme: function () {
28 this.isDark = !this.isDark; 39 this.mode = (this.mode + 1) % 3;
29 localStorage.overrideDark = this.isDark; 40 switch (this.mode) {
41 // Default behavior
42 case 0:
43 localStorage.removeItem("overrideDark");
44 break;
45 // Force light theme
46 case 1:
47 localStorage.overrideDark = false;
48 break;
49 // Force dark theme
50 case 2:
51 localStorage.overrideDark = true;
52 break;
53 default:
54 // Should be unreachable
55 break;
56 }
57
58 this.isDark = this.getIsDark();
30 this.$emit("updated", this.isDark); 59 this.$emit("updated", this.isDark);
31 }, 60 },
61
62 getIsDark: function () {
63 const values = [
64 matchMedia("(prefers-color-scheme: dark)").matches,
65 false,
66 true,
67 ];
68 return values[this.mode];
69 },
32 }, 70 },
33}; 71};
34</script> 72</script>
diff --git a/src/components/Message.vue b/src/components/Message.vue
index 5a1e0ea..00ce158 100644
--- a/src/components/Message.vue
+++ b/src/components/Message.vue
@@ -22,26 +22,52 @@ export default {
22 }, 22 },
23 data: function () { 23 data: function () {
24 return { 24 return {
25 show: false,
26 message: {}, 25 message: {},
27 }; 26 };
28 }, 27 },
29 created: async function () { 28 created: async function () {
30 // Look for a new message if an endpoint is provided. 29 // Look for a new message if an endpoint is provided.
31 this.message = Object.assign({}, this.item); 30 this.message = Object.assign({}, this.item);
32 if (this.item && this.item.url) { 31 await this.getMessage();
33 const fetchedMessage = await this.getMessage(this.item.url); 32 },
34 // keep the original config value if no value is provided by the endpoint 33 computed: {
35 for (const prop of ["title", "style", "content"]) { 34 show: function () {
36 if (prop in fetchedMessage && fetchedMessage[prop] !== null) { 35 return this.message.title || this.message.content;
37 this.message[prop] = fetchedMessage[prop]; 36 },
38 } 37 },
39 } 38 watch: {
40 } 39 item: function (item) {
41 this.show = this.message.title || this.message.content; 40 this.message = Object.assign({}, item);
41 },
42 }, 42 },
43 methods: { 43 methods: {
44 getMessage: function (url) { 44 getMessage: async function () {
45 if (!this.item) {
46 return;
47 }
48 if (this.item.url) {
49 let fetchedMessage = await this.downloadMessage(this.item.url);
50 console.log("done");
51 if (this.item.mapping) {
52 fetchedMessage = this.mapRemoteMessage(fetchedMessage);
53 }
54
55 // keep the original config value if no value is provided by the endpoint
56 const message = this.message;
57 for (const prop of ["title", "style", "content", "icon"]) {
58 if (prop in fetchedMessage && fetchedMessage[prop] !== null) {
59 message[prop] = fetchedMessage[prop];
60 }
61 }
62 this.message = { ...message }; // Force computed property to re-evaluate
63 }
64
65 if (this.item.refreshInterval) {
66 setTimeout(this.getMessage, this.item.refreshInterval);
67 }
68 },
69
70 downloadMessage: function (url) {
45 return fetch(url).then(function (response) { 71 return fetch(url).then(function (response) {
46 if (response.status != 200) { 72 if (response.status != 200) {
47 return; 73 return;
@@ -49,6 +75,15 @@ export default {
49 return response.json(); 75 return response.json();
50 }); 76 });
51 }, 77 },
78
79 mapRemoteMessage: function (message) {
80 let mapped = {};
81 // map property from message into mapped according to mapping config (only if field has a value):
82 for (const prop in this.item.mapping)
83 if (message[this.item.mapping[prop]])
84 mapped[prop] = message[this.item.mapping[prop]];
85 return mapped;
86 },
52 }, 87 },
53}; 88};
54</script> 89</script>
diff --git a/src/components/services/AdGuardHome.vue b/src/components/services/AdGuardHome.vue
index 6ef5302..19a2f7d 100644
--- a/src/components/services/AdGuardHome.vue
+++ b/src/components/services/AdGuardHome.vue
@@ -51,9 +51,9 @@ export default {
51 }, 51 },
52 methods: { 52 methods: {
53 fetchStatus: async function () { 53 fetchStatus: async function () {
54 this.status = await fetch( 54 this.status = await fetch(`${this.item.url}/control/status`, {
55 `${this.item.url}/control/status` 55 credentials: "include",
56 ).then((response) => response.json()); 56 }).then((response) => response.json());
57 }, 57 },
58 }, 58 },
59}; 59};
diff --git a/src/components/services/Generic.vue b/src/components/services/Generic.vue
index 3238ead..08bd3f6 100644
--- a/src/components/services/Generic.vue
+++ b/src/components/services/Generic.vue
@@ -1,16 +1,3 @@
1<script>
2export default {};
3</script>
4
5<style></style>
6*/
7
8<script>
9export default {};
10</script>
11
12<style></style>
13
14<template> 1<template>
15 <div> 2 <div>
16 <div 3 <div
@@ -20,7 +7,7 @@ export default {};
20 > 7 >
21 <a :href="item.url" :target="item.target" rel="noreferrer"> 8 <a :href="item.url" :target="item.target" rel="noreferrer">
22 <div class="card-content"> 9 <div class="card-content">
23 <div class="media"> 10 <div :class="mediaClass">
24 <div v-if="item.logo" class="media-left"> 11 <div v-if="item.logo" class="media-left">
25 <figure class="image is-48x48"> 12 <figure class="image is-48x48">
26 <img :src="item.logo" :alt="`${item.name} logo`" /> 13 <img :src="item.logo" :alt="`${item.name} logo`" />
@@ -33,7 +20,9 @@ export default {};
33 </div> 20 </div>
34 <div class="media-content"> 21 <div class="media-content">
35 <p class="title is-4">{{ item.name }}</p> 22 <p class="title is-4">{{ item.name }}</p>
36 <p class="subtitle is-6">{{ item.subtitle }}</p> 23 <p class="subtitle is-6" v-if="item.subtitle">
24 {{ item.subtitle }}
25 </p>
37 </div> 26 </div>
38 </div> 27 </div>
39 <div class="tag" :class="item.tagstyle" v-if="item.tag"> 28 <div class="tag" :class="item.tagstyle" v-if="item.tag">
@@ -51,11 +40,23 @@ export default {
51 props: { 40 props: {
52 item: Object, 41 item: Object,
53 }, 42 },
43 computed: {
44 mediaClass: function () {
45 return { media: true, "no-subtitle": !this.item.subtitle };
46 },
47 },
54}; 48};
55</script> 49</script>
56 50
57<style scoped lang="scss"> 51<style scoped lang="scss">
58.media-left img { 52.media-left {
59 max-height: 100%; 53 .image {
54 display: flex;
55 align-items: center;
56 }
57
58 img {
59 max-height: 100%;
60 }
60} 61}
61</style> 62</style>
diff --git a/src/components/services/Medusa.vue b/src/components/services/Medusa.vue
new file mode 100644
index 0000000..5720649
--- /dev/null
+++ b/src/components/services/Medusa.vue
@@ -0,0 +1,128 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">{{ item.subtitle }}</p>
20 </div>
21 <div class="notifs">
22 <strong
23 v-if="config !== null && config.system.news.unread > 0"
24 class="notif news"
25 title="News"
26 >{{ config.system.news.unread }}</strong
27 >
28 <strong
29 v-if="config !== null && config.main.logs.numWarnings > 0"
30 class="notif warnings"
31 title="Warning"
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>
56
57<script>
58export default {
59 name: "Medusa",
60 props: {
61 item: Object,
62 },
63 data: () => {
64 return {
65 config: null,
66 serverError: false,
67 };
68 },
69 created: function () {
70 this.fetchConfig();
71 },
72 methods: {
73 fetchConfig: function () {
74 fetch(`${this.item.url}/api/v2/config`, {
75 credentials: "include",
76 headers: { "X-Api-Key": `${this.item.apikey}` },
77 })
78 .then((response) => {
79 if (response.status != 200) {
80 throw new Error(response.statusText);
81 }
82 return response.json();
83 })
84 .then((conf) => {
85 this.config = conf;
86 })
87 .catch((e) => {
88 console.log(e);
89 this.serverError = true;
90 });
91 },
92 },
93};
94</script>
95
96<style scoped lang="scss">
97.media-left img {
98 max-height: 100%;
99}
100.notifs {
101 position: absolute;
102 color: white;
103 font-family: sans-serif;
104 top: 0.3em;
105 right: 0.5em;
106}
107.notif {
108 padding-right: 0.35em;
109 padding-left: 0.35em;
110 padding-top: 0.2em;
111 padding-bottom: 0.2em;
112 border-radius: 0.25em;
113 position: relative;
114 margin-left: 0.3em;
115 font-size: 0.8em;
116}
117.news {
118 background-color: #777777;
119}
120
121.warnings {
122 background-color: #d08d2e;
123}
124
125.errors {
126 background-color: #e51111;
127}
128</style>
diff --git a/src/components/services/PaperlessNG.vue b/src/components/services/PaperlessNG.vue
new file mode 100644
index 0000000..af13317
--- /dev/null
+++ b/src/components/services/PaperlessNG.vue
@@ -0,0 +1,84 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
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>
37
38<script>
39export default {
40 name: "Paperless",
41 props: {
42 item: Object,
43 },
44 data: () => ({
45 api: null,
46 }),
47 created() {
48 this.fetchStatus();
49 },
50 methods: {
51 fetchStatus: async function () {
52 if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing
53 var apikey = this.item.apikey;
54 if (!apikey) {
55 console.error(
56 "apikey is not present in config.yml for the paperless entry!"
57 );
58 return;
59 }
60 const url = `${this.item.url}/api/documents/`;
61 this.api = await fetch(url, {
62 credentials: "include",
63 headers: {
64 Authorization: "Token " + this.item.apikey,
65 },
66 })
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 },
76 },
77};
78</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 a9fd369..87f7090 100644
--- a/src/components/services/PiHole.vue
+++ b/src/components/services/PiHole.vue
@@ -64,7 +64,9 @@ export default {
64 methods: { 64 methods: {
65 fetchStatus: async function () { 65 fetchStatus: async function () {
66 const url = `${this.item.url}/api.php`; 66 const url = `${this.item.url}/api.php`;
67 this.api = await fetch(url) 67 this.api = await fetch(url, {
68 credentials: "include",
69 })
68 .then((response) => response.json()) 70 .then((response) => response.json())
69 .catch((e) => console.log(e)); 71 .catch((e) => console.log(e));
70 }, 72 },
@@ -83,13 +85,13 @@ export default {
83 &.enabled:before { 85 &.enabled:before {
84 background-color: #94e185; 86 background-color: #94e185;
85 border-color: #78d965; 87 border-color: #78d965;
86 box-shadow: 0 0 4px 1px #94e185; 88 box-shadow: 0 0 5px 1px #94e185;
87 } 89 }
88 90
89 &.disabled:before { 91 &.disabled:before {
90 background-color: #c9404d; 92 background-color: #c9404d;
91 border-color: #c42c3b; 93 border-color: #c42c3b;
92 box-shadow: 0 0 4px 1px #c9404d; 94 box-shadow: 0 0 5px 1px #c9404d;
93 } 95 }
94 96
95 &:before { 97 &:before {
diff --git a/src/components/services/Ping.vue b/src/components/services/Ping.vue
new file mode 100644
index 0000000..e693af4
--- /dev/null
+++ b/src/components/services/Ping.vue
@@ -0,0 +1,102 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
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 </p>
24 </div>
25 <div v-if="status" class="status" :class="status">
26 {{ status }}
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>
37
38<script>
39export default {
40 name: "Ping",
41 props: {
42 item: Object,
43 },
44 data: () => ({
45 status: null,
46 }),
47 created() {
48 this.fetchStatus();
49 },
50 methods: {
51 fetchStatus: async function () {
52 const url = `${this.item.url}`;
53 fetch(url, {
54 method: "HEAD",
55 cache: "no-cache",
56 credentials: "include",
57 })
58 .then((response) => {
59 if (!response.ok) {
60 throw Error(response.statusText);
61 }
62 this.status = "online";
63 })
64 .catch(() => {
65 this.status = "offline";
66 });
67 },
68 },
69};
70</script>
71
72<style scoped lang="scss">
73.media-left img {
74 max-height: 100%;
75}
76.status {
77 font-size: 0.8rem;
78 color: var(--text-title);
79
80 &.online:before {
81 background-color: #94e185;
82 border-color: #78d965;
83 box-shadow: 0 0 5px 1px #94e185;
84 }
85
86 &.offline:before {
87 background-color: #c9404d;
88 border-color: #c42c3b;
89 box-shadow: 0 0 5px 1px #c9404d;
90 }
91
92 &:before {
93 content: " ";
94 display: inline-block;
95 width: 7px;
96 height: 7px;
97 margin-right: 10px;
98 border: 1px solid #000;
99 border-radius: 7px;
100 }
101}
102</style>
diff --git a/src/components/services/Radarr.vue b/src/components/services/Radarr.vue
new file mode 100644
index 0000000..9d38292
--- /dev/null
+++ b/src/components/services/Radarr.vue
@@ -0,0 +1,151 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">{{ item.subtitle }}</p>
20 </div>
21 <div class="notifs">
22 <strong
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>
53
54<script>
55export default {
56 name: "Radarr",
57 props: {
58 item: Object,
59 },
60 data: () => {
61 return {
62 activity: null,
63 warnings: null,
64 errors: null,
65 serverError: false,
66 };
67 },
68 created: function () {
69 this.fetchConfig();
70 },
71 methods: {
72 fetchConfig: function () {
73 fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
74 .then((response) => {
75 if (response.status != 200) {
76 throw new Error(response.statusText);
77 }
78 return response.json();
79 })
80 .then((health) => {
81 this.warnings = 0;
82 this.errors = 0;
83 for (var i = 0; i < health.length; i++) {
84 if (health[i].type == "warning") {
85 this.warnings++;
86 } else if (health[i].type == "error") {
87 this.errors++;
88 }
89 }
90 })
91 .catch((e) => {
92 console.error(e);
93 this.serverError = true;
94 });
95 fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
96 .then((response) => {
97 if (response.status != 200) {
98 throw new Error(response.statusText);
99 }
100 return response.json();
101 })
102 .then((queue) => {
103 this.activity = 0;
104 for (var i = 0; i < queue.length; i++) {
105 if (queue[i].movie) {
106 this.activity++;
107 }
108 }
109 })
110 .catch((e) => {
111 console.error(e);
112 this.serverError = true;
113 });
114 },
115 },
116};
117</script>
118
119<style scoped lang="scss">
120.media-left img {
121 max-height: 100%;
122}
123.notifs {
124 position: absolute;
125 color: white;
126 font-family: sans-serif;
127 top: 0.3em;
128 right: 0.5em;
129}
130.notif {
131 padding-right: 0.35em;
132 padding-left: 0.35em;
133 padding-top: 0.2em;
134 padding-bottom: 0.2em;
135 border-radius: 0.25em;
136 position: relative;
137 margin-left: 0.3em;
138 font-size: 0.8em;
139}
140.activity {
141 background-color: #4fb5d6;
142}
143
144.warnings {
145 background-color: #d08d2e;
146}
147
148.errors {
149 background-color: #e51111;
150}
151</style>
diff --git a/src/components/services/Sonarr.vue b/src/components/services/Sonarr.vue
new file mode 100644
index 0000000..7851b6b
--- /dev/null
+++ b/src/components/services/Sonarr.vue
@@ -0,0 +1,151 @@
1<template>
2 <div>
3 <div class="card" :class="item.class">
4 <a :href="item.url" :target="item.target" rel="noreferrer">
5 <div class="card-content">
6 <div class="media">
7 <div v-if="item.logo" class="media-left">
8 <figure class="image is-48x48">
9 <img :src="item.logo" :alt="`${item.name} logo`" />
10 </figure>
11 </div>
12 <div v-if="item.icon" class="media-left">
13 <figure class="image is-48x48">
14 <i style="font-size: 35px" :class="['fa-fw', item.icon]"></i>
15 </figure>
16 </div>
17 <div class="media-content">
18 <p class="title is-4">{{ item.name }}</p>
19 <p class="subtitle is-6">{{ item.subtitle }}</p>
20 </div>
21 <div class="notifs">
22 <strong
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 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>
53
54<script>
55export default {
56 name: "Sonarr",
57 props: {
58 item: Object,
59 },
60 data: () => {
61 return {
62 activity: null,
63 warnings: null,
64 errors: null,
65 serverError: false,
66 };
67 },
68 created: function () {
69 this.fetchConfig();
70 },
71 methods: {
72 fetchConfig: function () {
73 fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`)
74 .then((response) => {
75 if (response.status != 200) {
76 throw new Error(response.statusText);
77 }
78 return response.json();
79 })
80 .then((health) => {
81 this.warnings = 0;
82 this.errors = 0;
83 for (var i = 0; i < health.length; i++) {
84 if (health[i].type == "warning") {
85 this.warnings++;
86 } else if (health[i].type == "error") {
87 this.errors++;
88 }
89 }
90 })
91 .catch((e) => {
92 console.error(e);
93 this.serverError = true;
94 });
95 fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`)
96 .then((response) => {
97 if (response.status != 200) {
98 throw new Error(response.statusText);
99 }
100 return response.json();
101 })
102 .then((queue) => {
103 this.activity = 0;
104 for (var i = 0; i < queue.length; i++) {
105 if (queue[i].series) {
106 this.activity++;
107 }
108 }
109 })
110 .catch((e) => {
111 console.error(e);
112 this.serverError = true;
113 });
114 },
115 },
116};
117</script>
118
119<style scoped lang="scss">
120.media-left img {
121 max-height: 100%;
122}
123.notifs {
124 position: absolute;
125 color: white;
126 font-family: sans-serif;
127 top: 0.3em;
128 right: 0.5em;
129}
130.notif {
131 padding-right: 0.35em;
132 padding-left: 0.35em;
133 padding-top: 0.2em;
134 padding-bottom: 0.2em;
135 border-radius: 0.25em;
136 position: relative;
137 margin-left: 0.3em;
138 font-size: 0.8em;
139}
140.activity {
141 background-color: #4fb5d6;
142}
143
144.warnings {
145 background-color: #d08d2e;
146}
147
148.errors {
149 background-color: #e51111;
150}
151</style>