initial commit

This commit is contained in:
2026-02-16 00:41:55 +03:00
commit 73abb199ed
11 changed files with 1088 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

48
assets/global.css Normal file
View File

@@ -0,0 +1,48 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
:root {
--color-info: hsl(43, 50%, 70%);
--color-warning: hsl(43, 50%, 70%);
/* Widget background color with alpha */
--bga: 70%;
--color-widget-background: hsl(
var(--color-widget-background-hsl-values),
var(--bga)
);
}
* {
box-sizing: border-box;
}
body {
font-family: 'Outfit', sans-serif;
background-image: url(https://images.pexels.com/photos/2098428/pexels-photo-2098428.jpeg);
/* backdrop-filter: blur(2px); */
background-repeat: no-repeat;
background-size: cover;
}
/* Blur effect for widget backgrounds */
.widget-content:not(.widget-content-frameless),
.widget-content-frame {
backdrop-filter: blur(5px);
box-shadow:
rgba(0, 0, 0, 0.02) 0px 1px 3px 0px,
rgba(27, 31, 35, 0.15) 0px 0px 0px 1px;
}
/* Make widgets same height */
.widget-type-dns-stats {
height: 100%;
}
.widget-type-dns-stats .widget-content {
height: calc(100% - var(--widget-gap));
}
.widget-type-dns-stats .widget-content .dns-stats {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.widget-type-prowlarr-custom .widget-content {
height: calc(100% - var(--widget-gap));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

33
config/bookmarks.yml Normal file
View File

@@ -0,0 +1,33 @@
- type: bookmarks
groups:
- links:
- title: Gmail
url: https://mail.google.com/mail/u/0/
- title: Amazon
url: https://www.amazon.com/
- title: Github
url: https://github.com/
- title: Wikipedia
url: https://en.wikipedia.org/
- title: Entertainment
color: 10 70 50
links:
- title: Netflix
url: https://www.netflix.com/
- title: Disney+
url: https://www.disneyplus.com/
- title: YouTube
url: https://www.youtube.com/
- title: Prime Video
url: https://www.primevideo.com/
- title: Social
color: 200 50 50
links:
- title: Reddit
url: https://www.reddit.com/
- title: Twitter
url: https://twitter.com/
- title: Instagram
url: https://www.instagram.com/

78
config/glance.yml Normal file
View File

@@ -0,0 +1,78 @@
server:
assets-path: /app/assets
branding:
hide-footer: true
theme:
background-color: 203 27 9
contrast-multiplier: 1.2
primary-color: 165 78 51
positive-color: 165 78 51
negative-color: 360 100 71
disable-picker: true
custom-css-file: /global.css
pages:
- name: Home
# Optionally, if you only have a single page you can hide the desktop navigation for a cleaner look
hide-desktop-navigation: true
columns:
- size: small
widgets:
- type: calendar
hide-header: true
first-day-of-week: sunday
- $include: bookmarks.yml
- size: full
widgets:
- type: split-column
max-columns: 4
widgets:
- $include: plausible.yml
css-class: widget-type-plausible-prowlarr-custom
- $include: uptime.yml
css-class: widget-type-uptime-custom
- $include: services.yml
- $include: raindrop.yml
css-class: widget-type-raindrop-custom
- size: small
widgets:
- type: clock
hide-header: true
time-format: 24h
date-format: d MMMM yyyy
show-seconds: true
show-timezone: true
timezone: Europe/Istanbul
- type: weather
hide-header: true
location: ${WEATHER_LOCATION}
units: metric
hour-format: 24h
# Add more pages here:
# - name: Your page name
# columns:
# - size: small
# widgets:
# # Add widgets here
# - size: full
# widgets:
# # Add widgets here
# - size: small
# widgets:
# # Add widgets here

343
config/media-server.yml Normal file
View File

@@ -0,0 +1,343 @@
- type: custom-api
title: Media Server History
frameless: true
cache: 5m
options:
media-server: '${MEDIA_SERVER_TYPE}'
base-url: ${JELLYFIN_URL}
api-key: ${JELLYFIN_TOKEN}
user-name: ${JELLYFIN_USER}
media-types: '${MEDIA_SERVER_TYPES}'
history-length: '${MEDIA_SERVER_HISTORY_LENGTH}'
small-column: true
compact: true
show-thumbnail: true
thumbnail-aspect-ratio: 'default'
show-user: false
time-absolute: false
time-format: 'Jan 02 15:04'
template: |
{{ $mediaServer := .Options.StringOr "media-server" "" }}
{{ $baseURL := .Options.StringOr "base-url" "" }}
{{ $apiKey := .Options.StringOr "api-key" "" }}
{{ $userName := .Options.StringOr "user-name" "" }}
{{ define "errorMsg" }}
<div class="widget-error-header">
<div class="color-negative size-h3">ERROR</div>
<svg class="widget-error-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"></path>
</svg>
</div>
<p class="break-all">{{ . }}</p>
{{ end }}
{{ if or
(eq $mediaServer "")
(eq $baseURL "")
(eq $apiKey "")
(and (eq $mediaServer "jellyfin") (eq $userName ""))
}}
{{ template "errorMsg" "Some required options are not set" }}
{{ else }}
{{ $historyLength := .Options.StringOr "history-length" "10" }}
{{ $mediaTypes := .Options.StringOr "media-types" "" }}
{{ if eq $mediaServer "tautulli" }}
{{ $mediaTypes = .Options.StringOr "media-types" "movie,episode,track" }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ $mediaTypes = .Options.StringOr "media-types" "Movie,Episode,Audio" }}
{{ end }}
{{ $isSmallColumn := .Options.BoolOr "small-column" false }}
{{ $isCompact := .Options.BoolOr "compact" true }}
{{ $showThumbnail := .Options.BoolOr "show-thumbnail" false }}
{{ $thumbAspectRatio := .Options.StringOr "thumbnail-aspect-ratio" "" }}
{{ $showUser := .Options.BoolOr "show-user" true }}
{{ $timeAbsolute := .Options.BoolOr "time-absolute" false }}
{{ $timeFormat := .Options.StringOr "time-format" "Jan 02 15:04" }}
{{ $userID := "" }}
{{ $historyRequestURL := "" }}
{{ $usersRequestURL := "" }}
{{ $historyCall := "" }}
{{ $usersCall := "" }}
{{ $history := "" }}
{{ $users := "" }}
{{ if eq $mediaServer "plex" }}
{{ $historyRequestURL = concat $baseURL "/status/sessions/history/all" }}
{{ $historyCall = newRequest $historyRequestURL
| withParameter "limit" $historyLength
| withParameter "sort" "viewedAt:desc"
| withHeader "Accept" "application/json"
| withHeader "X-Plex-Token" $apiKey
| getResponse }}
{{ if $historyCall.JSON.Exists "MediaContainer" }}
{{ $history = $historyCall.JSON.Array "MediaContainer.Metadata" }}
{{ else }}
{{ template "errorMsg" (concat "Could not fetch " $mediaServer " API.") }}
{{ end }}
{{ $usersRequestURL = concat $baseURL "/accounts" }}
{{ $usersCall = newRequest $usersRequestURL
| withHeader "Accept" "application/json"
| withHeader "X-Plex-Token" $apiKey
| getResponse }}
{{ $users = $usersCall.JSON.Array "MediaContainer.Account" }}
{{ else if eq $mediaServer "tautulli" }}
{{ $historyRequestURL = concat $baseURL "/api/v2" }}
{{ $historyCall = newRequest $historyRequestURL
| withParameter "apikey" $apiKey
| withParameter "cmd" "get_history"
| withParameter "length" $historyLength
| withParameter "media_type" $mediaTypes
| withHeader "Accept" "application/json"
| getResponse }}
{{ if eq $historyCall.Response.StatusCode 200 }}
{{ $history = $historyCall.JSON.Array "response.data.data" }}
{{ else }}
{{ template "errorMsg" (concat "Could not fetch " $mediaServer " API.") }}
{{ end }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ $usersRequestURL = concat $baseURL "/Users" }}
{{ $usersCall = newRequest $usersRequestURL
| withParameter "api_key" $apiKey
| withHeader "Accept" "application/json"
| getResponse }}
{{ $usersList := $usersCall.JSON.Array "" }}
{{ range $i, $user := $usersList }}
{{ if eq ($user.String "Name") $userName }}
{{ $userID = $user.String "Id" }}
{{ break }}
{{ end }}
{{ end }}
{{ if eq $userID "" }}
{{ template "errorMsg" (concat "User '" $userName "' not found.") }}
{{ end }}
{{ $historyRequestURL = concat $baseURL "/Users/" $userID "/Items" }}
{{ $historyCall = newRequest $historyRequestURL
| withParameter "api_key" $apiKey
| withParameter "Limit" $historyLength
| withParameter "IncludeItemTypes" $mediaTypes
| withParameter "Recursive" "true"
| withParameter "isPlayed" "true"
| withParameter "sortBy" "DatePlayed"
| withParameter "sortOrder" "Descending"
| withParameter "Fields" "UserDataLastPlayedDate"
| withHeader "Accept" "application/json"
| getResponse }}
{{ $history = $historyCall.JSON.Array "Items" }}
{{ end }}
{{ if and (eq $historyCall.Response.StatusCode 200) (eq (len $history) 0) }}
<p>Nothing has been played. Start streaming something!</p>
{{ else }}
<div class="carousel-container show-right-cutoff">
<div class="cards-horizontal carousel-items-container">
{{ range $n, $item := $history }}
{{ $mediaType := "" }}
{{ $isMovie := false }}
{{ $isShows := false }}
{{ $isMusic := false }}
{{ $title := "" }}
{{ $showTitle := "" }}
{{ $showSeason := "" }}
{{ $showEpisode := "" }}
{{ $artist := "" }}
{{ $albumTitle := "" }}
{{ $thumbURL := "" }}
{{ $playedAt := "" }}
{{ if eq $mediaServer "plex" }}
{{ $userID = $item.Int "accountID" }}
{{ range $n, $u := $users }}
{{ if eq $userID ($u.Int "id") }}
{{ $userName = $u.String "name" }}
{{ break }}
{{ end }}
{{ end }}
{{ $mediaType = $item.String "type" }}
{{ $isMovie = eq $mediaType "movie" }}
{{ $isShows = eq $mediaType "episode" }}
{{ $isMusic = eq $mediaType "track" }}
{{ $title = $item.String "title" }}
{{ if $isShows }}
{{ $showTitle = $item.String "grandparentTitle" }}
{{ $showSeason = $item.String "parentIndex" }}
{{ $showEpisode = $item.String "index" }}
{{ else if $isMusic }}
{{ $artist = $item.String "grandparentTitle" }}
{{ $albumTitle = $item.String "parentTitle" }}
{{ end }}
{{ $thumbID := $item.String "thumb" }}
{{ if or $isShows $isMusic}}
{{ $thumbID = $item.String "parentThumb" }}
{{ end }}
{{ $thumbURL = concat $baseURL $thumbID "?X-Plex-Token=" $apiKey }}
{{ $time := $item.String "viewedAt" }}
{{ if $timeAbsolute }}
{{ $playedAt = $time | parseLocalTime "unix" | formatTime $timeFormat }}
{{ else }}
{{ $playedAt = $time | parseRelativeTime "unix" }}
{{ end }}
{{ else if eq $mediaServer "tautulli" }}
{{ $userName = $item.String "user" }}
{{ $mediaType = $item.String "media_type" }}
{{ $isMovie = eq $mediaType "movie" }}
{{ $isShows = eq $mediaType "episode" }}
{{ $isMusic = eq $mediaType "track" }}
{{ $title = $item.String "title" }}
{{ if $isShows }}
{{ $showTitle = $item.String "grandparent_title" }}
{{ $showSeason = $item.String "parent_media_index" }}
{{ $showEpisode = $item.String "media_index" }}
{{ else if $isMusic }}
{{ $artist = $item.String "grandparent_title" }}
{{ $albumTitle = $item.String "parent_title" }}
{{ end }}
{{ $thumbID := $item.String "thumb" }}
{{ $thumbURL = concat $baseURL "/api/v2?apikey=" $apiKey "&cmd=pms_image_proxy&img=" $thumbID }}
{{ $time := $item.String "date" }}
{{ if $timeAbsolute }}
{{ $playedAt = $time | parseLocalTime "unix" | formatTime $timeFormat }}
{{ else }}
{{ $playedAt = $time | parseRelativeTime "unix" }}
{{ end }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ $mediaType = $item.String "Type" }}
{{ $isMovie = eq $mediaType "Movie" }}
{{ $isShows = eq $mediaType "Episode" }}
{{ $isMusic = eq $mediaType "Audio" }}
{{ $title = $item.String "Name" }}
{{ if $isShows }}
{{ $showTitle = $item.String "SeriesName" }}
{{ $showSeason = $item.String "ParentIndexNumber" }}
{{ $showEpisode = $item.String "IndexNumber" }}
{{ else if $isMusic }}
{{ $artist = $item.String "AlbumArtist" }}
{{ $albumTitle = $item.String "Album" }}
{{ end }}
{{ $thumbID := $item.String "Id" }}
{{ if $isShows }}
{{ $thumbID = $item.String "SeasonId" }}
{{ end }}
{{ $thumbURL = concat $baseURL "/Items/" $thumbID "/Images/Primary?api_key=" $apiKey }}
{{ $time := $item.String "UserData.LastPlayedDate" }}
{{ if $timeAbsolute }}
{{ $playedAt = $time | parseLocalTime "rfc3339" | formatTime $timeFormat }}
{{ else }}
{{ $playedAt = $time | parseRelativeTime "rfc3339" }}
{{ end }}
{{ end }}
{{ $showInfoFormat := concat "Season " $showSeason " Episode " $showEpisode}}
{{ if $isCompact }}
{{ $showInfoFormat = concat "S" $showSeason "E" $showEpisode}}
{{ end }}
<div class="card widget-content-frame">
{{ if $showThumbnail }}
<img src="{{ $thumbURL | safeURL }}"
alt="{{ $title }} thumbnail"
loading="lazy"
class="media-server-thumbnail shrink-0"
style="
object-fit: cover;
border-radius: var(--border-radius) var(--border-radius) 0 0;
{{ if eq $thumbAspectRatio "square" }}
aspect-ratio: 1;
{{ else if eq $thumbAspectRatio "portrait" }}
aspect-ratio: 3/4;
{{ else if eq $thumbAspectRatio "landscape" }}
aspect-ratio: 4/3;
{{ else }}
aspect-ratio: initial;
{{ end }}
"
/>
{{ end }}
<div class="grow padding-inline-widget margin-top-10 margin-bottom-10">
<ul class="flex flex-column justify-evenly margin-bottom-3 {{if $isSmallColumn}}size-h6{{end}}" style="height: 100%;">
{{ if $isCompact }}
<ul class="list-horizontal-text flex-nowrap">
{{ if $showUser }}
<li class="color-primary text-truncate">{{ $userName }}</li>
{{ end }}
{{ if $timeAbsolute }}
<li class="text-truncate">{{ $playedAt }}</li>
{{ else }}
<li class="shrink-0">
<span {{ $playedAt }}></span>
{{ if not $showUser }}
<span> ago</span>
{{ end }}
</li>
{{ end }}
</ul>
{{ if $isShows }}
<ul class="list-horizontal-text flex-nowrap">
<li class="text-truncate">{{ $showInfoFormat }}</li>
<li class="text-truncate">{{ $showTitle }}</li>
</ul>
{{ else if $isMusic }}
<ul class="list-horizontal-text flex-nowrap">
<li class="text-truncate">{{ $artist }}</li>
<li class="text-truncate">{{ $albumTitle }}</li>
</ul>
{{ end }}
<li class="text-truncate">{{ $title }}</li>
{{ else }}
{{ if $showUser }}
<li class="color-primary text-truncate">{{ $userName }}</li>
{{ end }}
{{ if $timeAbsolute }}
<li class="text-truncate">{{ $playedAt }}</li>
{{ else }}
<li class="text-truncate">
<span {{ $playedAt }}></span>
<span> ago</span>
</li>
{{ end }}
{{ if $isShows }}
<li class="text-truncate">{{ $showTitle }}</li>
<li class="text-truncate">{{ $showInfoFormat }}</li>
{{ else if $isMusic }}
<li class="text-truncate">{{ $artist }}</li>
<li class="text-truncate">{{ $albumTitle }}</li>
{{ end }}
<li class="text-truncate">{{ $title }}</li>
{{ end }}
</ul>
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}

