diff --git a/assets/js/app.ts b/assets/js/app.ts
index b532cc6b..527316d7 100644
--- a/assets/js/app.ts
+++ b/assets/js/app.ts
@@ -350,9 +350,12 @@ if (!csrfToken) {
throw new Error('CSRF token meta tag not found')
}
+// Detect user's timezone
+const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"
+
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
- params: { _csrf_token: csrfToken },
+ params: { _csrf_token: csrfToken, timezone: userTimezone },
hooks: { ...colocatedHooks, SensorChart, WebAuthnRegister, WebAuthnLogin, CopyToClipboard, ScrollToTop, AutoDismissFlash },
})
diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml
index fd1ab588..0f998f39 100644
--- a/k8s/deployment.yaml
+++ b/k8s/deployment.yaml
@@ -5,13 +5,13 @@ metadata:
name: towerops
namespace: towerops
spec:
- replicas: 4
+ replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
- maxSurge: 1 # Allow 1 extra pod during rollout
- maxUnavailable: 0 # Keep all pods running during rollout
- minReadySeconds: 10 # Wait 10s after pod is ready before continuing
+ maxSurge: 1 # Allow 1 extra pod during rollout
+ maxUnavailable: 0 # Keep all pods running during rollout
+ minReadySeconds: 10 # Wait 10s after pod is ready before continuing
selector:
matchLabels:
app: towerops
@@ -20,7 +20,7 @@ spec:
labels:
app: towerops
spec:
- terminationGracePeriodSeconds: 30 # Allow 30s for graceful shutdown
+ terminationGracePeriodSeconds: 30 # Allow 30s for graceful shutdown
imagePullSecrets:
- name: gitlab-registry
securityContext:
@@ -63,7 +63,7 @@ spec:
fieldRef:
fieldPath: metadata.namespace
- name: DEPLOY_TIMESTAMP
- value: "1970-01-01T00:00:00Z" # Placeholder, updated by GitLab CI on deploy
+ value: "1970-01-01T00:00:00Z" # Placeholder, updated by GitLab CI on deploy
- name: RELEASE_DISTRIBUTION
value: "name"
- name: RELEASE_NODE
@@ -108,7 +108,7 @@ spec:
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
- failureThreshold: 12 # 10s + (12 * 5s) = 70s max startup time
+ failureThreshold: 12 # 10s + (12 * 5s) = 70s max startup time
livenessProbe:
httpGet:
path: /health
diff --git a/lib/towerops_web/live/admin/dashboard_live.html.heex b/lib/towerops_web/live/admin/dashboard_live.html.heex
index c903de9b..5670be41 100644
--- a/lib/towerops_web/live/admin/dashboard_live.html.heex
+++ b/lib/towerops_web/live/admin/dashboard_live.html.heex
@@ -32,7 +32,7 @@
<:col :let={log} label="Superuser">{log.superuser && log.superuser.email}
<:col :let={log} label="Target">{log.target_user && log.target_user.email}
<:col :let={log} label="Time">
- {Calendar.strftime(log.inserted_at, "%Y-%m-%d %H:%M:%S")}
+ {ToweropsWeb.TimeHelpers.format_iso8601(log.inserted_at, @timezone)}
diff --git a/lib/towerops_web/live/admin/org_live/index.html.heex b/lib/towerops_web/live/admin/org_live/index.html.heex
index dc498dfa..9eda00ef 100644
--- a/lib/towerops_web/live/admin/org_live/index.html.heex
+++ b/lib/towerops_web/live/admin/org_live/index.html.heex
@@ -10,7 +10,9 @@
<:col :let={org} label="Name">{org.name}
<:col :let={org} label="Slug">{org.slug}
<:col :let={org} label="Members">{length(org.memberships)}
- <:col :let={org} label="Created">{Calendar.strftime(org.inserted_at, "%Y-%m-%d")}
+ <:col :let={org} label="Created">
+ {ToweropsWeb.TimeHelpers.format_date(org.inserted_at, @timezone)}
+
<:col :let={org} label="">
{user.email}
<:col :let={user} label="Superuser">{if user.is_superuser, do: "Yes", else: "No"}
<:col :let={user} label="Organizations">{length(user.organizations)}
- <:col :let={user} label="Joined">{Calendar.strftime(user.inserted_at, "%Y-%m-%d")}
+ <:col :let={user} label="Joined">
+ {ToweropsWeb.TimeHelpers.format_date(user.inserted_at, @timezone)}
+
<:col :let={user} label="">
<%= if agent.last_seen_at do %>
- {format_datetime(agent.last_seen_at)}
+ {format_datetime(agent.last_seen_at, @timezone)}
<% end %>
<%= if agent.last_ip do %>
@@ -124,7 +124,7 @@
<:col :let={agent} label="Created">
- {Calendar.strftime(agent.inserted_at, "%b %d, %Y")}
+ {ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
diff --git a/lib/towerops_web/live/agent_live/show.html.heex b/lib/towerops_web/live/agent_live/show.html.heex
index 997c12ab..2f1d810e 100644
--- a/lib/towerops_web/live/agent_live/show.html.heex
+++ b/lib/towerops_web/live/agent_live/show.html.heex
@@ -68,7 +68,7 @@
<%= if @agent_token.last_seen_at do %>
- {format_datetime(@agent_token.last_seen_at)}
+ {format_datetime(@agent_token.last_seen_at, @timezone)}
<% end %>
<%= if @agent_token.last_ip do %>
@@ -150,7 +150,7 @@
Created
- {format_datetime(@agent_token.inserted_at)}
+ {format_datetime(@agent_token.inserted_at, @timezone)}
{format_last_seen(@agent_token.inserted_at)} (created)
@@ -160,7 +160,7 @@
Last Updated
- {format_datetime(@agent_token.updated_at)}
+ {format_datetime(@agent_token.updated_at, @timezone)}
{format_last_seen(@agent_token.updated_at)}
@@ -172,7 +172,7 @@
Last Seen (Heartbeat)
- {format_datetime(@agent_token.last_seen_at)}
+ {format_datetime(@agent_token.last_seen_at, @timezone)}
<%= if @agent_token.last_seen_at do %>
diff --git a/lib/towerops_web/live/alert_live/index.html.heex b/lib/towerops_web/live/alert_live/index.html.heex
index 9b3cb062..ea50e007 100644
--- a/lib/towerops_web/live/alert_live/index.html.heex
+++ b/lib/towerops_web/live/alert_live/index.html.heex
@@ -106,13 +106,13 @@
Triggered:
- {Calendar.strftime(alert.triggered_at, "%Y-%m-%d %H:%M:%S UTC")}
+ {ToweropsWeb.TimeHelpers.format_iso8601(alert.triggered_at, @timezone)}
<%= if alert.acknowledged_at do %>
Acknowledged:
- {Calendar.strftime(alert.acknowledged_at, "%Y-%m-%d %H:%M:%S UTC")}
+ {ToweropsWeb.TimeHelpers.format_iso8601(alert.acknowledged_at, @timezone)}
<%= if alert.acknowledged_by do %>
by {alert.acknowledged_by.email}
<% end %>
@@ -122,7 +122,7 @@
<%= if alert.resolved_at do %>
Resolved:
- {Calendar.strftime(alert.resolved_at, "%Y-%m-%d %H:%M:%S UTC")}
+ {ToweropsWeb.TimeHelpers.format_iso8601(alert.resolved_at, @timezone)}
<% end %>
diff --git a/lib/towerops_web/live/dashboard_live.html.heex b/lib/towerops_web/live/dashboard_live.html.heex
index 2c6497ea..a4ed4e82 100644
--- a/lib/towerops_web/live/dashboard_live.html.heex
+++ b/lib/towerops_web/live/dashboard_live.html.heex
@@ -163,7 +163,7 @@
{alert.message}
- {Calendar.strftime(alert.triggered_at, "%H:%M")}
+ {ToweropsWeb.TimeHelpers.format_datetime(alert.triggered_at, @timezone)}
<% end %>
diff --git a/lib/towerops_web/live/device_live/index.html.heex b/lib/towerops_web/live/device_live/index.html.heex
index 26567888..03c051e7 100644
--- a/lib/towerops_web/live/device_live/index.html.heex
+++ b/lib/towerops_web/live/device_live/index.html.heex
@@ -94,11 +94,7 @@
<:col :let={eq} label="Last Checked">
- <%= if eq.last_checked_at do %>
- {Calendar.strftime(eq.last_checked_at, "%Y-%m-%d %H:%M")}
- <% else %>
- Never
- <% end %>
+ {ToweropsWeb.TimeHelpers.format_datetime(eq.last_checked_at, @timezone)}
diff --git a/lib/towerops_web/user_auth.ex b/lib/towerops_web/user_auth.ex
index 847f5484..eedd1e83 100644
--- a/lib/towerops_web/user_auth.ex
+++ b/lib/towerops_web/user_auth.ex
@@ -460,9 +460,30 @@ defmodule ToweropsWeb.UserAuth do
end
defp mount_current_scope(socket, session) do
- Phoenix.Component.assign_new(socket, :current_scope, fn ->
- build_scope_from_session(session)
- end)
+ socket =
+ Phoenix.Component.assign_new(socket, :current_scope, fn ->
+ build_scope_from_session(session)
+ end)
+
+ # Get timezone from user profile, connect params, or default to UTC
+ user = socket.assigns.current_scope && socket.assigns.current_scope.user
+ connect_timezone = get_in(socket.private, [:connect_params, "timezone"])
+
+ timezone =
+ cond do
+ user && user.timezone -> user.timezone
+ connect_timezone -> connect_timezone
+ true -> "UTC"
+ end
+
+ # If user is logged in but doesn't have a timezone set, update their profile
+ if user && !user.timezone && connect_timezone do
+ Task.start(fn ->
+ Accounts.update_user_profile(user, %{timezone: connect_timezone})
+ end)
+ end
+
+ Phoenix.Component.assign(socket, :timezone, timezone)
end
defp build_scope_from_session(session) do