From af8a424db4ba2a2006a45fd7008c2c4b312ef25a Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 23 May 2026 09:54:38 +0300 Subject: [PATCH 01/10] feat(argocd): added movienight argo app --- deploy/argocd/movienight.yaml | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 deploy/argocd/movienight.yaml diff --git a/deploy/argocd/movienight.yaml b/deploy/argocd/movienight.yaml new file mode 100644 index 0000000..b979143 --- /dev/null +++ b/deploy/argocd/movienight.yaml @@ -0,0 +1,62 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: movienight + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + project: default + source: + repoURL: https://github.com/devitq/movienight-backend.git + targetRevision: feat/setup-observability-in-k8s + path: deploy/helm/movienight + helm: + valuesObject: + postgres: + cluster: + enabled: true + database: movienight + owner: movienight + bootstrapSecretName: movienight-cnpg-bootstrap + storage: + size: 2Gi + + backend: + image: + tag: pr-45 + pullPolicy: Always + env: + - name: JELLYFIN_INTEGRATION_ENABLED + value: "true" + envFrom: + - secretRef: + name: movienight-backend + + gateway: + enabled: true + className: cilium + listenerHostname: movienight.itqdev.xyz + hostnames: + - movienight.itqdev.xyz + https: + enabled: true + secretName: movienight-tls + + observability: + enabled: true + + destination: + server: https://kubernetes.default.svc + namespace: movienight + + syncPolicy: + automated: + prune: true + selfHeal: true + enabled: true + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true + - ServerSideApply=true From be031c151ffd9e5dbc6f4c2fdba6bdc9c5409101 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sun, 19 Jul 2026 13:59:20 +0300 Subject: [PATCH 02/10] feat(plugins/jellyfin): updated onboarding process --- .../Configuration/ui.js | 253 +++++++++++++++--- 1 file changed, 213 insertions(+), 40 deletions(-) diff --git a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js index 156eb1c..baa6a11 100644 --- a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js +++ b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js @@ -205,6 +205,66 @@ box-sizing: border-box; width: 100%; } + .movieNightOnboardingDialog { + display: flex; + flex-direction: column; + max-height: min(820px, calc(100vh - 2em)); + width: min(720px, calc(100vw - 2em)); + } + .movieNightOnboardingDialog .dialog-content { + overflow-y: auto; + padding-right: .25em; + } + .movieNightOnboardingGrid { + display: grid; + gap: 1em; + grid-template-columns: 1fr 1fr; + } + .movieNightOnboardingWide { + grid-column: 1 / -1; + } + .movieNightChipGroup { + display: flex; + flex-wrap: wrap; + gap: .5em; + } + .movieNightChip { + background: rgba(255,255,255,.045); + border: var(--defaultLighterBorder, 1px solid rgba(255,255,255,.18)); + border-radius: 999px; + color: var(--textColor, #fff); + cursor: pointer; + font-size: .9em; + line-height: 1.2; + padding: .45em .85em; + transition: background-color .16s ease, border-color .16s ease; + user-select: none; + } + .movieNightChip:hover, + .movieNightChip:focus { + border-color: var(--uiAccentColor, #00a4dc); + outline: none; + } + .movieNightChip.is-selected { + background: var(--uiAccentColor, #0064d2); + border-color: var(--uiAccentColor, #0064d2); + color: #fff; + } + .movieNightSelect, + .movieNightTextarea { + background: rgba(0,0,0,.18); + border: var(--defaultLighterBorder, 1px solid rgba(255,255,255,.22)); + border-radius: var(--smallRadius, 8px); + box-sizing: border-box; + color: var(--textColor, #fff); + min-height: 2.7em; + padding: .55em .7em; + width: 100%; + } + .movieNightTextarea { + min-height: 5.5em; + resize: vertical; + } @media (max-width: 42em) { .movieNightRecommendation { grid-template-columns: 56px minmax(0, 1fr); @@ -231,6 +291,9 @@ grid-template-columns: 1fr; gap: 0; } + .movieNightOnboardingGrid { + grid-template-columns: 1fr; + } } `; document.head.appendChild(style); @@ -470,54 +533,108 @@ async function showOnboardingDialog() { const overlay = createOverlay(); - const dialog = createDialogBase('Welcome to MovieNight!'); - dialog.style.minWidth = '450px'; + const dialog = createDialogBase('Welcome to MovieNight'); + dialog.classList.add('movieNightOnboardingDialog'); + dialog.style.minWidth = '0'; const content = dialog.querySelector('.dialog-content'); const footer = dialog.querySelector('.dialog-footer'); content.innerHTML = ` -

