Merge branch 'Lissy93:master' into master

This commit is contained in:
Alberto 2023-01-25 16:50:02 +01:00 committed by GitHub
commit 8ef419b767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1305 additions and 215 deletions

6
.github/AUTHORS.txt vendored
View File

@ -64,6 +64,7 @@ cauterize <cauterize@programmer.net> - 2 commits
k073l <21180271+k073l@users.noreply.github.com> - 2 commits
liss-bot <87835202+liss-bot@users.noreply.github.com> - 2 commits
patrickheeney <patrickheeney@gmail.com> - 2 commits
rtm516 <rtm516@users.noreply.github.com> - 2 commits
ᗪєνιη <υн> - 2 commits
Alucarddelta <dreyer.brent@gmail.com> - 3 commits
Tuzi555 <jakub.tuzar@gmail.com> - 3 commits
@ -71,6 +72,7 @@ Walkx <71191962+walkxcode@users.noreply.github.com> - 3 commits
aterox <kenneth@kenneth.church> - 3 commits
bogyeong <boggyhint@gmail.com> - 3 commits
stanly0726 <37040069+stanly0726@users.noreply.github.com> - 3 commits
Alberto <aravac> - 4 commits
David <laso> - 4 commits
Lissy93 <alicia@omg.lol> - 4 commits
Niklas <abe> - 4 commits
@ -98,9 +100,9 @@ Alicia <yke> - 28 commits
repo-visualizer <repo-visualizer@users.noreply.github.com> - 71 commits
snyk-bot <snyk-bot@snyk.io> - 74 commits
Lissy93 <gh@d0h.co> - 78 commits
Alicia <o> - 130 commits
Alicia <o> - 132 commits
liss-bot <liss-bot@d0h.co> - 142 commits
Lissy93 <Lissy93@users.noreply.github.com> - 208 commits
Alicia <yke> - 250 commits
Alicia <yke> - 253 commits
Alicia <yke> - 440 commits
Alicia <yke> - 1488 commits

View File

@ -50,4 +50,4 @@ CMD [ "yarn", "start" ]
EXPOSE ${PORT}
# Run simple healthchecks every 5 mins, to check that everythings still great
HEALTHCHECK --interval=5m --timeout=2s --start-period=30s CMD yarn health-check
HEALTHCHECK --interval=5m --timeout=5s --start-period=30s CMD yarn health-check

View File

@ -552,28 +552,21 @@ Huge thanks to the sponsors helping to support Dashy's development!
<sub><b>Eddy Lazzarin</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Geolim4">
<img src="https://avatars.githubusercontent.com/u/1332071?u=5d84984897f094932c4829583fee19b121f3aab8&v=4" width="80;" alt="Geolim4"/>
<br />
<sub><b>Georges.L</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/AnandChowdhary">
<img src="https://avatars.githubusercontent.com/u/2841780?u=ca8e292b15abcc6cddaeae0abded0115c51b4789&v=4" width="80;" alt="AnandChowdhary"/>
<br />
<sub><b>Anand Chowdhary</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/davidpaulyoung">
<img src="https://avatars.githubusercontent.com/u/3418369?v=4" width="80;" alt="davidpaulyoung"/>
<br />
<sub><b>David Young</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/k-rol">
<img src="https://avatars.githubusercontent.com/u/4050412?u=1162510eec7b7aeb31d4c7c65d51d4f773d823b0&v=4" width="80;" alt="k-rol"/>
@ -608,15 +601,15 @@ Huge thanks to the sponsors helping to support Dashy's development!
<br />
<sub><b>Brian McGonagill</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/vlad-timofeev">
<img src="https://avatars.githubusercontent.com/u/11474041?v=4" width="80;" alt="vlad-timofeev"/>
<br />
<sub><b>Vlad Timofeev</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/PAPAMICA">
<img src="https://avatars.githubusercontent.com/u/29079741?v=4" width="80;" alt="PAPAMICA"/>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 MiB

After

