{"id":13,"date":"2026-10-25T18:40:42","date_gmt":"2026-10-25T18:40:42","guid":{"rendered":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/home\/"},"modified":"2026-11-04T15:56:35","modified_gmt":"2026-11-04T15:56:35","slug":"home","status":"publish","type":"page","link":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/","title":{"rendered":"Home"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">pmtest-e2a-10251617\u2019s PassageMail Blog and Voyage Tracker<\/h1>\n\n\n\n\n\n<h2 class=\"wp-block-heading\">Voyage Tracker<\/h2>\n\n\n    <div class=\"pm-tracker-block\" style=\"margin:1em 0\">\n            <style>.pm-mapmatch-controls,.pm-fix-controls{padding:8px 12px;background:#fff;color:#0b1a2b;font:13px\/1.4 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif}.pm-fix-controls select,.pm-match-row select{margin-left:6px;max-width:100%;padding:4px}.pm-fix-controls span{color:#607181}.pm-match-row{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;margin-top:5px}.pm-match-status{color:#607181}.pm-poi-marker{display:grid;place-items:center;width:32px;height:32px;padding:0;border:2px solid #fff;border-radius:50%;background:#7c3aed;color:#fff;font:20px sans-serif;box-shadow:0 1px 5px #0006;cursor:pointer;overflow:hidden}.pm-poi-marker img{width:100%;height:100%;object-fit:cover}.pm-poi-icon{border:0;background:none}.pm-poi-post{max-width:280px;max-height:300px;overflow:auto}.pm-poi-post img{max-width:100%;height:auto}<\/style>\n      <div id=\"pm-tr-131ed452-fix\" class=\"pm-fix-controls\"><\/div>\n      <div id=\"pm-tr-131ed452-match\" class=\"pm-mapmatch-controls\"><\/div>\n      <div id=\"pm-tr-131ed452-map\" style=\"height:420px;min-height:300px;width:100%;border-radius:6px;overflow:hidden\"><\/div>\n      <div id=\"pm-tr-131ed452-sum\" style=\"padding:8px 12px;background:#fff;border:1px solid #e2e8ee;border-top:none;border-radius:0 0 6px 6px;color:#607181;font-size:13px;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap\">\n        <span><strong style=\"color:#0b1a2b\">Positions:<\/strong> <span class=\"pm-count\">Loading\u2026<\/span><\/span>\n        <span><strong style=\"color:#0b1a2b\">Latest:<\/strong> <span class=\"pm-latest\">\u2014<\/span><\/span>\n        <span><a href=\"https:\/\/passagemail.com\/\" style=\"color:#e27600\" target=\"_blank\" rel=\"noopener\">PassageMail<\/a><\/span>\n      <\/div>\n    <\/div>\n    <script src=\"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-content\/plugins\/pm-tracker\/assets\/map-view.js?v=20261111-4\"><\/script>\n    <script src=\"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-content\/plugins\/pm-tracker\/assets\/poi.js?v=20261028-4\"><\/script>\n    <script src=\"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-content\/plugins\/pm-tracker\/assets\/mapmatch-owner-route-v1.js?v=20261028-3\"><\/script>\n    <script>\n    window.pmTrackerLoadFeed = window.pmTrackerLoadFeed || async function (seed) {\n  if (seed.owner_preview === true) return seed;\n  const seen = new Set(), tracks = new Map();\n  const positions = [];\n  let latest = seed.latest_at_iso, cursor = seed.next_cursor;\n  function append(page, slug) {\n    if (slug !== seed.slug || !Array.isArray(page.positions) || !Array.isArray(page.tracks)) {\n      throw new Error('Tracker response unavailable');\n    }\n    page.positions.forEach(p => positions.push(p));\n    page.tracks.forEach(t => tracks.set(t.track, t));\n    if ((page.latest_at_iso || '') > (latest || '')) latest = page.latest_at_iso;\n  }\n  try {\n    append(seed, seed.slug);\n    while (cursor !== null && cursor !== '') {\n      if (typeof cursor !== 'string' || seen.has(cursor)) throw new Error('Tracker cursor unavailable');\n      seen.add(cursor);\n      const controller = new AbortController();\n      \/\/ Regular browser HTTP only. No limit on page count or total positions.\n      const timer = setTimeout(() => controller.abort(), 10000);\n      let page;\n      try {\n        const url = seed.api + '\/positions?u=' + encodeURIComponent(seed.slug) + '&cursor=' + encodeURIComponent(cursor);\n        const response = await fetch(url, {\n          credentials: 'omit', redirect: 'error', cache: 'no-store', signal: controller.signal,\n          headers: {Accept: 'application\/json'}\n        });\n        if (response.status !== 200) throw new Error('Tracker unavailable');\n        page = await response.json();\n      } finally { clearTimeout(timer); }\n      append(page, page.blog_slug);\n      cursor = page.next_cursor === undefined ? null : page.next_cursor;\n    }\n    return Object.assign({}, seed, {positions, tracks: Array.from(tracks.values()), latest_at_iso: latest, next_cursor: null});\n  } catch (error) {\n    \/\/ Do not render a partial voyage as complete, empty, or still public after\n    \/\/ a mid-chain visibility change. Callers show an explicit unavailable state.\n    seed.positions = []; seed.tracks = []; seed.latest_at_iso = null;\n    positions.length = 0; tracks.clear();\n    throw error;\n  }\n};    (function () {\n      var RENDERER = \"leaflet\";\n      var MAP_ID = 'pm-tr-131ed452-map';\n      var SUM_ID = 'pm-tr-131ed452-sum';\n      var FIT_MODE = \"\";\n      function updateSum(PM, totalCount) {\n        var sum = document.getElementById(SUM_ID);\n        if (!sum) return;\n        sum.querySelector('.pm-count').textContent = totalCount;\n        sum.querySelector('.pm-latest').textContent = PM.latest_at_iso\n          ? window.pmTrackerFormatTime(PM.latest_at_iso) : '\u2014';\n      }\n\n      async function drawTracker(PM) {\n        var view = await window.pmTrackerCreateMap({ container: MAP_ID, slug: PM.slug, renderer: PM.slug === 'captruss' ? 'leaflet' : RENDERER, voyages: PM.voyages, tracks: PM.tracks });\n        var map = view.map;\n        view.setupFixControls('pm-tr-131ed452-fix');\n        if (typeof window.pmTrackerSetupPOI === 'function' && !PM.owner_preview) {\n          window.pmTrackerSetupPOI({ view: view, slug: PM.slug });\n        }\n        \/\/ GO reports from Mongo\n        var pts = (PM.positions || []).map(function (p) { return [p.lat, p.lon]; });\n        \/\/ App voyages from SQL \u2014 each voyage gets its own polyline.\n        var voyages = (PM.voyages || []).map(function (v) { return PM.slug === 'captruss' ? Object.assign({}, v, {color: '#2780AB'}) : v; });\n        if (PM.slug === 'captruss') {\n          view.legend();\n          view.trackDetails(view.latestPoint(PM.positions || []), view.latestPoint(voyages.flatMap(v => v.positions || [])));\n        }\n        var allPoints = pts.slice();\n        var voyagePolys = [];\n        var voyagePolyById = {};\n        voyages.forEach(function (v) {\n          var vpts = (v.positions || []).map(function (p) { return [p.lat, p.lon]; });\n          if (vpts.length) {\n            var color = v.color || '#2780AB';\n            var vpoly = view.line(vpts, { color: color, weight: 3, opacity: 0.85 }).addTo(map);\n            vpoly.bindTooltip(v.name || 'Track', { sticky: true });\n            voyagePolys.push(vpoly);\n            voyagePolyById[v.client_voyage_id] = vpoly;\n            allPoints = allPoints.concat(vpts);\n          }\n        });\n        view.points(PM.positions || [], 'Iridium GO position', '#e27600');\n        voyages.forEach(function (v) { view.points(v.positions || [], v.name || 'App position', v.color || '#2780AB'); });\n        if (typeof window.pmTrackerSetupMatching === 'function') {\n          window.pmTrackerSetupMatching({ view: view, map: map, voyages: voyages, polylines: voyagePolyById,\n            controlId: 'pm-tr-131ed452-match', slug: PM.slug, api: PM.api });\n        }\n        var totalCount = pts.length + voyages.reduce(function (a, v) { return a + (v.positions_count || (v.positions || []).length); }, 0);\n        var sum = document.getElementById('pm-tr-131ed452-sum');\n        sum.querySelector('.pm-count').textContent = totalCount;\n        sum.querySelector('.pm-latest').textContent = PM.latest_at_iso\n          ? view.formatTime(PM.latest_at_iso) : '\u2014';\n        if (allPoints.length === 0) { view.fit([], FIT_MODE); return; }\n        var goPoly = pts.length ? view.line(pts, { color: '#e27600', weight: 3, opacity: 0.85 }).addTo(map) : null;\n        \/\/ Marker on the newest position across all sources.\n        var latestPt = null, latestMs = -1;\n        (PM.positions || []).forEach(function (p) {\n          var ms = p.fix_at_iso ? Date.parse(p.fix_at_iso) : (p.fix_at_ms || 0);\n          if (ms > latestMs) { latestMs = ms; latestPt = p; }\n        });\n        voyages.forEach(function (v) {\n          (v.positions || []).forEach(function (p) {\n            if ((p.fix_at_ms || 0) > latestMs) { latestMs = p.fix_at_ms; latestPt = p; }\n          });\n        });\n        if (latestPt) view.marker([latestPt.lat, latestPt.lon]).bindPopup(view.positionDetails(latestPt, 'Latest position')).addTo(map);\n        view.fit(allPoints, FIT_MODE);\n      }\n\n      function boot() {\n        window.pmTrackerLoadFeed({\"slug\":\"rae-e2a-10251617\",\"tracker_name\":\"pmtest-e2a-10251617\\u2019s PassageMail Blog and Voyage Tracker\",\"vessel_name\":null,\"latest_at_iso\":null,\"tracks\":[],\"positions\":[],\"api\":\"https:\/\/passagemail.net\/api\/tracker\",\"next_cursor\":null,\"owner_preview\":false,\"voyages\":[]}).then(drawTracker).catch(function () {\n          var m = document.getElementById(MAP_ID); if (m) m.textContent = 'Tracker temporarily unavailable. Please try again.';\n          var s = document.getElementById(SUM_ID); if (s) s.textContent = 'Positions unavailable';\n        });\n      }\n      if (document.readyState === 'complete') boot();\n      else window.addEventListener('load', boot, { once: true });\n    })();\n    <\/script>\n    \n\n\n\n<h2 class=\"wp-block-heading\">Latest Posts<\/h2>\n\n\n\n<div class=\"wp-block-query is-layout-flow wp-block-query-is-layout-flow\"><\/div>\n\n\n","protected":false},"excerpt":{"rendered":"<p>pmtest-e2a-10251617\u2019s PassageMail Blog and Voyage Tracker Voyage Tracker Latest Posts<\/p>\n","protected":false},"author":85,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-13","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/pages\/13","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/users\/85"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/comments?post=13"}],"version-history":[{"count":4,"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/pages\/13\/revisions"}],"predecessor-version":[{"id":27,"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/pages\/13\/revisions\/27"}],"wp:attachment":[{"href":"https:\/\/blog.passagemail.com\/rae-e2a-10251617\/wp-json\/wp\/v2\/media?parent=13"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}