Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.dbaagent.controller;

import com.dbaagent.dto.DigestPreferenceResponse;
import com.dbaagent.model.DatabaseConnection;
import com.dbaagent.model.DigestDeliveryMethod;
import com.dbaagent.model.PersonaTag;
import com.dbaagent.model.UserDigestPreference;
import com.dbaagent.repository.CredentialRepository;
import com.dbaagent.service.DigestPreferenceSeedService;
import com.dbaagent.service.security.AccessControlService;
import com.dbaagent.service.UserDigestPreferenceService;
Expand All @@ -13,8 +16,12 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
* REST controller for managing per-user digest preferences.
Expand All @@ -32,21 +39,23 @@ public class DigestPreferenceController {
private final AccessControlService accessControlService;
private final DigestPreferenceSeedService seedService;
private final SlackDailyDigestService digestService;
private final CredentialRepository credentialRepository;

/**
* Get the current user's digest preferences.
* Get the current user's digest preferences (includes connection display names).
*/
@GetMapping
public ResponseEntity<List<UserDigestPreference>> getMyPreferences() {
public ResponseEntity<List<DigestPreferenceResponse>> getMyPreferences() {
String username = accessControlService.requireCurrentUsername();
return ResponseEntity.ok(preferenceService.getPreferencesForUser(username));
List<UserDigestPreference> prefs = preferenceService.getPreferencesForUser(username);
return ResponseEntity.ok(toResponses(prefs));
}

/**
* Create a new digest preference for the current user.
*/
@PostMapping
public ResponseEntity<UserDigestPreference> createPreference(@RequestBody CreatePreferenceRequest request) {
public ResponseEntity<DigestPreferenceResponse> createPreference(@RequestBody CreatePreferenceRequest request) {
String username = accessControlService.requireCurrentUsername();

DigestDeliveryMethod method;
Expand Down Expand Up @@ -80,14 +89,14 @@ public ResponseEntity<UserDigestPreference> createPreference(@RequestBody Create
request.timezone
);

return ResponseEntity.ok(preference);
return ResponseEntity.ok(toResponse(preference));
}

/**
* Update an existing preference.
*/
@PutMapping("/{id}")
public ResponseEntity<UserDigestPreference> updatePreference(
public ResponseEntity<DigestPreferenceResponse> updatePreference(
@PathVariable Long id,
@RequestBody UpdatePreferenceRequest request) {

Expand All @@ -112,14 +121,14 @@ public ResponseEntity<UserDigestPreference> updatePreference(
request.timezone
);

return ResponseEntity.ok(updated);
return ResponseEntity.ok(toResponse(updated));
}

/**
* Enable or disable a preference.
*/
@PatchMapping("/{id}/enabled")
public ResponseEntity<UserDigestPreference> setEnabled(
public ResponseEntity<DigestPreferenceResponse> setEnabled(
@PathVariable Long id,
@RequestBody Map<String, Boolean> body) {

Expand All @@ -138,7 +147,7 @@ public ResponseEntity<UserDigestPreference> setEnabled(
}

UserDigestPreference updated = preferenceService.setEnabled(id, enabled);
return ResponseEntity.ok(updated);
return ResponseEntity.ok(toResponse(updated));
}

/**
Expand Down Expand Up @@ -286,4 +295,58 @@ public record DigestStatusResponse(boolean perUserMode, long enabledPreferences,
public ResponseEntity<Map<String, String>> handleBadRequest(IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage() != null ? e.getMessage() : "Bad request"));
}

// ─────────────────────────────────────────────────────────────────────────
// Response mapping (connection display names)
// ─────────────────────────────────────────────────────────────────────────

private List<DigestPreferenceResponse> toResponses(List<UserDigestPreference> prefs) {
Map<String, String> namesById = resolveConnectionNames(prefs);
return prefs.stream()
.map(pref -> toResponse(pref, namesById))
.toList();
}

private DigestPreferenceResponse toResponse(UserDigestPreference pref) {
Map<String, String> namesById = resolveConnectionNames(List.of(pref));
return toResponse(pref, namesById);
}

private DigestPreferenceResponse toResponse(UserDigestPreference pref, Map<String, String> namesById) {
String connectionName = null;
if (pref.getConnectionId() != null) {
connectionName = namesById.get(pref.getConnectionId());
}
return DigestPreferenceResponse.builder()
.id(pref.getId())
.username(pref.getUsername())
.connectionId(pref.getConnectionId())
.connectionName(connectionName)
.enabled(pref.isEnabled())
.personaTag(pref.getPersonaTag() != null ? pref.getPersonaTag().name() : null)
.cronExpression(pref.getCronExpression())
.deliveryMethod(pref.getDeliveryMethod() != null ? pref.getDeliveryMethod().name() : null)
.timezone(pref.getTimezone())
.createdAt(pref.getCreatedAt())
.updatedAt(pref.getUpdatedAt())
.build();
}

private Map<String, String> resolveConnectionNames(List<UserDigestPreference> prefs) {
Set<String> ids = prefs.stream()
.map(UserDigestPreference::getConnectionId)
.filter(Objects::nonNull)
.filter(id -> !id.isBlank())
.collect(Collectors.toSet());
if (ids.isEmpty()) {
return Map.of();
}
Map<String, String> names = new HashMap<>();
for (DatabaseConnection conn : credentialRepository.findAllById(ids)) {
if (conn.getId() != null && conn.getConnectionName() != null) {
names.put(conn.getId(), conn.getConnectionName());
}
}
return names;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.dbaagent.dto;

import lombok.Builder;
import lombok.Value;

import java.time.LocalDateTime;

/**
* API view of a {@code UserDigestPreference} with a human-readable connection name.
*/
@Value
@Builder
public class DigestPreferenceResponse {
Long id;
String username;
String connectionId;
/** Display name for {@link #connectionId}; null when unknown or preference is connection-wide. */
String connectionName;
boolean enabled;
String personaTag;
String cronExpression;
String deliveryMethod;
String timezone;
LocalDateTime createdAt;
LocalDateTime updatedAt;
}
13 changes: 7 additions & 6 deletions docs/DIGEST_PREFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Note: Do not claim EMAIL or WhatsApp delivery is available. These are planned fo

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/digest/preferences` | GET | Get current user's preferences |
| `/api/digest/preferences` | GET | Get current user's preferences (includes `connectionName`) |
| `/api/digest/preferences` | POST | Create a new preference |
| `/api/digest/preferences/{id}` | PUT | Update a preference |
| `/api/digest/preferences/{id}/enabled` | PATCH | Enable/disable |
Expand Down Expand Up @@ -134,16 +134,17 @@ To migrate from legacy singleton mode to per-user mode:

The digest preferences are accessible from:

1. **Digest Section** (sidebar): Click the bell icon (🔔) to open preferences panel
2. **Preferences Panel**: Create, edit, enable/disable, delete preferences
1. **Digest Section** (sidebar): Click the **gear** (⚙) to open the preferences panel — single entry point (no competing bell)
2. **First-run**: Opening Digests with zero preferences (or none enabled) auto-opens the panel once (`localStorage` flag)
3. **Preferences Panel**: Create, edit, enable/disable, delete preferences

### Preferences Panel Features

- View all your digest subscriptions
- Pref cards show **connection display names** (API `connectionName`, with client-side fallback from the connections list; UUID only if name is missing)
- Toggle digests on/off per connection
- Change persona without recreating
- Change persona and schedule inline (compact controls — no duplicate meta labels)
- Quick schedule presets (8 AM, 9 AM, Noon, etc.) — stored on the preference and honored by the minute-tick scheduler
- Seed for all your connections at once
- Seed for all your connections at once (`POST /api/digest/preferences/seed/me`)

## Database Schema

Expand Down
Loading
Loading