Width:  |  Height:  |  Size: 16 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -32,28 +32,21 @@
<sub><b>Eddy Lazzarin</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Geolim4">
<img src="https://avatars.githubusercontent.com/u/1332071?u=5d84984897f094932c4829583fee19b121f3aab8&v=4" width="80;" alt="Geolim4"/>
<br />
<sub><b>Georges.L</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/AnandChowdhary">
<img src="https://avatars.githubusercontent.com/u/2841780?u=ca8e292b15abcc6cddaeae0abded0115c51b4789&v=4" width="80;" alt="AnandChowdhary"/>
<br />
<sub><b>Anand Chowdhary</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/davidpaulyoung">
<img src="https://avatars.githubusercontent.com/u/3418369?v=4" width="80;" alt="davidpaulyoung"/>
<br />
<sub><b>David Young</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/k-rol">
<img src="https://avatars.githubusercontent.com/u/4050412?u=1162510eec7b7aeb31d4c7c65d51d4f773d823b0&v=4" width="80;" alt="k-rol"/>
@ -88,15 +81,15 @@
<br />
<sub><b>Brian McGonagill</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/vlad-timofeev">
<img src="https://avatars.githubusercontent.com/u/11474041?v=4" width="80;" alt="vlad-timofeev"/>
<br />
<sub><b>Vlad Timofeev</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/PAPAMICA">
<img src="https://avatars.githubusercontent.com/u/29079741?v=4" width="80;" alt="PAPAMICA"/>
@ -232,6 +225,13 @@
<sub><b>UrekD</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/albcp">
<img src="https://avatars.githubusercontent.com/u/3170731?v=4" width="80;" alt="albcp"/>
<br />
<sub><b>Alberto</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Tracreed">
<img src="https://avatars.githubusercontent.com/u/6306365?v=4" width="80;" alt="Tracreed"/>
@ -252,15 +252,15 @@
<br />
<sub><b>Totto16</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/alucarddelta">
<img src="https://avatars.githubusercontent.com/u/20882097?v=4" width="80;" alt="alucarddelta"/>
<br />
<sub><b>Brent</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Bogyie">
<img src="https://avatars.githubusercontent.com/u/82003678?v=4" width="80;" alt="Bogyie"/>
@ -282,13 +282,21 @@
<sub><b>Rúben Silva</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/rtm516">
<img src="https://avatars.githubusercontent.com/u/5401186?v=4" width="80;" alt="rtm516"/>
<br />
<sub><b>rtm516</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/onedr0p">
<img src="https://avatars.githubusercontent.com/u/213795?v=4" width="80;" alt="onedr0p"/>
<br />
<sub><b>ᗪєνιη ᗷυнʟ</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/alexdelprete">
<img src="https://avatars.githubusercontent.com/u/7027842?v=4" width="80;" alt="alexdelprete"/>
@ -302,8 +310,7 @@
<br />
<sub><b>Stephen Rigney</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/moemoeq">
<img src="https://avatars.githubusercontent.com/u/1808434?v=4" width="80;" alt="moemoeq"/>
@ -324,20 +331,6 @@
<br />
<sub><b>Matthias Bilger</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/thomasdissert">
<img src="https://avatars.githubusercontent.com/u/11446531?v=4" width="80;" alt="thomasdissert"/>
<br />
<sub><b>Thomas Dissert</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/k073l">
<img src="https://avatars.githubusercontent.com/u/21180271?v=4" width="80;" alt="k073l"/>
<br />
<sub><b>K073l</b></sub>
</a>
</td></tr>
</table>
<!-- readme: contributors -end -->

View File