260
config/plausible.yml Normal file
View File

@@ -0,0 +1,260 @@
- type: custom-api
title: Site Statistics
hide-header: true
cache: 1m
subrequests:
# aveminakarabudak.com
a1:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/realtime/visitors?site_id=aveminakarabudak.com
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
s1:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/aggregate?site_id=aveminakarabudak.com&period=day&metrics=visitors,pageviews,visits
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
# ayrisapart.com
a2:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/realtime/visitors?site_id=ayrisapart.com
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
s2:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/aggregate?site_id=ayrisapart.com&period=day&metrics=visitors,pageviews,visits
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
# ayris.tech
a3:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/realtime/visitors?site_id=ayris.tech
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
s3:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/aggregate?site_id=ayris.tech&period=day&metrics=visitors,pageviews,visits
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
# irisiptv.online
a4:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/realtime/visitors?site_id=irisiptv.online
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
s4:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/aggregate?site_id=irisiptv.online&period=day&metrics=visitors,pageviews,visits
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
# screencapr.com
a5:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/realtime/visitors?site_id=screencapr.com
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
s5:
url: ${PLAUSIBLE_BASE_URL}/api/v1/stats/aggregate?site_id=screencapr.com&period=day&metrics=visitors,pageviews,visits
headers: { Authorization: "Bearer ${PLAUSIBLE_TOKEN}" }
template: |
<div class="plausible-multi-site">
<style>
.plausible-site-row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 16px;
padding: 16px;
border-bottom: 1px solid var(--color-widget-content-border);
align-items: center;
}
.plausible-site-row:last-child {
border-bottom: none;
}
.site-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
}
.site-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.site-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.site-domain {
font-weight: 800;
color: var(--color-highlight);
font-size: 1.25rem;
}
.site-metrics {
display: flex;
gap: 12px;
font-size: 0.85rem;
color: var(--color-subdue);
font-family: var(--font-family-mono);
}
.metric-item b {
color: var(--color-primary);
}
.realtime-badge {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
padding: 2px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 6px;
}
.realtime-dot {
width: 8px;
height: 8px;
background: currentColor;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(1.2); }
100% { opacity: 1; transform: scale(1); }
}
</style>
{{ $sites := . }}
<!-- Site Block 1 -->
{{ $a1 := .Subrequest "a1" }}
{{ $s1 := .Subrequest "s1" }}
<div class="plausible-site-row">
<div class="site-icon">
<img src="https://icon.horse/icon/aveminakarabudak.com" alt="" />
</div>
<div class="site-info">
<span class="site-domain">aveminakarabudak.com</span>
<div class="site-metrics">
{{ if eq $s1.Response.StatusCode 200 }}
<span class="metric-item">Visitors: <b>{{ $s1.JSON.Get "results.visitors.value" }}</b></span>
<span class="metric-item">Views: <b>{{ $s1.JSON.Get "results.pageviews.value" }}</b></span>
{{ end }}
</div>
</div>
<div class="site-status">
{{ if eq $a1.Response.StatusCode 200 }}
{{ $c1 := $a1.JSON.Raw }}
{{ if ne $c1 "0" }}
<div class="realtime-badge">
<span class="realtime-dot"></span>
{{ $c1 }} Active
</div>
{{ end }}
{{ end }}
</div>
</div>
<!-- Site Block 2 -->
{{ $a2 := .Subrequest "a2" }}
{{ $s2 := .Subrequest "s2" }}
<div class="plausible-site-row">
<div class="site-icon">
<img src="https://icon.horse/icon/ayrisapart.com" alt="" />
</div>
<div class="site-info">
<span class="site-domain">ayrisapart.com</span>
<div class="site-metrics">
{{ if eq $s2.Response.StatusCode 200 }}
<span class="metric-item">Visitors: <b>{{ $s2.JSON.Get "results.visitors.value" }}</b></span>
<span class="metric-item">Views: <b>{{ $s2.JSON.Get "results.pageviews.value" }}</b></span>
{{ end }}
</div>
</div>
<div class="site-status">
{{ if eq $a2.Response.StatusCode 200 }}
{{ $c2 := $a2.JSON.Raw }}
{{ if ne $c2 "0" }}
<div class="realtime-badge">
<span class="realtime-dot"></span>
{{ $c2 }} Active
</div>
{{ end }}
{{ end }}
</div>
</div>
<!-- Site Block 3 -->
{{ $a3 := .Subrequest "a3" }}
{{ $s3 := .Subrequest "s3" }}
<div class="plausible-site-row">
<div class="site-icon">
<img src="https://icon.horse/icon/ayris.tech" alt="" />
</div>
<div class="site-info">
<span class="site-domain">ayris.tech</span>
<div class="site-metrics">
{{ if eq $s3.Response.StatusCode 200 }}
<span class="metric-item">Visitors: <b>{{ $s3.JSON.Get "results.visitors.value" }}</b></span>
<span class="metric-item">Views: <b>{{ $s3.JSON.Get "results.pageviews.value" }}</b></span>
{{ end }}
</div>
</div>
<div class="site-status">
{{ if eq $a3.Response.StatusCode 200 }}
{{ $c3 := $a3.JSON.Raw }}
{{ if ne $c3 "0" }}
<div class="realtime-badge">
<span class="realtime-dot"></span>
{{ $c3 }} Active
</div>
{{ end }}
{{ end }}
</div>
</div>
<!-- Site Block 4 -->
{{ $a4 := .Subrequest "a4" }}
{{ $s4 := .Subrequest "s4" }}
<div class="plausible-site-row">
<div class="site-icon">
<img src="https://icon.horse/icon/irisiptv.online" alt="" />
</div>
<div class="site-info">
<span class="site-domain">irisiptv.online</span>
<div class="site-metrics">
{{ if eq $s4.Response.StatusCode 200 }}
<span class="metric-item">Visitors: <b>{{ $s4.JSON.Get "results.visitors.value" }}</b></span>
<span class="metric-item">Views: <b>{{ $s4.JSON.Get "results.pageviews.value" }}</b></span>
{{ end }}
</div>
</div>
<div class="site-status">
{{ if eq $a4.Response.StatusCode 200 }}
{{ $c4 := $a4.JSON.Raw }}
{{ if ne $c4 "0" }}
<div class="realtime-badge">
<span class="realtime-dot"></span>
{{ $c4 }} Active
</div>
{{ end }}
{{ end }}
</div>
</div>
<!-- Site Block 5 -->
{{ $a5 := .Subrequest "a5" }}
{{ $s5 := .Subrequest "s5" }}
<div class="plausible-site-row">
<div class="site-icon">
<img src="https://icon.horse/icon/screencapr.com" alt="" />
</div>
<div class="site-info">
<span class="site-domain">screencapr.com</span>
<div class="site-metrics">
{{ if eq $s5.Response.StatusCode 200 }}
<span class="metric-item">Visitors: <b>{{ $s5.JSON.Get "results.visitors.value" }}</b></span>
<span class="metric-item">Views: <b>{{ $s5.JSON.Get "results.pageviews.value" }}</b></span>
{{ end }}
</div>
</div>
<div class="site-status">
{{ if eq $a5.Response.StatusCode 200 }}
{{ $c5 := $a5.JSON.Raw }}
{{ if ne $c5 "0" }}
<div class="realtime-badge">
<span class="realtime-dot"></span>
{{ $c5 }} Active
</div>
{{ end }}
{{ end }}
</div>
</div>
</div>

