feat: Erinnerung zum Sichern von Story-Insights vor Ablauf
Ausgangspunkt: Instagram hält Story-Insights nach eigener Aussage nur
24 Stunden vor, auch der offizielle Datenexport enthält sie nicht mehr
danach. Der Standbild-Screenshot beim Prüfen entsteht direkt beim
Veröffentlichen, bevor nennenswerte Kennzahlen existieren — er kann
das strukturell nicht auffangen. Eine OAuth-Anbindung allein löst das
auch nicht: selbst mit API-Zugriff bräuchte es einen Abruf innerhalb
desselben 24h-Fensters.
- Migration 0007: asset.purpose ('initial' | 'insights', Default
'initial' erhält die Bedeutung aller Bestandszeilen). Ein Beitrag
kann jetzt mehrere Insights-Nachweise über die Zeit bekommen.
GetLatestAssetForSubmission berücksichtigt weiterhin nur 'initial',
damit ein späterer Insights-Upload nie den beim Archivieren
referenzierten Original-Screenshot verdrängt.
- internal/web/insights_reminder.go: computeInsightsReminder — reine,
ungetestete gegen echte Instagram-Daten, aber isoliert testbare
Logik fürs Erinnerungs-Timing (Produktentscheidung, keine Rechtsnorm,
daher nicht in rules/*.yaml).
- GET /beitraege/{id} zeigt die Erinnerung bei veröffentlichten
"story"-Beiträgen ohne existierendes insights-Asset; POST
/beitraege/{id}/insights speichert einen weiteren Screenshot (gleiche
Validierung wie das initiale Standbild, wiederverwendet über
readUploadedAsset/storeAsset mit purpose-Parameter).
- Bewusst nur In-App-Banner in dieser Ausbaustufe, kein Mail-/Push-
Versand — dafür fehlt aktuell ein SMTP-Relay/Versanddienst, siehe
CLAUDE.md-Hinweis dazu.
Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End gegen
einen laufenden Server verifiziert (Story archivieren → Erinnerung
sichtbar → Insights-Upload → Erinnerung verschwindet, Nachweis
gelistet, Mandantentrennung beim Upload durchgesetzt).
This commit is contained in:
@@ -69,7 +69,7 @@ type fakeStore struct {
|
||||
evidencePkgs map[string]store.EvidencePackage
|
||||
participants map[string]store.Participant
|
||||
auditLog []store.AuditEntry
|
||||
assets map[string]store.Asset // submissionID -> zuletzt hochgeladenes Asset
|
||||
assets map[string][]store.Asset // submissionID -> alle Assets, aeltestes zuerst
|
||||
|
||||
// platformConnections ist verschachtelt nach accountID -> platform,
|
||||
// wie die UNIQUE(account_id, platform)-Beschränkung der echten Tabelle.
|
||||
@@ -87,7 +87,7 @@ func newFakeStore() *fakeStore {
|
||||
findings: map[string][]store.Finding{},
|
||||
evidencePkgs: map[string]store.EvidencePackage{},
|
||||
participants: map[string]store.Participant{},
|
||||
assets: map[string]store.Asset{},
|
||||
assets: map[string][]store.Asset{},
|
||||
platformConnections: map[string]map[string]store.PlatformConnection{},
|
||||
}
|
||||
}
|
||||
@@ -350,24 +350,40 @@ func (f *fakeStore) GetLatestEvidencePackage(ctx context.Context, submissionID s
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) CreateAsset(ctx context.Context, submissionID, kind, path, sha256Hex string) (store.Asset, error) {
|
||||
func (f *fakeStore) CreateAsset(ctx context.Context, submissionID, kind, purpose, path, sha256Hex string) (store.Asset, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
a := store.Asset{
|
||||
ID: f.newID(), SubmissionID: submissionID, Kind: kind, Path: path, SHA256: sha256Hex, CreatedAt: time.Now(),
|
||||
ID: f.newID(), SubmissionID: submissionID, Kind: kind, Purpose: purpose,
|
||||
Path: path, SHA256: sha256Hex, CreatedAt: time.Now(),
|
||||
}
|
||||
f.assets[submissionID] = a
|
||||
f.assets[submissionID] = append(f.assets[submissionID], a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// GetLatestAssetForSubmission liefert wie die echte Store-Implementierung
|
||||
// nur das zuletzt hochgeladene Asset mit purpose="initial".
|
||||
func (f *fakeStore) GetLatestAssetForSubmission(ctx context.Context, submissionID string) (store.Asset, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
a, ok := f.assets[submissionID]
|
||||
if !ok {
|
||||
var latest store.Asset
|
||||
found := false
|
||||
for _, a := range f.assets[submissionID] {
|
||||
if a.Purpose == "initial" {
|
||||
latest = a
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return store.Asset{}, store.ErrNotFound
|
||||
}
|
||||
return a, nil
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) ListAssetsForSubmission(ctx context.Context, submissionID string) ([]store.Asset, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.assets[submissionID], nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) UpsertPlatformConnection(ctx context.Context, accountID, platform, platformUserID, accessToken, refreshToken string, expiresAt *time.Time) (store.PlatformConnection, error) {
|
||||
|
||||
Reference in New Issue
Block a user