@ -35,6 +35,8 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [GitHub Trending](#github-trending)
- [GitHub Profile Stats](#github-profile-stats)
- [Healthchecks Status](#healthchecks status)
- [Mvg Departure](#mvg-departure)
- [Mvg Connection](#mvg-connection)
- **[Self-Hosted Services Widgets](#self-hosted-services-widgets)**
- [System Info](#system-info)
- [Cron Monitoring](#cron-monitoring-health-checks)
@ -58,6 +60,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
- [Nextcloud PHP OPcache](#nextcloud-php-opcache-stats)
- [Sabnzbd](#sabnzbd)
- [Gluetun VPN Info](#gluetun-vpn-info)
- [Drone CI Build](#drone-ci-builds)
- **[System Resource Monitoring](#system-resource-monitoring)**
- [CPU Usage Current](#current-cpu-usage)
- [CPU Usage Per Core](#cpu-usage-per-core)
@ -1173,6 +1176,92 @@ Display status of one or more HealthChecks project(s). Works with healthcheck.io
---
### MVG Departure
Display departure time of a MVG (Münchner Verkehrs Gesellschaft) station.
From https://www.mvg.de/impressum.html:
> [...] Die Verarbeitung unserer Inhalte oder Daten durch Dritte erfordert unsere ausdrückliche Zustimmung. Für private, nicht-kommerzielle Zwecke, wird eine gemäßigte Nutzung ohne unsere ausdrückliche Zustimmung geduldet. Jegliche Form von Data-Mining stellt keine gemäßigte Nutzung dar.[...]
In other words: Private, noncomercial, moderate use of the API is tolerated. They dont consider data mining as moderate use. (This is not a legal advice)
#### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`location`** | `string` | Required | The name of the location (exact) or the location id, startin with `de:09162:`
**`limit`** | `integer` | _Optional_ | Limit number of entries, defaults to 10.
**`title`** | `string` | _Optional_ | A custom title to be displayed.
**`header`** | `bool` | _Optional_ | Shall the title be shown?
**`filters`** | `object` | _Optional_ | Filter results
**`filters.line`** | `string/array` | _Optional_ | Filter results for given line(s).
**`filters.product`** | `string/array` | _Optional_ | Filter results for specific product (TRAM, UBAHN, SBAHN, BUS).
**`filters.destination`** | `string/object` | _Optional_ | Filter results for specific destination(s)
```yaml
- type: mvg
options:
location: Marienplatz
limit: 5
```
#### Info
- **CORS**: 🟢 Enabled
- **Auth**: 🟢 Not Required
- **Price**: 🟢 Free / Private use only
- **Host**: [MVG](https://mvg.de)
- **Privacy**: _See [MVG Datenschutz](https://www.mvg.de/datenschutz-mvg.html)_
---
### MVG Connection
Display the next connection for two addresses/coordinates, stations or POI within Munich using MVG MVG (Münchner Verkehrs Gesellschaft).
From https://www.mvg.de/impressum.html:
> [...] Die Verarbeitung unserer Inhalte oder Daten durch Dritte erfordert unsere ausdrückliche Zustimmung. Für private, nicht-kommerzielle Zwecke, wird eine gemäßigte Nutzung ohne unsere ausdrückliche Zustimmung geduldet. Jegliche Form von Data-Mining stellt keine gemäßigte Nutzung dar.[...]
In other words: Private, noncomercial, moderate use of the API is tolerated. They dont consider data mining as moderate use. (This is not a legal advice)
#### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`origin`** | `string` | Required | Origin of the connection.
**`destination`** | `string` | Required | Destination of the connection.
**`title`** | `string` | _Optional_ | A custom title to be displayed.
**`header`** | `bool` | _Optional_ | Shall the title be shown?
**`filters`** | `object` | _Optional_ | Filter results
**`filters.line`** | `string/array` | _Optional_ | Filter results for given line(s).
**`filters.product`** | `string/array` | _Optional_ | Filter results for specific product (TRAM, UBAHN, SBAHN, BUS).
**`filters.destination`** | `string/object` | _Optional_ | Filter results for specific destination(s)
```yaml
- type: mvg-connection
options:
from: Marienplatz
from: Dachauer Straße 123
header: true
filters:
product: [UBAHN]
line: [U1,U2,U4,U5]
```
#### Info
- **CORS**: 🟢 Enabled
- **Auth**: 🟢 Not Required
- **Price**: 🟢 Free / Private use only
- **Host**: [MVG](https://mvg.de)
- **Privacy**: _See [MVG Datenschutz](https://www.mvg.de/datenschutz-mvg.html)_
---
## Self-Hosted Services Widgets
### System Info
@ -1947,6 +2036,42 @@ Display info from the Gluetun VPN container public IP API. This can show the IP
---
### Drone CI Builds
Display the last builds from a [Drone CI](https://www.drone.ci) instance. A self-hosted CI system that uses docker.
<p align="center"><img width="380" src="https://i.ibb.co/nQM3BXj/Bildschirm-foto-2023-01-07-um-01-31-45.png" /></p>
#### Options
**Field** | **Type** | **Required** | **Description**
--- | --- | --- | ---
**`host`** | `string` | Required | The histname of the Drone CI instance.
**`apiKey`** | `string` | Required | The API key (https://<your-drone-instance>/account).
**`limit`** | `integer` | _Optional_ | Limit the amounts of listed builds.
**`repo`** | `string` | _Optional_ | Show only builds of the specified repo
#### Example
```yaml
- type: drone-io
updateInterval: 30
options:
host: https://drone.somedomain.com
apiKey: my-very-secret-api-key
limit: 10
```
#### Info
- **CORS**: 🟢 Enabled
- **Auth**: 🟢 Required
- **Price**: 🟢 Free
- **Host**: Self-Hosted (see [Drone](https://www.drone.io))
- **Privacy**: _See [Drone](https://www.drone.io)_
---
## System Resource Monitoring
### Glances

View File

@ -4,14 +4,21 @@
* Note that exiting with code 1 indicates failure, and 0 is success
*/
const http = require('http');
const isSsl = !!process.env.SSL_PRIV_KEY_PATH && !!process.env.SSL_PUB_KEY_PATH;
const http = require(isSsl ? 'https' : 'http');
/* Location of the server to test */
const port = process.env.PORT || !!process.env.IS_DOCKER ? 80 : 4000;
const isDocker = !!process.env.IS_DOCKER;
const port = isSsl ? (process.env.SSL_PORT || (isDocker ? 443 : 4001)) : (process.env.PORT || isDocker ? 80 : 4000);
const host = process.env.HOST || '0.0.0.0';
const timeout = 2000;
const requestOptions = { host, port, timeout };
const agent = new http.Agent({
rejectUnauthorized: false, // Allow self-signed certificates
});
const requestOptions = { host, port, timeout, agent };
const startTime = new Date(); // Initialize timestamp to calculate time taken

View File

@ -0,0 +1,238 @@
<template>
<div class="droneci-builds-wrapper" v-if="builds">
<div
class="build-row"
v-for="build in builds" :key="build.id"
v-tooltip="infoTooltip(build)"
>
<div class="status">
<p :class="build.build.status">{{ build.build.status | formatStatus }}</p>
<span v-if="build.build.status == 'running'">
{{ build.build.started*1000 | formatTimeAgo }} ago
</span>
<span v-else-if="build.build.status != 'pending' ">
{{ formatBuildDuration(build) }}
</span>
<span v-else>
{{ build.build.created*1000 | formatTimeAgo }} ago
</span>
</div>
<div class="info">
<div class="build-name">
{{ build.name }}
<a
class="droneci-build-number"
:href="build.baseurl + '/' + build.slug + '/' +build.build.number"
target="_blank"
>{{ build.build.number }}</a>
</div>
<div class="build-desc">
<span class="droneci-extra">
<template v-if="build.build.event == 'pull_request'">
<a
:href="build.build.link"
target="_blank"
class="droneci-extra-info"
>#{{ formatPrId(build.build.link) }}</a> to
</template>
<template v-else-if="build.build.event == 'push'">
<a
:href="build.build.link"
target="_blank"
class="droneci-extra-info"
>push</a> to
</template>
<a
:href="build.git_http_url"
target="_blank"
class="droneci-extra-info"
>
{{ build.build.target }}
</a>
</span>
</div>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import { getTimeAgo, getTimeDifference, timestampToDateTime } from '@/utils/MiscHelpers';
export default {
mixins: [WidgetMixin],
components: {},
data() {
return {
builds: null,
};
},
filters: {
formatStatus(status) {
let symbol = '';
if (status === 'success') symbol = '✔';
if (status === 'failure' || status === 'error' || status === 'killed') symbol = '✘';
if (status === 'running') symbol = '❖';
if (status === 'skipped') symbol = '↠';
return `${symbol}`;
},
formatDate(timestamp) {
return timestampToDateTime(timestamp);
},
formatTimeAgo(timestamp) {
return getTimeAgo(timestamp);
},
},
computed: {
/* API endpoint, either for self-hosted or managed instance */
endpointBuilds() {
if (!this.options.host) this.error('drone.ci Host is required');
return `${this.options.host}/api/user/builds`;
},
endpointRepoInfo() {
if (!this.options.host) this.error('drone.ci Host is required');
return `${this.options.host}/api/repos/${this.options.repo}`;
},
endpointRepoBuilds() {
if (!this.options.host) this.error('drone.ci Host is required');
return `${this.options.host}/api/repos/${this.options.repo}/builds`;
},
repo() {
if (this.options.repo) return this.options.repo;
return false;
},
apiKey() {
if (!this.options.apiKey) {
this.error('An API key is required, please see the docs for more info');
}
return this.options.apiKey;
},
},
methods: {
/* Fetch new data, configured by updateInterval */
update() {
this.startLoading();
this.fetchData();
this.finishLoading();
},
/* Make GET request to Drone CI API endpoint */
fetchData() {
const authHeaders = { Authorization: `Bearer ${this.apiKey}` };
if (this.repo !== false) {
this.makeRequest(this.endpointRepoInfo, authHeaders).then(
(repoInfo) => {
this.makeRequest(this.endpointRepoBuilds, authHeaders).then(
(buildInfo) => {
this.processRepoBuilds(repoInfo, buildInfo);
},
);
},
);
} else {
this.makeRequest(this.endpointBuilds, authHeaders).then(
(response) => { this.processBuilds(response); },
);
}
},
/* Assign data variables to the returned data */
processBuilds(data) {
const results = data.slice(0, this.options.limit)
.map((obj) => ({ ...obj, baseurl: this.options.host }));
this.builds = results;
},
processRepoBuilds(repo, builds) {
const results = builds.slice(0, this.options.limit)
.map((obj) => ({ build: { ...obj }, baseurl: this.options.host, ...repo }));
this.builds = results;
},
infoTooltip(build) {
const content = `<b>Trigger:</b> ${build.build.event} by ${build.build.trigger}<br>`
+ `<b>Repo:</b> ${build.slug}<br>`
+ `<b>Branch:</b> ${build.build.target}<br>`;
return {
content, html: true, trigger: 'hover focus', delay: 250, classes: 'build-info-tt',
};
},
formatPrId(link) {
return link.split('/').pop();
},
formatBuildDuration(build) {
return getTimeDifference(build.build.started * 1000, build.build.finished * 1000);
},
},
};
</script>
<style scoped lang="scss">
.droneci-builds-wrapper {
color: var(--widget-text-color);
.build-row {
display: grid;
grid-template-columns: 1fr 2.5fr;
justify-content: left;
align-items: center;
padding: 0.25rem 0;
.status {
font-size: 1rem;
font-weight: bold;
p {
margin: 0;
color: var(--info);
&.success { color: var(--success); }
&.failure { color: var(--danger); }
&.error { color: var(--danger); }
&.running { color: var(--neutral); }
}
span {
font-size: 0.75rem;
color: var(--secondary);
}
}
.info {
div.build-name {
margin: 0.25rem 0;
font-weight: bold;
color: var(--widget-text-color);
a, a:hover, a:visited, a:active {
color: inherit;
text-decoration: none;
}
.droneci-build-number::before {
content: "#";
}
}
div.build-desc {
margin: 0;
font-size: 0.85rem;
color: var(--widget-text-color);
opacity: var(--dimming-factor);
a, a:hover, a:visited, a:active {
color: inherit;
text-decoration: none;
}
.droneci-extra {
.droneci-extra-info {
margin: 0.25em;
padding: 0em 0.25em;
background: var(--item-background);
border: 1px solid var(--primary);
border-radius: 5px;
}
}
}
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
}
</style>
<style lang="scss">
.build-info-tt {
min-width: 20rem;
}
</style>

View File

@ -0,0 +1,353 @@
<template>
<div class="mvg-wrapper" v-if="departures">
<template
v-for="departure in departures"
>
<div class="departure" v-bind:key="departure.key" v-tooltip="mvgTooltipDeparture(departure)">
<span :class="{live: departure.live}">
{{ departure.realtimeDepartureTime | formatDepartureTime }}
</span>
</div>
<div class='line'
v-bind:key="departure.key + 'line'"
>
<div
class="transport"
:class="['type-' + departure.transportType,
'line-' + departure.label,
]"
>{{ departure.label }}</div>
<div
class='destination'
v-tooltip="mvgTooltipDestination(departure)"
:class="{cancelled: departure.cancelled}">{{ departure.destination }}</div>
<span class="delay"
:class="{'has-delay': departure.realtimeDepartureTime > departure.plannedDepartureTime}"
>{{ Math.max(0,
(departure.realtimeDepartureTime - departure.plannedDepartureTime)/60000) }}</span>
<span class="occupancy"
:class="'occupancy-' + departure.occupancy"
v-if="departure.occupancy != 'UNKNOWN'"
v-tooltip="departure.occupancy"
></span>
</div>
</template>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import { widgetApiEndpoints } from '@/utils/defaults';
import { timestampToTime } from '@/utils/MiscHelpers';
export default {
mixins: [WidgetMixin],
components: {},
data() {
return {
departures: null,
locationSearch: null,
};
},
created() {
if (!this.isLocationId) {
this.makeRequest(this.endpointLocation).then(
(response) => {
const stations = response.filter((r) => r.type === 'STATION');
if (stations.length > 0) {
this.location = stations[0].globalId;
this.fetchData();
} else {
this.error('Cannot find station for specified string');
}
},
);
} else {
this.location = this.options.location;
}
},
filters: {
formatDepartureTime(timestamp) {
const msDifference = new Date(timestamp).getTime() - new Date().getTime();
const diff = Math.max(0, Math.round(msDifference / 60000));
return diff;
},
},
computed: {
isLocationId() {
if (!this.options.location) {
this.error('Location is required');
}
if (typeof this.options.location !== 'string') this.error('Location can only be a string');
if (this.options.location.startsWith('de:09162:')) return true;
return false;
},
offset() {
if (this.options.offset) return this.options.offset;
return 0;
},
limit() {
return this.options.limit || 10;
},
endpointDeparture() {
return `${widgetApiEndpoints.mvg}/departure?globalId=${this.location}&limit=30&offsetInMinutes=${this.offset}&transportTypes=UBAHN,TRAM,BUS,SBAHN`;
},
endpointLocation() {
return `${widgetApiEndpoints.mvg}/location?query=${encodeURIComponent(this.options.location)}`;
},
},
methods: {
update() {
this.startLoading();
this.fetchData();
this.finishLoading();
},
fetchData() {
if (this.location !== undefined) {
this.makeRequest(this.endpointDeparture).then(
(response) => { this.processData(response); },
);
}
},
/* Assign data variables to the returned data */
processData(data) {
let i = 0;
const results = [];
data
.filter(this.filter_results)
.sort(this.sort_results)
.slice(0, this.limit).forEach((dep) => {
results.push({ ...dep, key: `mvg-dep-${this.location}-${i}` });
i += 1;
});
this.departures = results;
},
ensure_array(value) {
if (typeof value === 'string') {
return [value];
}
return value;
},
filter_results(value) {
if (!this.options.filters) return true;
let useEntry = (
(!this.options.filters.line)
|| this.ensure_array(this.options.filters.line).includes(value.label)
);
useEntry = useEntry
&& (
(!this.options.filters.product)
|| this.ensure_array(this.options.filters.product)
.some(x => x.toLowerCase() === value.transportType.toLowerCase())
);
useEntry = useEntry
&& (
(!this.options.filters.destination)
|| this.ensure_array(this.options.filters.destination)
.some(x => x.toLowerCase() === value.destination.toLowerCase())
);
return useEntry;
},
sort_results(a, b) {
const depa = a.realtimeDepartureTime ? a.realtimeDepartureTime : a.plannedDepartureTime;
const depb = b.realtimeDepartureTime ? b.realtimeDepartureTime : b.plannedDepartureTime;
if (depa > depb) return 1;
if (depa < depb) return -1;
if (a.label < b.label) return 1;
if (a.label > b.label) return -1;
if (a.destination < b.destination) return 1;
if (a.destination > b.destination) return -1;
return 0;
},
makeUrl(cronId) {
const base = this.options.host || 'https://healthchecks.io';
return `${base}/checks/${cronId}/details`;
},
mvgTooltipDeparture(data) {
let departureDetails = '';
if (data.realtime) {
departureDetails += `Live: ${timestampToTime(data.realtimeDepartureTime)}<br />`;
}
departureDetails += `Planned: ${timestampToTime(data.plannedDepartureTime)}<br />`;
if (data.realtime) {
departureDetails += 'Live!<br />';
}
return {
content: departureDetails, html: true, trigger: 'hover', delay: 250, classes: 'mvg-info-tt',
};
},
mvgTooltipDestination(data) {
let departureDetails = `<b>Infos:</b><br />${data.messages.join('<br />')}`;
if (data.platform) {
departureDetails += `Platform: ${data.platform}<br />`;
}
if (data.cancelled) {
departureDetails += '<b>Cancelled!</b><br />';
}
return {
content: departureDetails, html: true, trigger: 'hover', delay: 250, classes: 'mvg-info-tt',
};
},
},
};
</script>
<style scoped lang="scss">
.mvg-wrapper {
display: grid;
justify-content: left;
grid-template-columns: 1fr 9fr;
color: var(--widget-text-color);
padding: 0.25rem 0;
grid-row-gap: 0.4em;
.departure {
min-width: 1rem;
font-size: 1.1rem;
font-weight: bold;
text-align: right;
margin-right: 0.2rem;
span.live {
color: var(--success);
}
}
.line {
background-color: #FFFFFF;
margin: 0;
padding-right: 0.2em;
border-radius: 0.2em;
display: grid;
grid-template-columns: 2.2em 1fr minmax(1.5em,max-content) 0.75em;
.type-UBAHN {
border: 0px;
}
.type-SBAHN {
border: 0px;
}
.type-BUS {
}
.type-TRAM {
}
.transport{
border-top-left-radius: 0.2em 0.2em;
border-bottom-left-radius: 0.2em 0.2em;
margin: 0em;
padding: 0.15em 0;
color: #FFFFFF;
margin-right: 0.40em;
text-align: center;
span {
min-width: 2em;
display: inline-block;
}
&.line-U1 {
background-color: #468447;
}
&.line-U2 {
background-color: #dd3d4d;
}
&.line-U3 {
background-color: #ef8824;
}
&.line-U4 {
background-color: #04af90;
}
&.line-U5 {
background-color: #b78730;
}
&.line-U6 {
background-color: #0472b3;
}
&.line-S1 {
background-color: #79c6e7;
}
&.line-S2 {
background-color: #9bc04c;
}
&.line-S3 {
background-color: #942d8d;
}
&.line-S4 {
background-color: #d4214d;
}
&.line-S5 {
background-color: #03a074;
}
&.line-S6 {
background-color: #03a074;
}
&.line-S7 {
background-color: #964438;
}
&.line-S8 {
background-color: #000000;
}
&.type-BUS {
background-color: #0d5c70;
}
}
.destination{
border-radius: 0.2em;
width: 100%;
background-color: #FFFFFF;
color: #000;
padding-top: 0.15em;
padding-bottom: 0.15em;
white-space: nowrap;
overflow: hidden;
span.cancelled {
color: var(--danger);
text-decoration: line-through;
}
span.destination {
overflow: clip;
margin-right: 0.25em;
width: 75%;
display: inline-block;
}
}
.delay{
padding: 0.15em;
font-weight: bold;
&.has-delay{
padding: 0.15em;
background-color: var(--danger);
color: #FFF;
border-radius: 0.2em;
}
}
.delay::before{
content: "+";
}
.occupancy{
display: inline-block;
padding: 0 0.15em;
border-radius: 0.2em;
&.occupancy-LOW {
color: green;
}
&.occupancy-MEDIUM {
color: orange;
}
&.occupancy-HIGH {
color: red;
}
}
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
</style>
<style lang="scss">
.ping-times-tt {
min-width: 20rem;
}
</style>
<style lang="scss">
.mvg-info-tt {
min-width: 20rem;
}
</style>

View File

@ -0,0 +1,372 @@
<template>
<div class="mvg-connections-outer-wrapper">
<div class="mvg-connections-header" v-if="showTitle">{{ connectionName }}</div>
<div class="mvg-wrapper" v-if="connections">
<div
v-for="connection in connections"
v-bind:key="connection.uniqueId"
class="line"
v-tooltip="mvgTooltipConnection(connection)"
>
<div
class="departure"
>
<span class="time"
>
{{connection.parts[0].from.plannedDeparture | formatTime}}
</span>
<span class="delay"
:class="{'has-delay': connection.parts[0].from.departureDelayInMinutes > 0}"
>{{ Math.max(parseInt(connection.parts[0].from.departureDelayInMinutes) || 0, 0) }}</span>
</div>
<div
class="changes"
>
<template
v-for="(part,index) in connection.parts"
>
<span
v-if="index > 0"
v-bind:key="'change-' + index"
class="change"
v-tooltip="part.from.name"
></span>
<span
v-bind:key="'transport-' + index"
:class="['type-' + part.line.transportType,
'line-' + part.line.label,
]"
v-if="part.line.transportType != 'PEDESTRIAN'"
class="transport"
>{{part.line.label}}</span>
<span v-else
v-bind:key="'transport-' + index"
>🚶</span>
</template>
</div>
<span class="time">
{{Date.parse(connection.parts[connection.parts.length-1]
.to.plannedDeparture) - Date.parse(connection.parts[0]
.from.plannedDeparture) | formatDuration}}
</span>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import { widgetApiEndpoints } from '@/utils/defaults';
export default {
mixins: [WidgetMixin],
components: {},
data() {
return {
connections: null,
locationSearch: null,
connectionName: null,
defaultTitle: "Connection",
locations: {
origin: undefined,
destination: undefined,
},
};
},
created() {
const promStart = this.getLocationId(this.start);
const promEnd = this.getLocationId(this.end);
Promise.all([promStart, promEnd]).then(
(results) => {
[this.locations.origin, this.locations.destination] = results.map((r) => r[0]);
this.defaultTitle = `${this.locations.origin.name} - ${this.locations.destination.name}`;
this.fetchData();
},
);
},
filters: {
formatDepartureTime(timestamp) {
const msDifference = new Date(timestamp).getTime() - new Date().getTime();
const diff = Math.max(0, Math.round(msDifference / 60000));
return diff;
},
formatTime(str) {
const d = new Date(Date.parse(str));
function ii(i) {
let s = `${i}`;
if (s.length < 2) s = `0${s}`;
return s;
}
return `${ii(d.getHours())}:${ii(d.getMinutes())}`;
},
formatDuration(val) {
function ii(i) {
let s = `${i}`;
if (s.length < 2) s = `0${s}`;
return s;
}
return `${Math.floor(val / 3600000)}:${ii(Math.floor(val / 60000))}`;
},
},
computed: {
start() {
return this.options.from || this.options.start || this.options.origin || 'Marienplatz';
},
end() {
return this.options.to || this.options.end || this.options.destination || 'Giesing';
},
title() {
if (this.options.title) {
return this.options.title;
}
return this.defaultTitle;
},
showTitle() {
return (this.options.header) ? this.options.header : true;
},
transportTypes() {
if (this.options.transportations) {
return this.options.transportations.join(',');
}
return 'UBAHN,TRAM,BUS,SBAHN';
},
},
methods: {
formatPoint(point, typ) {
if (point.type === 'ADDRESS' || point.type === 'POI') {
return `${typ}Latitude=${point.latitude}&${typ}Longitude=${point.longitude}`;
}
return `${typ}StationGlobalId=${point.globalId}`;
},
isLocationId(loc) {
if (!loc) {
this.error('Location is required');
}
if (typeof loc !== 'string') this.error('Location can only be a string');
return (loc.startsWith('de:09162:'));
},
getLocationId(loc) {
return this.makeRequest(this.getEndpointLocation(loc));
},
getEndpointLocation(loc) {
return `${widgetApiEndpoints.mvg}/location?query=${encodeURIComponent(loc)}`;
},
endpointConnection() {
return `${widgetApiEndpoints.mvg}/connection?${this.formatPoint(this.locations.origin, 'origin')}&${this.formatPoint(this.locations.destination, 'destination')}&routingDateTime=${(new Date()).toISOString()}&offsetInMinutes=${this.offset}&transportTypes=${this.transportTypes}`;
},
update() {
this.startLoading();
this.fetchData();
this.finishLoading();
},
fetchData() {
if (this.locations.origin !== undefined
&& this.locations.destination !== undefined) {
this.makeRequest(this.endpointConnection()).then(
(response) => { this.processData(response); },
);
}
},
/* Assign data variables to the returned data */
processData(data) {
this.connections = data;
},
ensure_array(value) {
if (typeof value === 'string') {
return [value];
}
return value;
},
mvgTooltipConnection(data) {
let connectionDetails = '';
const self = this;
function addStep(step) {
connectionDetails += `<b>${self.$options.filters.formatTime(step.plannedDeparture)}</b>
<span class="delay">+${Math.max(parseInt(step.departureDelayInMinutes, 10) || 0, 0)}</span>
<span>${step.name}</span>`;
}
addStep(data.parts[0].from);
data.parts.forEach((part) => {
addStep(part.to);
});
return {
content: connectionDetails, html: true, trigger: 'hover', delay: 250, classes: 'mvg-connection-detail',
};
},
},
};
</script>
<style scoped lang="scss">
.mvg-header {
color: var(--widget-text-color);
font-size:1.2em;
}
.mvg-wrapper {
display: grid;
justify-content: left;
grid-template-columns: 100%;
color: var(--widget-text-color);
padding: 0.25rem 0;
grid-row-gap: 0.4em;
.departure {
min-width: 1rem;
font-size: 1.1rem;
font-weight: bold;
text-align: right;
margin-right: 0.2rem;
span.live {
color: var(--success);
}
}
.line {
margin: 0em;
padding: 0em;
border-radius: 0.2em;
display: grid;
grid-template-columns: 2fr 5fr 0.75fr;
.changes {
text-align: center;
}
.type-UBAHN {
border: 0px;
}
.type-SBAHN {
border: 0px;
}
.type-BUS {
}
.type-TRAM {
background-color: #dd3d4d;
}
.transport{
border-radius: 0.2em;
margin: 0em;
padding: 0.15em 0.15em;
color: #FFFFFF;
margin-right: 0.40em;
margin-left: 0.40em;
text-align: center;
span {
min-width: 2em;
display: inline-block;
}
&.line-Fussweg {
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
}
&.line-U1 {
background-color: #468447;
}
&.line-U2 {
background-color: #dd3d4d;
}
&.line-U3 {
background-color: #ef8824;
}
&.line-U4 {
background-color: #04af90;
}
&.line-U5 {
background-color: #b78730;
}
&.line-U6 {
background-color: #0472b3;
}
&.line-S1 {
background-color: #79c6e7;
}
&.line-S2 {
background-color: #9bc04c;
}
&.line-S3 {
background-color: #942d8d;
}
&.line-S4 {
background-color: #d4214d;
}
&.line-S5 {
background-color: #03a074;
}
&.line-S6 {
background-color: #03a074;
}
&.line-S7 {
background-color: #964438;
}
&.line-S8 {
background-color: #000000;
}
&.type-BUS {
background-color: #0d5c70;
}
}
.destination{
border-radius: 0.2em;
width: 100%;
background-color: #FFFFFF;
color: #000;
padding-top: 0.15em;
padding-bottom: 0.15em;
white-space: nowrap;
overflow: hidden;
span.cancelled {
color: var(--danger);
text-decoration: line-through;
}
span.destination {
overflow: clip;
margin-right: 0.25em;
width: 75%;
display: inline-block;
}
}
.delay{
padding: 0.15em;
font-weight: bold;
&.has-delay{
padding: 0.15em;
background-color: var(--danger);
color: #FFF;
border-radius: 0.2em;
}
}
.delay::before{
content: "+";
}
.occupancy{
display: inline-block;
padding: 0 0.15em;
border-radius: 0.2em;
&.occupancy-LOW {
color: green;
}
&.occupancy-MEDIUM {
color: orange;
}
&.occupancy-HIGH {
color: red;
}
}
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
</style>
<style lang="scss">
.ping-times-tt {
min-width: 20rem;
}
</style>
<style lang="scss">
.mvg-connection-detail .tooltip-inner {
min-width: 20rem;
display: grid;
grid-template-columns: 2fr 1fr 6fr;
}
</style>

View File

@ -56,6 +56,7 @@ const COMPAT = {
'domain-monitor': 'DomainMonitor',
'code-stats': 'CodeStats',
'covid-stats': 'CovidStats',
'drone-ci': 'DroneCi',
embed: 'EmbedWidget',
'eth-gas-prices': 'EthGasPrices',
'exchange-rates': 'ExchangeRates',
@ -82,6 +83,8 @@ const COMPAT = {
image: 'ImageWidget',
joke: 'Jokes',
'mullvad-status': 'MullvadStatus',
mvg: 'Mvg',
'mvg-connection': 'MvgConnection',
'nd-cpu-history': 'NdCpuHistory',
'nd-load-history': 'NdLoadHistory',
'nd-ram-history': 'NdRamHistory',

View File

@ -236,6 +236,7 @@ module.exports = {
jokes: 'https://v2.jokeapi.dev/joke/',
news: 'https://api.currentsapi.services/v1/latest-news',
mullvad: 'https://am.i.mullvad.net/json',
mvg: 'https://www.mvg.de/api/fib/v2/',
publicIp: 'https://ipapi.co/json',
publicIp2: 'https://api.ipgeolocation.io/ipgeo',
publicIp3: 'http://ip-api.com/json',