Repository navigation
Build a standalone newsletter schedule and booking planner #112
Description
Build a standalone newsletter schedule and booking planner
Status: pending
Tags: enhancement, portal, frontend, backend, data, migration, design, P1
Depends on: None
Blocks: None
Scope
Replace the newsletter-planning portion of the private Newsletter worksheet with a focused authenticated planner. This issue owns newsletter/campaign slots, booking ownership, sponsor linkage, newsletter-specific alerts, and privacy-safe migration. General event scheduling and Berlin holidays remain in #110; sponsor/contact CRM records remain in #111.
Provide a responsive chronological schedule grouped by week/month, with filters for status and booked/unbooked state. Operators can create, edit, reschedule, cancel, and open a newsletter slot. Each slot stores publication date/time in Europe/Berlin, campaign number/label, status (open, reserved, drafting, scheduled, sent, cancelled), optional linked newsletter workflow bundle, bookedByUserId or bounded bookedByDisplayName, optional sponsorBookingId, optional public campaign URL, safe planning note, source provenance, optimistic version, and audit fields. The UI must visibly answer who booked each slot without exposing email/contact data.
Add dedicated on-demand DynamoDB storage and authenticated /api/newsletter-slots range/list/get/create/update/delete routes with indexed date access, bounded validation, optimistic concurrency, and deterministic sourceKey/idempotency. Unauthorized responses reveal no record existence/content. #111 is the authority for sponsor/contact details; #112 stores only the opaque sponsor booking ID and resolves display-safe sponsor summary through an integration boundary when authorized. A missing #111 record must not break scheduling.
Calculate server-side planning alerts for duplicate active slots on the same campaign/date, near-term open/unbooked slots, reserved slots missing a booker, approaching publication with no linked workflow bundle, and linked newsletter bundles missing required campaign link/status. Alerts use stable reason codes, severity, configurable lead windows, affected IDs, and fingerprinted dismissals.
Expose a read-only authenticated date-range projection/provider so #110 can optionally render newsletter slots as a separate overlay. #110 must not mutate newsletter records or duplicate them into its table; #112 remains independently usable.
Create a dry-run-first local importer for the private Newsletter sheet in ~/tmp/schedule.xlsx. Import only schedule/booking fields that map confidently to newsletter slots. Historical deliveries/opens/clicks/CTR/social impressions, posts, sponsor contacts/emails, private document links, and free-form sensitive notes are not imported. Sponsor names may be retained only as a bounded display-safe legacy booking label when needed to identify a booked slot; do not infer/create CRM contacts. Future #111 reconciliation may replace that label with sponsorBookingId.
Importer writes go through authenticated newsletter APIs only, never DynamoDB; local dry-run makes no network call, real writes require explicit API/environment confirmation, use deterministic row source keys plus idempotency headers, retry transient errors safely, and verify results through API. Logs/reports contain sheet row identifiers/counts/reason codes only; source/rejected data stays local and Git-ignored; fixtures are synthetic.
Acceptance Criteria
- Operators can view week/month-grouped newsletter slots, filter booked/unbooked/status, and create/edit/reschedule/cancel them responsively.
- Every slot clearly shows campaign/publication state and who booked it via user or display-safe fallback, without contact/email leakage.
- Optional
sponsorBookingIdintegrates with Add an authenticated sponsor CRM and booking management #111 through an authorization-aware seam; missing/unavailable CRM data degrades safely. - Newsletter alerts cover duplicate bookings, near-term open/unbooked or missing-booker slots, and incomplete linked newsletter workflows with stable tested reason codes.
-
Europe/Berlintimed/all-day semantics, DST handling, ISO week/month grouping, and year boundaries are correct. - Authenticated indexed APIs enforce validation, optimistic versioning and source/idempotency uniqueness and do not leak data to unauthorized callers.
- A read-only projection lets Build a Berlin-aware operations calendar #110 optionally show newsletter slots without duplicating ownership or enabling calendar-side mutation.
- Importer dry-run is offline; confirmed writes use only API; reruns create zero duplicates; only schedule/booking fields from
Newsletterare imported. - Performance analytics, contact emails, private docs/URLs, sensitive notes, other sheets, and hidden data never enter storage, logs, Git, fixtures, or screenshots.
- Route/integration tests cover CRUD/ranges/auth, concurrency/idempotency, alerts, Add an authenticated sponsor CRM and booking management #111 missing/authorized behavior, Build a Berlin-aware operations calendar #110 projection, and importer HTTP boundary.
- Synthetic Playwright screenshots cover booked/unbooked, alerts, editing, empty/error, mobile, and week/month/year boundaries.
- Full backend tests, typecheck, build, relevant Playwright, SAM validate lint, and SAM build pass.
Test Scenarios
Scenario: Book a newsletter slot
Given: An open upcoming slot
When: An operator reserves it, records the booker, and links a newsletter bundle
Then: Ownership is visible and alerts update from current server state
Scenario: Integrate without leaking CRM
Given: A slot with a #111 sponsor booking ID
When: An authorized or unauthorized viewer requests it, or #111 is unavailable
Then: Only authorized display-safe data appears and scheduling remains functional
Scenario: Import newsletter planning only
Given: A synthetic Newsletter sheet with schedule, performance, contact, duplicate, and invalid fields
When: Import dry-run then confirmed API write runs
Then: Valid slots appear once and analytics/contact/private values are excluded
Out of Scope
- General DataOps activities and Berlin public/school holidays (Build a Berlin-aware operations calendar #110).
- Sponsor/contact CRM creation, outreach, contracts, or contact history (Add an authenticated sponsor CRM and booking management #111).
- Newsletter performance/social analytics, Mailchimp synchronization/sending, copy drafting, or historical reporting.
- Other workbook sheets, guest/contact/growth/content-plan migration, and spreadsheet-grid recreation.
Dependencies
~/tmp/schedule.xlsxremains private/outside Git; all real reconciliation artifacts remain ignored/private.- Existing newsletter templates/bundles remain workflow truth and are linked, not duplicated.
- Add an authenticated sponsor CRM and booking management #111 may enrich sponsor display/linkage later; Build a standalone newsletter schedule and booking planner #112 can ship first using safe booking labels.
- Build a Berlin-aware operations calendar #110 consumes only the read-only newsletter projection when implemented.
Activity
[-]Replace newsletter spreadsheet with newsletter schedule[/-][+]Build a standalone newsletter schedule and booking planner[/+]alexeygrigorev commentedon Jul 12, 2026
Software Engineer handoff — READY FOR TEST
Implemented the standalone authenticated Newsletter planner locally without commit, deployment, or live import.
Delivered
GSI-Dateindexed chronological range access./api/newsletter-slotsCRUD/range/filter APIs with bounded validation, optimistic versions, deterministic source-key idempotency, booked/unbooked/status filters, and non-leaking unauthorized responses.Europe/Berlinsemantics, date-only slots, optional local publication time, DST-gap rejection, and explicit-offset requirement for the autumn overlap.sponsorBookingId, newsletter bundle, public HTTPS campaign URL, safe planning note, provenance, and audit timestamps.nullwhen missing./api/newsletter-slots/projectionfor future Build a Berlin-aware operations calendar #110 overlay consumption; it exposes only safe date/status/label/timezone/link fields and no mutation path.scripts/import-newsletter-slots.ts: reads only theNewslettersheet, skips hidden rows, requires an explicit schedule/booking header allowlist, ignores every unrecognized analytics/contact/link/note field and every other sheet, generates deterministic row source keys, makes no dry-run network call, requires exact API target confirmation for writes, retries transient failures, writes only through authenticated API, and outputs counts/statuses only.frontend/and backend fallback portal seams with month-grouped chronological slots, booked-by visibility, status/booked/date filters, create/edit/cancel status, alert display, empty/loading/error states, and responsive mobile behavior.Private workbook handling
Inspected
~/tmp/schedule.xlsxlocally only for structure/type shape. No raw cells, headers, links, contacts, notes, workbook artifacts, or rejected rows were logged, copied into Git, or placed in issue comments. The source remains outside the repository and no live import was performed.Verification
NODE_ENV=test SKIP_AUTH=true node --import tsx --test tests/newsletter-slots.test.ts— PASS 3/3npx playwright test e2e/newsletter-production-portal.spec.js— PASS 1/1npm test— PASS 1157/1157npm run typecheck— PASSnpm run build— PASSuv run pytest tests/infra/test_template.py— PASS 16/16sam validate --lint --template-file infra/template.full.yaml— PASSsam build --template-file infra/template.full.yaml— PASSgit diff --check— PASSTests cover authenticated CRUD/idempotency/version conflict/ranges, filters, privacy-safe projection, every alert family, invalid input/range, Berlin DST gap/overlap, offline synthetic workbook parsing, hidden/other-sheet exclusion, explicit target confirmation, and API-only write behavior.
Visual evidence
Generated and visually inspected clean synthetic artifacts:
backend/.tmp/newsletter-production-loading.pngbackend/.tmp/newsletter-production-populated-alert.pngbackend/.tmp/newsletter-production-edit.pngbackend/.tmp/newsletter-production-error.pngbackend/.tmp/newsletter-production-mobile.pngThe production portal test crosses the 2026/2027 year boundary and asserts the 390px document has no horizontal overflow.
Boundary confirmation
No general calendar/Berlin holiday behavior from #110, CRM/contact creation from #111, performance analytics, Mailchimp sending, other workbook sheet, raw spreadsheet grid, contact email, private link, or sensitive free-form note was implemented or imported.
alexeygrigorev commentedon Jul 12, 2026
CHANGES REQUESTED
Independent review found the foundation working, but multiple groomed acceptance criteria are not yet implemented or proven.
Blocking findings:
Passing evidence:
No implementation files or live data were changed.
alexeygrigorev commentedon Jul 12, 2026
Engineer handoff update (no commit, deploy, or live import performed):
Implemented the standalone authenticated Newsletter planner across the production portal and backend fallback seam, indexed DynamoDB storage/API, projection, filtering, optimistic concurrency, persistent alert dismissal/resolution/recurrence, #111 sponsor-booking lookup guards, bundle/workflow truth checks, and the privacy-allowlisted
Newsletterworkbook importer.Verification evidence:
npm run typecheck: passed.newsletter-slots.test.ts: 3/3 passed. Includes authenticated CRUD/idempotency/range/projection/conflict behavior, strict fields/DST/alert rules, explicit Add an authenticated sponsor CRM and booking management #111 unavailable booking rejection (422), and an already-stored orphan booking remaining readable withsponsor: null.backend/.tmp/newsletter-production-{loading,populated-alert,week,empty,edit,conflict,error,mobile}.png; layouts are clean, readable, and contain synthetic data only.backend/src/pages/index.html,backend/src/public/api.js, andbackend/src/public/app.js; production seam carries the complete Playwright interaction/state matrix. (A separate fallback Playwright attempt was removed because the repository E2E server selects the production frontend; no false passing evidence is claimed.)tests/infra/test_template.py16/16 passed.sam validate --lint --template-file infra/template.full.yaml: valid.make sam-build: succeeded.~/tmp/schedule.xlsxdry-run:Newsletter, header row 1, 23 accepted and 253 skipped (207 hidden, 4 missing/invalid date, 42 missing/invalid label); output contains coordinates/counts only, not cell values.git diff --check: passed. Privacy grep over the Build a standalone newsletter schedule and booking planner #112 route/data/importer diff found no credential/private workbook values. Workbook remains outside the repo and no live import ran.Known worktree note: the tree contains unrelated pre-existing dashboard/email/mailing changes; they were preserved and are not represented as #112 work. No commit was made.
14 remaining items
alexeygrigorev commentedon Jul 12, 2026
ACCEPTED
Narrow PM acceptance passes for commit
afe8249.The change is test-only and correctly addresses a clean-runner assumption: immediately before writing the synthetic newsletter XLSX fixture, the test recursively creates its ignored
.tmpparent directory. This makes the focused suite independent of leftover local artifacts while preserving the same synthetic fixture, importer behavior, privacy boundary, and production code.The commit changes exactly one test file with two additions, contains no runtime/router/infrastructure changes, passes the focused clean-missing-parent proof (4/4), passes its diff check, and adds no private workbook values, links, contacts, credentials, or artifacts. The generated fixture remains covered by
.tmp/ignore rules.The two current full-suite failures belong to uncommitted #110 calendar/router work and are absent from
afe8249; they neither invalidate this isolated #112 fix nor authorize including unrelated dirty files in the push. This commit is safe to push and deploy as part of the already accepted #112 change set, with the unrelated calendar work kept separate.Tester PASS: #112 (comment)
test: create newsletter import temp directory
alexeygrigorev commentedon Jul 12, 2026
ON-CALL PASS — CI fix and deployment
Fix commit
afe824924cf822f49ef4a6cb0e8de2d3c258aae3deployed successfully on the first attempt.dataops-v1isUPDATE_COMPLETE.DataOpsNewsletterSlotsTableisCREATE_COMPLETE; public-safe table name:dataops-v1-newsletter-slots.https://el4jt4z2k4lnxwvoqawrdcsedm0axjzc.lambda-url.eu-west-1.on.aws/.No live newsletter import was run. Issue #112 remains open for its explicit HUMAN migration/verification gate. Unrelated dirty local #110 work was untouched and did not affect the clean GitHub runner.
ON-CALL DONE: newsletter planner deployment healthy.
alexeygrigorev commentedon Jul 12, 2026
Uncommitted HUMAN import blocker fix ready for Tester review; no live write/import was attempted.
Operational changes:
NEWSLETTER_IMPORT_PORTAL_USERNAME+NEWSLETTER_IMPORT_PORTAL_PASSWORDare accepted only as a pair and produce outerAuthorization: Basic ...for both POST and verification GET requests through the deployed single-origin portal.NEWSLETTER_IMPORT_TOKENremains the session Bearer fallback.1900..9999. It derives unique years from accepted rows and issues boundedYYYY-01-01..YYYY-12-31queries, accumulating exactsourceKeymatches across chunks.Evidence:
9999, exact source-key verification, idempotent response accounting, paired-credential validation, target confirmation, and secret-free result serialization.Full backend run reached 1,164 passing tests but is currently red solely in concurrent uncommitted #110
tests/calendar.test.ts, which resolvesbackend/backend/src/pages/index.htmland raises ENOENT. I did not modify that unrelated work.Files changed only:
backend/scripts/import-newsletter-slots.tsbackend/tests/newsletter-slots.test.tsNo commit, push, deploy, or import performed.
alexeygrigorev commentedon Jul 12, 2026
TEST PASS
Narrow review of the deployed-auth and bounded verification importer fix passes.
Exact diff scope:
Authentication:
Verification/idempotency:
Commands/evidence:
The current full-suite #110 calendar ENOENT is outside these two files and does not affect this narrow verdict. No live import, write, implementation edit, or credential exposure occurred during review. The #112 HUMAN import blocker fix is ready for PM acceptance.
alexeygrigorev commentedon Jul 12, 2026
ACCEPTED
Narrow PM acceptance passes for the deployed-auth and bounded verification importer fix.
The change supports the intended controlled live import without weakening the accepted Newsletter API boundary. In the deployed single-origin portal, a complete Basic credential pair supplies the outer authenticated portal authorization for both slot creation and post-import verification. Without that pair, the existing operator session Bearer token remains available. Missing authentication or a half-configured Basic pair fails before any network mutation, and exact API URL confirmation remains mandatory.
The importer continues to write only through authenticated
/api/newsletter-slotsPOST requests with deterministic source/idempotency keys; it does not gain a DynamoDB or storage bypass. API validation, optimistic/idempotent behavior, retries, and source-field allowlisting remain in force. Credentials are absent from summaries and errors, and Basic credentials take precedence only when the complete pair is explicitly configured.Verification is now operationally bounded: it derives the distinct years represented by accepted rows, queries each
YYYY-01-01throughYYYY-12-31range through the same authenticated API, accumulates exactsourceKeymatches, and fails if any accepted row is absent. This avoids the impractical1900..9999request while preserving complete verification for the import batch. Synthetic evidence proves first-run creation, duplicate-only rerun, and exact verification counts.Tester verified unauthenticated rejection/authenticated acceptance, paired-credential fail-closed behavior with zero network calls, Basic and Bearer paths, bounded URLs, exact source-key verification, credential-free output, focused tests 5/5, typecheck, and scoped diff checks. The unrelated uncommitted #110 calendar failure is outside this two-file fix and must remain separate.
This clears the #112 HUMAN live-import blocker. It does not itself authorize including unrelated worktree changes or skipping the operator's final reconciliation.
Engineer handoff: #112 (comment)
Tester PASS: #112 (comment)
fix: support authenticated newsletter imports
alexeygrigorev commentedon Jul 12, 2026
ON-CALL PASS — import authentication fix
Commit
a85569b57a951162f382c5124f45856ccc516807deployed successfully on the first attempt.dataops-v1isUPDATE_COMPLETE.No newsletter import was run and no local files were changed by On-Call.
ON-CALL DONE: import-auth fix deployment healthy.
alexeygrigorev commentedon Jul 12, 2026
HUMAN sandbox import/reconciliation completed.
Sanitized evidence:
Newslettersheet dry-run: 23 accepted, 253 safely skipped (207 hidden rows, 4 invalid-date rows, 42 invalid-label rows); report contains coordinates/reason counts only..tmp/.a85569bis healthy: CI/OIDC/CloudFormation/seed/smoke passed anddataops-v1-newsletter-slotsis live.alexeygrigorev commentedon Jul 12, 2026
Completed: deployed, privately imported and reconciled, zero-create rerun confirmed, and live desktop/mobile portal flow verified.
feat: add newsletter slot planner
test: create newsletter import temp directory
fix: support authenticated newsletter imports