103
config/raindrop.yml Normal file
View File

@@ -0,0 +1,103 @@
- type: custom-api
title: Raindrop Latest Links
hide-header: true
cache: 1h
url: https://api.raindrop.io/rest/v1/raindrops/0?perpage=50
headers: { Authorization: "Bearer ${RAINDROP_TOKEN}" }
template: |
{{ if .JSON.Bool "result" }}
<div class="raindrop-list">
<style>
.raindrop-item {
display: flex;
gap: 16px;
padding: 16px;
border-bottom: 1px solid var(--color-widget-content-border);
align-items: center;
}
.raindrop-item:last-child {
border-bottom: none;
}
.raindrop-cover-container {
width: 48px;
height: 48px;
flex-shrink: 0;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
}
.raindrop-cover {
width: 100%;
height: 100%;
object-fit: cover;
}
.raindrop-info {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.raindrop-title {
font-weight: 800;
color: var(--color-highlight);
font-size: 1.2rem;
text-decoration: none;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.2;
}
.raindrop-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 0.85rem;
color: var(--color-subdue);
font-family: var(--font-family-mono);
}
.tag-badge {
color: var(--color-primary);
font-weight: 700;
}
</style>
<ul class="list collapsible-container" data-collapse-after="10">
{{ range .JSON.Array "items" }}
<li class="raindrop-item">
<div class="raindrop-cover-container">
{{ $cover := .String "cover" }}
{{ if $cover }}
<img src="{{ $cover }}" class="raindrop-cover" alt="" />
{{ else }}
<svg fill="currentColor" viewBox="0 0 24 24" width="24" height="24"><path d="M4.75 3.5a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5zM9 13l3-3 3 3M8 15v-2h8v2H8z"/></svg>
{{ end }}
</div>
<div class="raindrop-info">
<a href="{{ .String "link" }}" target="_blank" class="raindrop-title">
{{ .String "title" }}
</a>
<div class="raindrop-meta">
<span class="color-primary">#{{ .String "domain" }}</span>
<span>•</span>
<span {{ .String "created" | parseRelativeTime "rfc3339" }}></span>
{{ $tags := .Array "tags" }}
{{ if $tags }}
<span>•</span>
<span class="tag-badge">#{{ (index $tags 0).String "" }}</span>
{{ end }}
</div>
</div>
</li>
{{ end }}
</ul>
</div>
{{ else }}
<div class="color-negative padding-10 text-center">
No bookmarks found or API error.
</div>
{{ end }}

