feat: jalali calendar support and date formatting fixes

- Wire useDatepicker into IntlUtil and switch jalalian display locale
  to fa-IR for clean "1405/07/03 12:00:00" output (drops the awkward
  "AP" era suffix that "<lang>-u-ca-persian" produced)
- Drop in persian-calendar-suite for the jalali date picker, with a
  light/dark/ultra theme map and CSS overrides so the inline-styled
  input stays readable and bg matches the surrounding container
- Force LTR on the picker input so "1405/03/07 00:00" reads naturally
- Pass calendar setting through ClientInfoModal, ClientsPage Duration
  tooltip, and ClientFormModal's expiry picker
- Heuristic toMs() in ClientInfoModal so GORM's autoUpdateTime seconds
  render as a real date instead of "1348/11/01"
- Persist UpdatedAt on the ClientRecord row in client_service.Update;
  previously only the inbound settings JSON was bumped, so the panel
  never saw a fresh updated_at after editing a client
This commit is contained in:
MHSanaei 2026-05-22 16:33:00 +02:00
parent b72212bbb7
commit f929ea4b14
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
10 changed files with 211 additions and 65 deletions

View file

@ -1,12 +1,12 @@
{ {
"name": "3x-ui-frontend", "name": "3x-ui-frontend",
"version": "0.0.3", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "3x-ui-frontend", "name": "3x-ui-frontend",
"version": "0.0.3", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.2.3", "@ant-design/icons": "^6.2.3",
"@codemirror/lang-json": "^6.0.2", "@codemirror/lang-json": "^6.0.2",
@ -17,6 +17,7 @@
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"i18next": "^26.2.0", "i18next": "^26.2.0",
"otpauth": "^9.5.1", "otpauth": "^9.5.1",
"persian-calendar-suite": "^1.5.5",
"qs": "^6.15.2", "qs": "^6.15.2",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
@ -82,18 +83,6 @@
"react-dom": ">=18" "react-dom": ">=18"
} }
}, },
"node_modules/@ant-design/cssinjs/node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@ant-design/cssinjs/node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
"license": "MIT"
},
"node_modules/@ant-design/fast-color": { "node_modules/@ant-design/fast-color": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz",
@ -530,6 +519,18 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1", "version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@ -2271,21 +2272,6 @@
"react-dom": ">=18.0.0" "react-dom": ">=18.0.0"
} }
}, },
"node_modules/antd/node_modules/compute-scroll-into-view": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
"license": "MIT"
},
"node_modules/antd/node_modules/scroll-into-view-if-needed": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^3.0.2"
}
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -2460,6 +2446,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/compute-scroll-into-view": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
"license": "MIT"
},
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@ -2558,9 +2550,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.360", "version": "1.5.361",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
"integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -3666,9 +3658,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.45", "version": "2.0.46",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
"integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -3769,6 +3761,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/persian-calendar-suite": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/persian-calendar-suite/-/persian-calendar-suite-1.5.5.tgz",
"integrity": "sha512-KJSzN9q7MZKhfkm97X/j+nD6L0AQ5coUq/B7PpIklXAvRjkALwiV+KmYG0pfr546EQxO9l4fBwE7R1HPI3yT7w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -3956,6 +3957,15 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/scroll-into-view-if-needed": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^3.0.2"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",

View file

