Commit Graph

17 Commits

Author SHA1 Message Date
Alexander 00478e2466 Single frequency setting; per-user override; footer line
Admin-facing simplification:
- Dropped separate \"lookahead\" and \"historical lookahead\" tree
  prefs (and the once-per-month historical gate). A single
  \"send every N days\" number now drives both the cron cadence
  and the window each issue looks ahead for living + deceased
  events.
- Default 14, range 1–90, applies uniformly.

User-facing addition:
- The /my-account/{tree} subscription card gained an \"Email
  frequency\" select with options: use site default, weekly,
  every 2 weeks, monthly, every 2 months, quarterly. Stored as
  a per-tree-per-user preference.
- Dispatch now checks each recipient's own cadence against
  their own last-sent timestamp. Admin-added external addresses
  with no webtrees account always receive every run (no
  per-user state).
- Newsletter footer now reads \"You can change how often you
  receive this email, or unsubscribe entirely, in the Newsletter
  subscription section on your My account page\" — true now
  that the control exists.

German translations updated for the new strings; stale ones
removed.
2026-05-15 14:12:39 +02:00
Alexander 355a888e3b Let timeline column shrink to its content width
The fixed 140px width on the timeline TD reserved a lot of empty
space to the right of the date — on narrow viewports it squeezed
the cards severely. Replaced the hard width with the standard
\`width:1%\` email-table trick so the cell collapses to whatever
the date line actually needs (about 80-95px); the card column
inherits the rest.

Right padding dropped from 14 to 4 to tighten further.
2026-05-15 13:57:00 +02:00
Alexander f0858bb604 Apply user-tuned arrow head offsets 2026-05-15 13:54:56 +02:00
Alexander d8ba577e1e Drop em-dash before event label; show on own line below name 2026-05-15 13:49:10 +02:00
Alexander eaad7f844c Round rail ends; drop arrowhead lower and shift right
- Two new stub rows now bookend each section's rail. The top
  cap is a 4px-high TR whose rail TD carries border-top-left/
  -right-radius so the rail's top end visually rounds off.
  The bottom cap is an 18px-high TR with border-bottom-left/
  -right-radius so the rail's tail tapers into a rounded stop
  beneath the last card.
- The chevron arrowhead now lives in its own TR with no rail
  border, sitting 14px below the rounded rail-end and another
  ~18px of cell height below that — visibly separated from the
  rail rather than sitting flush against its tip.
- Chevron's horizontal offset shifted from -38 to -37 (one px
  right) and SVG width bumped to 26 to give the arrow a little
  more visual weight at its new lower position.
2026-05-15 13:43:00 +02:00
Alexander 6bad52e68d Centre dot + arrow on rail; chevron arrowhead with rounded caps
- Bumped the dot's margin-left from -31 to -33 so its centre
  lands on the rail's centre (the border-collapsed 4px border
  effectively leaves 26px between rail centre and content-start;
  half of a 14px dot is 7px, so margin = -(26+7)). margin-right
  bumped to 16 to preserve the gap to the date text.
- Replaced the CSS-triangle arrowhead with an inline SVG chevron
  (stroke-linecap:round, stroke-linejoin:round, stroke-width:3)
  so the cap matches the rounded "v" arrow look the user asked
  for. SVG sits at the bottom of its cell with the same -38
  offset as the dot pile so dot, chevron and rail share a single
  vertical axis.
2026-05-15 13:37:38 +02:00
Alexander 6f0a55de5c Continuous timeline rail across card gaps; arrowhead cap
The previous border-spacing approach made each card stand on its
own — but it also fragmented the rail. Each rail segment ended
at the bottom of its row, leaving visible breaks between cards.

Restructured each event row to wrap the avatar+content cells in
a nested card table sitting in the row's left outer TD. The
right TD carries the rail as its border-left. With the outer
section table now using border-collapse:collapse, consecutive
rows' left-borders touch and merge into one unbroken line that
runs through the card gaps.

Added a downward triangle TR after the last event of each
section as a visual cap on the rail. Pure CSS (border-style
trick), coloured to match the rail.
2026-05-15 13:31:18 +02:00
Alexander abe77a9b9d Gap between event cards; dotless rows drop day, keep just year
- Each event row is now a self-contained card (full border on
  all four sides, rounded all four corners). border-spacing on
  the section table inserts a 10px vertical gap between cards
  so they read as distinct entries. The timeline rail breaks
  along with the cards, which actually reads better — each event
  feels like its own beat on the timeline rather than rungs of a
  ladder.
- When two events share an upcoming day, the second row
  (dotless) used to repeat "17. MAI" — wasted vertical space.
  It now only prints the year, bumped up to 14px so it carries
  on its own. The always-visible year is also slightly larger
  (13px vs 11px) and tonally lifted from #aaa muted to #777
  tertiary so it pairs evenly with the day-month line above it.
2026-05-15 13:26:15 +02:00
Alexander 461c99fcd1 Rework timeline: outside the card, thicker rail, day-first date
Addresses feedback that the previous timeline competed for
attention with the year and felt visually trapped inside the
event card.

- The card surface (cream background, hairline border, rounded
  corners) is now built per-row from the avatar + content TDs.
  The timeline TD sits on the page background to the right of
  the card with a 16px gutter between them.
- Rail bumped from 1px to 4px, in a warm grey #cdc7be that
  reads as a deliberate ribbon rather than a divider.
- Dots are 14px (up from 10), with a 4px page-coloured halo so
  they punch through the rail. Single colour (nord10 blue) for
  every event — no more per-event-type tinting.
- Each calendar day shows exactly one dot: rows are walked in
  upcoming-anniversary order and any row whose $fact->jd matches
  the previous row renders without a dot but keeps its date
  text, so two May-17 deaths share one marker on the rail.
- Date display split into "17. MAI" (semi-bold 13px tracking)
  and "1759" (light 11px, muted) on a second line, so the day
  of the year reads as the primary axis and the year as
  supporting context.
- Relationship label moved from inline "(your great-aunt)" to a
  separate italic muted line beneath the name, so long
  relationship strings don't crowd the event label.
2026-05-15 13:21:35 +02:00
Alexander 105b09c4c5 Fix kin-distance metric: shortest descent from direct lineage
Replaces the previous "depth in generations along the strict
lineal chain" definition (which excluded siblings, aunts, cousins
entirely) with the metric the user actually wants: the number of
descent-steps separating the target from the recipient's closest
direct ancestor or descendant.

Examples relative to the recipient:
- sibling:        1  (parent → sibling)
- great-aunt:     1  (great-grandparent → great-aunt)
- nephew:         2  (parent → sibling → nephew)
- first cousin:   2  (grandparent → aunt → cousin)
- second cousin:  3
- ego, parents, grandparents, ..., children, ..., great-greats: 0
- own spouse, step-parents, brothers-in-law: inherit partner's
  distance (so spouse-of-distance-1 is also distance 1)

Implementation:
- Anchor set seeded with R's direct ancestors + R + direct
  descendants (capped at 25 generations to bound runaway data).
- Multi-source BFS expanding by descent only.
- Spouse propagation at every level so a person and their
  spouse always share the same distance.
- Memoised per (recipient xref, max distance).

Tree preference key and range kept (NEWSLETTER_LINEAL_DEPTH,
0–10, default 3); only the semantics and the user-facing label
+ help text change, with concrete examples in both English and
German.
2026-05-15 13:10:38 +02:00
Alexander ff743e484f Limit detailed view to lineal kin; rest as summary bullets
Per-recipient: only direct ancestors and direct descendants
within a configurable number of generations (default 3) get the
full row treatment (avatar, icon, timeline). Everyone else falls
through to a compact text-only bullet list at the bottom of the
same section.

- New tree preference NEWSLETTER_LINEAL_DEPTH (range 0–10,
  default 3) with a clearly-explained admin input.
- RelationshipPathFinder::linealKin() does two cheap recursive
  expansions (ancestors and descendants only — no spouse or
  sibling traversal) and returns the xref set. Memoised per
  recipient within a dispatch run.
- Avatar attachments are filtered per recipient to only the
  embeds actually referenced in their HTML, so summary-only rows
  no longer inflate per-email size with unused images.
- Recipients with no PREF_TREE_ACCOUNT_XREF (external admin
  addresses, users not linked to a record) see the entire
  newsletter in detail — no lineal anchor to filter against.
- German translations for the three new section kickers ("Other
  birthdays", etc.) and the admin input help text.
2026-05-15 13:01:41 +02:00
Alexander 3bc25a2bdb Add per-recipient relationship labels in newsletter
Each featured person now carries a parenthetical label relative
to the recipient: "Jane Doe (your mother) — 45th birthday",
"Karl Müller (your 4th great-grandfather) — death". Labels are
italic, muted, and only appear when a path can be computed.

- New RelationshipPathFinder service mirrors webtrees'
  RelationshipService::getCloseRelationship BFS but with a
  configurable depth (default 14 hops ≈ 7 generations) so it
  reaches great-great-grandparents and beyond. Results are
  memoised per (recipient xref, target xref) within one
  dispatch run.
- nameFromPath() formatting is delegated to webtrees so the
  label honours the configured UI language (German, English,
  etc.) and gendered/inflected forms.
- The recipient's tree-bound Individual is looked up via
  Tree::getUserPreference(user, PREF_TREE_ACCOUNT_XREF). External
  admin-added recipients (no webtrees account, no linked record)
  silently get no labels — names render plain.
- Trade-off: the view now renders once per recipient (instead of
  once per language group), because the relationship map is
  personalised. For typical subscriber counts the extra string-
  concat cost is negligible compared to the SMTP send itself.
2026-05-15 12:53:22 +02:00
Alexander a065d64c67 Fix: $font_stack missing from $avatar closure use() clause 2026-05-15 12:41:39 +02:00
Alexander a8511d2a1b Re-skin newsletter to BockenTheme light mode
Drops the editorial-serif palette in favour of the actual
website tokens so the newsletter and the site read as one
product:

- Cream #f8f6f1 page, surface #efecea card patches with #ddd
  hairline borders matching the .card component.
- Open Sans 300/400/500/600 throughout (Google Fonts @import,
  Helvetica/Arial fallback).
- Nord accent colours: nord10 blue for birth + links, nord3
  graphite for death, nord15 mauve for marriage anniversaries.
  Each event row's timeline dot is colour-coded to its event
  type, which gives the right-hand rail a quiet ribbon of
  meaning when several event types appear in one card.
- Soft elevation on cards (1px shadow), thin underlines on
  links — same affordance the site uses.
- Header/footer chrome simplified: small caps kicker in nord10,
  light-weight site title, no ornaments.
2026-05-15 12:38:33 +02:00
Alexander 4ceade9079 Editorial redesign: event icons, timeline, person links
Reworks the newsletter as a family-chronicle layout: ivory paper
background, deep oxblood ink, aged-gold accents, EB Garamond
display with Georgia body fallback.

- Inline SVG event icons (sparkle for birth, dagger for death,
  interlocked rings for marriage). Falls back silently in
  Outlook desktop; modern Gmail / Apple / iOS / Outlook 365
  render them.
- Right-side gold hairline timeline running through the date
  column of every event row, with a filled dot per entry.
- Person names link to their webtrees Individual page via
  Individual::url() (absolute URL through route() → BASE_URL),
  including the avatar circles.
- German strings added for the new section kickers
  ("Family Chronicle", "Living kin who will celebrate this
  fortnight.", "Marriages still intact.").
2026-05-15 12:31:51 +02:00
Alexander a07184ab3a Embed circular profile pictures in newsletter emails
Pull each individual's highlighted media image via webtrees'
Individual::findHighlightedMediaFile, attach as Symfony inline
parts with stable cid:avatar-<xref> identifiers, and render
border-radius:50% on the <img>. Couples on anniversaries show
both spouses' circles side-by-side.

Fallback when no image is available (privacy-hidden record, no
OBJE, external URL, unreadable file): a CSS-only coloured circle
with the person's initials. The hue is derived from a hash of
the XREF so the same person keeps the same colour across
newsletters.

Done via a NewsletterMailer subclass of EmailService that adds a
sendWithEmbeds() method — the parent's transport() and DKIM
config still apply, only the message-construction path differs.
2026-05-15 12:14:29 +02:00
Alexander 7ce8201082 Initial commit: webtrees Email Newsletter module
Recurring email newsletter for webtrees 2.2+. Each enabled tree
sends upcoming birthdays of living individuals, optional marriage
anniversaries of intact couples, and a once-per-calendar-month
historical section of births and deaths of deceased individuals.

Triggered exclusively by an external scheduler (system cron,
systemd timer, etc.) hitting a token-gated HTTP endpoint — never
on visitor page loads. The "is it due?" decision is idempotent
within the configured frequency window.

Per-user subscription is integrated into the built-in
/my-account/{tree} page via a custom view + a decorated
AccountUpdate handler. Admins can add external addresses and
trigger an immediate send for testing. Email body renders in
German for German-language users; English otherwise. Birthdays
and anniversaries are formatted with the upcoming-event ordinal
age (e.g. "45th birthday" / "45. Geburtstag").
2026-05-15 12:00:39 +02:00