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.message_content = True
intents.presences = True
intents.members = True
client = discord.Client(intents=intents)
@@ -582,15 +584,22 @@ async def beforeBackgroundLoop():
async def update_presence_tracking():
"""Track Discord presence for users with presence tracking enabled."""
print(f"[DEBUG] update_presence_tracking() called", flush=True)
try:
import core.adaptive_meds as adaptive_meds
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
settings = postgres.select(
"adaptive_med_settings", {"presence_tracking_enabled": True}
)
print(f"[DEBUG] Found {len(settings)} users with presence tracking enabled")
for setting in settings:
user_uuid = setting.get("user_uuid")
@@ -600,21 +609,35 @@ async def update_presence_tracking():
continue
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:
print(f"[DEBUG] No Discord ID for user {user_uuid}", flush=True)
continue
# Get the user from Discord
# Get the member from a shared guild (needed for presence data)
try:
discord_user = await client.fetch_user(int(discord_user_id))
if not discord_user:
member = None
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
# 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
presence = adaptive_meds.get_user_presence(user_uuid)
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
if is_online != was_online:
@@ -644,7 +667,12 @@ async def update_presence_tracking():
@tasks.loop(seconds=30)
async def presenceTrackingLoop():
"""Track Discord presence every 30 seconds."""
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
@@ -711,10 +739,12 @@ async def beforeSnitchCheckLoop():
@client.event
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()
backgroundLoop.start()
presenceTrackingLoop.start()
print(f"[DEBUG] Presence tracking loop started", flush=True)
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
DB_HOST=db
DB_PORT=5432
@@ -6,7 +6,7 @@ DB_NAME=app
DB_USER=app
DB_PASS=y8Khu7pJQZq6ywFDIJiqpx4zYmclHGHw
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
AI_CONFIG_PATH=/app/ai/ai_config.json

View File

@@ -127,6 +127,17 @@ export default function SettingsPage() {
.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 = () => {
setSaved(true);
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>
</div>
<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 ${
adaptiveMeds.adaptive_timing_enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
@@ -520,14 +538,24 @@ export default function SettingsPage() {
</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">
<p className="text-sm text-gray-600 dark:text-gray-400">
Typical wake time: <span className="font-medium text-gray-900 dark:text-gray-100">{presence.typical_wake_time}</span>
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Status: {presence.is_online ? '🟢 Online' : 'Offline'}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${presence.is_online ? 'bg-green-500' : 'bg-gray-400'}`} />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{presence.is_online ? 'Online' : 'Offline'}
</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>