@ -1,7 +1,7 @@
{ {
"name": "3x-ui-frontend", "name": "3x-ui-frontend",
"private": true, "private": true,
"version": "0.0.3", "version": "0.1.0",
"type": "module", "type": "module",
"description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).", "description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
"engines": { "engines": {
@ -25,6 +25,7 @@
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"i18next": "^26.2.0", "i18next": "^26.2.0",
"otpauth": "^9.5.1", "otpauth": "^9.5.1",
"persian-calendar-suite": "^1.5.5",
"qs": "^6.15.2", "qs": "^6.15.2",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",

View file

@ -0,0 +1,35 @@
.jdp-wrap {
width: 100%;
}
.jdp-wrap > * {
width: 100%;
}
.jdp-wrap input {
direction: ltr;
text-align: left;
unicode-bidi: plaintext;
}
.jdp-dark .jdp-wrap input,
.jdp-dark input {
color: rgba(255, 255, 255, 0.88) !important;
background-color: #23252b !important;
}
.jdp-ultra .jdp-wrap input,
.jdp-ultra input {
color: rgba(255, 255, 255, 0.88) !important;
background-color: #101013 !important;
}
.jdp-dark input::placeholder,
.jdp-ultra input::placeholder {
color: rgba(255, 255, 255, 0.30) !important;
}
.jdp-disabled {
pointer-events: none;
opacity: 0.6;
}

View file

@ -1,5 +1,12 @@
import { useMemo } from 'react';
import { DatePicker } from 'antd'; import { DatePicker } from 'antd';
import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import { PersianDateTimePicker } from 'persian-calendar-suite';
import { useDatepicker } from '@/hooks/useDatepicker';
import { useTheme } from '@/hooks/useTheme';
import './DateTimePicker.css';
interface DateTimePickerProps { interface DateTimePickerProps {
value: Dayjs | null; value: Dayjs | null;
@ -10,6 +17,33 @@ interface DateTimePickerProps {
disabled?: boolean; disabled?: boolean;
} }
const LIGHT_THEME = {
primaryColor: '#1677ff',
backgroundColor: '#ffffff',
borderColor: '#d9d9d9',
hoverColor: 'rgba(22, 119, 255, 0.10)',
selectedTextColor: '#ffffff',
textColor: 'rgba(0, 0, 0, 0.88)',
};
const DARK_THEME = {
primaryColor: '#1677ff',
backgroundColor: '#23252b',
borderColor: 'rgba(255, 255, 255, 0.12)',
hoverColor: 'rgba(22, 119, 255, 0.18)',
selectedTextColor: '#ffffff',
textColor: 'rgba(255, 255, 255, 0.88)',
};
const ULTRA_DARK_THEME = {
primaryColor: '#1677ff',
backgroundColor: '#101013',
borderColor: 'rgba(255, 255, 255, 0.08)',
hoverColor: 'rgba(22, 119, 255, 0.16)',
selectedTextColor: '#ffffff',
textColor: 'rgba(255, 255, 255, 0.88)',
};
export default function DateTimePicker({ export default function DateTimePicker({
value, value,
onChange, onChange,
@ -18,6 +52,38 @@ export default function DateTimePicker({
placeholder = '', placeholder = '',
disabled = false, disabled = false,
}: DateTimePickerProps) { }: DateTimePickerProps) {
const { datepicker } = useDatepicker();
const { isDark, isUltra } = useTheme();
const persianTheme = useMemo(() => {
if (isUltra) return ULTRA_DARK_THEME;
if (isDark) return DARK_THEME;
return LIGHT_THEME;
}, [isDark, isUltra]);
if (datepicker === 'jalalian') {
return (
<div className={`jdp-wrap${isDark ? ' jdp-dark' : ''}${isUltra ? ' jdp-ultra' : ''}${disabled ? ' jdp-disabled' : ''}`}>
<PersianDateTimePicker
value={value ? value.valueOf() : null}
onChange={(next: number | string | null) => {
if (next == null || next === '') {
onChange(null);
return;
}
const ms = typeof next === 'number' ? next : Number(next);
if (Number.isFinite(ms)) onChange(dayjs(ms));
}}
showTime={showTime}
outputFormat="timestamp"
persianNumbers
rtlCalendar
theme={persianTheme}
/>
</div>
);
}
return ( return (
<DatePicker <DatePicker
value={value} value={value}

36
frontend/src/env.d.ts vendored
View file

@ -27,3 +27,39 @@ interface Window {
X_UI_CUR_VER?: string; X_UI_CUR_VER?: string;
__SUB_PAGE_DATA__?: SubPageData; __SUB_PAGE_DATA__?: SubPageData;
} }
declare module 'persian-calendar-suite' {
import type { ComponentType, ReactNode } from 'react';
type DateInput = string | number | null;
type OutputFormat = 'iso' | 'shamsi' | 'gregorian' | 'hijri' | 'timestamp';
interface PersianDateTimePickerProps {
value?: DateInput;
onChange?: (value: number | string | null) => void;
defaultValue?: string | number | 'now' | null;
showTime?: boolean;
minuteStep?: number;
outputFormat?: OutputFormat;
showFooter?: boolean;
theme?: Record<string, unknown>;
disabledHours?: number[];
minDate?: string | Date | null;
maxDate?: string | Date | null;
enabledDates?: string[] | null;
disabledDates?: string[] | null;
disabledWeekDays?: number[];
persianNumbers?: boolean;
rtlCalendar?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
children?: ReactNode;
}
export const PersianDateTimePicker: ComponentType<PersianDateTimePickerProps>;
export const PersianCalendar: ComponentType<Record<string, unknown>>;
export const PersianDateRangePicker: ComponentType<Record<string, unknown>>;
export const PersianTimePicker: ComponentType<Record<string, unknown>>;
export const PersianTimeline: ComponentType<Record<string, unknown>>;
}

View file

@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import { import {
Button, Button,
Col, Col,
DatePicker,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@ -19,6 +18,7 @@ import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import { HttpUtil, RandomUtil } from '@/utils'; import { HttpUtil, RandomUtil } from '@/utils';
import DateTimePicker from '@/components/DateTimePicker';
import { TLS_FLOW_CONTROL } from '@/models/inbound'; import { TLS_FLOW_CONTROL } from '@/models/inbound';
import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import type { ClientRecord, InboundOption } from '@/hooks/useClients';
import './ClientFormModal.css'; import './ClientFormModal.css';
@ -416,11 +416,9 @@ export default function ClientFormModal({
</Form.Item> </Form.Item>
) : ( ) : (
<Form.Item label={t('pages.clients.expiryTime')}> <Form.Item label={t('pages.clients.expiryTime')}>
<DatePicker <DateTimePicker
value={form.expiryDate} value={form.expiryDate}
onChange={(d) => update('expiryDate', d || null)} onChange={(d) => update('expiryDate', d || null)}
showTime
style={{ width: '100%' }}
/> />
</Form.Item> </Form.Item>
)} )}

View file

@ -4,6 +4,7 @@ import { Button, Divider, Modal, Tag, Tooltip, message } from 'antd';
import { CopyOutlined } from '@ant-design/icons'; import { CopyOutlined } from '@ant-design/icons';
import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils'; import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
import { useDatepicker } from '@/hooks/useDatepicker';
import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import type { ClientRecord, InboundOption } from '@/hooks/useClients';
import './ClientInfoModal.css'; import './ClientInfoModal.css';
@ -30,16 +31,6 @@ interface ApiMsg<T = unknown> {
const DEFAULT_SUB: SubSettings = { enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }; const DEFAULT_SUB: SubSettings = { enable: false, subURI: '', subJsonURI: '', subJsonEnable: false };
function expiryLabel(ts?: number) {
if (!ts || ts <= 0) return '∞';
return IntlUtil.formatDate(ts);
}
function dateLabel(ts?: number) {
if (!ts || ts <= 0) return '-';
return IntlUtil.formatDate(ts);
}
export default function ClientInfoModal({ export default function ClientInfoModal({
open, open,
client, client,
@ -48,6 +39,9 @@ export default function ClientInfoModal({
subSettings = DEFAULT_SUB, subSettings = DEFAULT_SUB,
onOpenChange, onOpenChange,
}: ClientInfoModalProps) { }: ClientInfoModalProps) {
const { datepicker } = useDatepicker();
const expiryLabel = (ts?: number) => (!ts || ts <= 0 ? '∞' : IntlUtil.formatDate(ts, datepicker));
const dateLabel = (ts?: number) => (!ts || ts <= 0 ? '-' : IntlUtil.formatDate(ts, datepicker));
const { t } = useTranslation(); const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage(); const [messageApi, messageContextHolder] = message.useMessage();
const [links, setLinks] = useState<string[]>([]); const [links, setLinks] = useState<string[]>([]);

View file

@ -44,6 +44,7 @@ import { useTheme } from '@/hooks/useTheme';
import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useWebSocket } from '@/hooks/useWebSocket'; import { useWebSocket } from '@/hooks/useWebSocket';
import { useClients } from '@/hooks/useClients'; import { useClients } from '@/hooks/useClients';
import { useDatepicker } from '@/hooks/useDatepicker';
import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import type { ClientRecord, InboundOption } from '@/hooks/useClients';
import AppSidebar from '@/components/AppSidebar'; import AppSidebar from '@/components/AppSidebar';
import CustomStatistic from '@/components/CustomStatistic'; import CustomStatistic from '@/components/CustomStatistic';
@ -86,6 +87,7 @@ function readFilterState(): FilterState {
export default function ClientsPage() { export default function ClientsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { isDark, isUltra, antdThemeConfig } = useTheme(); const { isDark, isUltra, antdThemeConfig } = useTheme();
const { datepicker } = useDatepicker();
const { isMobile } = useMediaQuery(); const { isMobile } = useMediaQuery();
const [modal, modalContextHolder] = Modal.useModal(); const [modal, modalContextHolder] = Modal.useModal();
const [messageApi, messageContextHolder] = message.useMessage(); const [messageApi, messageContextHolder] = message.useMessage();
@ -296,7 +298,7 @@ export default function ClientsPage() {
const days = Math.round(row.expiryTime / -86400000); const days = Math.round(row.expiryTime / -86400000);
return `${t('pages.clients.delayedStart')}: ${days}d`; return `${t('pages.clients.delayedStart')}: ${days}d`;
} }
return IntlUtil.formatDate(row.expiryTime); return IntlUtil.formatDate(row.expiryTime, datepicker);
} }
function expiryRelative(row: ClientRecord) { function expiryRelative(row: ClientRecord) {

View file

@ -911,24 +911,22 @@ export class FileManager {
} }
export class IntlUtil { export class IntlUtil {
// When `calendar` is "jalalian", append the BCP-47 calendar extension // For Jalali display, always use fa-IR locale (its default calendar
// so Intl renders the date in the Persian (Jalali/Shamsi) calendar // is Persian) so we get a clean "1405/07/03 12:00:00" format with
// regardless of the UI language. Without it, only locales that // Persian digits, without the awkward "AP" era suffix that appears
// default to Persian (e.g. fa-IR) would show Jalali; en-US/ru/etc. // when other locales force `-u-ca-persian`.
// would keep showing Gregorian.
static formatDate(date, calendar = "gregorian") { static formatDate(date, calendar = "gregorian") {
const language = LanguageManager.getLanguage() const language = LanguageManager.getLanguage()
const locale = calendar === "jalalian" const locale = calendar === "jalalian" ? "fa-IR" : language
? `${language}-u-ca-persian`
: language
let intlOptions = { const intlOptions = {
year: "numeric", year: "numeric",
month: "numeric", month: "2-digit",
day: "numeric", day: "2-digit",
hour: "numeric", hour: "2-digit",
minute: "numeric", minute: "2-digit",
second: "numeric" second: "2-digit",
hour12: false,
} }
const intl = new Intl.DateTimeFormat( const intl = new Intl.DateTimeFormat(

View file

@ -604,6 +604,12 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
needRestart = true needRestart = true
} }
} }
if err := database.GetDB().Model(&model.ClientRecord{}).
Where("id = ?", id).
Update("updated_at", updated.UpdatedAt).Error; err != nil {
return needRestart, err
}
return needRestart, nil return needRestart, nil
} }