Private/ReportNetworkTimeline.ps1
|
# The timeline view of "Netzwerk im Bild" (ReportNetwork.ps1): the 30 days before the Run, # drawn from the Timed Events (ADR-0010). One lane per Server - merged as the nodes of the # path are, so that one Server is one lane however Windows logged it -, the machine's own # lane, the crashes, hangs, starts and sign-ins as markers above them, and the logs below, # each with how far back it reaches. Under it a day-by-hour strip with the totals per day # and per hour, for every lane or for the one whose name was clicked. # # An hour with at least one event is one stroke, in the colour of the Finding that judged # that kind of event, grey where none does: the timeline raises no alarm the Findings do # not. Where none of the logs a lane's events come from reaches back over the 30 days, the # stretch before the earliest of them is hatched: an empty stretch there is not a quiet one. # From when on is the model's (Get-ReportNetworkCoverage), not the script's. # # The geometry is the script's and is looked at, not tested: a Report rendered in a # headless browser from fixture Timed Events. $script:ReportNetworkTimelineCss = @' <style> /* Netzwerk im Bild: the 30 days (ReportNetworkTimeline.ps1). */ .nettimelegend{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:center;color:var(--dim);font-size:12.5px;margin:0 0 8px} .nettimelegend span{display:inline-flex;gap:6px;align-items:center} .nettime .grid{stroke:var(--line);stroke-width:1} .nettime .wkend{fill:var(--panel2)} .nettime .run{stroke:var(--accent);stroke-width:1.5} .nettime .head{font:600 10.5px var(--mono);letter-spacing:.1em;fill:var(--dim)} .nettime .lab{cursor:pointer} .nettime .lab:hover text,.nettime .lab.sel text{fill:var(--accent)} .nettime .lab.sel text{font-weight:600} .nettime .lab rect{fill:transparent} .nettime .ev{fill:var(--c)} .nettime .ev.NONE{fill:var(--faint);fill-opacity:.75} .nettime .mk{fill:var(--c);stroke:var(--panel);stroke-width:1} .nettime .mk.NONE{fill:var(--faint)} .nettime .mk.ring{fill:none;stroke:var(--c);stroke-width:2} .nettime .mk.ring.NONE{stroke:var(--faint)} .nettime .hit{cursor:default} .nettime .hit.go{cursor:pointer} .nettime .hit:focus{outline:none} .nettime .hit:hover .ev,.nettime .hit:focus .ev,.nettime .hit.sel .ev{stroke:var(--text);stroke-width:1} .nettime .gap{fill:var(--faint);fill-opacity:.12;stroke:var(--faint);stroke-opacity:.5;stroke-dasharray:3 3} .nettime .hatch{stroke:var(--faint);stroke-opacity:.35;stroke-width:1} .nettime .cov{stroke:var(--dim);stroke-width:3} .nettimeout{color:var(--faint);font-size:12px;margin:4px 2px 0} .netheatbar{display:flex;flex-wrap:wrap;gap:6px 14px;align-items:center;margin:14px 0 6px;color:var(--dim);font-size:12.5px} .netheatbar b{color:var(--text);font-weight:600} .netheatbar button{background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:2px 11px;color:var(--dim);font-size:12px} .netheatbar button:hover{color:var(--text);border-color:var(--faint)} .netheat .cell{fill:var(--accent)} .netheat .zero{fill:var(--panel2)} .netheat .tot{fill:var(--dim)} @media print{ .netheatbar button{display:none!important} .nettime .hit:hover .ev{stroke:none} .nettime,.netheat{-webkit-print-color-adjust:exact;print-color-adjust:exact} } </style> '@ $script:ReportNetworkTimelineScript = @' <script> // The 30 days: a lane per Server, the machine's lane, the markers and the logs, one stroke per // hour with an event; and the day-by-hour strip below, for all lanes or the one picked. Widths // are counted and not measured: the chapter may be hidden when it is drawn. if (window.GutcheckNet) GutcheckNet.view('timeline', function(root, net){ var svg = root.querySelector('svg.nettime'), heat = root.querySelector('svg.netheat'), d = net.data, w = net.words, r = w.timeline, esc = net.esc; var headBox = root.querySelector('.netheathead'), resetBox = root.querySelector('.netheatreset'), outBox = root.querySelector('.nettimeout'); if (!svg || !heat || !d.t) return; var DAYS = 30, ell = String.fromCharCode(8230), levels = net.levels; // A time as the model writes it, local, to the second. function parse(s){ var p = /^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)/.exec(s || ''); return p ? new Date(+p[1], +p[2] - 1, +p[3], +p[4], +p[5], +p[6]) : null; } function two(n){ return (n < 10 ? '0' : '') + n; } function dayText(t){ return two(t.getDate()) + '.' + two(t.getMonth() + 1) + '.'; } function timeText(t){ return two(t.getHours()) + ':' + two(t.getMinutes()) + ':' + two(t.getSeconds()); } function cut(s, n){ s = String(s); return s.length > n ? s.slice(0, Math.max(1, n - 1)) + ell : s; } var asof = parse(d.asof) || new Date(); var day0 = new Date(asof.getFullYear(), asof.getMonth(), asof.getDate() - (DAYS - 1)); // Which of the 30 days a time is on, counted in calendar days: a day with a change of the // clock is a day all the same. function dayOf(t){ return Math.round((new Date(t.getFullYear(), t.getMonth(), t.getDate()) - day0) / 864e5); } function dayAt(k){ return new Date(day0.getFullYear(), day0.getMonth(), day0.getDate() + k); } var W = 1100, LX = 236, RX = 44, PW = W - LX - RX, RH = 17, HW = PW / DAYS / 24; function xAt(t){ return LX + (dayOf(t) + (t.getHours() + t.getMinutes() / 60) / 24) / DAYS * PW; } // The events within the 30 days, each with its time and its row. var events = [], outside = 0; d.t.forEach(function(e, i){ var at = parse(e.Time); if (!at) return; var k = dayOf(at); if (k < 0 || k >= DAYS) { outside++; return; } var row = e.row === 'marker' ? (e.Mark === 'Crash' || e.Mark === 'Hang' ? 'crash' : 'start') : e.row; events.push({ e: e, i: i, at: at, row: row, day: k, hour: at.getHours() }); }); if (outBox) { outBox.hidden = !outside; outBox.textContent = outside ? r.Outside.replace('{0}', outside) : ''; } // Where a row's stretch that none of its logs reaches ends: from the model (tr), which // says from when on the logs of the row's events cover it together. // A log's own row: its own reach. function gapOf(row){ var c = row.log || (d.tr || {})[row.id]; if (!c) return null; if (c.Mark === 'Unread') return { until: null, log: c.Log, unread: true }; var at = parse(c.Time); return at && at > day0 ? { until: at, log: c.Log } : null; } // The rows, in their groups: the markers, this machine, the Servers, the logs. var rows = [], byId = {}; function add(group, id, label, extra){ var x = { group: group, id: id, label: label, ev: [], logs: [] }; for (var k in extra) x[k] = extra[k]; rows.push(x); byId[id] = x; return x; } var hasMark = function(ids){ return events.some(function(x){ return ids.indexOf(x.row) >= 0; }); }; if (hasMark(['crash'])) add(r.Group.Markers, 'crash', r.Row.Crash, { marker: true }); if (hasMark(['start'])) add(r.Group.Markers, 'start', r.Row.Start, { marker: true }); if (hasMark(['machine'])) add(r.Group.Machine, 'machine', d.machine || w.role.Machine, {}); (d.tl || []).forEach(function(l){ add(r.Group.Servers, l.id, l.label, { lane: l }); }); events.forEach(function(x){ var row = byId[x.row]; if (row) { row.ev.push(x); if (x.e.Log && row.logs.indexOf(x.e.Log) < 0) row.logs.push(x.e.Log); } }); var logRows = []; (d.tc || []).forEach(function(c){ if (!byId['log:' + c.Log]) logRows.push(add(r.Group.Logs, 'log:' + c.Log, c.Log.replace(/^Microsoft-Windows-/, ''), { log: c, logs: [c.Log] })); }); // The colour of an hour: the worst its events are drawn in, each by the part of its // Finding about its own Server, as every view colours (net.severity). function sevOf(list){ return net.worst(list.map(function(x){ return net.severity(x.e); })); } // The Finding a click leads to: that of the event drawn in the worst colour. function worstFinding(list){ var best = null, drawn = ''; list.forEach(function(x){ var f = net.finding(x.e), s = net.severity(x.e); if (f && (!best || levels.indexOf(s) < levels.indexOf(drawn))) { best = f; drawn = s; } }); return best; } // The events of one hour of one row, in words, with the Findings they belong to. function hourHtml(row, list){ var t = list[0].at, h = t.getHours(); var out = '<h4>' + esc(row.label) + '</h4><div class="sub">' + esc(r.InHour.replace('{0}', list.length).replace('{1}', dayText(t) + ' ' + two(h)).replace('{2}', two((h + 1) % 24))) + '</div><table>'; var seen = {}, finds = []; list.slice(0, 8).forEach(function(x){ var f = net.finding(x.e), what = [r.Mark[x.e.Mark] || x.e.Mark]; if (x.e.Subject && !row.lane) what.push(x.e.Subject); if (x.e.Detail && x.e.Detail !== what[0]) what.push(x.e.Detail); if (row.lane && x.e.Subject && x.e.Subject !== row.label) what.push(x.e.Subject); if (x.e.Address && row.lane && x.e.Address !== row.label) what.push(x.e.Address); out += '<tr class="' + (net.severity(x.e) || 'NONE') + '"><td><span class="dot"></span> ' + esc(what.join(net.dot)) + '<small>' + esc(f ? f.t : w.detail.NoFinding) + '</small></td><td class="v">' + esc(timeText(x.at)) + '</td></tr>'; }); out += '</table>'; if (list.length > 8) out += '<div class="none">' + esc(r.More.replace('{0}', list.length - 8)) + '</div>'; list.forEach(function(x){ var f = net.finding(x.e); if (f && !seen[x.e.Finding]) { seen[x.e.Finding] = 1; finds.push(f); } }); finds.sort(function(a, b){ return levels.indexOf(a.s) - levels.indexOf(b.s); }).forEach(function(f){ out += '<a class="jump find ' + f.s + '" href="#' + esc(f.a) + '"><span class="badge">' + esc(f.s) + '</span><b>' + esc(f.t) + '</b>' + (f.v ? '<small>' + esc(f.v) + '</small>' : '') + '</a>'; }); return out; } // The hatching of a stretch a row's log does not reach: lines, since a Report draws no // pattern it would have to fetch by a name. function gapSvg(row, y){ var gap = gapOf(row); if (!gap) return ''; var x2 = gap.unread ? LX + PW : Math.min(LX + PW, xAt(gap.until)), h = RH - 4, o = ''; var tip = gap.unread ? r.Log.GapUnread.replace('{0}', gap.log) : r.Log.Gap.replace('{0}', dayText(gap.until) + ' ' + timeText(gap.until).slice(0, 5)).replace('{1}', gap.log); o += '<g><title>' + esc(tip) + '</title><rect class="gap" x="' + LX + '" y="' + (y + 2) + '" width="' + Math.max(1, x2 - LX).toFixed(1) + '" height="' + h + '"/>'; for (var x = LX + 6; x + h <= x2; x += 8) o += '<line class="hatch" x1="' + x.toFixed(1) + '" y1="' + (y + 2 + h) + '" x2="' + (x + h).toFixed(1) + '" y2="' + (y + 2) + '"/>'; return o + '</g>'; } var selected = ''; function draw(){ var TOP = 26, y = TOP, out = '', lastGroup = null, placed = []; rows.forEach(function(row){ if (row.group !== lastGroup) { y += 6; out += '<text class="head" x="6" y="' + (y + 11) + '">' + esc(row.group.toUpperCase()) + '</text>'; y += 17; lastGroup = row.group; } placed.push({ row: row, y: y }); y += RH; }); var H = y + 22; // The days: weekends set apart, a line at each day, a date every second one, the Run. for (var k = 0; k < DAYS; k++) { var day = dayAt(k), x = LX + k / DAYS * PW, dw = PW / DAYS; if (day.getDay() === 0 || day.getDay() === 6) out = '<rect class="wkend" x="' + x.toFixed(1) + '" y="' + (TOP - 4) + '" width="' + dw.toFixed(1) + '" height="' + (H - TOP - 14) + '"/>' + out; out += '<line class="grid" x1="' + x.toFixed(1) + '" y1="' + (TOP - 4) + '" x2="' + x.toFixed(1) + '" y2="' + (H - 18) + '"/>'; if ((DAYS - 1 - k) % 2 === 0) out += '<text class="faint" x="' + (x + 2).toFixed(1) + '" y="' + (TOP - 10) + '" font-size="10">' + esc(dayText(day)) + '</text>'; } // The Run, with its word under the lanes, where no date stands. var rx = xAt(asof); out += '<line class="run" x1="' + rx.toFixed(1) + '" y1="' + (TOP - 4) + '" x2="' + rx.toFixed(1) + '" y2="' + (H - 14) + '"/>' + '<text class="faint" x="' + (rx - 3).toFixed(1) + '" y="' + (H - 4) + '" font-size="10" text-anchor="end">' + esc(r.Run + ' ' + dayText(asof) + ' ' + timeText(asof).slice(0, 5)) + '</text>'; placed.forEach(function(p){ var row = p.row, y = p.y; out += gapSvg(row, y); var lab = row.lane ? row.lane.label : row.label, tip = row.lane ? row.lane.names.concat(row.lane.addresses).join(net.dot) : (row.log ? row.log.Log : ''); out += '<g class="lab' + (selected === row.id ? ' sel' : '') + '" data-row="' + esc(row.id) + '"><rect x="0" y="' + y + '" width="' + (LX - 6) + '" height="' + RH + '"/>' + (tip ? '<title>' + esc(tip) + '</title>' : '') + '<text x="14" y="' + (y + 12) + '" font-size="11.5">' + esc(cut(lab, 34)) + '</text></g>'; if (row.log) { var c = row.log, at = parse(c.Time), x1 = c.Mark === 'Unread' ? null : (at && at > day0 ? xAt(at) : LX); if (x1 != null) out += '<line class="cov" x1="' + x1.toFixed(1) + '" y1="' + (y + RH / 2) + '" x2="' + (LX + PW) + '" y2="' + (y + RH / 2) + '"/>'; var said = c.Mark === 'Unread' ? r.Log.Unread : (at && at > day0 ? r.Log.Reaches.replace('{0}', dayText(at)) : r.Log.Whole); out += '<text class="dim" x="' + (LX + 4) + '" y="' + (y + 12) + '" font-size="10">' + esc(c.Mark === 'Unread' || (at && at > day0) ? said : '') + '</text>'; return; } // One stroke, or one mark, per hour with an event. var hours = {}; row.ev.forEach(function(x){ var key = x.day * 24 + x.hour; (hours[key] = hours[key] || []).push(x); }); Object.keys(hours).forEach(function(key){ var list = hours[key].sort(function(a, b){ return a.at - b.at; }), s = sevOf(list) || 'NONE', x = LX + Number(key) * HW; var go = worstFinding(list) ? ' go' : ''; out += '<g class="hit ' + s + go + '" data-row="' + esc(row.id) + '" data-h="' + key + '" tabindex="0" role="button">'; if (row.marker) { var cx = x + HW / 2, cy = y + RH / 2; if (row.id === 'crash') out += '<path class="mk ' + s + '" d="M' + cx.toFixed(1) + ' ' + (cy - 5) + 'l5 5l-5 5l-5 -5z"/>'; else out += '<circle class="mk ring ' + s + '" cx="' + cx.toFixed(1) + '" cy="' + cy + '" r="4"/>'; } else out += '<rect class="ev" x="' + x.toFixed(1) + '" y="' + (y + 3) + '" width="' + Math.max(3, HW).toFixed(1) + '" height="' + (RH - 6) + '" rx="1"/>'; out += '</g>'; }); out += '<text class="faint" x="' + (W - RX + 6) + '" y="' + (y + 12) + '" font-size="10">' + row.ev.length + '</text>'; }); svg.setAttribute('viewBox', '0 0 ' + W + ' ' + Math.ceil(H)); svg.style.maxWidth = W + 'px'; svg.innerHTML = out; Array.prototype.forEach.call(svg.querySelectorAll('.hit'), function(el){ var row = byId[el.getAttribute('data-row')], key = Number(el.getAttribute('data-h')); var list = row.ev.filter(function(x){ return x.day * 24 + x.hour === key; }).sort(function(a, b){ return a.at - b.at; }); function on(){ el.classList.add('sel'); net.showHtml(el, hourHtml(row, list)); } function off(){ el.classList.remove('sel'); net.hideDetail(); } function go(){ var f = worstFinding(list); if (f) net.jump(f.a); } el.addEventListener('mouseenter', on); el.addEventListener('focus', on); el.addEventListener('mouseleave', off); el.addEventListener('blur', off); el.addEventListener('click', go); el.addEventListener('keydown', function(e){ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } }); }); Array.prototype.forEach.call(svg.querySelectorAll('.lab'), function(el){ var row = byId[el.getAttribute('data-row')]; if (row.log) { el.style.cursor = 'default'; return; } el.addEventListener('click', function(){ selected = selected === row.id ? '' : row.id; draw(); }); var node = row.lane && row.lane.node ? d.n.filter(function(n){ return n.id === row.lane.node; })[0] : null; if (node) { el.addEventListener('mouseenter', function(){ net.showDetail(el, node); }); el.addEventListener('mouseleave', net.hideDetail); } }); strip(); } // Day by hour: how many events of the lanes shown fell in each hour of each day, with the // totals per day below and per hour beside. function strip(){ var shown = rows.filter(function(row){ return !row.log && (!selected || row.id === selected); }); var grid = [], perDay = [], perHour = [], total = 0, max = 0, k, h; for (k = 0; k < DAYS; k++) { grid.push([]); perDay.push(0); for (h = 0; h < 24; h++) grid[k].push(0); } for (h = 0; h < 24; h++) perHour.push(0); shown.forEach(function(row){ row.ev.forEach(function(x){ grid[x.day][x.hour]++; perDay[x.day]++; perHour[x.hour]++; total++; }); }); grid.forEach(function(c){ c.forEach(function(v){ if (v > max) max = v; }); }); var name = selected ? byId[selected].label : r.HeatAll; if (headBox) headBox.innerHTML = esc(r.Heat.replace('{0}', '')) + '<b>' + esc(name) + '</b>' + net.dot + esc(r.Count.replace('{0}', total)); if (resetBox) resetBox.hidden = !selected; var TOP = 24, CH = 9, cw = PW / DAYS, H = TOP + 24 * CH + 46, o = ''; for (k = 0; k < DAYS; k++) if ((DAYS - 1 - k) % 2 === 0) o += '<text class="faint" x="' + (LX + k * cw + 2).toFixed(1) + '" y="' + (TOP - 8) + '" font-size="10">' + esc(dayText(dayAt(k))) + '</text>'; for (h = 0; h < 24; h += 3) o += '<text class="faint" x="' + (LX - 8) + '" y="' + (TOP + h * CH + 8) + '" font-size="10" text-anchor="end">' + two(h) + ':00</text>'; o += '<text class="dim" x="14" y="' + (TOP + 12 * CH) + '" font-size="11">' + esc(r.Hour) + '</text>'; for (k = 0; k < DAYS; k++) for (h = 0; h < 24; h++) { var v = grid[k][h]; o += '<rect class="' + (v ? 'cell' : 'zero') + '" x="' + (LX + k * cw + 0.5).toFixed(1) + '" y="' + (TOP + h * CH + 0.5) + '" width="' + (cw - 1).toFixed(1) + '" height="' + (CH - 1) + '"' + (v ? ' fill-opacity="' + (0.25 + 0.75 * v / max).toFixed(2) + '"><title>' + esc(dayText(dayAt(k)) + ' ' + two(h) + ':00' + net.dot + r.Count.replace('{0}', v)) + '</title></rect>' : '/>'); } var dayMax = Math.max.apply(null, perDay.concat([1])), hourMax = Math.max.apply(null, perHour.concat([1])), by = TOP + 24 * CH + 6; for (k = 0; k < DAYS; k++) { var bh = perDay[k] / dayMax * 30; o += '<rect class="tot" x="' + (LX + k * cw + 1).toFixed(1) + '" y="' + (by + 30 - bh).toFixed(1) + '" width="' + (cw - 2).toFixed(1) + '" height="' + bh.toFixed(1) + '"><title>' + esc(dayText(dayAt(k)) + net.dot + r.Count.replace('{0}', perDay[k])) + '</title></rect>'; } o += '<text class="dim" x="' + (LX - 8) + '" y="' + (by + 28) + '" font-size="10" text-anchor="end">' + esc(r.PerDay) + '</text>'; for (h = 0; h < 24; h++) { var bw = perHour[h] / hourMax * (RX - 8); o += '<rect class="tot" x="' + (LX + PW + 4) + '" y="' + (TOP + h * CH + 1) + '" width="' + bw.toFixed(1) + '" height="' + (CH - 2) + '"><title>' + esc(two(h) + ':00' + net.dot + r.Count.replace('{0}', perHour[h])) + '</title></rect>'; } o += '<text class="dim" x="' + (W - 1) + '" y="' + (by + 28) + '" font-size="10" text-anchor="end">' + esc(r.PerHour) + '</text>'; heat.setAttribute('viewBox', '0 0 ' + W + ' ' + H); heat.style.maxWidth = W + 'px'; heat.innerHTML = o; } if (resetBox) resetBox.addEventListener('click', function(){ selected = ''; draw(); }); draw(); }); </script> '@ function ConvertTo-ReportNetworkTimelineView { <# .SYNOPSIS The timeline view: what it is drawn into, and its words. Pure. .DESCRIPTION Nothing where the Run read no event: a log's reach alone is no timeline. Nor where it measured no network (Test-ReportNetworkMeasured): crashes and sign-ins alone are no 30 days of the network; beside a network they are drawn. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)]$Model) if (-not @($Model.t).Count -or -not (Test-ReportNetworkMeasured -Measurement $Model.m -TimedEvent $Model.t)) { return } $legend = ('<span><svg width="12" height="12" aria-hidden="true"><path d="M6 1l5 5l-5 5l-5 -5z" fill="currentColor"/></svg>{0}</span>' + '<span><svg width="12" height="12" aria-hidden="true"><circle cx="6" cy="6" r="4" fill="none" stroke="currentColor" stroke-width="2"/></svg>{1}</span>' + '<span><svg width="22" height="12" aria-hidden="true"><rect x="0.5" y="0.5" width="21" height="11" fill="none" stroke="currentColor" stroke-dasharray="3 3"/><line x1="4" y1="11" x2="14" y2="1" stroke="currentColor"/></svg>{2}</span>') -f ` (ConvertTo-HtmlText (Get-Text 'Report.Network.Timeline.Legend.Crash')), (ConvertTo-HtmlText (Get-Text 'Report.Network.Timeline.Legend.Start')), (ConvertTo-HtmlText (Get-Text 'Report.Network.Timeline.Legend.Gap')) $html = ('<h3>{0}</h3><p class="netsub">{1} <span class="nethint">{2}</span></p><div class="nettimelegend">{3}</div>' + '<div class="netpanel"><svg class="nettime" role="img" aria-label="{0}"></svg></div><p class="nettimeout" hidden></p>' + '<div class="netheatbar"><span class="netheathead"></span><button type="button" class="netheatreset" hidden>{4}</button></div>' + '<div class="netpanel"><svg class="netheat" role="img" aria-label="{0}"></svg></div>') -f ` (ConvertTo-HtmlText (Get-Text 'Report.Network.Timeline.Heading')), (ConvertTo-HtmlText (Get-Text 'Report.Network.Timeline.Legend')), (ConvertTo-HtmlText (Get-Text 'Report.Network.Timeline.Hint')), $legend, (ConvertTo-HtmlText (Get-Text 'Report.Network.Timeline.HeatReset')) New-ReportNetworkView -Id 'timeline' -Title (Get-Text 'Report.Network.Timeline.Title') -Html $html ` -Css $script:ReportNetworkTimelineCss -Script $script:ReportNetworkTimelineScript ` -Word ([ordered]@{ Group = [ordered]@{ Markers = Get-Text 'Report.Network.Timeline.Group.Markers' Machine = Get-Text 'Report.Network.Timeline.Group.Machine' Servers = Get-Text 'Report.Network.Timeline.Group.Servers' Logs = Get-Text 'Report.Network.Timeline.Group.Logs' } Row = [ordered]@{ Crash = Get-Text 'Report.Network.Timeline.Row.Crash' Start = Get-Text 'Report.Network.Timeline.Row.Start' } Mark = [ordered]@{ Lost = Get-Text 'Report.Network.Timeline.Mark.Lost' Failed = Get-Text 'Report.Network.Timeline.Mark.Failed' ShareNotFound = Get-Text 'Report.Network.Timeline.Mark.ShareNotFound' Context = Get-Text 'Report.Network.Timeline.Mark.Context' Crash = Get-Text 'Report.Network.Timeline.Mark.Crash' Hang = Get-Text 'Report.Network.Timeline.Mark.Hang' Start = Get-Text 'Report.Network.Timeline.Mark.Start' SignIn = Get-Text 'Report.Network.Timeline.Mark.SignIn' } Log = [ordered]@{ Reaches = Get-Text 'Report.Network.Timeline.Log.Reaches' Whole = Get-Text 'Report.Network.Timeline.Log.Whole' Unread = Get-Text 'Report.Network.Timeline.Log.Unread' Gap = Get-Text 'Report.Network.Timeline.Log.Gap' GapUnread = Get-Text 'Report.Network.Timeline.Log.GapUnread' } Run = Get-Text 'Report.Network.Timeline.Run' Heat = Get-Text 'Report.Network.Timeline.Heat' HeatAll = Get-Text 'Report.Network.Timeline.HeatAll' Count = Get-Text 'Report.Network.Timeline.Count' PerDay = Get-Text 'Report.Network.Timeline.PerDay' PerHour = Get-Text 'Report.Network.Timeline.PerHour' Hour = Get-Text 'Report.Network.Timeline.Hour' InHour = Get-Text 'Report.Network.Timeline.InHour' More = Get-Text 'Report.Network.Timeline.More' Outside = Get-Text 'Report.Network.Timeline.Outside' }) } |