51
config/services.yml Normal file
View File

@@ -0,0 +1,51 @@
- type: custom-api
title: Services
hide-header: true
cache: 1m
url: ${SERVICES_JSON_URL}
template: |
<div class="services-list">
<style>
.service-row {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border-bottom: 1px solid var(--color-widget-content-border);
text-decoration: none;
}
.service-row:last-child {
border-bottom: none;
}
.service-icon-container {
width: 48px;
height: 48px;
flex-shrink: 0;
border-radius: 50%;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
}
.service-icon {
width: 100%;
height: 100%;
object-fit: cover;
}
.service-name {
font-weight: 800;
color: var(--color-highlight);
font-size: 1.25rem;
}
</style>
{{ range .JSON.Raw | fromJson }}
<a href="{{ .url }}" target="_blank" class="service-row">
<div class="service-icon-container">
<img src="{{ .icon }}" class="service-icon" alt="" />
</div>
<span class="service-name">{{ .title }}</span>
</a>
{{ end }}
</div>

139
config/uptime.yml Normal file
View File

@@ -0,0 +1,139 @@
- type: custom-api
title: Uptime Status
title-url: ${UPTIME_KUMA_URL}
hide-header: true
url: ${UPTIME_KUMA_URL}/api/status-page/${UPTIME_KUMA_STATUS_SLUG}
subrequests:
heartbeats:
url: ${UPTIME_KUMA_URL}/api/status-page/heartbeat/${UPTIME_KUMA_STATUS_SLUG}
cache: 5m
template: |
{{ $hb := .Subrequest "heartbeats" }}
{{ if not (.JSON.Exists "publicGroupList") }}
<p class="color-negative">Error reading response</p>
{{ else if eq (len (.JSON.Array "publicGroupList")) 0 }}
<p>No monitors found</p>
{{ else }}
<div class="uptime-list">
<style>
.uptime-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 16px;
padding: 16px;
border-bottom: 1px solid var(--color-widget-content-border);
align-items: center;
}
.uptime-item:last-child {
border-bottom: none;
}
.monitor-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
}
.monitor-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.monitor-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.monitor-name {
font-weight: 800;
color: var(--color-highlight);
font-size: 1.25rem;
text-decoration: none;
}
.monitor-meta {
display: flex;
gap: 8px;
font-size: 0.85rem;
color: var(--color-subdue);
font-family: var(--font-family-mono);
}
.status-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
</style>
{{ range .JSON.Array "publicGroupList" }}
{{ range .Array "monitorList" }}
{{ $id := .String "id" }}
{{ $name := .String "name" }}
{{ $hbPath := concat "heartbeatList." $id }}
{{ $hbArray := $hb.JSON.Array $hbPath }}
{{ $latest := "" }}
{{ range $hbArray }}{{ $latest = . }}{{ end }}
<div class="uptime-item">
<div class="monitor-icon">
{{ if eq $name "AvEmy" }} <img src="https://icon.horse/icon/aveminakarabudak.com" alt="" />
{{ else if eq $name "ayris.tech" }} <img src="https://icon.horse/icon/ayris.tech" alt="" />
{{ else if eq $name "AyrisApart" }} <img src="https://icon.horse/icon/ayrisapart.com" alt="" />
{{ else if eq $name "irisiptv.online" }} <img src="https://icon.horse/icon/irisiptv.online" alt="" />
{{ else if eq $name "screencapr.com" }} <img src="https://icon.horse/icon/screencapr.com" alt="" />
{{ else if eq $name "Portainer" }} <img src="https://icon.horse/icon/portainer.io" alt="" />
{{ else if eq $name "Backrest" }} <img src="https://icon.horse/icon/grest.dev" alt="" />
{{ else }}
<svg fill="currentColor" viewBox="0 0 24 24" width="24" height="24"><path d="M21 16.5C21 16.88 20.79 17.21 20.47 17.38L12.57 21.82C12.41 21.94 12.21 22 12 22C11.79 22 11.59 21.94 11.43 21.82L3.53 17.38C3.21 17.21 3 16.88 3 16.5V7.5C3 7.12 3.21 6.79 3.53 6.62L11.43 2.18C11.59 2.06 11.79 2 12 2C12.21 2 12.41 2.06 12.57 2.18L20.47 6.62C20.79 6.79 21 7.12 21 7.5V16.5Z"/></svg>
{{ end }}
</div>
<div class="monitor-info">
<a class="monitor-name" href="${UPTIME_KUMA_URL}/dashboard/{{ $id }}" target="_blank">
{{ $name }}
</a>
<div class="monitor-meta">
{{ if $latest }}
{{ if eq ($latest.String "status") "1" }}
<span class="color-positive">OK</span>
<span>•</span>
<span>{{ $latest.Int "ping" }}ms</span>
{{ else }}
<span class="color-negative">DOWN</span>
{{ if $latest.Exists "msg" }}
<span>•</span>
<span class="size-tiny">{{ $latest.String "msg" }}</span>
{{ end }}
{{ end }}
{{ else }}
<span class="color-subdue">No data</span>
{{ end }}
</div>
</div>
<div class="status-icon">
{{ if $latest }}
{{ if eq ($latest.String "status") "1" }}
<svg fill="var(--color-positive)" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd"></path>
</svg>
{{ else }}
<svg fill="var(--color-negative)" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd"></path>
</svg>
{{ end }}
{{ else }}
<svg fill="var(--color-subdue)" viewBox="0 0 20 20">
<path d="M10 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-2a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm-.75-8a.75.75 0 0 1 1.5 0v3a.75.75 0 0 1-1.5 0V8zm.75 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}

32
services.json Normal file
View File

@@ -0,0 +1,32 @@
[
{
"title": "Dashboard",
"url": "https://dash.ayris.tech",
"icon": "https://raw.githubusercontent.com/glance-project/glance/main/assets/logo.png"
},
{
"title": "Mail",
"url": "https://mail.ayris.tech",
"icon": "https://stalwart.io/img/logo.png"
},
{
"title": "n8n Automation",
"url": "https://auto.ayris.tech",
"icon": "https://n8n.io/images/press/n8n-logo.png"
},
{
"title": "Vaultwarden",
"url": "https://vault.ayris.tech",
"icon": "https://raw.githubusercontent.com/dani-garcia/vaultwarden/main/resources/vaultwarden-icon.png"
},
{
"title": "Analytics",
"url": "https://analytics.ayris.tech",
"icon": "https://plausible.io/assets/images/icon/plausible_logo.png"
},
{
"title": "Uptime",
"url": "http://uptimekuma-uko00s44cs8cokos4cgwk8oc.65.109.236.58.sslip.io/dashboard",
"icon": "https://uptime.kuma.pet/img/icon.png"
}
]