Files
Gardomatic/cmd/cli/demo_store.go
T
2026-09-14 19:58:31 +02:00

617 lines
42 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"crypto/sha256"
"database/sql"
_ "embed"
"encoding/hex"
"fmt"
"time"
)
const demoGardenName = "Sonnengarten"
//go:embed demo_assets/sonnengarten.jpg
var demoGardenImage []byte
//go:embed demo_assets/tomaten-gewaechshaus.jpg
var demoTomatoImage []byte
//go:embed demo_assets/apfelbaum-obstwiese.jpg
var demoAppleImage []byte
// ResetDemo replaces all user-created data with a coherent demo data set. The
// caller is responsible for guarding this destructive operation.
func (s *postgresStore) ResetDemo(ctx context.Context, input demoResetInput) (demoResetResult, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return demoResetResult{}, err
}
defer tx.Rollback()
// Serialize resets and keep the old demo usable until the complete new data
// set can be committed.
if _, err = tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(1279348146)`); err != nil {
return demoResetResult{}, fmt.Errorf("lock demo reset: %w", err)
}
if _, err = tx.ExecContext(ctx, `
DELETE FROM sessions;
DELETE FROM journal_attachments;
DELETE FROM gardens;
DELETE FROM species;
DELETE FROM tags;
DELETE FROM users;
DELETE FROM roles WHERE system = false;
DELETE FROM species_categories;
UPDATE application_settings
SET lifecycle_status_enabled = true,
lifecycle_removal_month = 12,
lifecycle_removal_day = 1,
timezone = 'Europe/Berlin',
updated_at = CURRENT_TIMESTAMP,
version = version + 1;
INSERT INTO species_categories (name, sort_order, lifecycle) VALUES
('Gehölz', 10, 'perennial'),
('Gemüse', 20, 'annual'),
('Kraut', 30, 'annual'),
('Obst', 40, 'perennial'),
('Staude', 50, 'perennial'),
('Blume', 60, 'annual')`); err != nil {
return demoResetResult{}, fmt.Errorf("clear demo data: %w", err)
}
var demoUserID, memberID, workerID, gardenID int64
if err = tx.QueryRowContext(ctx, `
INSERT INTO users (name, email, password_hash, activated, application_role, color)
VALUES ($1, $2, $3, true, 'application:user', '#1b9e77') RETURNING id`,
input.Name, input.Email, input.PasswordHash).Scan(&demoUserID); err != nil {
return demoResetResult{}, fmt.Errorf("create demo account: %w", err)
}
if err = tx.QueryRowContext(ctx, `
INSERT INTO users (name, email, password_hash, activated, application_role, color)
VALUES ('Mara Beispiel', 'mara@example.com', $1, true, 'application:user', '#7570b3') RETURNING id`,
input.PasswordHash).Scan(&memberID); err != nil {
return demoResetResult{}, fmt.Errorf("create example member: %w", err)
}
if err = tx.QueryRowContext(ctx, `
INSERT INTO users (name, email, password_hash, activated, application_role, color)
VALUES ('Timo Gartenfreund', 'timo@example.com', $1, true, 'application:user', '#d95f02') RETURNING id`,
input.PasswordHash).Scan(&workerID); err != nil {
return demoResetResult{}, fmt.Errorf("create example worker: %w", err)
}
if err = tx.QueryRowContext(ctx, `
INSERT INTO gardens (name, description)
VALUES ($1, 'Ein lebendiger Gemeinschaftsgarten mit Gemüsebeeten, Gewächshaus und kleiner Obstwiese.')
RETURNING id`, demoGardenName).Scan(&gardenID); err != nil {
return demoResetResult{}, fmt.Errorf("create demo garden: %w", err)
}
customRole := fmt.Sprintf("erntehilfe-%d", gardenID)
if _, err = tx.ExecContext(ctx, `
INSERT INTO roles (name, scope, label, garden_id) VALUES ($1, 'garden', 'Erntehilfe', $2)`, customRole, gardenID); err != nil {
return demoResetResult{}, fmt.Errorf("create demo garden role: %w", err)
}
if _, err = tx.ExecContext(ctx, `
INSERT INTO role_permissions (role_name, permission) VALUES
($1, 'garden:read'), ($1, 'tasks:read:own'), ($1, 'tasks:read:other'),
($1, 'tasks:complete:own'), ($1, 'tasks:complete:other')`, customRole); err != nil {
return demoResetResult{}, fmt.Errorf("create demo garden role permissions: %w", err)
}
if _, err = tx.ExecContext(ctx, `
INSERT INTO garden_members (garden_id, user_id, role) VALUES
($1, $2, 'owner'),
($1, $3, 'member'),
($1, $4, $5)`, gardenID, demoUserID, memberID, workerID, customRole); err != nil {
return demoResetResult{}, fmt.Errorf("create demo memberships: %w", err)
}
if _, err = tx.ExecContext(ctx, `
INSERT INTO garden_role_permission_overrides (garden_id, role_name, permission, granted) VALUES
($1, 'member', 'plants:delete:own', false),
($1, 'viewer', 'tasks:complete:other', true)`, gardenID); err != nil {
return demoResetResult{}, fmt.Errorf("create demo permission overrides: %w", err)
}
if _, err = tx.ExecContext(ctx, `
INSERT INTO garden_invites (garden_id, email, role, token_hash, invited_by, expires_at)
VALUES ($1, 'lea-beispiel@example.com', 'viewer', decode('b462f5f8befe77ecf5c611fd7e2f72138fefd4fa5b41f2f591cc8331187e94f7', 'hex'), $2, CURRENT_TIMESTAMP + interval '5 days')`, gardenID, demoUserID); err != nil {
return demoResetResult{}, fmt.Errorf("create demo invitation: %w", err)
}
imageIDs, err := seedDemoImages(ctx, tx, gardenID, demoUserID)
if err != nil {
return demoResetResult{}, err
}
if _, err = tx.ExecContext(ctx, `UPDATE gardens SET image_id = $2 WHERE id = $1`, gardenID, imageIDs["garden"]); err != nil {
return demoResetResult{}, fmt.Errorf("assign demo garden image: %w", err)
}
locationIDs, err := seedDemoLocations(ctx, tx, gardenID, demoUserID, imageIDs)
if err != nil {
return demoResetResult{}, err
}
speciesIDs, templateIDs, err := seedDemoSpecies(ctx, tx, gardenID, demoUserID, imageIDs)
if err != nil {
return demoResetResult{}, err
}
plantIDs, err := seedDemoPlants(ctx, tx, gardenID, demoUserID, memberID, locationIDs, speciesIDs, templateIDs, imageIDs)
if err != nil {
return demoResetResult{}, err
}
taskCount, err := seedDemoTasks(ctx, tx, gardenID, demoUserID, memberID, locationIDs, plantIDs, templateIDs)
if err != nil {
return demoResetResult{}, err
}
journalCount, attachmentCount, err := seedDemoJournal(ctx, tx, gardenID, demoUserID, memberID, imageIDs)
if err != nil {
return demoResetResult{}, err
}
if err = tx.Commit(); err != nil {
return demoResetResult{}, fmt.Errorf("commit demo reset: %w", err)
}
return demoResetResult{
Email: input.Email,
Garden: demoGardenName,
Images: len(imageIDs),
Locations: len(locationIDs),
Species: len(speciesIDs),
Plants: len(plantIDs),
Tasks: taskCount,
Journal: journalCount,
Attachments: attachmentCount,
}, nil
}
func seedDemoImages(ctx context.Context, tx *sql.Tx, gardenID, userID int64) (map[string]int64, error) {
images := []struct {
key, fileName, source string
data []byte
}{
{"garden", "sonnengarten-uebersicht.jpg", "demo", demoGardenImage},
{"tomato", "tomaten-im-gewaechshaus.jpg", "demo", demoTomatoImage},
{"apple", "apfelbaum-auf-der-obstwiese.jpg", "demo", demoAppleImage},
}
ids := make(map[string]int64, len(images))
for _, image := range images {
sum := sha256.Sum256(image.data)
var id int64
if err := tx.QueryRowContext(ctx, `
INSERT INTO images (garden_id, file_name, media_type, data, size, checksum, source, created_by)
VALUES ($1, $2, 'image/jpeg', $3, $4, $5, $6, $7) RETURNING id`,
gardenID, image.fileName, image.data, len(image.data), hex.EncodeToString(sum[:]), image.source, userID).Scan(&id); err != nil {
return nil, fmt.Errorf("create demo image %q: %w", image.fileName, err)
}
ids[image.key] = id
}
return ids, nil
}
func seedDemoLocations(ctx context.Context, tx *sql.Tx, gardenID, userID int64, images map[string]int64) (map[string]int64, error) {
ids := make(map[string]int64)
rows := []struct {
key, name, description, kind, parent string
area float64
sun, soil, reaction, attributes, imageKey string
}{
{"vegetable", "Gemüsegarten", "Der sonnige Bereich für Frucht-, Wurzel- und Blattgemüse. Eine Tropfleitung versorgt die Hauptreihen.", "Bereich", "", 42, "sunny", "moist", "neutral", `{"bewässerung":"Tropfschlauch","mulch":"Rasenschnitt","bodenprobe":"2026-03-18"}`, ""},
{"raised", "Hochbeet Süd", "Kompostreiche Erde, morgens zuerst kontrollieren. Die Beetkante wurde im Frühjahr erneuert.", "Hochbeet", "vegetable", 6.5, "sunny", "moist", "neutral", `{"baujahr":2024,"material":"Lärche","füllung":"Kompost und Gartenerde"}`, ""},
{"north_bed", "Beet Nord", "Halbschattiges Beet für Salate, Mangold und empfindliche Jungpflanzen.", "Beet", "vegetable", 9.75, "partial_shade", "moist", "neutral", `{"reihen":4,"fruchtfolge":"Blattgemüse","netz_vorhanden":true}`, ""},
{"greenhouse", "Gewächshaus", "Geschützt für Tomaten und wärmeliebende Kulturen. Automatische Fensterheber lüften ab 24 °C.", "Gewächshaus", "", 12, "sunny", "moist", "alkaline", `{"temperatur_sensor":"GH-01","bewässerung":"Tropfer","fensterheber":true}`, "tomato"},
{"herbs", "Kräuterecke", "Trockener Standort an der Natursteinmauer mit durchlässigem, kalkhaltigem Substrat.", "Beet", "", 5, "sunny", "dry", "alkaline", `{"substrat":"sandig-kiesig","trockenmauer":true}`, ""},
{"orchard", "Obstwiese", "Junge Obstgehölze mit insektenfreundlichem Unterwuchs und gestaffelter Mahd.", "Wiese", "", 85, "partial_shade", "moist", "neutral", `{"mahd":"zweimal jährlich","nistkästen":2,"bewässerung":"Gießringe"}`, "apple"},
{"compost", "Kompostplatz", "Drei Kammern für Frischmaterial, Rotte und reifen Kompost.", "Arbeitsbereich", "", 7.2, "partial_shade", "moist", "neutral", `{"kammern":3,"letztes_umsetzen":"2026-05-02","thermometer":true}`, ""},
{"pond", "Miniteich", "Flacher Naturteich mit Ausstiegshilfen für Insekten und Kleintiere.", "Wasserstelle", "", 3.4, "sunny", "boggy", "acidic", `{"tiefe_cm":55,"regenwasser":true,"fischfrei":true}`, ""},
}
for _, row := range rows {
var parentID any
if row.parent != "" {
parentID = ids[row.parent]
}
var imageID any
if row.imageKey != "" {
imageID = images[row.imageKey]
}
var id int64
err := tx.QueryRowContext(ctx, `
INSERT INTO locations (garden_id, parent_id, name, description, kind, area_sqm, sun_exposure, soil_condition, soil_reaction, attributes, image_id, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6, $7::sun_exposure, $8::soil_condition, $9::soil_reaction, $10::jsonb, $11, $12, $12)
RETURNING id`, gardenID, parentID, row.name, row.description, row.kind, row.area, row.sun, row.soil, row.reaction, row.attributes, imageID, userID).Scan(&id)
if err != nil {
return nil, fmt.Errorf("create demo location %q: %w", row.name, err)
}
ids[row.key] = id
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO entity_image_history (garden_id, entity_type, entity_id, image_id, changed_by) VALUES
($1, 'garden', $1, $2, $4),
($1, 'location', $5, $3, $4),
($1, 'location', $6, $7, $4)`, gardenID, images["garden"], images["tomato"], userID, ids["greenhouse"], ids["orchard"], images["apple"]); err != nil {
return nil, fmt.Errorf("record demo location images: %w", err)
}
return ids, nil
}
func seedDemoSpecies(ctx context.Context, tx *sql.Tx, gardenID, userID int64, images map[string]int64) (map[string]int64, map[string]int64, error) {
ids := make(map[string]int64)
type speciesSeed struct {
key, category, commonName, cultivar, botanicalName, notes string
spacing, height int
sowFrom, sowTo, plantFrom, plantTo, harvestFrom, harvestTo int
}
rows := []speciesSeed{
{"tomato", "Gemüse", "Tomate", "San Marzano", "Solanum lycopersicum", "Robuste Flaschentomate für Sauce und Salat.", 60, 180, 2, 4, 5, 6, 7, 10},
{"zucchini", "Gemüse", "Zucchini", "Cocozelle von Tripolis", "Cucurbita pepo", "Regelmäßig jung ernten, dann trägt die Pflanze lange.", 100, 70, 4, 5, 5, 6, 6, 10},
{"basil", "Kraut", "Basilikum", "Genoveser", "Ocimum basilicum", "Blüten ausknipsen und nur von oben ernten.", 25, 40, 3, 6, 5, 7, 6, 9},
{"strawberry", "Obst", "Erdbeere", "Mieze Schindler", "Fragaria × ananassa", "Aromatische Sorte; eine Befruchtersorte steht in der Nachbarparzelle.", 30, 25, 0, 0, 3, 8, 5, 7},
{"lavender", "Staude", "Lavendel", "Hidcote Blue", "Lavandula angustifolia", "Nach der Blüte leicht zurückschneiden.", 40, 55, 0, 0, 3, 6, 7, 8},
{"apple", "Gehölz", "Apfel", "Topaz", "Malus domestica", "Junger Halbstamm, im Sommer auf ausreichende Wasserversorgung achten.", 400, 350, 0, 0, 10, 3, 9, 10},
{"lettuce", "Gemüse", "Romanasalat", "Forellenschluss", "Lactuca sativa var. longifolia", "Historischer, rot gesprenkelter Romanasalat für den Satzanbau.", 28, 35, 2, 8, 3, 9, 5, 10},
{"carrot", "Gemüse", "Möhre", "Nantaise 2", "Daucus carota subsp. sativus", "Gleichmäßige, stumpfe Wurzeln; das Saatbeet bis zur Keimung feucht halten.", 5, 25, 3, 7, 0, 0, 6, 11},
{"chard", "Gemüse", "Mangold", "Bright Lights", "Beta vulgaris subsp. vulgaris", "Bunter Stielmangold für die fortlaufende Ernte der äußeren Blätter.", 35, 60, 3, 6, 4, 7, 6, 11},
{"rosemary", "Kraut", "Rosmarin", "Arp", "Salvia rosmarinus", "Relativ winterharte Sorte für den durchlässigen Platz an der warmen Mauer.", 70, 120, 3, 5, 4, 6, 4, 10},
{"currant", "Obst", "Rote Johannisbeere", "Rovada", "Ribes rubrum", "Späte Sorte mit langen Trauben, als dreitriebiger Strauch erzogen.", 140, 160, 0, 0, 10, 3, 7, 8},
{"sunflower", "Blume", "Sonnenblume", "Velvet Queen", "Helianthus annuus", "Dunkelrote, verzweigte Sonnenblume für Bestäuber und Schnittblumen.", 45, 180, 4, 5, 5, 6, 8, 10},
}
for _, row := range rows {
var id int64
err := tx.QueryRowContext(ctx, `
INSERT INTO species (
garden_id, category_id, common_name, cultivar, botanical_name,
sun_exposure, soil_condition, soil_reaction, spacing_cm, height_cm,
sow_month_from, sow_month_to, planting_month_from, planting_month_to,
harvest_month_from, harvest_month_to, notes, created_by, updated_by)
SELECT $1, id, $2, $3, $4, 'sunny', 'moist', 'neutral', $5, $6,
NULLIF($7, 0), NULLIF($8, 0), NULLIF($9, 0), NULLIF($10, 0),
NULLIF($11, 0), NULLIF($12, 0), $13, $14, $14
FROM species_categories WHERE name = $15
RETURNING id`, gardenID, row.commonName, row.cultivar, row.botanicalName,
row.spacing, row.height, row.sowFrom, row.sowTo, row.plantFrom, row.plantTo,
row.harvestFrom, row.harvestTo, row.notes, userID, row.category).Scan(&id)
if err != nil {
return nil, nil, fmt.Errorf("create demo species %q: %w", row.commonName, err)
}
ids[row.key] = id
}
type speciesDetails struct {
key, sun, soil, reaction, winter, attributes, imageKey string
sowFromDay, sowToDay, plantFromDay, plantToDay int
harvestFromDay, harvestToDay int
}
details := []speciesDetails{
{"tomato", "sunny", "moist", "neutral", "Frostfrei kultivieren; erst nach den Eisheiligen auspflanzen.", `{"saatgut":"bio","charge":"SM-2026-02","keimtemperatur_c":24,"partnerpflanzen":["Basilikum","Tagetes"]}`, "tomato", 15, 15, 15, 15, 1, 15},
{"zucchini", "sunny", "moist", "neutral", "Bei kalten Nächten mit Vlies schützen.", `{"saatgut":"samenfest","farbe":"grün gestreift","starkzehrer":true}`, "", 1, 15, 15, 15, 20, 10},
{"basil", "sunny", "moist", "neutral", "Unter 10 °C ins Haus oder Gewächshaus holen.", `{"verwendung":["Pesto","Tomatensalat"],"duft":"würzig","einjährig":true}`, "", 1, 15, 15, 15, 1, 30},
{"strawberry", "sunny", "moist", "acidic", "Im Winter mit Laub mulchen; Herzblätter frei lassen.", `{"geschmack":"walderdbeerartig","befruchter_benoetigt":true,"standjahr":2}`, "", 0, 0, 1, 31, 20, 10},
{"lavender", "sunny", "dry", "alkaline", "Vor Winternässe schützen; keine dichte Laubabdeckung.", `{"bluetenfarbe":"dunkelblau","insektenwert":"hoch","duft":true}`, "", 1, 31, 15, 30, 1, 31},
{"apple", "sunny", "moist", "neutral", "Stamm in den ersten Jahren mit Weißanstrich vor Frostrissen schützen.", `{"unterlage":"M7","wuchsform":"Halbstamm","pflanzjahr":2024,"lagerfaehig":true}`, "apple", 0, 0, 1, 31, 20, 31},
{"lettuce", "partial_shade", "moist", "neutral", "Bei starkem Frost mit Vlies abdecken.", `{"satzanbau":true,"schossfest":"mittel","ernte":"Blatt oder Kopf"}`, "", 15, 15, 15, 15, 1, 15},
{"carrot", "sunny", "moist", "neutral", "Späte Sätze mit Stroh abdecken oder frostfrei einlagern.", `{"kulturzeit_tage":105,"saatband":false,"lagerung":"kühl und sandig"}`, "", 1, 15, 0, 0, 15, 15},
{"chard", "sunny", "moist", "neutral", "Wurzelstock anhäufeln und mit Reisig schützen.", `{"stiele":["gelb","orange","rot","rosa"],"mehrfachernte":true}`, "", 15, 30, 15, 15, 1, 30},
{"rosemary", "sunny", "dry", "alkaline", "Topf mit Jute umwickeln und bei Kahlfrost mit Vlies beschatten.", `{"winterhaerte_c":-18,"immergruen":true,"verwendung":"Küche und Duft"}`, "", 1, 31, 15, 30, 1, 31},
{"currant", "partial_shade", "moist", "neutral", "Winterhart; Wurzelbereich im Herbst mit Kompost mulchen.", `{"erziehungsform":"Strauch","triebe":3,"netzeinsatz":true}`, "", 0, 0, 1, 31, 1, 15},
{"sunflower", "sunny", "moist", "neutral", "Jungpflanzen vor Spätfrost schützen.", `{"bluetenfarbe":"dunkelrot","verzweigt":true,"insektenwert":"hoch"}`, "", 1, 31, 15, 15, 1, 15},
}
for _, detail := range details {
var imageID any
if detail.imageKey != "" {
imageID = images[detail.imageKey]
}
if _, err := tx.ExecContext(ctx, `
UPDATE species SET sun_exposure=$2, soil_condition=$3, soil_reaction=$4,
winter_protection=$5, attributes=$6::jsonb, image_id=$7,
sow_day_from=NULLIF($8,0), sow_day_to=NULLIF($9,0),
planting_day_from=NULLIF($10,0), planting_day_to=NULLIF($11,0),
harvest_day_from=NULLIF($12,0), harvest_day_to=NULLIF($13,0)
WHERE id=$1`, ids[detail.key], detail.sun, detail.soil, detail.reaction, detail.winter,
detail.attributes, imageID, detail.sowFromDay, detail.sowToDay, detail.plantFromDay,
detail.plantToDay, detail.harvestFromDay, detail.harvestToDay); err != nil {
return nil, nil, fmt.Errorf("complete demo species %q: %w", detail.key, err)
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO care_instructions (species_id, text, status, created_by, updated_by) VALUES
($1, 'Morgens bodennah gießen und Blätter trocken halten.', 'good', $7, $7),
($1, 'Seitentriebe regelmäßig ausgeizen und locker anbinden.', 'testing', $7, $7),
($2, 'Früchte ab etwa 15 cm Länge laufend ernten.', 'good', $7, $7),
($3, 'Nur bei trockenem Boden gießen; Staunässe vermeiden.', 'good', $7, $7),
($4, 'Nach der Ernte alte Blätter entfernen und mit Kompost versorgen.', 'planned', $7, $7),
($5, 'Nach der Hauptblüte höchstens um ein Drittel einkürzen.', 'untested', $7, $7),
($6, 'In längeren Trockenphasen einmal pro Woche durchdringend wässern.', 'testing', $7, $7)`,
ids["tomato"], ids["zucchini"], ids["lavender"], ids["strawberry"], ids["basil"], ids["apple"], userID); err != nil {
return nil, nil, fmt.Errorf("create demo care instructions: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO care_instructions (species_id, text, status, created_by, updated_by) VALUES
($1, 'Morgens ernten und bei Hitze mit einem Schattiernetz vor dem Schossen schützen.', 'testing', $7, $7),
($2, 'Saatrillen bis zur Keimung gleichmäßig feucht halten und früh vereinzeln.', 'good', $7, $7),
($3, 'Immer nur die äußeren Blätter abbrechen; das Herz stehen lassen.', 'good', $7, $7),
($4, 'Sparsam gießen und überschüssiges Wasser aus dem Untersetzer entfernen.', 'good', $7, $7),
($5, 'Nach der Ernte alte Fruchttriebe bodennah entfernen.', 'planned', $7, $7),
($6, 'Junge Pflanzen rechtzeitig an einen stabilen Stab anbinden.', 'untested', $7, $7)`,
ids["lettuce"], ids["carrot"], ids["chard"], ids["rosemary"], ids["currant"], ids["sunflower"], userID); err != nil {
return nil, nil, fmt.Errorf("create extended demo care instructions: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO entity_image_history (garden_id, entity_type, entity_id, image_id, changed_by) VALUES
($1, 'species', $2, $3, $6),
($1, 'species', $4, $5, $6)`, gardenID, ids["tomato"], images["tomato"], ids["apple"], images["apple"], userID); err != nil {
return nil, nil, fmt.Errorf("record demo species images: %w", err)
}
templates := make(map[string]int64)
templateRows := []struct {
key, species, title, description, trigger string
monthFrom, monthTo, offsetFrom, offsetTo any
priority int
}{
{"tomato_fertilize", "tomato", "Tomaten düngen", "Organischen Flüssigdünger sparsam dosieren.", "relative_to_planting", nil, nil, 21, 28, 3},
{"tomato_harvest", "tomato", "Tomaten ernten", "Reife Früchte pflücken und Schadstellen kontrollieren.", "month_of_year", 7, 10, nil, nil, 0},
{"zucchini_harvest", "zucchini", "Zucchini ernten", "Früchte bei 15 bis 20 cm Länge mit einem sauberen Messer schneiden.", "month_of_year", 6, 10, nil, nil, 0},
{"basil_pinching", "basil", "Basilikumspitzen ernten", "Triebspitzen oberhalb eines Blattpaares abschneiden.", "relative_to_last_task", nil, nil, 7, 10, 0},
{"strawberry_mulch", "strawberry", "Erdbeeren mulchen", "Stroh unter die Fruchtstände legen.", "month_of_year", 5, 5, nil, nil, 0},
{"apple_prune", "apple", "Apfelbaum schneiden", "Krone auslichten und steile Wasserschosse entfernen.", "month_of_year", 2, 3, nil, nil, 3},
{"lettuce_water", "lettuce", "Salat kontrollieren und gießen", "Bodenfeuchte prüfen und bei Bedarf morgens wässern.", "relative_to_sowing", nil, nil, 3, 5, 3},
{"carrot_thin", "carrot", "Möhren vereinzeln", "Schwächere Sämlinge entfernen und fünf Zentimeter Abstand herstellen.", "relative_to_sowing", nil, nil, 18, 24, 0},
{"rosemary_winter", "rosemary", "Rosmarin winterfest machen", "Topf schützen und Regenschutz kontrollieren.", "month_of_year", 11, 12, nil, nil, 3},
{"currant_prune", "currant", "Johannisbeere auslichten", "Einen alten Haupttrieb entfernen und Jungtriebe anbinden.", "relative_to_harvest", nil, nil, 7, 21, 0},
{"sunflower_support", "sunflower", "Sonnenblumen stützen", "Stäbe kontrollieren und Bindung locker nachführen.", "relative_to_species_planting", nil, nil, 14, 21, 0},
}
for _, row := range templateRows {
var id int64
err := tx.QueryRowContext(ctx, `
INSERT INTO species_task_templates (
species_id, title, description, trigger_type, month_from, month_to,
offset_days_from, offset_days_to, priority)
VALUES ($1, $2, $3, $4::task_trigger_type, $5, $6, $7, $8, $9)
RETURNING id`, ids[row.species], row.title, row.description, row.trigger,
row.monthFrom, row.monthTo, row.offsetFrom, row.offsetTo, row.priority).Scan(&id)
if err != nil {
return nil, nil, fmt.Errorf("create demo task template %q: %w", row.title, err)
}
templates[row.key] = id
}
type templateDetails struct {
key, recurrence, unit, durationUnit string
interval, triggerOffset, duration int
}
templateUpdates := []templateDetails{
{"tomato_fertilize", "weekly", "day", "day", 2, 21, 1},
{"tomato_harvest", "weekly", "day", "day", 1, 0, 2},
{"zucchini_harvest", "weekly", "day", "day", 1, 0, 2},
{"basil_pinching", "weekly", "day", "day", 1, 7, 1},
{"strawberry_mulch", "yearly", "day", "week", 1, 0, 1},
{"apple_prune", "yearly", "week", "week", 1, 0, 2},
{"lettuce_water", "weekly", "day", "day", 1, 3, 1},
{"carrot_thin", "", "day", "day", 1, 18, 2},
{"rosemary_winter", "yearly", "day", "week", 1, 0, 2},
{"currant_prune", "yearly", "week", "week", 1, 1, 2},
{"sunflower_support", "weekly", "week", "day", 1, 2, 1},
}
for _, detail := range templateUpdates {
if _, err := tx.ExecContext(ctx, `
UPDATE species_task_templates SET recurrence=$2, recurrence_interval=$3,
trigger_offset=$4, trigger_offset_unit=$5, duration=$6, duration_unit=$7,
interval_days=CASE WHEN $2='weekly' THEN 7 ELSE NULL END
WHERE id=$1`, templates[detail.key], detail.recurrence, detail.interval,
detail.triggerOffset, detail.unit, detail.duration, detail.durationUnit); err != nil {
return nil, nil, fmt.Errorf("complete demo task template %q: %w", detail.key, err)
}
}
return ids, templates, nil
}
func seedDemoPlants(ctx context.Context, tx *sql.Tx, gardenID, demoUserID, memberID int64, locations, species, templates, images map[string]int64) (map[string]int64, error) {
ids := make(map[string]int64)
type plantSeed struct {
key, species, name, notes, location, status, attributes, imageKey string
quantity, ageDays, removedDays int
owner int64
}
rows := []plantSeed{
{"tomato", "tomato", "Tomatenreihe am Fenster", "Vier kräftige Pflanzen, zweitriebig gezogen. Pflanze zwei trägt bereits den ersten Fruchtstand.", "greenhouse", "alive", `{"anzucht":"Fensterbank","topfgroesse_l":12,"triebzahl":2,"messpunkt":"GH-T1"}`, "tomato", 4, 92, 0, demoUserID},
{"zucchini", "zucchini", "Zucchini im Hochbeet", "Die westliche Pflanze ist etwas kleiner, bildet aber neue Blätter.", "raised", "alive", `{"pflanzen":["Ost","West"],"mulch":"Rasenschnitt","erste_bluete":true}`, "", 2, 70, 0, memberID},
{"basil", "basil", "Basilikum zwischen den Tomaten", "Mischkultur für Küche und Gewächshausklima.", "greenhouse", "alive", `{"ernteintervall_tage":7,"letzte_ernte":"vor 4 Tagen"}`, "", 6, 48, 0, demoUserID},
{"strawberry", "strawberry", "Erdbeerreihe", "Im Frühjahr mit Kompost versorgt und mit Stroh unterlegt.", "vegetable", "alive", `{"standjahr":2,"reihenlaenge_m":4.5,"bewässerung":"Tropfschlauch"}`, "", 12, 410, 0, memberID},
{"lavender", "lavender", "Lavendel an der Mauer", "Wichtige Insektenweide am Hauptweg; Rückschnitt nach der Blüte vormerken.", "herbs", "alive", `{"pflanzabstand_cm":45,"bienenbesuche":"sehr häufig"}`, "", 3, 760, 0, demoUserID},
{"apple", "apple", "Apfelbaum am Nordrand", "Baumscheibe frei halten; Bindung am Pfahl monatlich kontrollieren.", "orchard", "alive", `{"stammumfang_cm":12,"baumpfahl":true,"giessring_l":60}`, "apple", 1, 980, 0, demoUserID},
{"seedling", "", "Überraschungssämling", "Noch ohne bestimmte Art. Blattform und Entwicklung weiter fotografisch dokumentieren.", "herbs", "alive", `{"bestimmung":"offen","fundort":"Kompostrand","markierung":"gelber Stab"}`, "", 1, 24, 0, memberID},
{"lettuce", "lettuce", "Salatsatz Juni", "Gestaffelt gepflanzt; die äußeren Blätter können bereits geerntet werden.", "north_bed", "alive", `{"satz":3,"jungpflanzenquelle":"eigene Anzucht","schattiernetz":true}`, "", 18, 38, 0, memberID},
{"carrots", "carrot", "Möhrenreihe am Rand", "Keimung war lückig; nach dem Vereinzeln sind fünf Zentimeter Abstand geplant.", "raised", "alive", `{"reihen":2,"reihenabstand_cm":25,"markiersaat":"Radieschen"}`, "", 36, 54, 0, demoUserID},
{"chard", "chard", "Bunter Mangold", "Vier Farbtypen gemischt, regelmäßig nur außen beernten.", "north_bed", "alive", `{"farben":["gelb","orange","rot","rosa"],"überwinterungsversuch":true}`, "", 9, 110, 0, demoUserID},
{"rosemary", "rosemary", "Rosmarin im Terrakottatopf", "Steht regengeschützt direkt an der warmen Natursteinmauer.", "herbs", "alive", `{"topf_l":35,"substrat":"mineralisch","mobil":true}`, "", 1, 620, 0, memberID},
{"currant", "currant", "Johannisbeerstrauch Rovada", "Drei Haupttriebe, Beerennetz wird erst bei beginnender Reife aufgelegt.", "orchard", "alive", `{"haupttriebe":3,"ernteprognose_kg":2.5,"netz":"eingelagert"}`, "", 1, 830, 0, demoUserID},
{"sunflowers", "sunflower", "Sonnenblumen am Zaun", "Dunkelrote Blüten als Windschutz und Futterquelle für Insekten.", "vegetable", "alive", `{"reihe_m":6,"stützen":8,"schnittblumen":true}`, "", 14, 65, 0, memberID},
{"infested_currant", "currant", "Johannisbeere mit Blattläusen", "An den jungen Triebspitzen sitzen Blattlauskolonien; Nützlingsaktivität beobachten.", "orchard", "infested", `{"schädling":"Blattlaus","befall":"leicht","maßnahme":"abwarten und Nützlinge fördern"}`, "", 1, 810, 0, memberID},
{"old_zucchini", "zucchini", "Zucchini vom Vorjahr", "Abgeerntete Pflanze als Beispiel für eine abgeschlossene Saison.", "raised", "harvested", `{"ernte_kg":18.4,"letzte_frucht_cm":24,"kompostiert":true}`, "", 1, 430, 180, demoUserID},
{"dead_lettuce", "lettuce", "Ausgefallener Frühjahrssalat", "Nach einem starken Schneckenfraß nicht wieder ausgetrieben.", "north_bed", "dead", `{"ursache":"Schneckenfraß","ersatzpflanzung":"Salatsatz Juni"}`, "", 6, 120, 72, memberID},
}
for _, row := range rows {
var speciesID any
if row.species != "" {
speciesID = species[row.species]
}
var imageID any
if row.imageKey != "" {
imageID = images[row.imageKey]
}
var id int64
err := tx.QueryRowContext(ctx, `
INSERT INTO plants (garden_id, species_id, name, notes, acquired_at, status, removed_at, attributes, image_id, created_by, updated_by, planted_by)
VALUES ($1, $2, $3, $4, CURRENT_DATE - $5::integer, $6::plant_status,
CASE WHEN $7 > 0 THEN CURRENT_DATE - $7::integer ELSE NULL END, $8::jsonb, $9, $10, $10, $10)
RETURNING id`, gardenID, speciesID, row.name, row.notes, row.ageDays, row.status, row.removedDays, row.attributes, imageID, row.owner).Scan(&id)
if err != nil {
return nil, fmt.Errorf("create demo plant %q: %w", row.name, err)
}
ids[row.key] = id
if _, err = tx.ExecContext(ctx, `
INSERT INTO plant_locations (plant_id, location_id, quantity, planted_at, removed_at, notes)
VALUES ($1, $2, $3, CURRENT_DATE - $4::integer,
CASE WHEN $5 > 0 THEN CURRENT_DATE - $5::integer ELSE NULL END,
CASE WHEN $5 > 0 THEN 'Zuordnung mit dem Pflanzenstatus abgeschlossen' ELSE 'Standort geprüft und dokumentiert' END)`,
id, locations[row.location], row.quantity, row.ageDays, row.removedDays); err != nil {
return nil, fmt.Errorf("place demo plant %q: %w", row.name, err)
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO plant_status_history (plant_id, from_status, to_status, reason, effective_at)
VALUES
($1, 'alive', 'harvested', 'Saison beendet und letzte Früchte geerntet', CURRENT_DATE - 180),
($2, 'alive', 'dead', 'Nach starkem Schneckenfraß nicht wieder ausgetrieben', CURRENT_DATE - 72),
($3, 'alive', 'infested', 'Leichter Blattlausbefall an den Triebspitzen entdeckt', CURRENT_DATE - 4)`,
ids["old_zucchini"], ids["dead_lettuce"], ids["infested_currant"]); err != nil {
return nil, fmt.Errorf("create demo plant history: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO entity_image_history (garden_id, entity_type, entity_id, image_id, changed_by) VALUES
($1, 'plant', $2, $3, $6),
($1, 'plant', $4, $5, $6)`, gardenID, ids["tomato"], images["tomato"], ids["apple"], images["apple"], demoUserID); err != nil {
return nil, fmt.Errorf("record demo plant images: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO task_template_opt_outs (plant_id, template_id)
VALUES ($1, $2)`, ids["rosemary"], templates["rosemary_winter"]); err != nil {
return nil, fmt.Errorf("create demo task template opt-out: %w", err)
}
return ids, nil
}
func seedDemoTasks(ctx context.Context, tx *sql.Tx, gardenID, demoUserID, memberID int64, locations, plants, templates map[string]int64) (int, error) {
type taskSeed struct {
key, title, description, plant, location, template, recurrence, repeatFrom, statusOnCompletion string
startDays, endDays, priority, recurrenceInterval int
completed, active bool
owner int64
}
rows := []taskSeed{
{"vent_previous", "Gewächshaus lüften", "Beide Türen öffnen, Dachfenster prüfen und abends wieder schließen.", "", "greenhouse", "", "daily", "", "", -1, -1, 0, 1, true, true, demoUserID},
{"vent_today", "Gewächshaus lüften", "Morgens Temperatur und Luftfeuchte notieren, anschließend querlüften.", "", "greenhouse", "", "daily", "vent_previous", "", 0, 0, 0, 1, false, true, demoUserID},
{"tomato_prune", "Tomaten ausgeizen", "Seitentriebe unterhalb des ersten Fruchtstands entfernen und Schnüre nachspannen.", "tomato", "greenhouse", "", "weekly", "", "", -3, -1, 5, 1, false, true, demoUserID},
{"raised_water", "Hochbeet gründlich gießen", "Am frühen Morgen etwa 15 Liter langsam unter der Mulchschicht verteilen.", "", "raised", "", "", "", "", 0, 0, 3, 1, false, true, memberID},
{"tomato_fertilize", "Tomaten düngen", "Organischen Flüssigdünger 1:100 dosieren und nur auf bereits feuchten Boden geben.", "tomato", "greenhouse", "tomato_fertilize", "weekly", "", "", 2, 4, 3, 2, false, true, demoUserID},
{"strawberry_check", "Erdbeeren auf Schnecken prüfen", "Besonders unter dem Stroh kontrollieren und reife Früchte direkt ernten.", "strawberry", "vegetable", "", "weekly", "", "", 5, 7, 0, 1, false, true, memberID},
{"apple_water", "Apfelbaum wässern", "Sechzig Liter langsam im Gießring verteilen und die Baumbindung kontrollieren.", "apple", "orchard", "", "weekly", "", "", 9, 9, 3, 1, false, true, demoUserID},
{"lettuce_water", "Salat kontrollieren und gießen", "Bodenfeuchte prüfen, äußere erntereife Blätter schneiden und Läuse kontrollieren.", "lettuce", "north_bed", "lettuce_water", "weekly", "", "", 1, 2, 3, 1, false, true, memberID},
{"carrot_thin", "Möhren vereinzeln", "Auf fünf Zentimeter Abstand ausdünnen und anschließend vorsichtig einschlämmen.", "carrots", "raised", "carrot_thin", "", "", "", 3, 5, 0, 1, false, true, demoUserID},
{"chard_harvest", "Mangold ernten", "Je Pflanze zwei große Außenblätter mit sauberem Messer schneiden.", "chard", "north_bed", "", "weekly", "", "", 4, 4, 0, 1, false, true, memberID},
{"aphid_check", "Blattlausbefall kontrollieren", "Triebspitzen fotografieren, Marienkäferlarven zählen und Befallsstärke dokumentieren.", "infested_currant", "orchard", "", "daily", "", "", 0, 2, 5, 1, false, true, memberID},
{"compost_turn", "Kompost umsetzen", "Material aus Kammer zwei mischen, Feuchtigkeit prüfen und Temperatur messen.", "", "compost", "", "monthly", "", "", 12, 14, 3, 1, false, true, demoUserID},
{"pond_level", "Wasserstand im Miniteich prüfen", "Nur aufgefangenes Regenwasser nachfüllen und die Ausstiegshilfe freihalten.", "", "pond", "", "weekly", "", "", 6, 6, 0, 1, false, true, memberID},
{"herbs_weed", "Kräuterecke jäten", "Quecke vollständig mit Wurzeln entfernen und freien Boden mit Splitt abdecken.", "", "herbs", "", "monthly", "", "", -8, -7, 0, 1, true, true, memberID},
{"zucchini_harvest", "Zucchini ernten", "Sechs junge Früchte geerntet und das Gesamtgewicht im Gartentagebuch notiert.", "zucchini", "raised", "zucchini_harvest", "weekly", "", "", -6, -5, 0, 1, true, true, demoUserID},
{"old_zucchini_close", "Zucchinisaison abschließen", "Letzte Frucht ernten, Pflanze entfernen und gesundes Material kompostieren.", "old_zucchini", "raised", "", "", "", "harvested", -182, -180, 0, 1, true, false, demoUserID},
}
ids := make(map[string]int64, len(rows))
for _, row := range rows {
var plantID, locationID, templateID, repeatFromID any
if row.plant != "" {
plantID = plants[row.plant]
}
if row.location != "" {
locationID = locations[row.location]
}
if row.template != "" {
templateID = templates[row.template]
}
if row.repeatFrom != "" {
repeatFromID = ids[row.repeatFrom]
}
var id int64
err := tx.QueryRowContext(ctx, `
INSERT INTO tasks (
garden_id, plant_id, location_id, template_id, title, description,
due_at_start, due_at_end, generated_for, completed_at, completed_by,
priority, active, recurrence, recurrence_interval, repeat_from_id,
plant_status_on_completion, created_by)
VALUES ($1, $2, $3, $4, $5, $6,
CURRENT_DATE + $7::integer, CURRENT_DATE + $8::integer,
CASE WHEN $4::bigint IS NULL THEN NULL ELSE CURRENT_DATE END,
CASE WHEN $9 THEN CURRENT_TIMESTAMP - interval '6 days' ELSE NULL END,
CASE WHEN $9 THEN $10::bigint ELSE NULL END,
$11, $12, $13, $14, $15, NULLIF($16, '')::plant_status, $10)
RETURNING id`,
gardenID, plantID, locationID, templateID, row.title, row.description,
row.startDays, row.endDays, row.completed, row.owner, row.priority, row.active,
row.recurrence, row.recurrenceInterval, repeatFromID, row.statusOnCompletion).Scan(&id)
if err != nil {
return 0, fmt.Errorf("create demo task %q: %w", row.title, err)
}
ids[row.key] = id
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO tags (garden_id, name) VALUES
($1, 'Dringend'), ($1, 'Bewässerung'), ($1, 'Ernte'),
($1, 'Beobachtung'), ($1, 'Gemeinschaft'), ($1, 'Nützlinge'), ($1, 'Gewächshaus')`, gardenID); err != nil {
return 0, fmt.Errorf("create demo tags: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO task_tags (task_id, tag_id)
SELECT tasks.id, tags.id FROM tasks JOIN tags ON tags.garden_id=tasks.garden_id
WHERE tasks.garden_id=$1 AND (
(tasks.title='Tomaten ausgeizen' AND tags.name IN ('Dringend','Gewächshaus')) OR
(tasks.title='Hochbeet gründlich gießen' AND tags.name='Bewässerung') OR
(tasks.title='Apfelbaum wässern' AND tags.name='Bewässerung') OR
(tasks.title='Blattlausbefall kontrollieren' AND tags.name IN ('Dringend','Beobachtung','Nützlinge')) OR
(tasks.title='Zucchini ernten' AND tags.name='Ernte'))`, gardenID); err != nil {
return 0, fmt.Errorf("tag demo tasks: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO plant_tags (plant_id, tag_id)
SELECT plants.id, tags.id FROM plants JOIN tags ON tags.garden_id=plants.garden_id
WHERE plants.garden_id=$1 AND (
(plants.name='Erdbeerreihe' AND tags.name='Ernte') OR
(plants.name='Johannisbeere mit Blattläusen' AND tags.name IN ('Beobachtung','Nützlinge')) OR
(plants.name='Tomatenreihe am Fenster' AND tags.name='Gewächshaus'))`, gardenID); err != nil {
return 0, fmt.Errorf("tag demo plants: %w", err)
}
return len(rows), nil
}
func seedDemoJournal(ctx context.Context, tx *sql.Tx, gardenID, demoUserID, memberID int64, images map[string]int64) (int, int, error) {
if _, err := tx.ExecContext(ctx, `
INSERT INTO journal_entries (garden_id, author_id, entry_type, title, body, created_at, updated_at) VALUES
($1, $2, 'pinboard', 'Willkommen im Sonnengarten', E'Diese Demo zeigt einen vollständig gepflegten Gemeinschaftsgarten.\n\n**Zum Ausprobieren:** Aufgaben filtern, Pflanzen nach Standort öffnen, Pflegehinweise bewerten und im Tagebuch stöbern. Die Daten werden stündlich zurückgesetzt.', CURRENT_TIMESTAMP - interval '21 days', CURRENT_TIMESTAMP - interval '20 days'),
($1, $3, 'pinboard', 'Gießdienst am Wochenende', E'Für Samstag sind **28 °C** angekündigt. Bitte zuerst Gewächshaus und Hochbeet versorgen, danach die Gießringe auf der Obstwiese.\n\nDie Kräuterecke nur gießen, wenn Rosmarin und Lavendel deutlich schlapp wirken.', CURRENT_TIMESTAMP - interval '3 days', CURRENT_TIMESTAMP - interval '2 days'),
($1, $3, 'journal', 'Die ersten Erdbeeren', E'Heute konnten wir die ersten reifen Erdbeeren probieren. Das Stroh hält die Früchte sauber.\n\n- Ernte: 620 g\n- Geschmack: sehr aromatisch\n- Beobachtung: zwei Früchte mit Vogelfraß\n\nDas Netz bringen wir erst an, wenn der Schaden zunimmt.', CURRENT_TIMESTAMP - interval '12 days', CURRENT_TIMESTAMP - interval '12 days'),
($1, $2, 'journal', 'Tomaten wachsen gut an', E'Alle vier Pflanzen haben neue Blätter und kräftige Seitentriebe gebildet. Pflanze zwei trägt den ersten Fruchtstand.\n\nDie Tropfbewässerung läuft morgens für zwanzig Minuten. Beim nächsten Termin werden die Schnüre nachgespannt und die unteren Blätter ausgelichtet.', CURRENT_TIMESTAMP - interval '7 days', CURRENT_TIMESTAMP - interval '6 days'),
($1, $3, 'journal', 'Nützlinge an der Johannisbeere', E'Am befallenen Strauch wurden heute **vier Marienkäferlarven** und mehrere Schwebfliegen beobachtet. Deshalb verzichten wir vorerst auf eine Behandlung.\n\nIn zwei Tagen vergleichen wir Fotos derselben markierten Triebspitze.', CURRENT_TIMESTAMP - interval '2 days', CURRENT_TIMESTAMP - interval '2 days'),
($1, $2, 'journal', 'Komposttemperatur steigt', E'Kammer zwei erreicht nach dem Umsetzen 54 °C. Das Material ist feucht wie ein ausgedrückter Schwamm und riecht angenehm erdig.\n\nBeim nächsten Umsetzen trockenes Häckselmaterial aus Kammer eins beimischen.', CURRENT_TIMESTAMP - interval '1 day', CURRENT_TIMESTAMP - interval '1 day')`,
gardenID, demoUserID, memberID); err != nil {
return 0, 0, fmt.Errorf("create demo journal entries: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO journal_entry_tags (journal_entry_id, tag_id)
SELECT journal_entries.id, tags.id
FROM journal_entries JOIN tags ON tags.garden_id = journal_entries.garden_id
WHERE journal_entries.garden_id = $1
AND ((journal_entries.title = 'Die ersten Erdbeeren' AND tags.name = 'Ernte')
OR (journal_entries.title = 'Tomaten wachsen gut an' AND tags.name IN ('Beobachtung','Gewächshaus'))
OR (journal_entries.title = 'Willkommen im Sonnengarten' AND tags.name = 'Gemeinschaft')
OR (journal_entries.title = 'Gießdienst am Wochenende' AND tags.name IN ('Gemeinschaft','Bewässerung'))
OR (journal_entries.title = 'Nützlinge an der Johannisbeere' AND tags.name IN ('Beobachtung','Nützlinge'))
OR (journal_entries.title = 'Komposttemperatur steigt' AND tags.name = 'Beobachtung'))`, gardenID); err != nil {
return 0, 0, fmt.Errorf("tag demo journal entries: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO species_tags (species_id, tag_id)
SELECT species.id, tags.id FROM species JOIN tags ON tags.garden_id=species.garden_id
WHERE species.garden_id=$1 AND (
(species.common_name IN ('Tomate','Basilikum') AND tags.name='Gewächshaus') OR
(species.common_name IN ('Erdbeere','Zucchini','Rote Johannisbeere') AND tags.name='Ernte') OR
(species.common_name IN ('Lavendel','Sonnenblume') AND tags.name='Nützlinge'))`, gardenID); err != nil {
return 0, 0, fmt.Errorf("tag demo species: %w", err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO journal_attachments (journal_entry_id, image_id, file_name, media_type, data, size)
SELECT id, $2, 'tomaten-gewaechshaus.jpg', 'image/jpeg', $3, $4
FROM journal_entries WHERE garden_id=$1 AND title='Tomaten wachsen gut an'`,
gardenID, images["tomato"], demoTomatoImage, len(demoTomatoImage)); err != nil {
return 0, 0, fmt.Errorf("create demo journal attachment: %w", err)
}
return 6, 1, nil
}