diff --git a/client/index.css b/client/index.css
index a0c2cb6..a3cc945 100644
--- a/client/index.css
+++ b/client/index.css
@@ -80,7 +80,9 @@
--secondary: 0.275 0.018 245;
--secondary-foreground: 0.94 0.007 245;
--muted: 0.275 0.016 245;
- --muted-foreground: 0.76 0.012 245;
+ /* Secondary text. Raised 0.76 → 0.85 — at 0.76 labels and hints sat too
+ close to the card background (L 0.235) and strained the eyes. */
+ --muted-foreground: 0.85 0.012 245;
--accent: 0.32 0.045 158;
--accent-foreground: 0.965 0.006 245;
--destructive: 0.66 0.18 26;
@@ -172,20 +174,22 @@
}
+ /* Faded muted-text variants: keep them readable in dark mode — Tailwind's
+ raw /40–/70 alphas dissolve into the background. */
.dark .text-muted-foreground\/40 {
- color: oklch(var(--muted-foreground) / 0.72);
+ color: oklch(var(--muted-foreground) / 0.8);
}
.dark .text-muted-foreground\/50 {
- color: oklch(var(--muted-foreground) / 0.78);
+ color: oklch(var(--muted-foreground) / 0.85);
}
.dark .text-muted-foreground\/60 {
- color: oklch(var(--muted-foreground) / 0.84);
+ color: oklch(var(--muted-foreground) / 0.9);
}
.dark .text-muted-foreground\/70 {
- color: oklch(var(--muted-foreground) / 0.9);
+ color: oklch(var(--muted-foreground) / 0.95);
}
/* Custom Scrollbar */
diff --git a/client/pages/ProfilePage.jsx b/client/pages/ProfilePage.jsx
index a806d4d..ee8ceec 100644
--- a/client/pages/ProfilePage.jsx
+++ b/client/pages/ProfilePage.jsx
@@ -21,7 +21,7 @@ function displayNameOf(profile) {
return profile.display_name || profile.displayName || profile.name || '';
}
-function asSettings(data) {
+export function asSettings(data) {
return data?.settings || data?.notifications || data || {};
}
@@ -360,7 +360,9 @@ function EditProfile({ profile, onSaved }) {
);
}
-function NotificationPreferences({ settings, onSaved }) {
+// Exported: rendered on the Settings page ("Notifications" section). Lives here
+// because it shares asSettings/CheckRow/SectionCard with the rest of this file.
+export function NotificationPreferences({ settings, onSaved }) {
const [form, setForm] = useState(settings);
const [saving, setSaving] = useState(false);
@@ -427,7 +429,8 @@ const PUSH_CHANNELS = [
{ value: 'telegram', label: 'Telegram', urlLabel: 'Server URL / n/a', urlHint: 'Leave blank for api.telegram.org', tokenLabel: 'Bot token', chatIdLabel: 'Chat ID' },
];
-function PushNotifications({ settings, onSaved }) {
+// Exported: rendered on the Settings page ("Notifications" section).
+export function PushNotifications({ settings, onSaved }) {
const [enabled, setEnabled] = useState(!!settings.notify_push_enabled);
const [channel, setChannel] = useState(settings.push_channel || 'ntfy');
const [url, setUrl] = useState(settings.push_url || '');
@@ -898,7 +901,6 @@ function ProfileNav() {
const items = [
['#account', 'Account'],
['#security', 'Security'],
- ['#notifications', 'Notifications'],
['#privacy', 'Privacy'],
];
return (
@@ -950,7 +952,7 @@ export default function ProfilePage() {
Profile
-
Manage your account, notification preferences, and password.
+
Manage your account, security, and privacy. Notification preferences live in Settings.
@@ -968,10 +970,7 @@ export default function ProfilePage() {
-
- {!loading &&
}
- {!loading &&
api.profileSettings().then(setSettings).catch(() => {})} />}
-
+ {/* Notification preferences moved to Settings → Notifications */}
diff --git a/client/pages/SettingsPage.jsx b/client/pages/SettingsPage.jsx
index 74cfc00..745f3a9 100644
--- a/client/pages/SettingsPage.jsx
+++ b/client/pages/SettingsPage.jsx
@@ -13,12 +13,36 @@ import {
import { Switch } from '@/components/ui/switch';
import { useTheme } from '@/contexts/ThemeContext';
import { useAuth } from '@/hooks/useAuth';
+import { NotificationPreferences, PushNotifications, asSettings } from '@/pages/ProfilePage';
export const LINK_IMPORT_PREF_KEY = 'link_import_ask';
export function getLinkImportPref() {
return localStorage.getItem(LINK_IMPORT_PREF_KEY) !== 'false';
}
+// ─── Notifications (moved here from Profile — these are app-behavior
+// preferences; Profile keeps identity, security, and privacy) ──────────────
+
+function NotificationsSection() {
+ const [settings, setSettings] = useState(null);
+
+ const load = useCallback(() => {
+ api.profileSettings()
+ .then((data) => setSettings(asSettings(data)))
+ .catch((err) => toast.error(err.message || 'Failed to load notification settings.'));
+ }, []);
+
+ useEffect(() => { load(); }, [load]);
+
+ if (!settings) return null;
+ return (
+
+ );
+}
+
// ─── Card wrapper ─────────────────────────────────────────────────────────────
function SectionCard({ title, children }) {
@@ -255,6 +279,7 @@ export default function SettingsPage() {
tracker_bank_projection_banner_snoozed_until: '',
tracker_show_search_sort: 'true',
tracker_show_summary_cards: 'true',
+ tracker_show_safe_to_spend: 'true',
tracker_show_overdue_command_center: 'true',
tracker_show_drift_insights: 'true',
};
@@ -289,6 +314,7 @@ export default function SettingsPage() {
tracker_bank_projection_banner_snoozed_until: settings.tracker_bank_projection_banner_snoozed_until || '',
tracker_show_search_sort: settings.tracker_show_search_sort,
tracker_show_summary_cards: settings.tracker_show_summary_cards,
+ tracker_show_safe_to_spend: settings.tracker_show_safe_to_spend,
tracker_show_overdue_command_center: settings.tracker_show_overdue_command_center,
tracker_show_drift_insights: settings.tracker_show_drift_insights,
});
@@ -329,7 +355,7 @@ export default function SettingsPage() {
{/* Page header — flat on background */}
Settings
-
Manage your display and billing preferences
+
Manage your display, billing, and notification preferences
{/* Appearance */}
@@ -400,6 +426,16 @@ export default function SettingsPage() {
aria-label="Show Summary cards"
/>
+
+ set('tracker_show_safe_to_spend', String(checked))}
+ aria-label="Show Safe to Spend"
+ />
+
+ {/* Notifications — email + push reminder preferences (save independently) */}
+
+
+
+
diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx
index 6daa11e..45f71a7 100644
--- a/client/pages/TrackerPage.jsx
+++ b/client/pages/TrackerPage.jsx
@@ -387,6 +387,7 @@ export default function TrackerPage() {
const showSearchSort = settingEnabled(trackerSettings.tracker_show_search_sort);
const showSummaryCards = settingEnabled(trackerSettings.tracker_show_summary_cards);
const showOverdueCommandCenter = settingEnabled(trackerSettings.tracker_show_overdue_command_center);
+ const showSafeToSpend = settingEnabled(trackerSettings.tracker_show_safe_to_spend);
const showDriftInsights = settingEnabled(trackerSettings.tracker_show_drift_insights);
const showBankProjectionBanner = settingEnabled(trackerSettings.tracker_show_bank_projection_banner) &&
(!bannerSnoozedUntil || bannerSnoozedUntil <= today);
@@ -848,7 +849,7 @@ export default function TrackerPage() {
) : null}
{/* ── Safe to Spend ── */}
- {!isError && !loading && isCurrentMonth && cashflow && (
+ {!isError && !loading && showSafeToSpend && isCurrentMonth && cashflow && (
setEditStartingOpen(true)}
diff --git a/services/userSettings.js b/services/userSettings.js
index 942ac3f..fd5c04d 100644
--- a/services/userSettings.js
+++ b/services/userSettings.js
@@ -17,6 +17,7 @@ const USER_SETTING_KEYS = [
'tracker_bank_projection_banner_snoozed_until',
'tracker_show_search_sort',
'tracker_show_summary_cards',
+ 'tracker_show_safe_to_spend',
'tracker_show_overdue_command_center',
'tracker_show_drift_insights',
'tracker_table_columns',
@@ -28,6 +29,7 @@ const USER_SETTING_DEFAULTS = {
tracker_bank_projection_banner_snoozed_until: '',
tracker_show_search_sort: 'true',
tracker_show_summary_cards: 'true',
+ tracker_show_safe_to_spend: 'true',
tracker_show_overdue_command_center: 'true',
tracker_show_drift_insights: 'true',
tracker_table_columns: '["due","expected","previous","paid","paid_date","status","action","notes"]',