Immich-photo-manager timeline-gaps

install
source · Clone the upstream repo
git clone https://github.com/drolosoft/immich-photo-manager
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/drolosoft/immich-photo-manager "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/timeline-gaps" ~/.claude/skills/drolosoft-immich-photo-manager-timeline-gaps && rm -rf "$T"
manifest: skills/timeline-gaps/SKILL.md
source content

Timeline Gaps

⚠️ Connection Required — ALWAYS CHECK FIRST

Before doing ANYTHING else in this skill, call

ping
on the Immich MCP server.

  • If
    ping
    succeeds → proceed with the skill normally.
  • If
    ping
    fails or the MCP tools are not available → STOP. Do not continue. Tell the user:

Immich is not connected. This plugin needs a running Immich MCP server to work.

Run /setup-immich-photo-manager to configure your Immich connection. You'll need:

  1. Your Immich server URL (e.g.,
    http://192.168.1.100:2283
    )
  2. An Immich API key (how to create one)
  3. The MCP server configured (see /setup-immich-photo-manager)

Nothing in this plugin will work until the connection is configured.

Do NOT skip this check. Do NOT try to run any other tool first. Always ping, always block if it fails.

Analyze the photo timeline month by month to detect gaps, anomalies, and coverage issues across import sources. Helps users discover missing imports, backup failures, or periods where photos only exist in one ecosystem.

When to Use

  • After importing from multiple sources to verify nothing was missed
  • Periodic checkup to ensure continuous coverage
  • Before deleting an import source to verify the other source covers those periods
  • Investigating why certain memories seem to be missing

Analysis Workflow

Step 1: Build Monthly Timeline

Generate a complete month-by-month matrix across all sources:

WITH months AS (
  SELECT generate_series(
    date_trunc('month', min("localDateTime")),
    date_trunc('month', max("localDateTime")),
    '1 month'::interval
  ) as month
  FROM asset WHERE "deletedAt" IS NULL
),
source_counts AS (
  SELECT
    date_trunc('month', "localDateTime") as month,
    CASE
      WHEN "originalPath" LIKE '%Apple%' THEN 'Apple'
      WHEN "originalPath" LIKE '%Google%' THEN 'Google'
      ELSE 'Other'
    END as source,
    count(*) as cnt
  FROM asset WHERE "deletedAt" IS NULL
  GROUP BY 1, 2
)
SELECT
  m.month,
  coalesce(sum(cnt) FILTER (WHERE source = 'Apple'), 0) as apple,
  coalesce(sum(cnt) FILTER (WHERE source = 'Google'), 0) as google,
  coalesce(sum(cnt) FILTER (WHERE source = 'Other'), 0) as other,
  coalesce(sum(cnt), 0) as total
FROM months m
LEFT JOIN source_counts sc ON m.month = sc.month
GROUP BY m.month
ORDER BY m.month;

Step 2: Classify Each Month

Apply classification rules:

ClassificationRuleAction
EMPTY0 photos totalFlag as critical gap
SPARSE<10 photos AND user averaged >50/month that yearFlag as suspicious
SINGLE-SOURCEOne source has >90% of photosNote dependency risk
NORMALAbove thresholdsNo action needed
# Classification logic
avg_monthly = total_photos / total_months

for month in timeline:
    if month.total == 0:
        month.status = 'EMPTY'
    elif month.total < max(10, avg_monthly * 0.1):
        month.status = 'SPARSE'
    elif month.dominant_source_pct > 0.9 and len(sources) > 1:
        month.status = 'SINGLE_SOURCE'
    else:
        month.status = 'NORMAL'

Step 3: Detect Patterns

Look for systematic issues:

-- Consecutive empty months (indicates a bulk import failure)
-- Alternating source dominance (normal for dual-ecosystem users)
-- Sudden drops in a source (might indicate sync stopped)
-- Recent months with much lower counts (import not yet complete?)

Step 4: Generate Report

TIMELINE ANALYSIS
══════════════════════════════════════

Coverage: Jan 2014 → Mar 2026 (147 months)

GAPS FOUND
  Empty months:      3
    - Aug 2015: 0 photos (Apple: 0, Google: 0)
    - Feb 2016: 0 photos (Apple: 0, Google: 0)
    - Nov 2019: 0 photos (Apple: 0, Google: 0)

  Sparse months:     7
    - Jan 2015: 4 photos (avg for 2015: 89/month)
    - Mar 2017: 2 photos (avg for 2017: 112/month)
    ...

SOURCE COVERAGE
  Apple Photos:  Jan 2016 → Mar 2026 (dominates 2016, 2018, 2024-2026)
  Google Photos: Mar 2014 → Dec 2023 (dominates 2017, 2019-2023)

  Single-source months: 48 (32%)
    Apple-only: 28 months
    Google-only: 20 months

YEAR OVERVIEW
  2014: ████░░░░░░░░  142 photos (Google only)
  2015: ████████░░░░  891 photos (sparse in Aug)
  2016: ██████████░░  1,204 photos (Apple dominant)
  ...

RECOMMENDATIONS
  1. Investigate Aug 2015, Feb 2016, Nov 2019 — check backups
  2. Google Photos ends Dec 2023 — intentional or missed import?
  3. 7 sparse months may indicate partial imports — cross-check with phone backups

Step 5: Visual Timeline (Optional)

If the user wants an HTML output, generate an interactive timeline using a heatmap grid:

  • Rows = years, columns = months
  • Color intensity = photo count
  • Color hue = dominant source (blue=Apple, red=Google, green=Other)
  • Hover shows exact counts per source
  • Empty cells highlighted in yellow/red

Cross-Source Gap Analysis

For users with multiple import sources, check if gaps in one source are covered by another:

-- Months where Apple has photos but Google doesn't
SELECT date_trunc('month', "localDateTime") as month, count(*)
FROM asset
WHERE "deletedAt" IS NULL AND "originalPath" LIKE '%Apple%'
AND date_trunc('month', "localDateTime") NOT IN (
  SELECT DISTINCT date_trunc('month', "localDateTime")
  FROM asset WHERE "deletedAt" IS NULL AND "originalPath" LIKE '%Google%'
)
GROUP BY 1 ORDER BY 1;

This answers: "If I delete all Google photos, which months would I lose coverage for?"

Important Notes

  • Read-only — this skill never modifies data
  • Uses
    localDateTime
    (not
    createdAt
    ) for accurate chronological analysis
  • Photos with suspicious dates (midnight/noon) are flagged but still counted
  • The "sparse" threshold adapts to the user's average volume — what's sparse for a heavy photographer is different from a casual one
  • Generate_series requires PostgreSQL — this won't work with SQLite