Compare commits

..

3 Commits

Author SHA1 Message Date
Chelsea
3d3b80fe96 partial 2026-02-17 05:46:59 +00:00
Chelsea
596467628f feat: add Discord presence status indicator in settings
Add a visual status indicator showing:
- Online/offline status with colored dot indicator
- Last seen timestamp
- Typical wake time (if available)

The indicator now displays whenever Discord notifications are enabled,
not just when presence tracking is active.
2026-02-17 04:37:13 +00:00
Chelsea
a0126d0aba fix: include adaptive_mode when enabling adaptive timing toggle
The API requires adaptive_mode when adaptive_timing_enabled is true,
but the frontend was only sending the enabled flag. This caused 400
errors when users tried to toggle adaptive timing on.

Now the toggle sends both fields when enabling, satisfying the API
validation requirements.
2026-02-17 04:35:55 +00:00
5 changed files with 74 additions and 16 deletions

View File

@@ -38,6 +38,8 @@ CACHE_FILE = "/app/user_cache.pkl"
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
intents.presences = True
intents.members = True
client = discord.Client(intents=intents) client = discord.Client(intents=intents)
@@ -582,15 +584,22 @@ async def beforeBackgroundLoop():
async def update_presence_tracking(): async def update_presence_tracking():
"""Track Discord presence for users with presence tracking enabled.""" """Track Discord presence for users with presence tracking enabled."""
print(f"[DEBUG] update_presence_tracking() called", flush=True)
try: try:
import core.adaptive_meds as adaptive_meds import core.adaptive_meds as adaptive_meds
import core.postgres as postgres import core.postgres as postgres
print(f"[DEBUG] Running presence tracking. Guilds: {len(client.guilds)}", flush=True)
for guild in client.guilds:
print(f"[DEBUG] Guild: {guild.name} ({guild.id}) - Members: {guild.member_count}")
# Get all users with presence tracking enabled # Get all users with presence tracking enabled
settings = postgres.select( settings = postgres.select(
"adaptive_med_settings", {"presence_tracking_enabled": True} "adaptive_med_settings", {"presence_tracking_enabled": True}
) )
print(f"[DEBUG] Found {len(settings)} users with presence tracking enabled")
for setting in settings: for setting in settings:
user_uuid = setting.get("user_uuid") user_uuid = setting.get("user_uuid")
@@ -600,21 +609,35 @@ async def update_presence_tracking():
continue continue
discord_user_id = notif_settings[0].get("discord_user_id") discord_user_id = notif_settings[0].get("discord_user_id")
print(f"[DEBUG] Looking for Discord user: {discord_user_id}", flush=True)
if not discord_user_id: if not discord_user_id:
print(f"[DEBUG] No Discord ID for user {user_uuid}", flush=True)
continue continue
# Get the user from Discord # Get the member from a shared guild (needed for presence data)
try: try:
discord_user = await client.fetch_user(int(discord_user_id)) member = None
if not discord_user: target_id = int(discord_user_id)
# Search through all guilds the bot is in
for guild in client.guilds:
member = guild.get_member(target_id)
print(f"[DEBUG] Checked guild {guild.name}, member: {member}", flush=True)
if member:
break
if not member:
print(f"[DEBUG] User {discord_user_id} not found in any shared guild", flush=True)
continue continue
# Check if user is online # Check if user is online
is_online = discord_user.status != discord.Status.offline is_online = member.status != discord.Status.offline
print(f"[DEBUG] User status: {member.status}, is_online: {is_online}", flush=True)
# Get current presence from DB # Get current presence from DB
presence = adaptive_meds.get_user_presence(user_uuid) presence = adaptive_meds.get_user_presence(user_uuid)
was_online = presence.get("is_currently_online") if presence else False was_online = presence.get("is_currently_online") if presence else False
print(f"[DEBUG] Previous state: {was_online}, Current: {is_online}", flush=True)
# Update presence if changed # Update presence if changed
if is_online != was_online: if is_online != was_online:
@@ -644,7 +667,12 @@ async def update_presence_tracking():
@tasks.loop(seconds=30) @tasks.loop(seconds=30)
async def presenceTrackingLoop(): async def presenceTrackingLoop():
"""Track Discord presence every 30 seconds.""" """Track Discord presence every 30 seconds."""
await update_presence_tracking() try:
await update_presence_tracking()
except Exception as e:
print(f"[ERROR] presenceTrackingLoop failed: {e}", flush=True)
import traceback
traceback.print_exc()
@presenceTrackingLoop.before_loop @presenceTrackingLoop.before_loop
@@ -711,10 +739,12 @@ async def beforeSnitchCheckLoop():
@client.event @client.event
async def on_ready(): async def on_ready():
print(f"Bot logged in as {client.user}") print(f"Bot logged in as {client.user}", flush=True)
print(f"Connected to {len(client.guilds)} guilds", flush=True)
loadCache() loadCache()
backgroundLoop.start() backgroundLoop.start()
presenceTrackingLoop.start() presenceTrackingLoop.start()
print(f"[DEBUG] Presence tracking loop started", flush=True)
snitchCheckLoop.start() snitchCheckLoop.start()