Pick your preferences to get better recommendations.

-
- -
-
-
- -
-
-
- -
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+ + +
`; - const genres = ["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Thriller", "Animation", "Documentary"]; - const eras = ["1980s", "1990s", "2000s", "2010s", "2020s"]; - const types = ["FILM", "SERIES"]; + const genres = [ + "Action", + "Adventure", + "Animation", + "Comedy", + "Crime", + "Documentary", + "Drama", + "Fantasy", + "Horror", + "Mystery", + "Romance", + "Sci-Fi", + "Thriller" + ]; + const eras = ["1970s", "1980s", "1990s", "2000s", "2010s", "2020s"]; + const plotTypes = [ + "coming of age", + "crime investigation", + "family drama", + "heist", + "mind bending", + "political intrigue", + "quest", + "road trip", + "space adventure", + "survival" + ]; + const moods = [ + { label: "Tense", value: "tense" }, + { label: "Slow burn", value: "slow-burn" }, + { label: "Feel good", value: "feel-good" }, + { label: "Dark", value: "dark" }, + { label: "Romantic", value: "romantic" }, + { label: "Focused", value: "focused" } + ]; + const types = [ + { label: "Movies", value: "FILM" }, + { label: "Series", value: "SERIES" }, + { label: "Episodes", value: "EPISODE" }, + { label: "Other", value: "OTHER" } + ]; - const selections = { genres: new Set(), eras: new Set(), types: new Set() }; - - const createChip = (text, container, type) => { - const chip = document.createElement('div'); - chip.innerText = text; - chip.style.cssText = 'padding:0.4em 1em; border-radius:2em; border:1px solid #444; cursor:pointer; font-size:0.9em; transition:all 0.2s;'; - chip.onclick = () => { - if (selections[type].has(text)) { - selections[type].delete(text); - chip.style.backgroundColor = 'transparent'; - chip.style.borderColor = '#444'; - } else { - selections[type].add(text); - chip.style.backgroundColor = '#0064d2'; - chip.style.borderColor = '#0064d2'; - } - }; - container.appendChild(chip); + const selections = { + genres: new Set(), + eras: new Set(), + plotTypes: new Set(), + moods: new Set(), + types: new Set(["FILM"]) }; - genres.forEach(g => createChip(g, content.querySelector('.genre-chips'), 'genres')); - eras.forEach(e => createChip(e, content.querySelector('.era-chips'), 'eras')); - types.forEach(t => createChip(t, content.querySelector('.type-chips'), 'types')); + genres.forEach((genre) => createOnboardingChip(genre, content.querySelector('.genre-chips'), selections.genres)); + eras.forEach((era) => createOnboardingChip(era, content.querySelector('.era-chips'), selections.eras)); + plotTypes.forEach((plotType) => createOnboardingChip(plotType, content.querySelector('.plot-chips'), selections.plotTypes)); + moods.forEach((mood) => createOnboardingChip(mood, content.querySelector('.mood-chips'), selections.moods)); + types.forEach((type) => createOnboardingChip(type, content.querySelector('.type-chips'), selections.types)); const btnSave = document.createElement('button'); btnSave.className = 'emby-button raised button-submit'; @@ -529,10 +646,20 @@ const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); }; btnSave.onclick = async () => { + const customPlotTypes = splitOnboardingList(content.querySelector('.plot-input').value); + const people = splitOnboardingList(content.querySelector('.people-input').value); const payload = { weightedGenres: Object.fromEntries([...selections.genres].map(g => [g, 5])), + plotTypes: uniqueValues([...selections.plotTypes, ...customPlotTypes]), eras: [...selections.eras], - contentTypes: [...selections.types] + castAndDirectors: people, + moods: [...selections.moods], + contentTypes: [...selections.types], + likedFilmIds: [], + dislikedFilmIds: [], + libraryFilmIds: [], + watchedFilmIds: [], + recommendationStyle: content.querySelector('.recommendation-style').value }; cleanup(); await completeOnboarding(payload); @@ -544,6 +671,39 @@ document.body.appendChild(overlay); } + function createOnboardingChip(option, container, selections) { + const normalized = typeof option === 'string' ? { label: option, value: option } : option; + const chip = document.createElement('button'); + chip.type = 'button'; + chip.className = 'movieNightChip'; + chip.textContent = normalized.label; + chip.setAttribute('aria-pressed', selections.has(normalized.value) ? 'true' : 'false'); + if (selections.has(normalized.value)) { + chip.classList.add('is-selected'); + } + + chip.onclick = () => { + if (selections.has(normalized.value)) { + selections.delete(normalized.value); + } else { + selections.add(normalized.value); + } + + const selected = selections.has(normalized.value); + chip.classList.toggle('is-selected', selected); + chip.setAttribute('aria-pressed', selected ? 'true' : 'false'); + }; + container.appendChild(chip); + } + + function splitOnboardingList(value) { + return uniqueValues((value || '').split(/[,\n;]+/).map((item) => item.trim()).filter(Boolean)); + } + + function uniqueValues(values) { + return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; + } + async function checkOnboarding() { if (window.movieNightOnboardingChecked) return; @@ -553,7 +713,7 @@ try { const prefs = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${userId}/Preferences`)); - if (!prefs || (!Object.keys(prefs.weightedGenres || {}).length && !prefs.eras?.length)) { + if (!hasOnboardingPreferences(prefs)) { showOnboardingDialog(); } } catch (err) { @@ -561,6 +721,19 @@ } } + function hasOnboardingPreferences(prefs) { + if (!prefs) return false; + + return Boolean( + Object.keys(prefs.weightedGenres || {}).length || + prefs.plotTypes?.length || + prefs.eras?.length || + prefs.castAndDirectors?.length || + prefs.moods?.length || + prefs.contentTypes?.length + ); + } + async function completeOnboarding(payload) { const userId = ApiClient.getCurrentUserId(); try { From b0b18d17b1efeb003fe600071aabe91eaec99c63 Mon Sep 17 00:00:00 2001 From: ITQ Date: Mon, 27 Jul 2026 17:42:56 +0300 Subject: [PATCH 03/10] chore(argocd/movienight): added proper requests and limits --- deploy/argocd/movienight.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/deploy/argocd/movienight.yaml b/deploy/argocd/movienight.yaml index b979143..085f216 100644 --- a/deploy/argocd/movienight.yaml +++ b/deploy/argocd/movienight.yaml @@ -22,6 +22,14 @@ spec: bootstrapSecretName: movienight-cnpg-bootstrap storage: size: 2Gi + extraSpec: + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 100m + memory: 256Mi backend: image: @@ -33,6 +41,13 @@ spec: envFrom: - secretRef: name: movienight-backend + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: 500m + memory: 512Mi gateway: enabled: true From e83898a0b45b288cf949a5ab872e183f442050bd Mon Sep 17 00:00:00 2001 From: ITQ Date: Tue, 28 Jul 2026 20:13:13 +0300 Subject: [PATCH 04/10] chore(): extendend limits for backend --- deploy/argocd/movienight.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/argocd/movienight.yaml b/deploy/argocd/movienight.yaml index 085f216..06a3254 100644 --- a/deploy/argocd/movienight.yaml +++ b/deploy/argocd/movienight.yaml @@ -47,7 +47,7 @@ spec: memory: 512Mi limits: cpu: 500m - memory: 512Mi + memory: 768Mi gateway: enabled: true From 961a491fe416498c430d0619cf99639976438bad Mon Sep 17 00:00:00 2001 From: ITQ Date: Wed, 29 Jul 2026 17:53:57 +0300 Subject: [PATCH 05/10] chore(argocd/movienight): added proper labels to fix constant sync --- deploy/argocd/movienight.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/argocd/movienight.yaml b/deploy/argocd/movienight.yaml index 06a3254..080608f 100644 --- a/deploy/argocd/movienight.yaml +++ b/deploy/argocd/movienight.yaml @@ -6,6 +6,7 @@ metadata: namespace: argocd annotations: argocd.argoproj.io/sync-wave: "0" + argocd.argoproj.io/compare-options: ServerSideDiff=true,IncludeMutationWebhook=true spec: project: default source: From f8ecb6efbe87dc48942765b55e6e7f6f7a5dcb04 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Aug 2026 17:06:59 +0300 Subject: [PATCH 06/10] chore(argocd/movienight): o11y fixes --- deploy/argocd/movienight.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/argocd/movienight.yaml b/deploy/argocd/movienight.yaml index 080608f..1a89967 100644 --- a/deploy/argocd/movienight.yaml +++ b/deploy/argocd/movienight.yaml @@ -62,6 +62,8 @@ spec: observability: enabled: true + victoriaMetrics: + url: "http://vmsingle-main.observability.svc.cluster.local:8429" destination: server: https://kubernetes.default.svc From e18a66b222212b6e2b58f420f606931061993242 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Aug 2026 17:14:02 +0300 Subject: [PATCH 07/10] feat(helm/movienight): added scrape crd --- .../templates/observability/scrape.yaml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 deploy/helm/movienight/templates/observability/scrape.yaml diff --git a/deploy/helm/movienight/templates/observability/scrape.yaml b/deploy/helm/movienight/templates/observability/scrape.yaml new file mode 100644 index 0000000..4d2960b --- /dev/null +++ b/deploy/helm/movienight/templates/observability/scrape.yaml @@ -0,0 +1,34 @@ +{{- if .Values.observability.enabled }} +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: {{ include "movienight.fullname" . }}-scrape + labels: + {{- include "movienight.componentLabels" (dict "root" . "component" "scrape") | nindent 4 }} + {{- with .Values.observability.scrape.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if .Values.global.annotations }} + annotations: + {{- with .Values.global.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "movienight.componentSelectorLabels" (dict "root" . "component" "backend") | nindent 4 }} + + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + + endpoints: + - port: management + path: /actuator/prometheus + interval: 10s + + podTargetLabels: + - app.kubernetes.io/component + - app.kubernetes.io/instance +{{- end }} From 53d646f4efc261cd7561c649a5ea4dbb5e379a4c Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Aug 2026 14:33:19 +0000 Subject: [PATCH 08/10] hotfix(): fixed values --- deploy/helm/movienight/values.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/helm/movienight/values.yaml b/deploy/helm/movienight/values.yaml index 9d26eac..e876bee 100644 --- a/deploy/helm/movienight/values.yaml +++ b/deploy/helm/movienight/values.yaml @@ -125,6 +125,8 @@ observability: metrics: applicationTag: movienight httpServerRequestsHistogram: "true" + scrape: + labels: {} vmagent: scrapeInterval: 10s backendJobName: movienight-backend From 5796c5780e70442056b7ae58685a4359ce2a7694 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Aug 2026 14:35:34 +0000 Subject: [PATCH 09/10] hotfix(): fixed ident in helm chart --- deploy/helm/movienight/templates/observability/scrape.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/helm/movienight/templates/observability/scrape.yaml b/deploy/helm/movienight/templates/observability/scrape.yaml index 4d2960b..c680a93 100644 --- a/deploy/helm/movienight/templates/observability/scrape.yaml +++ b/deploy/helm/movienight/templates/observability/scrape.yaml @@ -17,7 +17,7 @@ metadata: spec: selector: matchLabels: - {{- include "movienight.componentSelectorLabels" (dict "root" . "component" "backend") | nindent 4 }} + {{- include "movienight.componentSelectorLabels" (dict "root" . "component" "backend") | nindent 6 }} namespaceSelector: matchNames: From 9578879cbc820ce9399456f608760d0702e824a7 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Aug 2026 17:36:25 +0300 Subject: [PATCH 10/10] chore(): updated rev for argocd --- deploy/argocd/movienight.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/argocd/movienight.yaml b/deploy/argocd/movienight.yaml index 1a89967..7e908ef 100644 --- a/deploy/argocd/movienight.yaml +++ b/deploy/argocd/movienight.yaml @@ -10,8 +10,8 @@ metadata: spec: project: default source: - repoURL: https://github.com/devitq/movienight-backend.git - targetRevision: feat/setup-observability-in-k8s + repoURL: https://git.itqdev.xyz/ITQ/movienight-backend.git + targetRevision: develop path: deploy/helm/movienight helm: valuesObject: