{"connections":[{"sourceLocalID":5,"sourcePortID":"outlet_output","targetLocalID":8,"targetPortID":"inlet_input"},{"sourceLocalID":0,"sourcePortID":"outlet_output","targetLocalID":4,"targetPortID":"inlet_input"},{"sourceLocalID":2,"sourcePortID":"outlet_output","targetLocalID":5,"targetPortID":"inlet_input"},{"sourceLocalID":4,"sourcePortID":"outlet_submitted","targetLocalID":1,"targetPortID":"inlet_input"},{"sourceLocalID":6,"sourcePortID":"outlet_output","targetLocalID":2,"targetPortID":"inlet_input"},{"sourceLocalID":8,"sourcePortID":"outlet_yes","targetLocalID":0,"targetPortID":"inlet_input"}],"hideGrid":false,"name":"In Your Face Meeting Reminder","nodes":[{"config":{"html":"<!doctype html>\n<html>\n  <head>\n    <meta charset='utf-8'>\n    <style>\n  html, body { margin: 0; padding: 0; height: 100%; }\n  body {\n    background: rgba(40, 6, 6, 0.2);\n    font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', Helvetica, Arial, sans-serif;\n    color: #fff;\n    display: flex; align-items: center; justify-content: center;\n    -webkit-font-smoothing: antialiased;\n  }\n  .card {\n    width: min(760px, 86vw);\n    padding: 40px 48px;\n    border-radius: 24px;\n    background: rgba(140, 18, 18, 0.85);\n    border: 1px solid rgba(255, 120, 120, 0.35);\n    box-shadow: 0 30px 80px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.12);\n    backdrop-filter: blur(18px);\n    -webkit-backdrop-filter: blur(18px);\n    text-align: center;\n  }\n  .eyebrow {\n    font-size: 1em; letter-spacing: 0.22em; text-transform: uppercase;\n    color: white; margin-bottom: 20px;\n  }\n  h1 {\n    margin: 0 0 20px; font-size: 24px; font-weight: 700; line-height: 1.2;\n    letter-spacing: -0.01em;\n  }\n  .title {\n    font-size: 20px; font-weight: 500; color: rgba(255,255,255,0.92);\n    margin: 0 0 6px; line-height: 1.35;\n  }\n  .time { font-size: 15px; color: rgba(255, 210, 210, 0.8); margin-bottom: 30px; }\n  .actions { display: flex; gap: 12px; justify-content: center; }\n  button {\n    font: inherit; font-size: 15px; font-weight: 600;\n    padding: 12px 26px; border-radius: 12px; cursor: pointer;\n    border: 1px solid rgba(255,255,255,0.35); transition: transform .08s ease, background .15s ease;\n  }\n  button:hover { transform: translateY(-1px); }\n  .primary { background: rgba(255,255,255,0.95); color: #8c1212; border-color: transparent; }\n  .primary:hover { background: #fff; }\n  .secondary { background: rgba(255,255,255,0.10); color: #fff; }\n  .secondary:hover { background: rgba(255,255,255,0.18); }\n</style>\n  </head>\n  <body>\n    <div class='card'>\n      <div class='eyebrow'>Meeting starting</div>\n      <h1>A meeting you're supposed to be in is about to start.</h1>\n      <p class='title'>{{output.meetingTitle}}</p>\n      <div class='time'>Starts {{output.meetingStart}}</div>\n      <div class='actions'>\n        <button class='primary' id='go'>Open Meeting</button>\n      <button class='secondary' onclick='watchflows.dismiss()'>Dismiss</button>\n      </div>\n    </div>\n    <script>\n    var url = '{{output.meetingURL}}';\n    var go = document.getElementById('go');\n    if (!url) { go.textContent = 'Got it'; }\n    go.addEventListener('click', function () {\n      if (url) { watchflows.emit({ url: url }); } else { watchflows.dismiss(); }\n    });\n    setInterval(function(){ if (window.watchflows && watchflows.heartbeat) watchflows.heartbeat(); }, 30000);\n  </script>\n  </body>\n</html>","outputKey":"html"},"customName":"Meeting Takeover Page","isDisabled":false,"localID":0,"typeID":"transformer.html_template","x":1500,"y":260},{"config":{"allow_non_http_schemes":false,"url":"{{submitted.url}}"},"customName":"Open Meeting Link","isDisabled":false,"localID":1,"typeID":"action.open_url","x":2200,"y":380},{"config":{"days":7,"hours":2,"includeAllDay":false,"maxEvents":50,"range":"next_hours"},"customName":"Read Upcoming Meetings","isDisabled":false,"localID":2,"typeID":"action.apple_calendar_list_events","x":460,"y":260},{"config":{"text":"**Find Starting Meeting** reads `leadSeconds` from **Every Minute** and picks the first timed event that starts that many seconds from now. It checks a one-minute window, so each meeting shows once. It takes a join link from the event's location or notes. The card still appears without one. It uses the JavaScript built into macOS, so there is nothing to install."},"isDisabled":false,"localID":3,"typeID":"note","x":800,"y":460},{"config":{"chrome":"none","closeIsDismiss":true,"height":600,"html":"{{html}}","timeoutMinutes":15,"title":"Meeting is starting","width":900,"windowMode":"fullscreen"},"customName":"Meeting Is Starting","isDisabled":false,"localID":4,"typeID":"action.html_view","x":1860,"y":320},{"config":{"outputSchema":"{\n  \"required\": [\n    \"shouldShow\",\n    \"meetingTitle\",\n    \"meetingURL\",\n    \"meetingStart\"\n  ],\n  \"properties\": {\n    \"shouldShow\": {\n      \"type\": \"boolean\"\n    },\n    \"meetingTitle\": {\n      \"type\": \"string\"\n    },\n    \"meetingURL\": {\n      \"type\": \"string\"\n    },\n    \"meetingStart\": {\n      \"type\": \"string\"\n    }\n  }\n}","script":"# Runs with tools every Mac has: /bin/bash hands the script below to\n# /usr/bin/osascript as JavaScript. No Python, no developer tools.\n/usr/bin/osascript -l JavaScript <<'JS'\nfunction run() {\n  var env = $.NSProcessInfo.processInfo.environment;\n  function getenv(name) {\n    var v = ObjC.unwrap(env.objectForKey(name));\n    return typeof v === 'string' ? v : undefined;\n  }\n\n  // Lead time in seconds. Missing, empty or not a number means 300.\n  var rawLead = getenv('PAYLOAD_leadSeconds') || '300';\n  var lead = 300;\n  if (/^\\s*[+-]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?\\s*$/.test(rawLead)) {\n    var n = Math.trunc(Number(rawLead));\n    if (isFinite(n)) { lead = n; }\n  }\n  var poll = 60; // the schedule fires every 60s; show only in the one window that crosses the lead mark\n\n  // Events from Read Upcoming Meetings. Anything unreadable means no events.\n  var events = [];\n  try { events = JSON.parse(getenv('PAYLOAD_events') || '[]'); } catch (err) { events = []; }\n  if (!Array.isArray(events)) { events = []; }\n\n  function daysIn(y, m) { var t = new Date(0); t.setUTCFullYear(y, m, 0); return t.getUTCDate(); }\n\n  // ISO 8601 start time, for example 2026-10-04T14:00:00Z or 2026-10-04T16:00:00+02:00.\n  // Returns epoch milliseconds, or null when the text is not a valid time.\n  function parseISO(value) {\n    if (typeof value !== 'string' || !value) { return null; }\n    var s = value.replace(/Z/g, '+00:00');\n    var m = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[\\s\\S](\\d{2})(?::(\\d{2})(?::(\\d{2})(?:\\.(\\d{3}|\\d{6}))?)?)?)?(?:([+-])(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{6}))?)?)?$/.exec(s);\n    if (!m) { return null; }\n    var y = +m[1], mo = +m[2], d = +m[3];\n    var h = m[4] ? +m[4] : 0, mi = m[5] ? +m[5] : 0, se = m[6] ? +m[6] : 0;\n    var ms = m[7] ? Math.floor(+(m[7] + '000000').slice(0, 6) / 1000) : 0;\n    if (y < 1 || mo < 1 || mo > 12 || d < 1 || d > daysIn(y, mo) || h > 23 || mi > 59 || se > 59) { return null; }\n    var t = new Date(0);\n    var offsetMs = 0;\n    if (m[8]) {\n      offsetMs = ((+m[9]) * 3600 + (+m[10]) * 60 + (m[11] ? +m[11] : 0)) * 1000;\n      if (+m[10] > 59 || (m[11] && +m[11] > 59) || offsetMs >= 86400000) { return null; }\n      if (m[8] === '-') { offsetMs = -offsetMs; }\n      t.setUTCFullYear(y, mo - 1, d);\n      t.setUTCHours(h, mi, se, ms);\n      return t.getTime() - offsetMs;\n    }\n    // No offset: read it as local time.\n    t.setFullYear(y, mo - 1, d);\n    t.setHours(h, mi, se, ms);\n    return t.getTime();\n  }\n\n  // Python's whitespace, so a link ends where the old script ended it.\n  var link = new RegExp('https?://[^\\\\t\\\\n\\\\v\\\\f\\\\r\\\\x1c-\\\\x1f \\\\x85\\\\xa0\\\\u1680\\\\u2000-\\\\u200a\\\\u2028\\\\u2029\\\\u202f\\\\u205f\\\\u3000]+');\n  function text(v) { return typeof v === 'string' ? v : (v === undefined || v === null ? '' : String(v)); }\n  function pad(x) { return (x < 10 ? '0' : '') + x; }\n\n  var now = Date.now();\n  var out = { shouldShow: false, meetingTitle: '', meetingURL: '', meetingStart: '' };\n\n  for (var i = 0; i < events.length; i++) {\n    var e = events[i];\n    if (!e || typeof e !== 'object' || Array.isArray(e)) { continue; }\n    var start = parseISO(e.startISO);\n    if (start === null) { continue; }\n    var secs = (start - now) / 1000;\n    if ((lead - poll) < secs && secs <= lead) {\n      var url = '';\n      var fields = [e.location, e.notes];\n      for (var f = 0; f < fields.length; f++) {\n        var found = link.exec(typeof fields[f] === 'string' ? fields[f] : '');\n        if (found) { url = found[0].replace(/[>.,)]+$/, ''); break; }\n      }\n      var local = new Date(start);\n      out = {\n        shouldShow: true,\n        meetingTitle: text(e.title),\n        meetingURL: url,\n        meetingStart: pad(local.getHours()) + ':' + pad(local.getMinutes())\n      };\n      break;\n    }\n  }\n\n  // ASCII-only JSON, the same bytes for any title in any language.\n  return JSON.stringify(out).replace(/[\\u007f-￿]/g, function (c) {\n    return '\\\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);\n  });\n}\nJS\n","shell":"/bin/bash"},"customName":"Find Starting Meeting","isDisabled":false,"localID":5,"typeID":"action.script","x":800,"y":320},{"config":{"customArgs":{"leadSeconds":"60"},"customSeconds":60,"interval":"1m","mode":"interval","time":"07:00","weekdays":"mon,tue,wed,thu,fri"},"customName":"Every Minute","isDisabled":false,"localID":6,"typeID":"trigger.schedule","x":100,"y":200},{"config":{"_height":520,"_width":460,"text":"**In Your Face Meeting Reminder covers your screen just before a meeting starts, so you cannot miss it.**\n\n**What it does**\n- Checks your calendars every minute while the flow is on.\n- About one minute before a timed event starts, a full-screen card shows its title and start time. All-day events are skipped.\n- If the event's location or notes contain a web link, Open Meeting opens it. Otherwise the button reads Got it and closes the card.\n\n**Set it up**\n- Allow full access to Calendars when macOS asks. If you said no before, turn Watchflows on in System Settings, Privacy & Security, Calendars.\n- Turn the flow on.\n\n**Make it yours**\n- To be warned earlier, change `leadSeconds` in Custom Arguments on **Every Minute**. Use 60 or more, for example 300 for five minutes.\n- To watch one calendar only, pick it in Calendar on **Read Upcoming Meetings**.\n- Change the wording and colours in the HTML on **Meeting Takeover Page**."},"isDisabled":false,"localID":7,"typeID":"note","x":100,"y":-360},{"config":{"caseInsensitive":false,"field":"","logic":"and","operator":"","rules":[{"field":"output.shouldShow","negate":false,"operator":"equals","value":"true"}],"value":""},"customName":"Meeting Starts Now","isDisabled":false,"localID":8,"typeID":"condition.compare","x":1160,"y":380}],"version":1}