View File

@@ -1,4 +1,4 @@
DISCORD_BOT_TOKEN=MTQ2NzYwMTc2ODM0NjE2MTE3Mw.G7BKQ-.kivCRj7mOl6aS5VyX4RW9hirqzm7qJ8nJOVMpE DISCORD_BOT_TOKEN=MTQ3MDY0MjgyMDI1MDQ3MjYyMQ.Gczvus.1WuWxd72NDoLFC7BCjAixnMo5eS8wenqTIZ54I
API_URL=http://app:5000 API_URL=http://app:5000
DB_HOST=db DB_HOST=db
DB_PORT=5432 DB_PORT=5432
@@ -6,7 +6,7 @@ DB_NAME=app
DB_USER=app DB_USER=app
DB_PASS=y8Khu7pJQZq6ywFDIJiqpx4zYmclHGHw DB_PASS=y8Khu7pJQZq6ywFDIJiqpx4zYmclHGHw
JWT_SECRET=bf773b4562221bef4d304ae5752a68931382ea3e98fe38394a098f73e0c776e1 JWT_SECRET=bf773b4562221bef4d304ae5752a68931382ea3e98fe38394a098f73e0c776e1
OPENROUTER_API_KEY=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 OPENROUTER_API_KEY=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
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
AI_CONFIG_PATH=/app/ai/ai_config.json AI_CONFIG_PATH=/app/ai/ai_config.json

View File

@@ -127,6 +127,17 @@ export default function SettingsPage() {
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}, []); }, []);
// Poll for presence updates every 10 seconds
useEffect(() => {
if (!notif.discord_enabled || !adaptiveMeds.presence_tracking_enabled) return;
const interval = setInterval(() => {
api.adaptiveMeds.getPresence().then((data: PresenceStatus) => setPresence(data));
}, 10000);
return () => clearInterval(interval);
}, [notif.discord_enabled, adaptiveMeds.presence_tracking_enabled]);
const flashSaved = () => { const flashSaved = () => {
setSaved(true); setSaved(true);
setTimeout(() => setSaved(false), 1500); setTimeout(() => setSaved(false), 1500);
@@ -427,7 +438,14 @@ export default function SettingsPage() {
<p className="text-sm text-gray-500 dark:text-gray-400">Adjust medication times based on your wake time</p> <p className="text-sm text-gray-500 dark:text-gray-400">Adjust medication times based on your wake time</p>
</div> </div>
<button <button
onClick={() => updateAdaptiveMeds({ adaptive_timing_enabled: !adaptiveMeds.adaptive_timing_enabled })} onClick={() => {
const newEnabled = !adaptiveMeds.adaptive_timing_enabled;
const updates: Partial<AdaptiveMedSettings> = { adaptive_timing_enabled: newEnabled };
if (newEnabled) {
updates.adaptive_mode = adaptiveMeds.adaptive_mode;
}
updateAdaptiveMeds(updates);
}}
className={`w-12 h-7 rounded-full transition-colors ${ className={`w-12 h-7 rounded-full transition-colors ${
adaptiveMeds.adaptive_timing_enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600' adaptiveMeds.adaptive_timing_enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`} }`}
@@ -520,14 +538,24 @@ export default function SettingsPage() {
</p> </p>
)} )}
{adaptiveMeds.presence_tracking_enabled && presence.typical_wake_time && ( {notif.discord_enabled && (
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-400"> <div className="flex items-center justify-between">
Typical wake time: <span className="font-medium text-gray-900 dark:text-gray-100">{presence.typical_wake_time}</span> <div className="flex items-center gap-2">
</p> <div className={`w-2 h-2 rounded-full ${presence.is_online ? 'bg-green-500' : 'bg-gray-400'}`} />
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
Status: {presence.is_online ? '🟢 Online' : 'Offline'} {presence.is_online ? 'Online' : 'Offline'}
</p> </span>
</div>
<span className="text-xs text-gray-500 dark:text-gray-400">
{presence.last_online_at ? `Last seen: ${new Date(presence.last_online_at).toLocaleString()}` : 'Never seen online'}
</span>
</div>
{presence.typical_wake_time && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Typical wake time: <span className="font-medium text-gray-700 dark:text-gray-300">{presence.typical_wake_time}</span>
</p>
)}
</div> </div>
)} )}
</div> </div>