diff --git a/brewman/gunicorn.conf.py b/brewman/gunicorn.conf.py index 3e5ab8c1..a520c2fd 100644 --- a/brewman/gunicorn.conf.py +++ b/brewman/gunicorn.conf.py @@ -3,6 +3,9 @@ import multiprocessing import os +# --- Worker sizing ----------------------------------------------------------- +# Prefer explicit WEB_CONCURRENCY in containers. Otherwise fall back to +# WORKERS_PER_CORE * cpu_count(), with a minimum of 2 and optional MAX_WORKERS. workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") max_workers_str = os.getenv("MAX_WORKERS") use_max_workers = None @@ -10,12 +13,6 @@ if max_workers_str: use_max_workers = int(max_workers_str) web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) -host = os.getenv("HOST", "0.0.0.0") -port = os.getenv("PORT", "9994") -bind_env = os.getenv("BIND", None) -use_loglevel = os.getenv("LOG_LEVEL", "info") -use_bind = bind_env if bind_env else f"{host}:{port}" - cores = multiprocessing.cpu_count() workers_per_core = float(workers_per_core_str) default_web_concurrency = workers_per_core * cores @@ -26,27 +23,53 @@ else: web_concurrency = max(int(default_web_concurrency), 2) if use_max_workers: web_concurrency = min(web_concurrency, use_max_workers) -accesslog_var = os.getenv("ACCESS_LOG", "-") -use_accesslog = accesslog_var or None -errorlog_var = os.getenv("ERROR_LOG", "-") -use_errorlog = errorlog_var or None -graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120") -timeout_str = os.getenv("TIMEOUT", "120") -keepalive_str = os.getenv("KEEP_ALIVE", "5") -# Gunicorn config variables -loglevel = use_loglevel +# --- Bind / logging ---------------------------------------------------------- +host = os.getenv("HOST", "0.0.0.0") +port = os.getenv("PORT", "9994") +bind_env = os.getenv("BIND", None) +use_bind = bind_env if bind_env else f"{host}:{port}" + +# Tell Gunicorn to trust X-Forwarded-For headers from the reverse proxy +forwarded_allow_ips = "*" + +# Optional: Ensure proxy protocol is recognized +proxy_allow_ips = "*" + +loglevel = os.getenv("LOG_LEVEL", "info") +accesslog_var = os.getenv("ACCESS_LOG", "-") +accesslog = accesslog_var or None +errorlog_var = os.getenv("ERROR_LOG", "-") +errorlog = errorlog_var or None + +worker_tmp_dir = "/dev/shm" + +# --- Timeouts / keepalive ---------------------------------------------------- +graceful_timeout = int(os.getenv("GRACEFUL_TIMEOUT", "120")) +timeout = int(os.getenv("TIMEOUT", "120")) +keepalive = int(os.getenv("KEEP_ALIVE", "5")) + +# --- Robustness knobs (recommended) ----------------------------------------- +# Recycle workers gradually to mitigate memory leaks / fragmentation without +# full container restarts. Tune via env if needed. +max_requests = int(os.getenv("MAX_REQUESTS", "10000")) +max_requests_jitter = int(os.getenv("MAX_REQUESTS_JITTER", "1000")) + +# Prevent slow clients from holding connections forever (defaults are fine). +# You can tune these via env if you ever need to. +# worker_connections matters only for async worker types; kept here for clarity. +worker_connections = int(os.getenv("WORKER_CONNECTIONS", "1000")) + +# Helpful in containerized environments: ensure workers are responsive. +# (Defaults are okay; leaving commented unless you want strict behavior.) +# heartbeat_interval = int(os.getenv("HEARTBEAT_INTERVAL", "30")) + +# --- Gunicorn config vars ---------------------------------------------------- workers = web_concurrency bind = use_bind -errorlog = use_errorlog -worker_tmp_dir = "/dev/shm" -accesslog = use_accesslog -graceful_timeout = int(graceful_timeout_str) -timeout = int(timeout_str) -keepalive = int(keepalive_str) -# For debugging and testing +# --- Debug print (keep if you like) ------------------------------------------ log_data = { "loglevel": loglevel, "workers": workers, @@ -56,10 +79,42 @@ log_data = { "keepalive": keepalive, "errorlog": errorlog, "accesslog": accesslog, + "worker_tmp_dir": worker_tmp_dir, + "max_requests": max_requests, + "max_requests_jitter": max_requests_jitter, + "worker_connections": worker_connections, # Additional, non-gunicorn variables "workers_per_core": workers_per_core, "use_max_workers": use_max_workers, "host": host, "port": port, + "cores": cores, } print(json.dumps(log_data)) # noqa: T201 + + +# Variables Gunicorn reads +# (these must be module-level names) +# fmt: off +# Core +loglevel = loglevel +workers = workers +bind = bind + +# Logging +accesslog = accesslog +errorlog = errorlog + +# Runtime +worker_tmp_dir = worker_tmp_dir +graceful_timeout = graceful_timeout +timeout = timeout +keepalive = keepalive + +# Recycling +max_requests = max_requests +max_requests_jitter = max_requests_jitter + +# Concurrency (relevant for some worker types; harmless otherwise) +worker_connections = worker_connections +# fmt: on diff --git a/overlord/src/app/employee/employee-detail/employee-detail.component.html b/overlord/src/app/employee/employee-detail/employee-detail.component.html index 1a160b0c..5d164bee 100644 --- a/overlord/src/app/employee/employee-detail/employee-detail.component.html +++ b/overlord/src/app/employee/employee-detail/employee-detail.component.html @@ -64,7 +64,7 @@ - @if (!item().isActive) { + @if (!form.isActive().value()) { Leaving Date @@ -76,7 +76,7 @@
- + @if (!!item().id) { } diff --git a/overlord/src/app/employee/employee-detail/employee-detail.component.ts b/overlord/src/app/employee/employee-detail/employee-detail.component.ts index 0f871636..f036dc8d 100644 --- a/overlord/src/app/employee/employee-detail/employee-detail.component.ts +++ b/overlord/src/app/employee/employee-detail/employee-detail.component.ts @@ -1,5 +1,5 @@ import { Component, inject, afterNextRender, linkedSignal, input, computed } from '@angular/core'; -import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; +import { form as createForm, FormField, FormRoot, required } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatOptionModule } from '@angular/material/core'; @@ -28,8 +28,8 @@ export interface EmployeeDetailFormData { points: number; isActive: boolean; costCentre: string; - joiningDate: string; - leavingDate: string | null; + joiningDate: Date; + leavingDate: Date | null; } @Component({ @@ -70,12 +70,22 @@ export class EmployeeDetailComponent { points: itemVal.points || 0, isActive: itemVal.isActive ?? true, costCentre: itemVal.costCentre?.id ?? '', - joiningDate: itemVal.joiningDate, - leavingDate: itemVal.leavingDate, + joiningDate: itemVal.joiningDate ? moment(itemVal.joiningDate, 'DD-MMM-YYYY').toDate() : new Date(), + leavingDate: itemVal.leavingDate ? moment(itemVal.leavingDate, 'DD-MMM-YYYY').toDate() : null, }), }); - form = createForm(this.model); + form = createForm(this.model, (f) => { + required(f.name); + required(f.designation); + required(f.salary); + required(f.points); + required(f.joiningDate); + required(f.leavingDate, { + when: ({ valueOf }) => !valueOf(f.isActive), + message: 'Leaving date is required', + }); + }); costCentresResource = this.costCentreService.list(); costCentres = computed(() => this.costCentresResource.value() || []); @@ -87,6 +97,11 @@ export class EmployeeDetailComponent { } save() { + if (this.form().invalid()) { + const errorMsg = this.form().errorSummary()[0]?.message || 'Leaving date is required'; + this.snackBar.open(errorMsg, 'Danger'); + return; + } this.ser.saveOrUpdate(this.getItem()).subscribe({ next: () => { this.snackBar.open('', 'Success'); @@ -133,7 +148,8 @@ export class EmployeeDetailComponent { item.isActive = formValue.isActive; item.costCentre.id = formValue.costCentre ?? ''; item.joiningDate = moment(formValue.joiningDate).format('DD-MMM-YYYY'); - item.leavingDate = item.isActive ? null : moment(formValue.leavingDate).format('DD-MMM-YYYY'); + item.leavingDate = + item.isActive || !formValue.leavingDate ? null : moment(formValue.leavingDate).format('DD-MMM-YYYY'); return item; } } diff --git a/overlord/src/app/journal/journal.component.css b/overlord/src/app/journal/journal.component.css index f4854f18..47106110 100644 --- a/overlord/src/app/journal/journal.component.css +++ b/overlord/src/app/journal/journal.component.css @@ -16,19 +16,242 @@ justify-content: center; } -.img-container { +/* Attachments Section */ +.attachments-section { + display: flex; + flex-direction: column; + gap: 10px; + margin: 6px 0 16px; +} + +.attachments-header { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 500; + color: var(--mat-sys-on-surface-variant, #64748b); + user-select: none; +} + +.attachments-header-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: var(--mat-sys-primary, #0284c7); +} + +.attachments-badge { + font-size: 11px; + font-weight: 600; + padding: 1px 7px; + border-radius: 9999px; + background: var(--mat-sys-secondary-container, #e2e8f0); + color: var(--mat-sys-on-secondary-container, #1e293b); +} + +.attachments-grid { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 16px; +} + +.attachment-card { position: relative; + width: 110px; + height: 155px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + border: 1px solid var(--mat-sys-outline-variant, #e2e8f0); + background: var(--mat-sys-surface, #ffffff); + transition: + transform 0.2s ease, + box-shadow 0.2s ease; } -.img-container .overlay { - display: none; +.attachment-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); } -.img-container:hover > .overlay { - display: inline-block; +.thumbnail-wrapper { + position: relative; + width: 100%; + height: 100%; + cursor: pointer; + outline: none; +} + +.thumbnail-wrapper:focus-visible { + outline: 2px solid var(--mat-sys-primary, #0284c7); + outline-offset: -2px; +} + +.thumbnail-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transition: transform 0.25s ease; +} + +.thumbnail-wrapper:hover .thumbnail-img { + transform: scale(1.05); +} + +.zoom-overlay { position: absolute; - left: 60px; - top: 0; + inset: 0; + background: rgba(0, 0, 0, 0.35); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s ease; +} + +.thumbnail-wrapper:hover .zoom-overlay, +.thumbnail-wrapper:focus-visible .zoom-overlay { + opacity: 1; +} + +.zoom-icon { + color: #ffffff; + font-size: 28px; + width: 28px; + height: 28px; + filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5)); +} + +.attachment-card .delete-btn { + position: absolute; + top: 6px; + right: 6px; + width: 32px; + height: 32px; + min-width: 32px; + padding: 0; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(4px); + border-radius: 50%; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + color: #dc2626; + opacity: 0; + transition: + opacity 0.2s ease, + background-color 0.2s ease, + color 0.2s ease, + transform 0.15s ease; + z-index: 3; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.attachment-card:hover .delete-btn, +.attachment-card:focus-within .delete-btn { + opacity: 1; +} + +.attachment-card .delete-btn:hover, +.attachment-card .delete-btn:focus-visible { + background: #dc2626; + color: #ffffff; + transform: scale(1.08); +} + +.attachment-card .delete-btn mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + line-height: 18px; +} + +.upload-tile { + width: 110px; + height: 155px; + border-radius: 12px; + border: 2px dashed var(--mat-sys-outline, #cbd5e1); + background: var(--mat-sys-surface-container-low, #f8fafc); + cursor: pointer; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + transition: + border-color 0.2s ease, + background-color 0.2s ease, + transform 0.2s ease; +} + +.upload-tile:hover { + border-color: var(--mat-sys-primary, #0284c7); + background: rgba(2, 132, 199, 0.05); + transform: translateY(-2px); +} + +.upload-tile:focus-visible { + outline: 2px solid var(--mat-sys-primary, #0284c7); + outline-offset: 2px; +} + +.upload-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 8px; + gap: 6px; +} + +.upload-icon-circle { + width: 42px; + height: 42px; + border-radius: 50%; + background: var(--mat-sys-primary-container, #e0f2fe); + color: var(--mat-sys-on-primary-container, #0284c7); + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease; +} + +.upload-tile:hover .upload-icon-circle { + transform: scale(1.08); +} + +.upload-icon-circle mat-icon { + font-size: 22px; + width: 22px; + height: 22px; + line-height: 22px; +} + +.upload-title { + font-size: 12px; + font-weight: 600; + color: var(--mat-sys-on-surface, #1e293b); +} + +.upload-subtitle { + font-size: 10px; + color: var(--mat-sys-on-surface-variant, #64748b); +} + +.file-input-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } .gold { diff --git a/overlord/src/app/journal/journal.component.html b/overlord/src/app/journal/journal.component.html index 2de866e4..43e90074 100644 --- a/overlord/src/app/journal/journal.component.html +++ b/overlord/src/app/journal/journal.component.html @@ -131,29 +131,70 @@
-
- @for (item of form.files().value(); track item) { -
- img - -
- } -
-
-
- @for (item of form.files().value(); track item) { -
- img - -
- } -
-
-
- @for (item of form.files().value(); track item) { -
- img - -
- } -
-
-
- @for (item of form.files().value(); track item) { -
- img - -
- } -
-
-
- @for (item of form.files().value(); track item) { -
- img - -
- } -
-