= {
mention:
};
-// Format timestamp to relative time
+// Format timestamp to relative time (Unix seconds)
function formatRelativeTime(timestamp: number): string {
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
+ return '';
+ }
const now = Date.now() / 1000;
const diff = now - timestamp;
@@ -29,6 +32,9 @@ function formatRelativeTime(timestamp: number): string {
if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`;
const date = new Date(timestamp * 1000);
+ if (Number.isNaN(date.getTime())) {
+ return '';
+ }
return date.toLocaleDateString('de-DE');
}
diff --git a/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx b/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx
index 371f650..77cf5cd 100644
--- a/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx
+++ b/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx
@@ -10,6 +10,7 @@ import {
import { FaDownload, FaLink, FaPlay } from 'react-icons/fa';
import { WorkflowFile } from '../../../hooks/usePlayground';
import styles from './ConnectedFilesList.module.css';
+import { formatBinaryDataSizeBytes } from '../../../utils/formatDataSize';
export interface ConnectedFilesListActionButton {
type: 'edit' | 'delete' | 'download' | 'view' | 'copy' | 'connect' | 'play' | 'remove';
@@ -240,7 +241,7 @@ export function ConnectedFilesList({
- {formatFileSize(file.fileSize)}
+ {formatBinaryDataSizeBytes(file.fileSize)}
{file.source && (
@@ -371,13 +372,5 @@ export function ConnectedFilesList({
);
}
-function formatFileSize(bytes: number): string {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
-}
-
export default ConnectedFilesList;
diff --git a/src/components/UiComponents/Messages/MessageUtils.ts b/src/components/UiComponents/Messages/MessageUtils.ts
index 3d40945..ff67d51 100644
--- a/src/components/UiComponents/Messages/MessageUtils.ts
+++ b/src/components/UiComponents/Messages/MessageUtils.ts
@@ -2,6 +2,8 @@
* Utility functions for message formatting and styling
*/
+import { formatBinaryDataSizeBytes } from '../../../utils/formatDataSize';
+
/**
* Formats a timestamp to a readable date/time string
* Handles both Unix timestamps in seconds and milliseconds
@@ -50,16 +52,8 @@ export const formatTimestamp = (timestamp?: number): string => {
}
};
-/**
- * Formats file size to human-readable format
- */
-export const formatFileSize = (bytes: number): string => {
- if (bytes === 0) return '0 Bytes';
- const k = 1024;
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
-};
+/** File / attachment sizes (bytes). Same as formatBinaryDataSizeBytes (B … TB). */
+export const formatFileSize = formatBinaryDataSizeBytes;
/**
* Gets status badge color class based on status
diff --git a/src/hooks/useConfirm.tsx b/src/hooks/useConfirm.tsx
index bb9fbab..ec190d7 100644
--- a/src/hooks/useConfirm.tsx
+++ b/src/hooks/useConfirm.tsx
@@ -104,7 +104,7 @@ export function useConfirm() {
padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 500,
border: '1px solid var(--color-border, #444)',
background: 'transparent',
- color: 'var(--text-secondary, #aaa)',
+ color: 'var(--text-primary, #e8e8e8)',
cursor: 'pointer',
}}
>
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index 58a989b..b3706d9 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -10,6 +10,48 @@ import api from '../api';
const _ACCESS_REF_TYPES = new Set(['mandate_access', 'feature_access']);
+/** API uses PowerOnModel.sysCreatedAt (seconds); legacy clients used createdAt. */
+function _coerceToUnixSeconds(value: unknown): number | undefined {
+ if (value == null) return undefined;
+ if (typeof value === 'number' && Number.isFinite(value)) {
+ return value > 1e12 ? value / 1000 : value;
+ }
+ if (typeof value === 'string') {
+ const trimmed = value.trim();
+ if (!trimmed) return undefined;
+ const asNum = Number(trimmed);
+ if (!Number.isNaN(asNum)) {
+ return asNum > 1e12 ? asNum / 1000 : asNum;
+ }
+ const parsed = Date.parse(trimmed);
+ if (!Number.isNaN(parsed)) return parsed / 1000;
+ }
+ return undefined;
+}
+
+function _normalizeNotificationFromApi(raw: Record): UserNotification {
+ const partial = raw as unknown as UserNotification;
+ const createdAt =
+ _coerceToUnixSeconds(raw.createdAt) ??
+ _coerceToUnixSeconds(raw.sysCreatedAt) ??
+ (Number.isFinite(partial.createdAt) ? partial.createdAt : 0) ??
+ 0;
+ return {
+ ...partial,
+ createdAt,
+ readAt: _coerceToUnixSeconds(raw.readAt) ?? partial.readAt,
+ actionedAt: _coerceToUnixSeconds(raw.actionedAt) ?? partial.actionedAt,
+ expiresAt: _coerceToUnixSeconds(raw.expiresAt) ?? partial.expiresAt,
+ };
+}
+
+function _normalizeNotificationList(data: unknown): UserNotification[] {
+ if (!Array.isArray(data)) return [];
+ return data.map(item =>
+ _normalizeNotificationFromApi(item && typeof item === 'object' ? (item as Record) : {})
+ );
+}
+
// Types
export interface NotificationAction {
actionId: string;
@@ -30,6 +72,7 @@ export interface UserNotification {
actions?: NotificationAction[];
actionTaken?: string;
actionResult?: string;
+ /** Creation time as Unix seconds (UTC); filled from API sysCreatedAt when needed */
createdAt: number;
readAt?: number;
actionedAt?: number;
@@ -74,7 +117,7 @@ export function useNotifications() {
const url = `/api/notifications${queryString ? `?${queryString}` : ''}`;
const response = await api.get(url);
- const data = response.data as UserNotification[];
+ const data = _normalizeNotificationList(response.data);
setNotifications(data);
return data;
} catch (err: any) {
@@ -101,9 +144,9 @@ export function useNotifications() {
const listRes = await api.get('/api/notifications', {
params: { status: 'unread', limit: 25 },
});
- const list = listRes.data as UserNotification[];
+ const list = _normalizeNotificationList(listRes.data);
if (
- Array.isArray(list) &&
+ list.length > 0 &&
list.some(n => n.referenceType && _ACCESS_REF_TYPES.has(n.referenceType))
) {
window.dispatchEvent(new Event('features-changed'));
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index 94f73ce..ff8af52 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -107,8 +107,8 @@ const VoiceSettingsTab: React.FC = () => {
setLoading(true);
try {
const [prefsData, languagesData] = await Promise.all([
- request({ url: '/api/local/voice-preferences', method: 'get' }),
- request({ url: '/api/local/voice/languages', method: 'get' }),
+ request({ url: '/api/voice/preferences', method: 'get' }),
+ request({ url: '/api/voice/languages', method: 'get' }),
]);
const langList = (languagesData as any)?.languages || [];
@@ -135,7 +135,7 @@ const VoiceSettingsTab: React.FC = () => {
const _loadVoicesForLanguage = useCallback(async (lang: string) => {
setLoadingVoices(true);
try {
- const result = await request({ url: '/api/local/voice/voices', method: 'get', params: { language: lang } });
+ const result = await request({ url: '/api/voice/voices', method: 'get', params: { language: lang } });
setAddVoices((result as any)?.voices || []);
setAddVoiceName('');
} catch { setAddVoices([]); }
@@ -167,7 +167,7 @@ const VoiceSettingsTab: React.FC = () => {
const mapObj: Record = {};
voiceMap.forEach(e => { mapObj[e.language] = { voiceName: e.voiceName || '' }; });
await request({
- url: '/api/local/voice-preferences',
+ url: '/api/voice/preferences',
method: 'put',
data: { sttLanguage, ttsLanguage: sttLanguage, ttsVoiceMap: mapObj },
});
@@ -185,9 +185,9 @@ const VoiceSettingsTab: React.FC = () => {
setTesting(lang);
try {
const result: any = await request({
- url: '/api/local/voice/test',
+ url: '/api/voice/test',
method: 'post',
- data: { language: lang, voiceId: voice || undefined, text: `Hallo, das ist ein Stimmtest in ${lang}.` },
+ data: { language: lang, voiceId: voice || undefined },
});
if (result?.success && result?.audio) {
const audio = new Audio(`data:audio/mp3;base64,${result.audio}`);
diff --git a/src/pages/Store.tsx b/src/pages/Store.tsx
index 1162d26..ee605c1 100644
--- a/src/pages/Store.tsx
+++ b/src/pages/Store.tsx
@@ -9,6 +9,7 @@ import { FaCogs, FaComments, FaHeadset, FaProjectDiagram } from 'react-icons/fa'
import { useLanguage } from '../providers/language/LanguageContext';
import { useStore } from '../hooks/useStore';
import type { StoreFeature, UserMandate } from '../api/storeApi';
+import { formatBinaryDataSizeFromMebibytes } from '../utils/formatDataSize';
import styles from './Store.module.css';
const FEATURE_ICONS: Record = {
@@ -176,7 +177,13 @@ const StorePage: React.FC = () => {
)}
{subscriptionInfo.maxDataVolumeMB != null && (
- {currentLanguage === 'de' ? 'Speicher' : 'Storage'}: {subscriptionInfo.maxDataVolumeMB} MB
+ {currentLanguage === 'de' ? 'Speicher' : 'Storage'}:{' '}
+ {formatBinaryDataSizeFromMebibytes(subscriptionInfo.maxDataVolumeMB)}
+
+ )}
+ {subscriptionInfo.budgetAiCHF != null && subscriptionInfo.budgetAiCHF > 0 && (
+
+ {currentLanguage === 'de' ? 'AI-Budget' : 'AI budget'}: {subscriptionInfo.budgetAiCHF} CHF
)}
{subscriptionInfo.status === 'TRIALING' && subscriptionInfo.trialEndsAt && (
diff --git a/src/pages/billing/BillingAdmin.tsx b/src/pages/billing/BillingAdmin.tsx
index 62245fd..07c3be0 100644
--- a/src/pages/billing/BillingAdmin.tsx
+++ b/src/pages/billing/BillingAdmin.tsx
@@ -87,6 +87,9 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi
const [formData, setFormData] = useState({
warningThresholdPercent: Number(settings?.warningThresholdPercent ?? 10),
notifyOnWarning: settings?.notifyOnWarning ?? true,
+ autoRechargeEnabled: settings?.autoRechargeEnabled ?? false,
+ rechargeAmountCHF: Number(settings?.rechargeAmountCHF ?? 10),
+ rechargeMaxPerMonth: Number(settings?.rechargeMaxPerMonth ?? 3),
});
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
@@ -96,6 +99,9 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi
setFormData({
warningThresholdPercent: Number(settings.warningThresholdPercent ?? 10),
notifyOnWarning: settings.notifyOnWarning ?? true,
+ autoRechargeEnabled: settings.autoRechargeEnabled ?? false,
+ rechargeAmountCHF: Number(settings.rechargeAmountCHF ?? 10),
+ rechargeMaxPerMonth: Number(settings.rechargeMaxPerMonth ?? 3),
});
}
}, [settings]);
@@ -154,6 +160,49 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi
+
+
+
+
+
+
+ {formData.autoRechargeEnabled && (
+
+ )}