> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canadava.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Aéroports VATSIM actifs

> Couverture ATIS VATSIM en direct pour les huit aéroports canadiens couverts par vACA. Voyez quels terrains ont un contrôleur ATIS en ligne en ce moment, consultez la transcription complète et le METAR courant, puis sautez directement à l'aperçu du terrain.

export const ActiveAtis = ({airports = [], title = "Active VATSIM Airports", loadingLabel = "Tuning broadcasts", errorLabel = "Unable to reach VATSIM data feed", refreshLabel = "Refresh", offlineLabel = "No ATIS online", frequencyLabel = "Frequency", transcriptLabel = "Transcript", viewBriefingLabel = "View briefing", observedLabel = "Updated", agoLabel = "min ago", justNowLabel = "just now", noActiveLabel = "No VATSIM ATIS broadcasts in coverage right now. The feed refreshes every minute.", countSuffixLabel = "live", arrivalsLabel = "Arrivals", departuresLabel = "Departures", weatherLabel = "Weather", windLabel = "Wind", visibilityLabel = "Visibility", cloudsLabel = "Clouds", tempDewLabel = "Temp · Dew", altimeterLabel = "Altimeter", calmLabel = "Calm", variableLabel = "Variable", gustingLabel = "gusting", fromLabel = "from", atLabel = "at", metarObservedLabel = "Observed", metarLoadingLabel = "Fetching latest observation", metarErrorLabel = "Unable to fetch live METAR."}) => {
  const WX_CODES = {
    MI: "shallow",
    PR: "partial",
    BC: "patches",
    DR: "drifting",
    BL: "blowing",
    SH: "showers",
    TS: "thunderstorm",
    FZ: "freezing",
    DZ: "drizzle",
    RA: "rain",
    SN: "snow",
    SG: "snow grains",
    IC: "ice crystals",
    PL: "ice pellets",
    GR: "hail",
    GS: "small hail",
    UP: "unknown precip",
    BR: "mist",
    FG: "fog",
    FU: "smoke",
    VA: "volcanic ash",
    DU: "dust",
    SA: "sand",
    HZ: "haze",
    PY: "spray",
    PO: "dust whirls",
    SQ: "squalls",
    FC: "funnel cloud",
    SS: "sandstorm",
    DS: "duststorm"
  };
  const CLOUD_LABELS = {
    FEW: "Few",
    SCT: "Scattered",
    BKN: "Broken",
    OVC: "Overcast",
    CLR: "Clear",
    SKC: "Sky clear",
    NCD: "No clouds",
    NSC: "No significant clouds",
    VV: "Vertical visibility"
  };
  const parseMetar = input => {
    if (!input || typeof input !== "string") return null;
    const beforeRmk = input.split(/\s+RMK\b/)[0].trim();
    if (beforeRmk.length < 10) return null;
    const tokens = beforeRmk.split(/\s+/);
    let i = 0;
    if (tokens[i] === "METAR" || tokens[i] === "SPECI") i++;
    let station = null;
    if ((/^[A-Z]{4}$/).test(tokens[i] || "")) station = tokens[i++];
    let time = null;
    const tm = (tokens[i] || "").match(/^(\d{2})(\d{2})(\d{2})Z$/);
    if (tm) {
      time = {
        day: +tm[1],
        hour: +tm[2],
        minute: +tm[3]
      };
      i++;
    }
    while (tokens[i] === "AUTO" || tokens[i] === "COR" || tokens[i] === "NIL") i++;
    let wind = null;
    const wt = tokens[i] || "";
    if ((/^00000(KT|MPS|KMH)$/).test(wt)) {
      wind = {
        calm: true
      };
      i++;
    } else {
      const wm = wt.match(/^(VRB|\d{3})(\d{2,3})(?:G(\d{2,3}))?(KT|MPS|KMH)$/);
      if (wm) {
        wind = {
          dir: wm[1] === "VRB" ? "VRB" : +wm[1],
          speed: +wm[2],
          gust: wm[3] ? +wm[3] : null,
          unit: wm[4]
        };
        i++;
        if ((/^\d{3}V\d{3}$/).test(tokens[i] || "")) i++;
      }
    }
    let visibility = null;
    if (tokens[i] === "CAVOK") {
      visibility = {
        cavok: true
      };
      i++;
    } else if ((/^\d{4}$/).test(tokens[i] || "")) {
      visibility = {
        value: +tokens[i],
        unit: "m"
      };
      i++;
    } else if ((/^\d+$/).test(tokens[i] || "") && (/^\d+\/\d+SM$/).test(tokens[i + 1] || "")) {
      const whole = +tokens[i];
      const fm = tokens[i + 1].match(/^(\d+)\/(\d+)SM$/);
      visibility = {
        value: whole + +fm[1] / +fm[2],
        unit: "SM"
      };
      i += 2;
    } else {
      const sm = (tokens[i] || "").match(/^(M)?(\d+(?:\/\d+)?)SM$/);
      if (sm) {
        const v = sm[2].includes("/") ? +sm[2].split("/")[0] / +sm[2].split("/")[1] : +sm[2];
        visibility = {
          value: v,
          unit: "SM",
          lessThan: !!sm[1]
        };
        i++;
      }
    }
    const weather = [];
    const wxRe = /^(VC|\+|-)?((?:MI|PR|BC|DR|BL|SH|TS|FZ){0,2})((?:DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)+)$/;
    while (i < tokens.length && wxRe.test(tokens[i])) {
      weather.push(tokens[i]);
      i++;
    }
    const clouds = [];
    while (i < tokens.length) {
      const t = tokens[i];
      if (t === "CLR" || t === "SKC" || t === "NCD" || t === "NSC") {
        clouds.push({
          cover: t,
          base: null
        });
        i++;
      } else {
        const cm = t.match(/^(FEW|SCT|BKN|OVC)(\d{3})(CB|TCU)?$/);
        if (cm) {
          clouds.push({
            cover: cm[1],
            base: +cm[2] * 100,
            type: cm[3] || null
          });
          i++;
          continue;
        }
        const vvm = t.match(/^VV(\d{3}|\/\/\/)$/);
        if (vvm) {
          clouds.push({
            cover: "VV",
            base: vvm[1] === "///" ? null : +vvm[1] * 100
          });
          i++;
          continue;
        }
        break;
      }
    }
    let temp = null, dewpoint = null;
    const td = (tokens[i] || "").match(/^(M)?(\d{2})\/(M)?(\d{2})$/);
    if (td) {
      temp = (td[1] ? -1 : 1) * +td[2];
      dewpoint = (td[3] ? -1 : 1) * +td[4];
      i++;
    }
    let altimeter = null;
    const altA = (tokens[i] || "").match(/^A(\d{4})$/);
    const altQ = (tokens[i] || "").match(/^Q(\d{4})$/);
    if (altA) {
      altimeter = {
        value: +altA[1] / 100,
        unit: "inHg"
      };
      i++;
    } else if (altQ) {
      altimeter = {
        value: +altQ[1],
        unit: "hPa"
      };
      i++;
    }
    const ceilingLayer = clouds.find(c => c.cover === "BKN" || c.cover === "OVC");
    const ceiling = ceilingLayer ? ceilingLayer.base : null;
    let visSM = Infinity;
    if (visibility?.cavok) visSM = 10; else if (visibility?.unit === "SM") visSM = visibility.value; else if (visibility?.unit === "m") visSM = visibility.value / 1609;
    let flightCategory = "VFR";
    if (ceiling !== null && ceiling < 500 || visSM < 1) flightCategory = "LIFR"; else if (ceiling !== null && ceiling < 1000 || visSM < 3) flightCategory = "IFR"; else if (ceiling !== null && ceiling < 3000 || visSM <= 5) flightCategory = "MVFR";
    return {
      station,
      time,
      wind,
      visibility,
      weather,
      clouds,
      temp,
      dewpoint,
      altimeter,
      flightCategory,
      raw: input
    };
  };
  const normalized = (airports || []).filter(a => a && a.icao).map(a => ({
    ...a,
    icao: a.icao.toUpperCase()
  }));
  const icaoSet = normalized.map(a => a.icao);
  const [feed, setFeed] = useState(null);
  const [metars, setMetars] = useState({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [refreshing, setRefreshing] = useState(false);
  const [expanded, setExpanded] = useState(() => new Set());
  const [nowTick, setNowTick] = useState(Date.now());
  const fetchFeed = async isUserRefresh => {
    if (icaoSet.length === 0) return;
    try {
      if (isUserRefresh) setRefreshing(true); else setLoading(true);
      setError(null);
      const res = await fetch("https://data.vatsim.net/v3/vatsim-data.json", {
        cache: "no-store"
      });
      if (!res.ok) throw new Error("http");
      const json = await res.json();
      const all = Array.isArray(json.atis) ? json.atis : [];
      const re = new RegExp(`^(${icaoSet.join("|")})(?:_[AD])?_ATIS$`, "i");
      const byIcao = {};
      for (const a of all) {
        const cs = (a.callsign || "").toUpperCase();
        const m = cs.match(re);
        if (!m) continue;
        const base = m[1];
        const split = cs.includes("_A_ATIS") ? "arrival" : cs.includes("_D_ATIS") ? "departure" : null;
        const text = Array.isArray(a.text_atis) ? a.text_atis.join(" ") : "";
        const broadcast = {
          callsign: a.callsign,
          atisCode: a.atis_code || "",
          frequency: a.frequency,
          text,
          lastUpdated: a.last_updated,
          split
        };
        if (!byIcao[base]) byIcao[base] = [];
        byIcao[base].push(broadcast);
      }
      setFeed(byIcao);
    } catch (_) {
      setError("fetch");
    } finally {
      setLoading(false);
      if (isUserRefresh) setTimeout(() => setRefreshing(false), 600);
    }
  };
  const fetchOneMetar = async code => {
    try {
      const res = await fetch(`https://metar.vatsim.net/${code}`, {
        cache: "no-store"
      });
      if (!res.ok) return null;
      const text = (await res.text()).trim();
      if (!text || !(/[A-Z]{4}\s+\d{6}Z/).test(text)) return null;
      return text;
    } catch (_) {
      return null;
    }
  };
  const fetchAllMetars = async () => {
    if (icaoSet.length === 0) return;
    const entries = await Promise.all(icaoSet.map(async code => [code, await fetchOneMetar(code)]));
    const next = {};
    for (const [code, raw] of entries) next[code] = raw;
    setMetars(next);
  };
  useEffect(() => {
    fetchFeed(false);
    const interval = setInterval(() => fetchFeed(false), 60 * 1000);
    return () => clearInterval(interval);
  }, []);
  useEffect(() => {
    fetchAllMetars();
    const interval = setInterval(fetchAllMetars, 5 * 60 * 1000);
    return () => clearInterval(interval);
  }, []);
  useEffect(() => {
    const t = setInterval(() => setNowTick(Date.now()), 30 * 1000);
    return () => clearInterval(t);
  }, []);
  const toggle = icao => {
    setExpanded(prev => {
      const next = new Set(prev);
      if (next.has(icao)) next.delete(icao); else next.add(icao);
      return next;
    });
  };
  const formatFreq = f => {
    if (!f) return "---.---";
    const n = parseFloat(f);
    if (Number.isNaN(n)) return f;
    return n.toFixed(3);
  };
  const ageLabel = iso => {
    if (!iso) return null;
    const t = Date.parse(iso);
    if (Number.isNaN(t)) return null;
    const diffMin = Math.max(0, Math.round((nowTick - t) / 60000));
    if (diffMin === 0) return justNowLabel;
    return `${diffMin} ${agoLabel}`;
  };
  const formatWind = w => {
    if (!w) return "—";
    if (w.calm) return calmLabel;
    const dir = w.dir === "VRB" ? variableLabel : `${String(w.dir).padStart(3, "0")}°`;
    let s = `${fromLabel} ${dir} ${atLabel} ${w.speed} kt`;
    if (w.gust) s += `, ${gustingLabel} ${w.gust} kt`;
    return s;
  };
  const formatVisibility = v => {
    if (v.cavok) return "CAVOK";
    if (v.unit === "SM") {
      const prefix = v.lessThan ? "<" : "";
      const val = Number.isInteger(v.value) ? v.value : v.value.toFixed(1);
      return `${prefix}${val} SM`;
    }
    if (v.unit === "m") {
      if (v.value >= 9999) return "≥10 km";
      if (v.value >= 1000) return `${(v.value / 1000).toFixed(1)} km`;
      return `${v.value} m`;
    }
    return "";
  };
  const formatWeather = wxList => wxList.map(tok => {
    let intensity = "";
    let rest = tok;
    if (rest.startsWith("+")) {
      intensity = "Heavy ";
      rest = rest.slice(1);
    } else if (rest.startsWith("-")) {
      intensity = "Light ";
      rest = rest.slice(1);
    } else if (rest.startsWith("VC")) {
      intensity = "Vicinity ";
      rest = rest.slice(2);
    }
    const parts = [];
    for (let p = 0; p < rest.length; p += 2) {
      const w = WX_CODES[rest.substr(p, 2)];
      if (w) parts.push(w);
    }
    const phrase = (intensity + parts.join(" ")).trim();
    return phrase.charAt(0).toUpperCase() + phrase.slice(1);
  }).join(", ");
  const formatClouds = cs => cs.map(c => {
    const lbl = CLOUD_LABELS[c.cover] || c.cover;
    if (c.base == null) return lbl;
    const ft = c.base.toLocaleString("en-US");
    const type = c.type === "CB" ? " (CB)" : c.type === "TCU" ? " (TCU)" : "";
    return `${lbl} ${ft} ft${type}`;
  }).join(", ");
  const formatTempDew = (t, d) => {
    if (t == null) return null;
    const toF = c => Math.round(c * 9 / 5 + 32);
    let s = `${t}°C / ${toF(t)}°F`;
    if (d != null) s += ` · ${d}°C / ${toF(d)}°F`;
    return s;
  };
  const formatAltimeter = a => {
    if (a.unit === "inHg") return `${a.value.toFixed(2)} inHg · ${Math.round(a.value * 33.8639)} hPa`;
    return `${a.value} hPa · ${(a.value / 33.8639).toFixed(2)} inHg`;
  };
  const formatMetarObserved = metar => {
    if (!metar?.time) return null;
    const now = new Date();
    let year = now.getUTCFullYear();
    let month = now.getUTCMonth();
    if (metar.time.day > now.getUTCDate()) {
      month -= 1;
      if (month < 0) {
        month = 11;
        year -= 1;
      }
    }
    const obs = new Date(Date.UTC(year, month, metar.time.day, metar.time.hour, metar.time.minute));
    if (isNaN(obs)) return null;
    const hh = String(obs.getUTCHours()).padStart(2, "0");
    const mm = String(obs.getUTCMinutes()).padStart(2, "0");
    const diffMin = Math.max(0, Math.round((nowTick - obs.getTime()) / 60000));
    return `${metarObservedLabel} ${hh}${mm}Z · ${diffMin === 0 ? justNowLabel : `${diffMin} ${agoLabel}`}`;
  };
  const rows = normalized.map(a => {
    const broadcasts = feed && feed[a.icao] ? feed[a.icao] : [];
    broadcasts.sort((x, y) => {
      const order = {
        arrival: 0,
        departure: 1
      };
      const a1 = x.split == null ? 2 : order[x.split];
      const b1 = y.split == null ? 2 : order[y.split];
      return a1 - b1;
    });
    const primary = broadcasts[0] || null;
    return {
      meta: a,
      broadcasts,
      isOnline: broadcasts.length > 0,
      primary
    };
  }).sort((x, y) => {
    if (x.isOnline !== y.isOnline) return x.isOnline ? -1 : 1;
    return x.meta.icao.localeCompare(y.meta.icao);
  });
  const onlineCount = rows.filter(r => r.isOnline).length;
  const total = rows.length;
  const anyOnline = onlineCount > 0;
  const mostRecentUpdate = rows.flatMap(r => r.broadcasts.map(b => b.lastUpdated)).filter(Boolean).sort().pop();
  const renderMetarCard = (code, name) => {
    const raw = metars[code];
    if (raw === undefined) {
      return <div className="kb-metar">
          <div className="kb-metar-header">
            <span className="kb-metar-eyebrow">{weatherLabel}</span>
          </div>
          <div className="kb-metar-body">
            <div className="kb-metar-content">
              <div className="kb-metar-skeleton kb-metar-skeleton--long" />
              <div className="kb-metar-skeleton kb-metar-skeleton--short" />
              <div className="kb-metar-meta">
                <span className="kb-metar-meta-status">{metarLoadingLabel}</span>
              </div>
            </div>
          </div>
        </div>;
    }
    if (raw === null) {
      return <div className="kb-metar">
          <div className="kb-metar-header">
            <span className="kb-metar-eyebrow">{weatherLabel}</span>
          </div>
          <div className="kb-metar-body">
            <div className="kb-metar-content">
              <div className="kb-metar-error">{metarErrorLabel}</div>
            </div>
          </div>
        </div>;
    }
    const metar = parseMetar(raw);
    if (!metar) {
      return <div className="kb-metar">
          <div className="kb-metar-header">
            <span className="kb-metar-eyebrow">{weatherLabel}</span>
          </div>
          <div className="kb-metar-body">
            <div className="kb-metar-content">
              <pre className="kb-metar-raw">{raw}</pre>
            </div>
          </div>
        </div>;
    }
    const decoder = `https://metar-taf.com/${code}`;
    const windDeg = typeof metar.wind?.dir === "number" ? metar.wind.dir : null;
    const obsLabel = formatMetarObserved(metar);
    return <div className="kb-metar">
        <div className="kb-metar-header">
          <span className="kb-metar-eyebrow">{weatherLabel}</span>
          <a className="kb-metar-decoder-link" href={decoder} target="_blank" rel="noopener noreferrer">
            Metar & Taf Visual decoder
            <svg className="kb-metar-decoder-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <path d="M7 7h10v10" />
              <path d="M7 17 17 7" />
            </svg>
          </a>
        </div>
        <div className="kb-metar-body">
          <div className="kb-metar-icon" aria-hidden="true">
            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2" />
              <path d="M9.6 4.6A2 2 0 1 1 11 8H2" />
              <path d="M12.6 19.4A2 2 0 1 0 14 16H2" />
            </svg>
          </div>
          <div className="kb-metar-content">
            <div className="kb-metar-title">
              <span className="kb-metar-icao">{code}</span>
              {name ? <span className="kb-metar-name">{name}</span> : null}
              {metar.flightCategory && <span className={`kb-metar-cat kb-metar-cat--${metar.flightCategory.toLowerCase()}`}>
                  {metar.flightCategory}
                </span>}
            </div>
            <pre className="kb-metar-raw">{metar.raw}</pre>
            <div className="kb-metar-decoded">
              {metar.wind && <div className="kb-metar-stat">
                  <span className="kb-metar-stat-label">{windLabel}</span>
                  <span className="kb-metar-stat-value">
                    {windDeg !== null && <svg className="kb-metar-wind-arrow" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{
      transform: `rotate(${windDeg}deg)`
    }}>
                        <polygon points="12 2 19 21 12 17 5 21 12 2" />
                      </svg>}
                    {formatWind(metar.wind)}
                  </span>
                </div>}
              {metar.visibility && <div className="kb-metar-stat">
                  <span className="kb-metar-stat-label">{visibilityLabel}</span>
                  <span className="kb-metar-stat-value">{formatVisibility(metar.visibility)}</span>
                </div>}
              {metar.weather.length > 0 && <div className="kb-metar-stat">
                  <span className="kb-metar-stat-label">{weatherLabel}</span>
                  <span className="kb-metar-stat-value">{formatWeather(metar.weather)}</span>
                </div>}
              {metar.clouds.length > 0 && <div className="kb-metar-stat">
                  <span className="kb-metar-stat-label">{cloudsLabel}</span>
                  <span className="kb-metar-stat-value">{formatClouds(metar.clouds)}</span>
                </div>}
              {metar.temp != null && <div className="kb-metar-stat">
                  <span className="kb-metar-stat-label">{tempDewLabel}</span>
                  <span className="kb-metar-stat-value">{formatTempDew(metar.temp, metar.dewpoint)}</span>
                </div>}
              {metar.altimeter && <div className="kb-metar-stat">
                  <span className="kb-metar-stat-label">{altimeterLabel}</span>
                  <span className="kb-metar-stat-value">{formatAltimeter(metar.altimeter)}</span>
                </div>}
            </div>
            {obsLabel && <div className="kb-metar-meta">
                <span className="kb-metar-meta-time">{obsLabel}</span>
              </div>}
          </div>
        </div>
      </div>;
  };
  const renderRow = row => {
    const {meta, broadcasts, isOnline, primary} = row;
    const isExpanded = expanded.has(meta.icao);
    const code = primary?.atisCode || "—";
    const handleClick = () => {
      if (isOnline) toggle(meta.icao);
    };
    return <div key={meta.icao} className={"kb-active-atis-row" + (isOnline ? " kb-active-atis-row--online" : " kb-active-atis-row--offline") + (isExpanded ? " kb-active-atis-row--expanded" : "")}>
        <button type="button" className="kb-active-atis-row-header" onClick={handleClick} disabled={!isOnline} aria-expanded={isOnline ? isExpanded : undefined} aria-controls={isOnline ? `aa-${meta.icao}-detail` : undefined}>
          <span className={"kb-active-atis-dot" + (isOnline ? " kb-active-atis-dot--on" : "")} aria-hidden="true" />
          <span className="kb-active-atis-icao">{meta.icao}</span>
          <span className="kb-active-atis-name-col">
            <span className="kb-active-atis-name">{meta.name}</span>
            {!isOnline && <span className="kb-active-atis-offline-label">{offlineLabel}</span>}
          </span>
          {isOnline && <span className="kb-active-atis-info-letter" aria-label={`Info ${code}`}>
              {code}
            </span>}
          {isOnline && primary?.frequency && <span className="kb-active-atis-freq">{formatFreq(primary.frequency)}</span>}
          {isOnline && broadcasts.length > 1 && <span className="kb-active-atis-split-count" title={`${broadcasts.length} splits`}>
              {broadcasts.length}×
            </span>}
          {isOnline && <span className="kb-active-atis-chevron" aria-hidden="true">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <polyline points="9 18 15 12 9 6" />
              </svg>
            </span>}
        </button>

        {isOnline && isExpanded && <div id={`aa-${meta.icao}-detail`} className="kb-active-atis-detail" role="region">
            <div className="kb-active-atis-broadcasts">
              {broadcasts.map((b, i) => <div className="kb-active-atis-broadcast" key={`${meta.icao}-${i}`}>
                  <div className="kb-active-atis-broadcast-head">
                    <span className="kb-active-atis-broadcast-callsign">
                      {b.callsign}
                      {b.split && <span className="kb-active-atis-split-tag">
                          {b.split === "arrival" ? arrivalsLabel : departuresLabel}
                        </span>}
                    </span>
                    {b.frequency && <span className="kb-active-atis-broadcast-freq">
                        <span className="kb-active-atis-broadcast-freq-label">{frequencyLabel}</span>
                        <span className="kb-active-atis-broadcast-freq-value">{formatFreq(b.frequency)}</span>
                      </span>}
                    {b.lastUpdated && <span className="kb-active-atis-broadcast-age">
                        {observedLabel} {ageLabel(b.lastUpdated)}
                      </span>}
                  </div>
                  {b.text && <div className="kb-active-atis-transcript-wrap">
                      <span className="kb-active-atis-transcript-label">{transcriptLabel}</span>
                      <pre className="kb-active-atis-transcript">{b.text}</pre>
                    </div>}
                </div>)}
            </div>

            <div className="kb-active-atis-metar-wrap">
              {renderMetarCard(meta.icao, meta.name)}
            </div>

            {meta.href && <a className="kb-active-atis-cta" href={meta.href}>
                <span className="kb-active-atis-cta-text">
                  {viewBriefingLabel}
                  <span className="kb-active-atis-cta-name">{meta.name}</span>
                </span>
                <span className="kb-active-atis-cta-arrow" aria-hidden="true">
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <line x1="5" y1="12" x2="19" y2="12" />
                    <polyline points="13 6 19 12 13 18" />
                  </svg>
                </span>
              </a>}
          </div>}
      </div>;
  };
  return <div className="kb-active-atis">
      <div className="kb-active-atis-header">
        <span className="kb-active-atis-eyebrow">
          <span className={"kb-active-atis-dot" + (anyOnline ? " kb-active-atis-dot--on" : "")} aria-hidden="true" />
          {title}
        </span>
        <div className="kb-active-atis-header-right">
          {!loading && !error && <span className={"kb-active-atis-count" + (anyOnline ? " kb-active-atis-count--on" : "")}>
              <span className="kb-active-atis-count-num">{onlineCount}</span>
              <span className="kb-active-atis-count-sep">/</span>
              <span className="kb-active-atis-count-total">{total}</span>
              <span className="kb-active-atis-count-label">{countSuffixLabel}</span>
            </span>}
          <button type="button" className={"kb-active-atis-refresh" + (refreshing ? " refreshing" : "")} onClick={() => fetchFeed(true)} aria-label={refreshLabel} title={refreshLabel}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
              <path d="M21 3v5h-5" />
              <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
              <path d="M3 21v-5h5" />
            </svg>
          </button>
        </div>
      </div>

      {loading && <div className="kb-active-atis-loading">
          <div className="kb-active-atis-skeleton kb-active-atis-skeleton--long" />
          <div className="kb-active-atis-skeleton kb-active-atis-skeleton--short" />
          <div className="kb-active-atis-skeleton kb-active-atis-skeleton--long" />
          <div className="kb-active-atis-meta-status">{loadingLabel}</div>
        </div>}

      {!loading && error && <div className="kb-active-atis-message">{errorLabel}</div>}

      {!loading && !error && <div className="kb-active-atis-list">{rows.map(renderRow)}</div>}

      {!loading && !error && !anyOnline && <div className="kb-active-atis-empty">{noActiveLabel}</div>}

      <div className="kb-active-atis-meta">
        {mostRecentUpdate && <span className="kb-active-atis-meta-time">
            {observedLabel} {ageLabel(mostRecentUpdate)}
          </span>}
      </div>
    </div>;
};

Cette page est un suivi en direct de la couverture ATIS VATSIM aux huit aéroports canadiens couverts par vACA. Les terrains en ligne apparaissent en haut avec leur lettre d'information courante et leur fréquence. Cliquez sur n'importe quelle ligne active pour afficher la transcription ATIS complète, le dernier <Tooltip tip="Compte rendu météorologique d'aérodrome" cta="Voir le glossaire" href="/fr/aops/additional-resources/glossary/metar">METAR</Tooltip>, ainsi qu'un raccourci vers l'aperçu du terrain.

Les données proviennent du flux public VATSIM et se rafraîchissent une fois par minute - utile pour un coup d'œil rapide avant le refoulement afin de voir si le terrain de départ ou d'arrivée a un contrôleur ATIS en ligne.

<ActiveAtis
  title="Aéroports VATSIM actifs"
  loadingLabel="Recherche des diffusions"
  errorLabel="Impossible de joindre le flux de données VATSIM"
  refreshLabel="Actualiser"
  offlineLabel="Aucun ATIS en ligne"
  frequencyLabel="Fréquence"
  transcriptLabel="Transcription"
  viewBriefingLabel="Voir l'aperçu de"
  observedLabel="Reçu il y a"
  agoLabel="min"
  justNowLabel="à l'instant"
  noActiveLabel="Aucune diffusion ATIS VATSIM en couverture pour le moment. Le flux se rafraîchit chaque minute."
  countSuffixLabel="actifs"
  arrivalsLabel="Arrivées"
  departuresLabel="Départs"
  airports={[
{ icao: "CYYZ", name: "Toronto Pearson", href: "/fr/aops/airports/toronto-pearson" },
{ icao: "CYUL", name: "Montréal-Trudeau", href: "/fr/aops/airports/montreal-trudeau" },
{ icao: "CYVR", name: "Vancouver", href: "/fr/aops/airports/vancouver-international" },
{ icao: "CYYC", name: "Calgary", href: "/fr/aops/airports/calgary-international" },
{ icao: "CYOW", name: "Ottawa", href: "/fr/aops/airports/ottawa" },
{ icao: "CYQB", name: "Québec", href: "/fr/aops/airports/quebec-city" },
{ icao: "CYEG", name: "Edmonton", href: "/fr/aops/airports/edmonton" },
{ icao: "CYHZ", name: "Halifax", href: "/fr/aops/airports/halifax" }
]}
/>

<Card title="Retour aux aéroports" icon="arrow-left" href="/fr/aops/airports/index">
  Retourner à l'aperçu des aéroports
</Card>
