Skip to content

Commit ebba2d6

Browse files
committed
feat: added dynamic timezone
1 parent be6a5a2 commit ebba2d6

4 files changed

Lines changed: 231 additions & 9 deletions

File tree

imageroot/actions/configure-module/20configure

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ else:
2525
auth_token = secrets.token_urlsafe(32)
2626
# n8n CSRF settings
2727
host = data.get("host", "")
28+
timezone = data.get("timezone", "UTC")
2829
n8n_URL = "https://" + host
2930
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS = data.get(
3031
"OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS", "true"
@@ -48,8 +49,8 @@ SMTP_USERNAME = smtp_settings["username"]
4849
SMTP_PASSWORD = smtp_settings["password"]
4950

5051
n8n_config = {
51-
"GENERIC_TIMEZONE": "Europe/Berlin",
52-
"TZ": "Africa/Nairobi",
52+
"GENERIC_TIMEZONE": timezone,
53+
"TZ": timezone,
5354
"N8N_HOST": n8n_URL,
5455
"EXECUTIONS_MODE": "queue",
5556
"QUEUE_BULL_REDIS_HOST": "n8n-redis",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# Copyright (C) 2024 Nethesis S.r.l.
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
#
7+
8+
import json
9+
import sys
10+
import subprocess
11+
import os
12+
import agent
13+
14+
timezones_directory = "/usr/share/zoneinfo/posix/"
15+
command = "find * -type f -or -type l"
16+
# Get the list of timezones
17+
accepted_timezone_list = subprocess.check_output(
18+
command, cwd=timezones_directory, text=True, shell=True
19+
).splitlines()
20+
21+
# Get local timezone
22+
# Works for both Rocky Linux and Debian systems
23+
local_timezone = ""
24+
try:
25+
# Method 1: Read from /etc/timezone (Debian/Ubuntu)
26+
if os.path.exists("/etc/timezone"):
27+
with open("/etc/timezone", "r") as f:
28+
local_timezone = f.read().strip()
29+
# Method 2: Read symlink from /etc/localtime (Rocky Linux/RHEL)
30+
elif os.path.islink("/etc/localtime"):
31+
localtime_path = os.path.realpath("/etc/localtime")
32+
# Extract timezone from path like /usr/share/zoneinfo/Europe/Rome
33+
if "/zoneinfo/" in localtime_path:
34+
local_timezone = localtime_path.split("/zoneinfo/")[1]
35+
# Method 3: Use timedatectl as fallback
36+
if not local_timezone:
37+
result = subprocess.run(
38+
["timedatectl", "show", "-p", "Timezone", "--value"],
39+
capture_output=True,
40+
text=True,
41+
)
42+
if result.returncode == 0:
43+
local_timezone = result.stdout.strip()
44+
except Exception as e:
45+
print(f"Warning: Could not determine timezone: {e}", file=sys.stderr)
46+
# If all methods fail, use UTC as default
47+
local_timezone = "UTC"
48+
49+
50+
config = {
51+
"accepted_timezone_list": accepted_timezone_list,
52+
"local_timezone": local_timezone,
53+
}
54+
55+
json.dump(config, fp=sys.stdout)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "n8n defaults",
4+
"$id": "http://nethserver.org/json-schema/task/output/n8n/get-defaults",
5+
"description": "Get n8n default configuration values",
6+
"examples": [
7+
{
8+
"accepted_timezone_list": [
9+
"Europe/Amsterdam",
10+
"Europe/Berlin",
11+
"Europe/Brussels",
12+
"Europe/London"
13+
],
14+
"local_timezone": "Europe/Rome"
15+
},
16+
{
17+
"accepted_timezone_list": ["Europe/Amsterdam", "Europe/Berlin"],
18+
"local_timezone": "UTC"
19+
}
20+
],
21+
"type": "object",
22+
"additionalProperties": false,
23+
"required": ["accepted_timezone_list", "local_timezone"],
24+
"properties": {
25+
"accepted_timezone_list": {
26+
"description": "List of accepted timezones",
27+
"type": "array",
28+
"items": {
29+
"type": "string"
30+
}
31+
},
32+
"local_timezone": {
33+
"description": "The current system timezone",
34+
"type": "string"
35+
}
36+
}
37+
}

ui/src/views/Settings.vue

Lines changed: 136 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@
5858
$t("settings.enabled")
5959
}}</template>
6060
</cv-toggle>
61+
<NsComboBox
62+
v-model.trim="timezone"
63+
:autoFilter="true"
64+
:autoHighlight="true"
65+
:title="$t('settings.timezone')"
66+
:label="$t('settings.timezone_placeholder')"
67+
:options="timezoneList"
68+
:userInputLabel="core.$t('common.user_input_l')"
69+
:acceptUserInput="false"
70+
:showItemType="true"
71+
:invalid-message="$t(error.timezone)"
72+
:disabled="loading.getConfiguration || loading.configureModule"
73+
tooltipAlignment="start"
74+
tooltipDirection="top"
75+
ref="timezone"
76+
>
77+
<template slot="tooltip">
78+
{{ $t("settings.timezone_tooltip") }}
79+
</template>
80+
</NsComboBox>
6181
<div v-if="error.configureModule" class="bx--row">
6282
<div class="bx--col">
6383
<NsInlineNotification
@@ -109,18 +129,24 @@ export default {
109129
n8n_ADMIN_PASSWORD: "",
110130
isLetsEncryptEnabled: false,
111131
isHttpToHttpsEnabled: false,
132+
timezone: "",
133+
timezoneList: [],
112134
loading: {
113135
getConfiguration: false,
114136
configureModule: false,
137+
getDefaults: false,
115138
},
116139
error: {
117140
getConfiguration: "",
141+
timezone: "",
118142
configureModule: "",
119143
host: "",
120144
n8n_ADMIN_USER: "",
121145
n8n_ADMIN_PASSWORD: "",
122146
lets_encrypt: "",
123147
http2https: "",
148+
getStatus: "",
149+
getDefaults: "",
124150
},
125151
};
126152
},
@@ -129,6 +155,8 @@ export default {
129155
},
130156
created() {
131157
this.getConfiguration();
158+
this.getStatus();
159+
this.getDefaults();
132160
},
133161
beforeRouteEnter(to, from, next) {
134162
next((vm) => {
@@ -141,6 +169,48 @@ export default {
141169
next();
142170
},
143171
methods: {
172+
async getStatus() {
173+
this.loading.getStatus = true;
174+
this.error.getStatus = "";
175+
const taskAction = "get-status";
176+
const eventId = this.getUuid();
177+
// register to task error
178+
this.core.$root.$once(
179+
`${taskAction}-aborted-${eventId}`,
180+
this.getStatusAborted,
181+
);
182+
// register to task completion
183+
this.core.$root.$once(
184+
`${taskAction}-completed-${eventId}`,
185+
this.getStatusCompleted,
186+
);
187+
const res = await to(
188+
this.createModuleTaskForApp(this.instanceName, {
189+
action: taskAction,
190+
extra: {
191+
title: this.$t("action." + taskAction),
192+
isNotificationHidden: true,
193+
eventId,
194+
},
195+
}),
196+
);
197+
const err = res[0];
198+
if (err) {
199+
console.error(`error creating task ${taskAction}`, err);
200+
this.error.getStatus = this.getErrorMessage(err);
201+
this.loading.getStatus = false;
202+
return;
203+
}
204+
},
205+
getStatusAborted(taskResult, taskContext) {
206+
console.error(`${taskContext.action} aborted`, taskResult);
207+
this.error.getStatus = this.$t("error.generic_error");
208+
this.loading.getStatus = false;
209+
},
210+
getStatusCompleted(taskContext, taskResult) {
211+
this.status = taskResult.output;
212+
this.loading.getStatus = false;
213+
},
144214
async getConfiguration() {
145215
this.loading.getConfiguration = true;
146216
this.error.getConfiguration = "";
@@ -150,14 +220,14 @@ export default {
150220
this.core.$root.$off(taskAction + "-aborted");
151221
this.core.$root.$once(
152222
taskAction + "-aborted",
153-
this.getConfigurationAborted
223+
this.getConfigurationAborted,
154224
);
155225
156226
// register to task completion
157227
this.core.$root.$off(taskAction + "-completed");
158228
this.core.$root.$once(
159229
taskAction + "-completed",
160-
this.getConfigurationCompleted
230+
this.getConfigurationCompleted,
161231
);
162232
163233
const res = await to(
@@ -167,7 +237,7 @@ export default {
167237
title: this.$t("action." + taskAction),
168238
isNotificationHidden: true,
169239
},
170-
})
240+
}),
171241
);
172242
const err = res[0];
173243
@@ -204,6 +274,10 @@ export default {
204274
}
205275
isValidationOk = false;
206276
}
277+
if (!this.timezone) {
278+
this.error.timezone = "common.required";
279+
isValidationOk = false;
280+
}
207281
208282
return isValidationOk;
209283
},
@@ -235,21 +309,21 @@ export default {
235309
this.core.$root.$off(taskAction + "-aborted");
236310
this.core.$root.$once(
237311
taskAction + "-aborted",
238-
this.configureModuleAborted
312+
this.configureModuleAborted,
239313
);
240314
241315
// register to task validation
242316
this.core.$root.$off(taskAction + "-validation-failed");
243317
this.core.$root.$once(
244318
taskAction + "-validation-failed",
245-
this.configureModuleValidationFailed
319+
this.configureModuleValidationFailed,
246320
);
247321
248322
// register to task completion
249323
this.core.$root.$off(taskAction + "-completed");
250324
this.core.$root.$once(
251325
taskAction + "-completed",
252-
this.configureModuleCompleted
326+
this.configureModuleCompleted,
253327
);
254328
255329
const res = await to(
@@ -259,14 +333,15 @@ export default {
259333
host: this.host,
260334
lets_encrypt: this.isLetsEncryptEnabled,
261335
http2https: this.isHttpToHttpsEnabled,
336+
timezone: this.timezone,
262337
},
263338
extra: {
264339
title: this.$t("settings.instance_configuration", {
265340
instance: this.instanceName,
266341
}),
267342
description: this.$t("settings.configuring"),
268343
},
269-
})
344+
}),
270345
);
271346
const err = res[0];
272347
@@ -288,6 +363,60 @@ export default {
288363
// reload configuration
289364
this.getConfiguration();
290365
},
366+
async getDefaults() {
367+
this.loading.getDefaults = true;
368+
369+
const taskAction = "get-defaults";
370+
const eventId = this.getUuid();
371+
372+
// register to task error
373+
this.core.$root.$once(
374+
`${taskAction}-aborted-${eventId}`,
375+
this.getDefaultsAborted,
376+
);
377+
378+
// register to task completion
379+
this.core.$root.$once(
380+
`${taskAction}-completed-${eventId}`,
381+
this.getDefaultsCompleted,
382+
);
383+
384+
const res = await to(
385+
this.createModuleTaskForApp(this.instanceName, {
386+
action: taskAction,
387+
extra: {
388+
title: this.$t("action." + taskAction),
389+
isNotificationHidden: true,
390+
eventId,
391+
},
392+
}),
393+
);
394+
const err = res[0];
395+
396+
if (err) {
397+
console.error(`error creating task ${taskAction}`, err);
398+
this.error.getDefaults = this.getErrorMessage(err);
399+
this.loading.getDefaults = false;
400+
return;
401+
}
402+
},
403+
getDefaultsAborted(taskResult, taskContext) {
404+
console.error(`${taskContext.action} aborted`, taskResult);
405+
this.error.getDefaults = this.$t("error.generic_error");
406+
this.loading.getDefaults = false;
407+
},
408+
getDefaultsCompleted(taskContext, taskResult) {
409+
this.timezoneList = [];
410+
taskResult.output.accepted_timezone_list.forEach((value) =>
411+
this.timezoneList.push({
412+
name: value,
413+
label: value,
414+
value: value,
415+
}),
416+
);
417+
this.loading.getDefaults = false;
418+
this.isProxyInstalled = taskResult.output.proxy_status.proxy_installed;
419+
},
291420
},
292421
};
293422
</script>

0 commit comments

Comments
 (0)