見出し画像

【第568回】 自家製アプリ Automation Activity Viewer で処理時間を把握

最近なぜか Automation Studio でエラーが発生することが多くなり、各アクティビティの処理にどれくらい時間がかかっているのか を調査する機会が増えてきました。

以前、Automation Studio の各アクティビティについて、開始時刻・終了時刻・処理時間をまとめて確認するための SQL を紹介しました。

SELECT TOP 10000 
    AutomationName COLLATE japanese_cs_as_ks_ws AS [AutomationName],
    ActivityName COLLATE japanese_cs_as_ks_ws AS [ActivityName],
    ActivityInstanceStep,
    CONVERT(VARCHAR(19), DATEADD(HH, 9, ActivityInstanceStartTime_UTC), 120) AS [ActivityInstanceStartTime],
    CONVERT(VARCHAR(19), DATEADD(HH, 9, ActivityInstanceEndTime_UTC), 120) AS [ActivityInstanceEndTime],
    FORMAT(DATEDIFF(S, ActivityInstanceStartTime_UTC, ActivityInstanceEndTime_UTC) / 60, '00') + ':' + 
    FORMAT(DATEDIFF(S, ActivityInstanceStartTime_UTC, ActivityInstanceEndTime_UTC) % 60, '00') AS [Duration]
FROM 
    _automationactivityinstance
WHERE 
    AutomationName = 'XXXXXXXXXXXXXXX'
    AND DATEADD(HH, 9, ActivityInstanceStartTime_UTC) 
        BETWEEN '2026-08-23 00:00:00' AND '2026-08-24 00:00:00'
ORDER BY
    CONVERT(INT, LEFT(ActivityInstanceStep, CHARINDEX('.', ActivityInstanceStep) - 1)) ASC,
    CONVERT(INT, SUBSTRING(
        ActivityInstanceStep,
        CHARINDEX('.', ActivityInstanceStep) + 1,
        LEN(ActivityInstanceStep)
    )) ASC,
    ActivityName COLLATE japanese_cs_as_ks_ws ASC,
    ActivityInstanceStartTime_UTC ASC

この SQL を使えば、オートメーションの各ステップに配置されているアクティビティについて、どの処理にどれくらい時間がかかっているのかを一括で確認できます

ただ、実際に調査するたびに、

  • 「この SQL を探す」

  • 「Query Studio に貼り付ける」

  • 「オートメーション名をコピー&ペーストする」

  • 「検索する日付を書き換える」

といった作業を繰り返すのは、少し面倒です。

そこで、もっと簡単に、誰でもサクッと Automation Studio の処理時間を確認できないかと思い、Marketing Cloud Engagement 内で動作する自家製アプリを作ってみました。

名前は、シンプルに「Automation Activity Viewer」です。

  • お試し版なら、5 分程度で実装可能です。

  • ログイン版でも、30 分程度で実装可能です。

まず「どんなものなのか試してみたい」という方は、ログイン認証を設定しない「お試し版」を CloudPages に作成して、実際に使ってみてください

使ってみて「これは便利そう」と感じた場合に、ログイン認証を追加したバージョンへ変更 するのがよいと思います。

注意
ログイン認証を設定しない場合、CloudPages の URL を知っている人であればページへアクセスできます。そのため、ページ上に表示されるオートメーション名などの情報を、不特定多数の人に見られる可能性があります。

「お試し版」はあくまで動作確認用途として利用し、確認が終わったら CloudPages を非公開にするか、削除してください。

「お試し版」の実装方法は非常に簡単です。

以下のスクリプトを、CloudPages の HTML ブロックにそのまま貼り付けるだけです。

<script runat="server">
Platform.Load("Core","1.1.1");

/*
  Automation Activity Viewer 3.2 No-Login Edition
  ============================================================
  Production build for Salesforce Marketing Cloud Engagement.

  - Single CloudPage
  - No Marketing Cloud login protection
  - No OAuth
  - No Data Extension
  - No Query Activity
  - Unicode automation names
  - Latest 31 days only
  - Input values persist after submit
  - Exact Automation Studio step reconstruction
  - Activity type shown only when reliably identified
  - Unresolved activity types are shown as "Unknown"
  - Marketing Cloud account/user local time conversion
  - Responsive UI and CSV export
*/

var MAX_ROWS = 10000;
var submitted = false;

var automationName = "";
var startDate = "";
var endDate = "";
var resultFilter = "";
var displayRows = [];

var errorMessage = "";
var infoMessage = "";

var automationNames = [];
var resultRows = [];
var attemptRows = [];

/*
  v14 join diagnostics.
  These arrays are populated only in memory for this page request.
  Nothing is written to a Data Extension.
*/
var diagStaticTasks = [];
var diagStaticActivities = [];
var diagTaskInstances = [];
var diagActivityInstances = [];

var accountLocalToday = "";
var accountLocalYesterday = "";
var accountLocalOldest = "";

var resultCount = 0;
var completedCount = 0;
var totalDurationSec = 0;
var avgDurationSec = 0;
var maxDurationSec = 0;

/*
  Automation-level duration KPIs.
  Each ProgramInstanceID represents one execution of the selected automation.
  Run duration = earliest activity start to latest activity end in that execution.
*/
var automationRunCount = 0;
var avgAutomationDurationSec = 0;
var maxAutomationDurationSec = 0;

var diag = {
    automationsLoaded: 0,
    validInstanceDates: 0,
    invalidInstanceDates: 0,
    candidateProgramInstanceIds: 0,
    staticTasks: 0,
    staticActivities: 0,
    taskInstances: 0,
    activityInstances: 0,
    mappedSteps: 0,
    unmappedSteps: 0
};

function esc(v) {
    if (v == null) return "";
    return String(v)
        .replace(/&/g,"&amp;")
        .replace(/</g,"&lt;")
        .replace(/>/g,"&gt;")
        .replace(/"/g,"&quot;")
        .replace(/'/g,"&#39;");
}

function jsEsc(v) {
    if (v == null) return "";
    return String(v)
        .replace(/\\/g,"\\\\")
        .replace(/"/g,'\\"')
        .replace(/\r/g,"\\r")
        .replace(/\n/g,"\\n")
        .replace(/</g,"\\u003c")
        .replace(/>/g,"\\u003e");
}

function pad2(n) {
    n = parseInt(n,10) || 0;
    return n < 10 ? "0"+n : String(n);
}

function monthNumber(mon) {
    var m = String(mon || "");

    if (m == "Jan") return 1;
    if (m == "Feb") return 2;
    if (m == "Mar") return 3;
    if (m == "Apr") return 4;
    if (m == "May") return 5;
    if (m == "Jun") return 6;
    if (m == "Jul") return 7;
    if (m == "Aug") return 8;
    if (m == "Sep") return 9;
    if (m == "Oct") return 10;
    if (m == "Nov") return 11;
    if (m == "Dec") return 12;

    return 0;
}

/*
  Parse an API date into its SOURCE CLOCK components.
  We intentionally do NOT convert GMT-06:00 to UTC here.

  Marketing Cloud SOAP dates are displayed in the Marketing Cloud
  system clock (commonly GMT-06:00). SystemDateToLocalDate expects
  a Marketing Cloud system time, so we preserve the clock portion
  and let MC perform the local conversion.
*/
function parseSystemDate(value) {
    if (!value) {
        return {
            valid:false,
            year:null,
            systemString:"",
            epoch:null
        };
    }

    var s = String(value);
    var m;

    /* ISO / SQL style */
    m = s.match(
        /(\d{4})-(\d{1,2})-(\d{1,2})[T\s](\d{1,2}):(\d{1,2}):(\d{1,2})/
    );

    if (m) {
        var y1 = parseInt(m[1],10);

        if (y1 <= 1) {
            return {valid:false,year:y1,systemString:"",epoch:null};
        }

        var sys1 =
            y1+"-"+
            pad2(m[2])+"-"+
            pad2(m[3])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y1,
            systemString:sys1,
            epoch:Date.UTC(
                y1,
                parseInt(m[2],10)-1,
                parseInt(m[3],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    /* US slash style */
    m = s.match(
        /(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})/
    );

    if (m) {
        var y2 = parseInt(m[3],10);

        if (y2 <= 1) {
            return {valid:false,year:y2,systemString:"",epoch:null};
        }

        var sys2 =
            y2+"-"+
            pad2(m[1])+"-"+
            pad2(m[2])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y2,
            systemString:sys2,
            epoch:Date.UTC(
                y2,
                parseInt(m[1],10)-1,
                parseInt(m[2],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    /* MC/.NET style: Thu, 03 Sep 2026 11:52:31 GMT-06:00 */
    m = s.match(
        /(?:[A-Za-z]{3},?\s+)?(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s+GMT[+-]\d{2}:?\d{2}/
    );

    if (m) {
        var mo3 = monthNumber(m[2]);
        var y3 = parseInt(m[3],10);

        if (y3 <= 1 || mo3 === 0) {
            return {valid:false,year:y3,systemString:"",epoch:null};
        }

        var sys3 =
            y3+"-"+
            pad2(mo3)+"-"+
            pad2(m[1])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y3,
            systemString:sys3,
            epoch:Date.UTC(
                y3,
                mo3-1,
                parseInt(m[1],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    /* JS Date style: Thu Sep 03 2026 11:52:31 GMT-0600 */
    m = s.match(
        /(?:[A-Za-z]{3}\s+)?([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s+GMT[+-]\d{4}/
    );

    if (m) {
        var mo4 = monthNumber(m[1]);
        var y4 = parseInt(m[3],10);

        if (y4 <= 1 || mo4 === 0) {
            return {valid:false,year:y4,systemString:"",epoch:null};
        }

        var sys4 =
            y4+"-"+
            pad2(mo4)+"-"+
            pad2(m[2])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y4,
            systemString:sys4,
            epoch:Date.UTC(
                y4,
                mo4-1,
                parseInt(m[2],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    return {
        valid:false,
        year:null,
        systemString:"",
        epoch:null
    };
}

function systemToLocalDisplay(systemString) {
    if (!systemString) return "";

    try {
        var localValue =
            Platform.Function.SystemDateToLocalDate(
                systemString
            );

        /*
          Keep the returned local value readable without trying to
          reinterpret its timezone again.
        */
        var parsed =
            parseSystemDate(
                localValue
            );

        if (parsed.valid) {
            return parsed.systemString;
        }

        return String(localValue);

    } catch(e) {
        return systemString;
    }
}

function localDateToSystemEpoch(localDate,endOfDay) {
    try {
        var localString =
            localDate+
            (endOfDay ? " 23:59:59" : " 00:00:00");

        var systemValue =
            Platform.Function.LocalDateToSystemDate(
                localString
            );

        var parsed =
            parseSystemDate(
                systemValue
            );

        return parsed.valid
            ? parsed.epoch
            : null;

    } catch(e) {
        return null;
    }
}

function isoDateFromParts(year,month,day) {
    return (
        String(year)+"-"+
        pad2(month)+"-"+
        pad2(day)
    );
}

function accountLocalDateOffset(dayOffset) {
    try {
        var localNow =
            Platform.Function.SystemDateToLocalDate(
                Platform.Function.Now()
            );

        var shifted =
            parseInt(dayOffset,10) === 0
            ? localNow
            : Platform.Function.DateAdd(
                localNow,
                parseInt(dayOffset,10),
                "D"
              );

        var formatted =
            Platform.Function.FormatDate(
                shifted,
                "yyyy-MM-dd"
            );

        if (
            formatted &&
            /^\d{4}-\d{2}-\d{2}$/.test(
                String(formatted)
            )
        ) {
            return String(formatted);
        }
    } catch(ignoreFormatDate) {}

    try {
        var fallbackLocal =
            Platform.Function.SystemDateToLocalDate(
                Platform.Function.Now()
            );

        var d =
            new Date(
                String(fallbackLocal)
            );

        if (!isNaN(d.getTime())) {
            d.setDate(
                d.getDate()+
                parseInt(dayOffset,10)
            );

            return (
                d.getFullYear()+"-"+
                pad2(d.getMonth()+1)+"-"+
                pad2(d.getDate())
            );
        }
    } catch(ignoreNativeDate) {}

    return "";
}

function fmtDuration(sec) {
    sec = parseInt(sec,10) || 0;

    var h = Math.floor(sec/3600);
    var m = Math.floor((sec%3600)/60);
    var s = sec%60;

    return h > 0
        ? pad2(h)+":"+pad2(m)+":"+pad2(s)
        : pad2(m)+":"+pad2(s);
}

function durationSeconds(aEpoch,bEpoch) {
    if (
        aEpoch == null ||
        bEpoch == null ||
        bEpoch < aEpoch
    ) {
        return 0;
    }

    return Math.floor(
        (bEpoch-aEpoch)/1000
    );
}

function daysBetween(a,b) {
    var x = String(a).split("-");
    var y = String(b).split("-");

    if (x.length != 3 || y.length != 3) {
        return 9999;
    }

    var d1 = Date.UTC(+x[0],+x[1]-1,+x[2]);
    var d2 = Date.UTC(+y[0],+y[1]-1,+y[2]);

    return Math.floor((d2-d1)/86400000);
}

function retrieveAll(api,type,cols,filter) {
    var out = [];
    var res = api.retrieve(type,cols,filter);

    if (res && res.Results) {
        for (
            var i=0;
            i<res.Results.length &&
            out.length<MAX_ROWS;
            i++
        ) {
            out.push(res.Results[i]);
        }
    }

    while (
        res &&
        res.HasMoreRows === true &&
        out.length < MAX_ROWS
    ) {
        res =
            api.getNextBatch(
                type,
                res.RequestID
            );

        if (res && res.Results) {
            for (
                var j=0;
                j<res.Results.length &&
                out.length<MAX_ROWS;
                j++
            ) {
                out.push(res.Results[j]);
            }
        }
    }

    return out;
}

function addAttempt(objectType,label,ok,count,detail) {
    attemptRows.push({
        objectType:objectType,
        label:label,
        ok:ok,
        count:count,
        detail:detail || ""
    });
}

function safeRetrieve(api,type,cols,filter,label) {
    try {
        var rows =
            retrieveAll(
                api,
                type,
                cols,
                filter
            );

        addAttempt(
            type,
            label,
            true,
            rows.length,
            ""
        );

        return rows;

    } catch(e) {
        addAttempt(
            type,
            label,
            false,
            0,
            e && e.message
                ? String(e.message)
                : String(e)
        );

        return [];
    }
}

function retrieveAutomationList(api) {
    var rows = [];
    var res =
        api.retrieve(
            "Program",
            [
                "Name",
                "ObjectID"
            ]
        );

    if (res && res.Results) {
        for (var i=0;i<res.Results.length;i++) {
            rows.push(res.Results[i]);
        }
    }

    while (res && res.HasMoreRows === true) {
        res =
            api.getNextBatch(
                "Program",
                res.RequestID
            );

        if (res && res.Results) {
            for (var j=0;j<res.Results.length;j++) {
                rows.push(res.Results[j]);
            }
        }
    }

    var all = [];
    var seen = {};

    for (var k=0;k<rows.length;k++) {
        var name =
            String(
                rows[k].Name || ""
            ).replace(/^\s+|\s+$/g,"");

        if (!name) continue;

        if (!seen[name]) {
            seen[name] = true;
            all.push({
                Name:name,
                ObjectID:rows[k].ObjectID || ""
            });
        }
    }

    return all;
}

function findAutomation(api,name,list) {
    for (var i=0;i<list.length;i++) {
        if (
            String(list[i].Name) ===
            String(name)
        ) {
            return list[i];
        }
    }

    var types = ["Automation","Program"];

    for (var t=0;t<types.length;t++) {
        var rows =
            safeRetrieve(
                api,
                types[t],
                [
                    "Name",
                    "ObjectID",
                    "ProgramID",
                    "CustomerKey",
                    "Status"
                ],
                {
                    Property:"Name",
                    SimpleOperator:"equals",
                    Value:name
                },
                "Name = "+name
            );

        if (rows.length) {
            return rows[0];
        }
    }

    return null;
}

function getStaticTasks(api,automationObjectID) {
    /*
      Important:
      Describe on this stack confirms these fields are retrievable.
      Do NOT request AutomationTaskType / TaskType here because a single
      non-retrievable property can make the whole retrieve fail.
    */
    return safeRetrieve(
        api,
        "Task",
        [
            "ObjectID",
            "Program.ObjectID",
            "Name",
            "Sequence"
        ],
        {
            Property:"Program.ObjectID",
            SimpleOperator:"equals",
            Value:automationObjectID
        },
        "Program.ObjectID = "+automationObjectID
    );
}

function getStaticActivities(api,automationObjectID) {
    /*
      v12 Describe proved that ObjectID, Task.ObjectID, Sequence,
      PartnerAPIObjectTypeID, Definition.ObjectID and Definition are
      retrievable on this stack.
    */
    return safeRetrieve(
        api,
        "Activity",
        [
            "ObjectID",
            "CustomerKey",
            "Program.ObjectID",
            "Task.ObjectID",
            "Name",
            "Sequence",
            "PartnerAPIObjectTypeID",
            "Definition.ObjectID"
        ],
        {
            Property:"Program.ObjectID",
            SimpleOperator:"equals",
            Value:automationObjectID
        },
        "Program.ObjectID = "+automationObjectID
    );
}

function getProgramInstances(api,automation) {
    var combined = [];
    var seen = {};

    var autoCols = [
        "ProgramInstanceID",
        "ObjectID",
        "ProgramID",
        "CustomerKey",
        "Name",
        "Status",
        "StatusMessage",
        "StatusLastUpdate",
        "StartTime",
        "CompletedTime",
        "ScheduledTime",
        "CreatedDate",
        "ModifiedDate"
    ];

    var progCols = [
        "ObjectID",
        "ProgramID",
        "CustomerKey",
        "Name",
        "StatusMessage",
        "StatusLastUpdate",
        "CreatedDate",
        "ModifiedDate"
    ];

    var probes = [];

    if (automation.ProgramID) {
        probes.push({
            type:"AutomationInstance",
            cols:autoCols,
            label:"ProgramID = "+automation.ProgramID,
            filter:{
                Property:"ProgramID",
                SimpleOperator:"equals",
                Value:automation.ProgramID
            }
        });

        probes.push({
            type:"ProgramInstance",
            cols:progCols,
            label:"ProgramID = "+automation.ProgramID,
            filter:{
                Property:"ProgramID",
                SimpleOperator:"equals",
                Value:automation.ProgramID
            }
        });
    }

    if (automation.CustomerKey) {
        probes.push({
            type:"AutomationInstance",
            cols:autoCols,
            label:"CustomerKey = "+automation.CustomerKey,
            filter:{
                Property:"CustomerKey",
                SimpleOperator:"equals",
                Value:automation.CustomerKey
            }
        });

        probes.push({
            type:"ProgramInstance",
            cols:progCols,
            label:"CustomerKey = "+automation.CustomerKey,
            filter:{
                Property:"CustomerKey",
                SimpleOperator:"equals",
                Value:automation.CustomerKey
            }
        });
    }

    if (automation.Name) {
        probes.push({
            type:"AutomationInstance",
            cols:autoCols,
            label:"Name = "+automation.Name,
            filter:{
                Property:"Name",
                SimpleOperator:"equals",
                Value:automation.Name
            }
        });

        probes.push({
            type:"ProgramInstance",
            cols:progCols,
            label:"Name = "+automation.Name,
            filter:{
                Property:"Name",
                SimpleOperator:"equals",
                Value:automation.Name
            }
        });
    }

    for (var p=0;p<probes.length;p++) {
        var rows =
            safeRetrieve(
                api,
                probes[p].type,
                probes[p].cols,
                probes[p].filter,
                probes[p].label
            );

        for (var r=0;r<rows.length;r++) {
            var item = rows[r];

            item.__source =
                probes[p].type;

            var key =
                [
                    item.__source,
                    item.ProgramInstanceID || "",
                    item.ObjectID || "",
                    item.ProgramID || "",
                    item.CustomerKey || "",
                    item.StatusLastUpdate || "",
                    item.CreatedDate || "",
                    item.ModifiedDate || ""
                ].join("|");

            if (!seen[key]) {
                seen[key] = true;
                combined.push(item);
            }
        }
    }

    return combined;
}

function effectiveSystemDate(row) {
    var candidates = [
        row.StartTime,
        row.ScheduledTime,
        row.StatusLastUpdate,
        row.CreatedDate,
        row.ModifiedDate
    ];

    for (var i=0;i<candidates.length;i++) {
        var parsed =
            parseSystemDate(
                candidates[i]
            );

        if (parsed.valid) {
            return parsed;
        }
    }

    return {
        valid:false,
        year:null,
        systemString:"",
        epoch:null
    };
}

function getTaskInstances(api,pid) {
    return safeRetrieve(
        api,
        "TaskInstance",
        [
            "ObjectID",
            "TaskDefinition.ObjectID",
            "Program.ObjectID",
            "ProgramInstance.ObjectID",
            "Name",
            "Sequence",
            "CreatedDate",
            "ModifiedDate"
        ],
        {
            Property:"ProgramInstance.ObjectID",
            SimpleOperator:"equals",
            Value:pid
        },
        "ProgramInstance.ObjectID = "+pid
    );
}

function getActivityInstances(api,pid) {
    return safeRetrieve(
        api,
        "ActivityInstance",
        [
            "ProgramID",
            "ObjectID",
            "CustomerKey",
            "ProgramInstance.ObjectID",
            "TaskInstance.ObjectID",
            "ActivityDefinition.ObjectID",
            "Name",
            "Status",
            "StatusMessage",
            "StatusLastUpdate",
            "SequenceID",
            "PartnerAPIObjectTypeID",
            "CreatedDate",
            "ModifiedDate"
        ],
        {
            Property:"ProgramInstance.ObjectID",
            SimpleOperator:"equals",
            Value:pid
        },
        "ProgramInstance.ObjectID = "+pid
    );
}

function statusLabel(instance) {
    if (instance.StatusMessage) {
        var message =
            String(
                instance.StatusMessage
            );

        var lower =
            message.toLowerCase();

        if (
            lower.indexOf("complete") >= 0 ||
            lower.indexOf("success") >= 0
        ) {
            return "Completed";
        }

        if (
            lower.indexOf("error") >= 0 ||
            lower.indexOf("fail") >= 0
        ) {
            return "Error";
        }

        return message;
    }

    var s =
        parseInt(
            instance.Status,
            10
        );

    if (s === 1) return "Completed";
    if (s < 0) return "Error";

    return "Status "+String(
        instance.Status == null
        ? ""
        : instance.Status
    );
}

function statusClass(label) {
    var l =
        String(
            label || ""
        ).toLowerCase();

    if (
        l.indexOf("complete") >= 0 ||
        l.indexOf("success") >= 0
    ) {
        return "completed";
    }

    if (
        l.indexOf("error") >= 0 ||
        l.indexOf("fail") >= 0
    ) {
        return "error";
    }

    if (
        l.indexOf("run") >= 0 ||
        l.indexOf("execut") >= 0
    ) {
        return "running";
    }

    return "other";
}


function normalizeTaskType(value) {
    var raw = String(value || "");
    var v = raw.toLowerCase();

    if (!raw) return "";

    if (v.indexOf("query") >= 0) return "SQL Query";
    if (v.indexOf("filter") >= 0) return "Filter";
    if (v.indexOf("script") >= 0 || v.indexOf("javascript") >= 0) return "Script";
    if (v.indexOf("extract") >= 0) return "Data Extract";
    if (v.indexOf("import") >= 0) return "Import File";
    if (v.indexOf("transfer") >= 0 || v.indexOf("ftp") >= 0) return "File Transfer";
    if (v.indexOf("email") >= 0 || v.indexOf("send") >= 0) return "Email Send";
    if (v.indexOf("report") >= 0) return "Report";
    if (v.indexOf("wait") >= 0) return "Wait";
    if (v.indexOf("verification") >= 0) return "Verification";
    if (v.indexOf("push") >= 0) return "Push";
    if (v.indexOf("sms") >= 0 || v.indexOf("mobileconnect") >= 0) return "SMS";
    if (v.indexOf("salesforce") >= 0) return "Salesforce Send";
    if (v.indexOf("event") >= 0) return "Event";

    return raw;
}

function activityTypeLabel(typeId) {
    var id = parseInt(typeId,10);

    if (id === 42) return "Email Send";
    if (id === 43) return "Import File";
    if (id === 45) return "Refresh Group";
    if (id === 53) return "File Transfer";
    if (id === 73) return "Data Extract";
    if (id === 84) return "Report";
    if (id === 300) return "SQL Query";
    if (id === 303) return "Filter";
    if (id === 423) return "Script";
    if (id === 425) return "Data Factory Utility";
    if (id === 427) return "Build Audience";
    if (id === 467) return "Wait";
    if (id === 724) return "Refresh Mobile Filtered List";
    if (id === 725) return "Send SMS";
    if (id === 726) return "Import Mobile Contacts";
    if (id === 733) return "Interaction Studio";
    if (id === 736) return "Send Push";
    if (id === 749) return "Fire Event";
    if (id === 756) return "Interaction Studio Date Event";
    if (id === 771) return "Salesforce Send";
    if (id === 783) return "GroupConnect";
    if (id === 1000) return "Verification";
    if (id === 1010) return "Thunderhead Transfer";
    if (id === 1101) return "Interaction Studio Decision";
    if (id === 1701) return "Predictive Intelligence Recommendation";

    return "Unknown";
}

function soapValue(obj,path) {
    if (!obj || !path) return null;

    try {
        if (
            typeof obj[path] != "undefined" &&
            obj[path] !== null &&
            obj[path] !== ""
        ) {
            return obj[path];
        }
    } catch(ignoreFlat) {}

    try {
        var parts =
            String(path).split(".");

        var current =
            obj;

        for (
            var i=0;
            i<parts.length;
            i++
        ) {
            if (
                current == null ||
                typeof current[parts[i]] == "undefined"
            ) {
                return null;
            }

            current =
                current[parts[i]];
        }

        return current;
    } catch(ignoreNested) {
        return null;
    }
}

function hasProperty(obj,key) {
    if (!obj) return false;

    try {
        return typeof obj[key] != "undefined";
    } catch(e) {
        return false;
    }
}

function hasOwnValue(obj,key) {
    if (!obj) return false;

    try {
        return typeof obj[key] != "undefined" &&
               obj[key] !== null;
    } catch(e) {
        return false;
    }
}

function inferActivityTypeFromDefinition(definition) {
    if (!definition) return "";

    var signature = "";

    try {
        signature =
            Stringify(
                definition
            );
    } catch(ignoreStringify) {
        try {
            signature =
                String(
                    definition
                );
        } catch(ignoreString) {
            signature = "";
        }
    }

    function sigHas(token) {
        return (
            String(signature)
                .indexOf(
                    '"'+token+'"'
                ) >= 0
        );
    }

    /*
      Signatures observed in the v12 raw SOAP payload.
    */
    if (
        sigHas("QueryText") ||
        sigHas("TargetUpdateType") ||
        sigHas("DataExtensionTarget")
    ) {
        return "SQL Query";
    }

    if (
        sigHas("SubscriberImportType") ||
        sigHas("FieldMappingType") ||
        sigHas("ControlColumnDefaultAction") ||
        sigHas("DestinationType")
    ) {
        return "Import File";
    }

    if (
        sigHas("ExtractType") ||
        sigHas("DataExtractTypeID") ||
        sigHas("DataFields")
    ) {
        return "Data Extract";
    }

    if (
        sigHas("TransferType") ||
        sigHas("FileTransferLocation") ||
        sigHas("IsUpload")
    ) {
        return "File Transfer";
    }

    if (
        sigHas("ScriptLanguage") ||
        sigHas("Script")
    ) {
        return "Script";
    }

    if (
        sigHas("EmailID") ||
        sigHas("SendDefinitionList")
    ) {
        return "Email Send";
    }

    if (
        sigHas("FilterDefinition") ||
        sigHas("FilterActivity")
    ) {
        return "Filter";
    }

    return "";
}

function probeDefinitionType(api,definitionObjectID) {
    /*
      Final OAuth-free fallback.

      A Definition.ObjectID belongs to a concrete SOAP definition object.
      When the generic Activity payload does not expose a usable type ID or
      a recognizable Definition shape, probe a small set of common
      Automation Studio definition objects by ObjectID.

      A failed/unsupported object probe is ignored. No OAuth or REST is used.
    */
    if (!definitionObjectID) return "";

    var probes = [
        { objectType:"QueryDefinition",          label:"SQL Query" },
        { objectType:"ImportDefinition",         label:"Import File" },
        { objectType:"DataExtractActivity",      label:"Data Extract" },
        { objectType:"DataExtractDefinition",    label:"Data Extract" },
        { objectType:"FileTransferActivity",     label:"File Transfer" },
        { objectType:"FileTransferDefinition",   label:"File Transfer" },
        { objectType:"ScriptActivity",           label:"Script" },
        { objectType:"ScriptActivityDefinition", label:"Script" },
        { objectType:"EmailSendDefinition",      label:"Email Send" },
        { objectType:"FilterActivity",           label:"Filter" },
        { objectType:"FilterDefinition",         label:"Filter" }
    ];

    for (var i=0;i<probes.length;i++) {
        try {
            var r = api.retrieve(
                probes[i].objectType,
                ["ObjectID"],
                {
                    Property:"ObjectID",
                    SimpleOperator:"equals",
                    Value:String(definitionObjectID)
                }
            );

            if (
                r &&
                r.Results &&
                r.Results.length > 0
            ) {
                return probes[i].label;
            }
        } catch(ignoreProbe) {}
    }

    return "";
}

function resolveActivityType(api,actDef,ainst) {
    var staticTypeId =
        actDef &&
        actDef.PartnerAPIObjectTypeID != null
        ? actDef.PartnerAPIObjectTypeID
        : "";

    var instanceTypeId =
        ainst &&
        ainst.PartnerAPIObjectTypeID != null
        ? ainst.PartnerAPIObjectTypeID
        : "";

    var typeId =
        staticTypeId !== ""
        ? staticTypeId
        : instanceTypeId;

    var byId =
        activityTypeLabel(
            typeId
        );

    if (
        byId &&
        byId != "Unknown"
    ) {
        return {
            label:byId,
            typeId:typeId,
            source:"PartnerAPIObjectTypeID"
        };
    }

    var byShape =
        inferActivityTypeFromDefinition(
            actDef
            ? actDef.Definition
            : null
        );

    if (byShape) {
        return {
            label:byShape,
            typeId:typeId,
            source:"Definition structure"
        };
    }

    /*
      Product behavior:
      Do not guess or perform speculative definition-object probes.
      If neither PartnerAPIObjectTypeID nor the returned Definition
      structure identifies the activity reliably, display "Unknown".
    */
    return {
        label:"Unknown",
        typeId:typeId,
        source:"Unresolved"
    };
}

/* ---------------------------------------------------------
   WSProxy
--------------------------------------------------------- */
var api =
    new Script.Util.WSProxy();

try {
    api.setClientId({
        ID:
        Platform.Function.AuthenticatedMemberID(),
        UserID:
        Platform.Function.AuthenticatedEmployeeID()
    });
} catch(ignoreClient) {}

/* ---------------------------------------------------------
   Suggestions
--------------------------------------------------------- */
var automationList = [];

try {
    automationList =
        retrieveAutomationList(
            api
        );

    diag.automationsLoaded =
        automationList.length;

    for (
        var li=0;
        li<automationList.length;
        li++
    ) {
        if (
            automationList[li].Name
        ) {
            automationNames.push(
                String(
                    automationList[li].Name
                )
            );
        }
    }
} catch(ignoreSuggestionFailure) {}

/*
  Date-input defaults and limits are based on the current Marketing Cloud
  account/user local date.
*/
accountLocalToday =
    accountLocalDateOffset(0);

accountLocalYesterday =
    accountLocalDateOffset(-1);

accountLocalOldest =
    accountLocalDateOffset(-30);

/* ---------------------------------------------------------
   Search
--------------------------------------------------------- */
var action =
    Platform.Request.GetFormField(
        "action"
    );

if (action === "search" || action === "filter") {
    submitted = true;

    automationName =
        String(
            Platform.Request.GetFormField(
                "automationName"
            ) || ""
        )
        .replace(
            /^\s+|\s+$/g,
            ""
        );

    startDate =
        String(
            Platform.Request.GetFormField(
                "startDate"
            ) || ""
        );

    endDate =
        String(
            Platform.Request.GetFormField(
                "endDate"
            ) || ""
        );

    resultFilter =
        String(
            Platform.Request.GetFormField(
                "resultFilter"
            ) || ""
        )
        .replace(
            /^\s+|\s+$/g,
            ""
        );

    try {
        if (
            !automationName ||
            !startDate ||
            !endDate
        ) {
            throw new Error(
                "Automation Name, Start Date, and End Date are required."
            );
        }

        var span =
            daysBetween(
                startDate,
                endDate
            );

        if (span < 0) {
            throw new Error(
                "End Date must be the same as or later than Start Date."
            );
        }

        if (span > 31) {
            throw new Error(
                "The maximum search range is 31 days."
            );
        }

        /*
          Convert the user's LOCAL date range back to Marketing Cloud
          SYSTEM time, then compare against the source system timestamps
          returned by SOAP.
        */
        var startSystemEpoch =
            localDateToSystemEpoch(
                startDate,
                false
            );

        var endSystemEpoch =
            localDateToSystemEpoch(
                endDate,
                true
            );

        if (
            startSystemEpoch == null ||
            endSystemEpoch == null
        ) {
            throw new Error(
                "The selected local date range could not be converted to Marketing Cloud system time."
            );
        }

        var automation =
            findAutomation(
                api,
                automationName,
                automationList
            );

        if (!automation) {
            throw new Error(
                "No automation was found with that exact name."
            );
        }

        var staticTasks =
            getStaticTasks(
                api,
                automation.ObjectID
            );

        var staticActivities =
            getStaticActivities(
                api,
                automation.ObjectID
            );

        diag.staticTasks =
            staticTasks.length;

        diag.staticActivities =
            staticActivities.length;

        diagStaticTasks =
            staticTasks;

        diagStaticActivities =
            staticActivities;

        var staticTaskSequence = {};

        for (
            var st=0;
            st<staticTasks.length;
            st++
        ) {
            var staticTaskId =
                String(
                    staticTasks[st].ObjectID
                );

            /*
              Task.Sequence is zero-based in the SOAP definition.
              Example from v12:
                Sequence 0 -> Step 1
                Sequence 1 -> Step 2
                Sequence 2 -> Step 3
                Sequence 3 -> Step 4
            */
            staticTaskSequence[
                staticTaskId
            ] =
                (
                    parseInt(
                        staticTasks[st].Sequence,
                        10
                    ) || 0
                ) + 1;
        }

        /*
          ActivityInstance.ActivityDefinition.ObjectID points to the static
          Activity.ObjectID, NOT to Activity.Definition.ObjectID.

          Keep all three maps because CustomerKey / Definition.ObjectID are
          still useful fallbacks on other stacks.
        */
        var activityByObjectID = {};
        var activityByDefinition = {};
        var activityByCustomerKey = {};

        for (
            var sa=0;
            sa<staticActivities.length;
            sa++
        ) {
            var act =
                staticActivities[sa];

            if (
                act.ObjectID
            ) {
                activityByObjectID[
                    String(
                        act.ObjectID
                    )
                ] = act;
            }

            var staticDefinitionObjectID =
                soapValue(
                    act,
                    "Definition.ObjectID"
                );

            if (
                staticDefinitionObjectID
            ) {
                activityByDefinition[
                    String(
                        staticDefinitionObjectID
                    )
                ] = act;
            }

            if (
                act.CustomerKey
            ) {
                activityByCustomerKey[
                    String(
                        act.CustomerKey
                    )
                ] = act;
            }
        }

        var instanceRows =
            getProgramInstances(
                api,
                automation
            );

        var candidateIds = [];
        var seenPid = {};

        for (
            var ir=0;
            ir<instanceRows.length;
            ir++
        ) {
            var instRow =
                instanceRows[ir];

            var effective =
                effectiveSystemDate(
                    instRow
                );

            if (
                effective.valid
            ) {
                diag.validInstanceDates++;
            } else {
                diag.invalidInstanceDates++;
                continue;
            }

            if (
                effective.epoch <
                startSystemEpoch ||
                effective.epoch >
                endSystemEpoch
            ) {
                continue;
            }

            var pid = "";

            if (
                instRow.ProgramInstanceID
            ) {
                pid =
                    String(
                        instRow.ProgramInstanceID
                    );
            } else if (
                instRow.__source ===
                "ProgramInstance" &&
                instRow.ObjectID
            ) {
                pid =
                    String(
                        instRow.ObjectID
                    );
            }

            if (
                pid &&
                !seenPid[pid]
            ) {
                seenPid[pid] = true;
                candidateIds.push(pid);
            }
        }

        diag.candidateProgramInstanceIds =
            candidateIds.length;

        for (
            var ci=0;
            ci<candidateIds.length;
            ci++
        ) {
            var pid =
                candidateIds[ci];

            var taskInstances =
                getTaskInstances(
                    api,
                    pid
                );

            var activityInstances =
                getActivityInstances(
                    api,
                    pid
                );

            diag.taskInstances +=
                taskInstances.length;

            diag.activityInstances +=
                activityInstances.length;

            for (
                var dti=0;
                dti<taskInstances.length;
                dti++
            ) {
                diagTaskInstances.push(
                    taskInstances[dti]
                );
            }

            for (
                var dai=0;
                dai<activityInstances.length;
                dai++
            ) {
                diagActivityInstances.push(
                    activityInstances[dai]
                );
            }

            var taskSequence = {};
            var taskInstanceMeta = {};

            for (
                var ti=0;
                ti<taskInstances.length;
                ti++
            ) {
                var taskInstanceId =
                    String(
                        taskInstances[ti].ObjectID
                    );

                var taskDefinitionRaw =
                    soapValue(
                        taskInstances[ti],
                        "TaskDefinition.ObjectID"
                    );

                var taskDefinitionId =
                    taskDefinitionRaw
                    ? String(
                        taskDefinitionRaw
                      )
                    : "";

                var mappedTaskNumber =
                    taskDefinitionId &&
                    staticTaskSequence[
                        taskDefinitionId
                    ] != null
                    ? staticTaskSequence[
                        taskDefinitionId
                      ]
                    : 0;

                var instanceTaskNumber =
                    (
                        parseInt(
                            taskInstances[ti].Sequence,
                            10
                        ) || 0
                    ) + 1;

                taskSequence[
                    taskInstanceId
                ] =
                    mappedTaskNumber > 0
                    ? mappedTaskNumber
                    : instanceTaskNumber;

                taskInstanceMeta[
                    taskInstanceId
                ] = {
                    taskDefinitionId:
                        taskDefinitionId,
                    taskNumber:
                        mappedTaskNumber > 0
                        ? mappedTaskNumber
                        : instanceTaskNumber,
                    activityType:""
                };
            }

            for (
                var ai=0;
                ai<activityInstances.length;
                ai++
            ) {
                var ainst =
                    activityInstances[ai];

                var actDef = null;

                var instanceActivityDefinitionID =
                    soapValue(
                        ainst,
                        "ActivityDefinition.ObjectID"
                    );

                var instanceDefinitionID =
                    soapValue(
                        ainst,
                        "Definition.ObjectID"
                    );

                /*
                  v14 diagnostics confirmed:
                  ActivityInstance.CustomerKey == Static Activity.CustomerKey.
                */
                if (
                    instanceActivityDefinitionID &&
                    activityByObjectID[
                        String(
                            instanceActivityDefinitionID
                        )
                    ]
                ) {
                    actDef =
                        activityByObjectID[
                            String(
                                instanceActivityDefinitionID
                            )
                        ];
                }
                else if (
                    instanceDefinitionID &&
                    activityByDefinition[
                        String(
                            instanceDefinitionID
                        )
                    ]
                ) {
                    actDef =
                        activityByDefinition[
                            String(
                                instanceDefinitionID
                            )
                        ];
                }
                else if (
                    ainst.CustomerKey &&
                    activityByCustomerKey[
                        String(
                            ainst.CustomerKey
                        )
                    ]
                ) {
                    actDef =
                        activityByCustomerKey[
                            String(
                                ainst.CustomerKey
                            )
                        ];
                }

                var taskNo = 0;
                var taskTypeFromInstance = "";

                var instanceTaskInstanceID =
                    soapValue(
                        ainst,
                        "TaskInstance.ObjectID"
                    );

                var staticActivityTaskID =
                    actDef
                    ? soapValue(
                        actDef,
                        "Task.ObjectID"
                      )
                    : null;

                if (
                    instanceTaskInstanceID &&
                    taskInstanceMeta[
                        String(
                            instanceTaskInstanceID
                        )
                    ]
                ) {
                    var taskMeta =
                        taskInstanceMeta[
                            String(
                                instanceTaskInstanceID
                            )
                        ];

                    taskNo =
                        taskMeta.taskNumber || 0;
                }

                if (
                    taskNo <= 0 &&
                    staticActivityTaskID &&
                    staticTaskSequence[
                        String(
                            staticActivityTaskID
                        )
                    ] != null
                ) {
                    taskNo =
                        staticTaskSequence[
                            String(
                                staticActivityTaskID
                            )
                        ];
                }

                if (
                    taskNo <= 0 &&
                    instanceTaskInstanceID &&
                    taskSequence[
                        String(
                            instanceTaskInstanceID
                        )
                    ] != null
                ) {
                    taskNo =
                        taskSequence[
                            String(
                                instanceTaskInstanceID
                            )
                        ];
                }

                var activityNo = 0;

                if (
                    actDef &&
                    actDef.Sequence != null
                ) {
                    activityNo =
                        (
                            parseInt(
                                actDef.Sequence,
                                10
                            ) || 0
                        ) + 1;
                }
                else if (
                    ainst.SequenceID != null
                ) {
                    var rawSequenceId =
                        parseInt(
                            ainst.SequenceID,
                            10
                        );

                    activityNo =
                        isNaN(
                            rawSequenceId
                        )
                        ? 1
                        : (
                            rawSequenceId <= 0
                            ? 1
                            : rawSequenceId
                          );
                }
                else if (
                    taskNo > 0
                ) {
                    activityNo = 1;
                }

                var step = "-";

                if (
                    taskNo > 0 &&
                    activityNo > 0
                ) {
                    step =
                        taskNo+"."+activityNo;

                    diag.mappedSteps++;
                } else if (
                    activityNo > 0
                ) {
                    step =
                        String(
                            activityNo
                        );

                    diag.unmappedSteps++;
                } else {
                    diag.unmappedSteps++;
                }

                var created =
                    parseSystemDate(
                        ainst.CreatedDate
                    );

                var modified =
                    parseSystemDate(
                        ainst.ModifiedDate ||
                        ainst.StatusLastUpdate
                    );

                var duration =
                    durationSeconds(
                        created.valid
                        ? created.epoch
                        : null,
                        modified.valid
                        ? modified.epoch
                        : (
                            created.valid
                            ? created.epoch
                            : null
                          )
                    );

                var label =
                    statusLabel(
                        ainst
                    );

                var css =
                    statusClass(
                        label
                    );

                var resolvedType =
                    resolveActivityType(
                        api,
                        actDef,
                        ainst
                    );

                resultRows.push({
                    programInstanceID:
                        pid,
                    step:
                        step,
                    taskNo:
                        taskNo,
                    activityNo:
                        activityNo,
                    activityName:
                        actDef &&
                        actDef.Name
                        ? actDef.Name
                        : (
                            ainst.Name ||
                            "(Unnamed activity)"
                          ),
                    activityTypeId:
                        resolvedType.typeId,
                    activityType:
                        resolvedType.label,
                    activityTypeSource:
                        resolvedType.source,
                    createdSystem:
                        created.valid
                        ? created.systemString
                        : "",
                    modifiedSystem:
                        modified.valid
                        ? modified.systemString
                        : "",
                    createdLocal:
                        created.valid
                        ? systemToLocalDisplay(
                            created.systemString
                          )
                        : "-",
                    modifiedLocal:
                        modified.valid
                        ? systemToLocalDisplay(
                            modified.systemString
                          )
                        : "-",
                    durationSec:
                        duration,
                    duration:
                        fmtDuration(
                            duration
                        ),
                    status:
                        label,
                    statusClass:
                        css,
                    statusMessage:
                        ainst.StatusMessage || ""
                });

                if (
                    css ===
                    "completed"
                ) {
                    completedCount++;
                }

                totalDurationSec +=
                    duration;

                if (
                    duration >
                    maxDurationSec
                ) {
                    maxDurationSec =
                        duration;
                }
            }
        }

        resultRows.sort(
            function(a,b) {
                if (
                    a.createdSystem <
                    b.createdSystem
                ) return -1;

                if (
                    a.createdSystem >
                    b.createdSystem
                ) return 1;

                if (
                    a.taskNo !==
                    b.taskNo
                ) {
                    return (
                        a.taskNo -
                        b.taskNo
                    );
                }

                if (
                    a.activityNo !==
                    b.activityNo
                ) {
                    return (
                        a.activityNo -
                        b.activityNo
                    );
                }

                if (
                    a.activityName <
                    b.activityName
                ) return -1;

                if (
                    a.activityName >
                    b.activityName
                ) return 1;

                return 0;
            }
        );

        resultCount =
            resultRows.length;

        /*
          Calculate automation-level durations by ProgramInstanceID.
          One automation execution duration is the SUM of the durations
          of all returned activity instances in that execution.
        */
        var runStats = {};

        for (var rs=0;rs<resultRows.length;rs++) {
            var rr =
                resultRows[rs];

            var runKey =
                String(
                    rr.programInstanceID || ""
                );

            if (!runKey) continue;

            if (!runStats[runKey]) {
                runStats[runKey] = 0;
            }

            runStats[runKey] +=
                Number(
                    rr.durationSec || 0
                );
        }

        var automationDurationTotalSec = 0;

        for (var runId in runStats) {
            if (!runStats.hasOwnProperty(runId)) continue;

            var runDurationSec =
                Math.max(
                    0,
                    Math.round(
                        runStats[runId]
                    )
                );

            automationRunCount++;
            automationDurationTotalSec +=
                runDurationSec;

            if (
                runDurationSec >
                maxAutomationDurationSec
            ) {
                maxAutomationDurationSec =
                    runDurationSec;
            }
        }

        avgAutomationDurationSec =
            automationRunCount
            ? Math.round(
                automationDurationTotalSec /
                automationRunCount
              )
            : 0;

        /*
          Keep the legacy activity average variable populated internally,
          although it is no longer shown as a KPI.
        */
        avgDurationSec =
            resultCount
            ? Math.round(
                totalDurationSec /
                resultCount
              )
            : 0;

        if (!candidateIds.length) {
            infoMessage =
                "No execution instances were found inside the selected local date range.";
        }

    } catch(e) {
        errorMessage =
            e && e.message
            ? String(
                e.message
              )
            : String(e);
    }

}

/*
  Results toolbar fallback.
  Filtering and CSV export are performed server-side so these controls
  do not depend on client-side JavaScript in the Marketing Cloud shell.
*/
displayRows = resultRows;

if (submitted && !errorMessage && resultFilter) {
    var filteredRows = [];
    var filterNeedle = String(resultFilter).toLowerCase();

    for (var fr=0; fr<resultRows.length; fr++) {
        var filterRow = resultRows[fr];

        var searchable =
            String(filterRow.step || "") + " " +
            String(filterRow.activityName || "") + " " +
            String(filterRow.activityType || "") + " " +
            String(filterRow.createdLocal || "") + " " +
            String(filterRow.modifiedLocal || "") + " " +
            String(filterRow.duration || "") + " " +
            String(filterRow.status || "") + " " +
            String(filterRow.statusMessage || "");

        if (searchable.toLowerCase().indexOf(filterNeedle) >= 0) {
            filteredRows.push(filterRow);
        }
    }

    displayRows = filteredRows;
}

function csvEscape(value) {
    var s = String(value == null ? "" : value);
    s = s.replace(/\r?\n|\r/g, " ");
    return '"' + s.replace(/"/g, '""') + '"';
}

var csvDownloadHref = "";

if (submitted && !errorMessage) {
    var csvLines = [];
    csvLines.push(
        [
            "Step",
            "Activity",
            "Type",
            "Start",
            "End",
            "Duration",
            "Status"
        ].join(",")
    );

    for (var cr=0; cr<displayRows.length; cr++) {
        var csvRow = displayRows[cr];

        csvLines.push(
            [
                csvEscape(csvRow.step),
                csvEscape(csvRow.activityName),
                csvEscape(csvRow.activityType),
                csvEscape(csvRow.createdLocal),
                csvEscape(csvRow.modifiedLocal),
                csvEscape(csvRow.duration),
                csvEscape(csvRow.status)
            ].join(",")
        );
    }

    /*
      Use a normal download link instead of changing the CloudPage response.
      A Content Builder CloudPage can add its own HTML wrapper to the HTTP
      response, which is why the previous "CSV" contained HTML.
    */
    /*
      In Marketing Cloud SSJS, encodeURIComponent can encode spaces as "+".
      A data: URI does not convert "+" back to a space, so normalize only
      literal "+" produced by the encoder to %20. Real plus signs are
      already encoded as %2B and remain intact.
    */
    var encodedCsv =
        encodeURIComponent(
            csvLines.join("\r\n")
        ).replace(/\+/g,"%20");

    csvDownloadHref =
        "data:text/csv;charset=utf-8,%EF%BB%BF" +
        encodedCsv;
}
</script>

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="color-scheme" content="light">
<title>Automation Activity Viewer</title>

<style>
:root{
 --page:#f4f7fb;--surface:#fff;--text:#182230;--muted:#667085;--line:#e4e7ec;
 --brand:#5b5ce2;--brand2:#7c3aed;--ok:#067647;--okbg:#ecfdf3;
 --info:#175cd3;--infobg:#eff8ff;--bad:#b42318;--badbg:#fef3f2;
 --warn:#b54708;--warnbg:#fffaeb;--shadow:0 22px 55px rgba(16,24,40,.08)
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
 margin:0;min-width:320px;color:var(--text);
 background:
 radial-gradient(circle at 8% 0,rgba(91,92,226,.14),transparent 30rem),
 radial-gradient(circle at 92% 3%,rgba(124,58,237,.10),transparent 28rem),
 var(--page);
 font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif
}
button,input{font:inherit}
.page{width:calc(100% - 40px);max-width:1440px;min-width:0;margin:auto;padding:32px 0 56px}
.hero{
 position:relative;overflow:hidden;padding:36px;color:#fff;border-radius:28px;
 background:linear-gradient(125deg,#24275f,#5558d9 52%,#7c3aed);
 box-shadow:0 26px 70px rgba(72,63,178,.22)
}
.hero:after{
 content:"";position:absolute;width:330px;height:330px;right:-90px;top:-190px;
 border-radius:50%;background:rgba(255,255,255,.09)
}
.eyebrow{
 display:flex;align-items:center;gap:9px;margin-bottom:14px;font-size:12px;font-weight:850;
 letter-spacing:.12em;text-transform:uppercase;opacity:.86
}
.dot{
 width:9px;height:9px;border-radius:50%;background:#9cf2bd;
 box-shadow:0 0 0 5px rgba(156,242,189,.16)
}
h1{
 position:relative;z-index:1;margin:0;font-size:clamp(32px,4.3vw,54px);
 line-height:1.04;letter-spacing:-.045em
}
.hero p{
 position:relative;z-index:1;max-width:860px;margin:16px 0 0;
 color:rgba(255,255,255,.8);font-size:15px;line-height:1.75
}
.chips{position:relative;z-index:1;display:flex;flex-wrap:wrap;gap:9px;margin-top:25px}
.chip{
 padding:8px 11px;border:1px solid rgba(255,255,255,.14);border-radius:999px;
 background:rgba(255,255,255,.11);font-size:12px;font-weight:780
}

.panel{
 min-width:0;margin-top:20px;border:1px solid var(--line);border-radius:20px;
 background:rgba(255,255,255,.96);box-shadow:var(--shadow);overflow:hidden
}
.search{padding:22px}
.title{margin:0;font-size:19px;letter-spacing:-.02em}
.sub{margin:6px 0 0;color:var(--muted);font-size:13px;line-height:1.55}
.grid{
 display:flex;flex-wrap:wrap;gap:14px;margin-top:19px;align-items:flex-end
}
.grid>div{min-width:0}
.grid .auto{flex:2 1 360px}
.grid>div:not(.auto):not(.searchbtn){flex:1 1 180px}
.grid .searchbtn{flex:0 1 auto}
.grid .searchbtn .btn{min-width:96px}
label{display:block;margin:0 0 7px;color:#344054;font-size:12px;font-weight:820}
.control{
 width:100%;height:46px;padding:0 13px;color:var(--text);border:1px solid #d0d5dd;
 border-radius:12px;outline:0;background:#fff
}
.control:focus{border-color:#8587eb;box-shadow:0 0 0 4px rgba(91,92,226,.11)}
.btn{
 display:inline-flex;align-items:center;justify-content:center;height:46px;padding:0 19px;color:#fff;
 border:0;border-radius:12px;background:linear-gradient(135deg,var(--brand),var(--brand2));
 font-size:13px;font-weight:850;cursor:pointer;box-shadow:0 11px 24px rgba(91,92,226,.24)
}
.btn:disabled{opacity:.6;cursor:wait}
.btn2{
 height:38px;padding:0 14px;color:#344054;border:1px solid #d0d5dd;border-radius:11px;
 background:#fff;font-size:12px;font-weight:800;cursor:pointer
}

.notice{
 margin-top:16px;padding:13px 15px;border:1px solid;border-radius:12px;
 font-size:13px;line-height:1.55
}
.err{color:var(--bad);border-color:#fecdca;background:var(--badbg)}
.info{color:var(--info);border-color:#b2ddff;background:var(--infobg)}

.kpis{
 display:grid;
 grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));
 gap:14px;margin-top:18px
}
.kpi{
 padding:19px;border:1px solid var(--line);border-radius:17px;background:#fff;
 box-shadow:0 8px 24px rgba(16,24,40,.04)
}
.kl{color:var(--muted);font-size:12px;font-weight:780}
.kv{margin-top:8px;font-size:27px;font-weight:900;line-height:1;letter-spacing:-.04em}
.kc{margin-top:8px;color:#98a2b3;font-size:11px}

.toolbar{
 display:flex;align-items:center;justify-content:space-between;gap:18px;padding:18px 20px;
 border-bottom:1px solid var(--line)
}
.toolbar-left{min-width:0}
.toolbar-left h2{
 overflow:hidden;margin:0;font-size:18px;text-overflow:ellipsis;white-space:nowrap
}
.meta{margin-top:5px;color:var(--muted);font-size:12px}
.actions{display:flex;gap:8px}
.filter{width:250px;height:38px}

.scroll{width:100%;max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}
table{width:100%;min-width:980px;border-collapse:separate;border-spacing:0}
th{
 position:sticky;top:0;z-index:2;padding:12px 16px;color:#667085;background:#f8fafc;
 border-bottom:1px solid var(--line);font-size:11px;font-weight:850;letter-spacing:.055em;
 text-align:left;text-transform:uppercase;white-space:nowrap
}
td{padding:14px 16px;border-bottom:1px solid #eef1f5;font-size:13px;vertical-align:middle}
tbody tr:hover{background:#fbfbfe}
.step{
 display:inline-flex;min-width:54px;justify-content:center;padding:5px 9px;color:#4d4fc6;
 border-radius:9px;background:#f0f1ff;font-weight:850
}
.name{font-weight:780}
.type-pill{
 display:inline-flex;padding:5px 9px;border-radius:999px;background:#f2f4f7;
 color:#475467;font-size:11px;font-weight:800;white-space:nowrap
}
.muted{color:var(--muted);white-space:nowrap}
.dur{display:flex;min-width:155px;align-items:center;gap:10px}
.dv{min-width:50px;font-variant-numeric:tabular-nums;font-weight:820}
.bar{width:78px;height:7px;overflow:hidden;border-radius:999px;background:#eceef3}
.bar i{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#6668e8,#9a62e8)}
.badge{display:inline-flex;padding:5px 9px;border-radius:999px;font-size:11px;font-weight:850;white-space:nowrap}
.completed{color:var(--ok);background:var(--okbg)}
.running{color:var(--info);background:var(--infobg)}
.error{color:var(--bad);background:var(--badbg)}
.other{color:#475467;background:#f2f4f7}
.msg{max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted)}

.empty{padding:64px 24px;text-align:center}
.empty b{
 display:grid;width:62px;height:62px;margin:0 auto 16px;place-items:center;color:#5658cf;
 border-radius:19px;background:#f0f1ff;font-size:22px
}
.empty h3{margin:0;font-size:17px}
.empty p{max-width:540px;margin:8px auto 0;color:var(--muted);font-size:13px;line-height:1.6}

.diag{padding:18px 20px}
.diag summary{cursor:pointer;font-weight:850;font-size:13px}
.diag-grid{
 display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px
}
.diag-card{
 padding:12px;border:1px solid var(--line);border-radius:12px;background:#fafbfc
}
.diag-label{color:var(--muted);font-size:11px;font-weight:780}
.diag-value{margin-top:4px;font-size:18px;font-weight:900}

.footer{
 display:flex;justify-content:space-between;gap:18px;margin-top:18px;color:#98a2b3;font-size:11px
}

@media(max-width:1280px){
 .grid .auto{flex-basis:100%}
 .grid .searchbtn{flex:1 1 100%}
 .grid .searchbtn .btn{width:100%}
 .toolbar{align-items:stretch;flex-direction:column}
 .actions{width:100%}
 .filter{width:100%}
}

@media(max-width:760px){
 .page{width:calc(100% - 24px);padding-top:14px}
 .hero{padding:25px 20px;border-radius:22px}
 h1{font-size:34px}
 .search{padding:16px}
 .grid{display:block}
 .grid>div{width:100%;margin-top:14px}
 .grid>div:first-child{margin-top:0}
 .grid .searchbtn .btn{width:100%}
 .actions{flex-direction:column}
 .btn2{width:100%}
 .toolbar{padding:16px}
 .toolbar-left h2{white-space:normal}
 .meta{line-height:1.5}
 table{min-width:860px}
}

@media(max-width:480px){
 .page{width:calc(100% - 16px)}
 .hero{padding:22px 16px}
 h1{font-size:30px}
 .chips{gap:6px}
 .chip{padding:7px 9px;font-size:11px}
 .kpis,.diag-grid{grid-template-columns:1fr}
 .kpi{padding:16px}
}

/* Results toolbar release fix */
.actions{
  display:flex;
  align-items:center;
  justify-content:flex-end;
  gap:10px;
  flex-wrap:wrap;
}
.actions .filter{
  flex:1 1 280px;
  min-width:220px;
  max-width:420px;
}
.actions .btn2{
  flex:0 0 auto;
  white-space:nowrap;
}
@media(max-width:760px){
  .actions{
    width:100%;
    justify-content:stretch;
  }
  .actions .filter{
    flex:1 1 100%;
    max-width:none;
    min-width:0;
  }
  .actions .btn2{
    flex:1 1 140px;
  }
}

/* Final search-form alignment */
.grid{
  align-items:flex-start;
}
.grid > div{
  align-self:flex-start;
}
.grid label{
  display:block;
  min-height:18px;
  margin-bottom:7px;
}
.grid .control,
.grid input[type="date"]{
  margin-top:0;
}

/* Final Search button alignment */
.searchbtn{
  align-self:flex-start;
  padding-top:25px;
}
@media(max-width:760px){
  .searchbtn{
    padding-top:0;
  }
}
</style>
</head>

<body>
<main class="page">

<section class="hero">
 <div class="eyebrow"><span class="dot"></span>Marketing Cloud Engagement</div>
 <h1>Automation Activity Viewer</h1>
 <p>
  Inspect recent Automation Studio activity execution history directly from
  CloudPages. Start time, end time, duration, status, step, and identifiable
  activity type are shown in your current Marketing Cloud account/user local time.
 </p>
 <div class="chips">
  <span class="chip">Single CloudPage</span>
  <span class="chip">No Login Required</span>
  <span class="chip">Local Time Aware</span>
  <span class="chip">Unicode Ready</span>
  <span class="chip">CSV Export</span>
 </div>

</section>

<section class="panel search">
 <h2 class="title">Search activity history</h2>
 <p class="sub">
  Select an Automation Studio automation by typing to filter the available names. The selectable date window
  is limited to the latest 31 days, and dates are interpreted in your current
  Marketing Cloud account/user local time.
 </p>

 <form method="post" id="searchForm">
  <input type="hidden" name="action" value="search">

  <div class="grid">

   <div class="auto">
    <label for="automationName">Automation</label>
<script runat="server">
var selectedAutomationValue =
    automationName
        ? String(automationName)
        : "";

Write(
    '<input class="control" id="automationName" name="automationName" type="text" '+
    'list="automationSuggestions" autocomplete="off" '+
    'placeholder="Start typing an automation name..." value="'+
    esc(selectedAutomationValue)+
    '" required>'
);
</script>

    <datalist id="automationSuggestions">
<script runat="server">
for(var n=0;n<automationNames.length;n++){
    Write(
        '<option value="'+
        esc(automationNames[n])+
        '"></option>'
    );
}
</script>
    </datalist>

    <div class="hint">
<script runat="server">
Write(
    String(automationNames.length)+
    " automations available in this Business Unit"
);
</script>
    </div>
   </div>

   <div>
    <label for="startDate">Start Date</label>
<script runat="server">
var initialStartDate =
    startDate ||
    accountLocalYesterday ||
    accountLocalToday;

Write(
    '<input class="control" id="startDate" name="startDate" type="date" value="'+
    esc(initialStartDate)+
    '" min="'+
    esc(accountLocalOldest)+
    '" max="'+
    esc(accountLocalToday)+
    '" required>'
);
</script>
   </div>

   <div>
    <label for="endDate">End Date</label>
<script runat="server">
var initialEndDate =
    endDate ||
    accountLocalYesterday ||
    accountLocalToday;

Write(
    '<input class="control" id="endDate" name="endDate" type="date" value="'+
    esc(initialEndDate)+
    '" min="'+
    esc(accountLocalOldest)+
    '" max="'+
    esc(accountLocalToday)+
    '" required>'
);
</script>
   </div>

   <div class="searchbtn">
    <button class="btn" id="searchButton" type="submit">Search</button>
   </div>

  </div>
 </form>

 <p class="sub" style="margin-top:12px">
  Activity types are displayed only when they can be identified reliably from
  the available SOAP metadata. Otherwise, the type is shown as <strong>Unknown</strong>.
 </p>

<script runat="server">
if(errorMessage){
    Write(
        '<div class="notice err"><strong>Unable to load results.</strong><br>'+
        esc(errorMessage)+
        '</div>'
    );
}

if(infoMessage){
    Write(
        '<div class="notice info">'+
        esc(infoMessage)+
        '</div>'
    );
}
</script>
</section>

<script runat="server">
if(submitted && !errorMessage){
</script>

<section class="kpis">

 <div class="kpi">
  <div class="kl">Activities</div>
  <div class="kv"><script runat="server">Write(resultCount);</script></div>
  <div class="kc">Activity instances returned</div>
 </div>

 <div class="kpi">
  <div class="kl">Longest Activity Duration</div>
  <div class="kv"><script runat="server">Write(fmtDuration(maxDurationSec));</script></div>
  <div class="kc">Longest returned activity instance</div>
 </div>

 <div class="kpi">
  <div class="kl">Longest Automation Run</div>
  <div class="kv"><script runat="server">Write(fmtDuration(maxAutomationDurationSec));</script></div>
  <div class="kc">Longest summed activity duration in one automation execution</div>
 </div>

 <div class="kpi">
  <div class="kl">Average Automation Duration</div>
  <div class="kv"><script runat="server">Write(fmtDuration(avgAutomationDurationSec));</script></div>
  <div class="kc">Average of summed activity durations across automation executions</div>
 </div>

</section>

<section class="panel">

 <div class="toolbar">

  <div class="toolbar-left">
   <h2><script runat="server">Write(esc(automationName));</script></h2>

   <div class="meta">
<script runat="server">
Write(
    esc(startDate)+
    " to "+
    esc(endDate)+
    " &middot; "+
    (resultFilter ? (displayRows.length+" of "+resultCount) : resultCount)+
    " activity instances &middot; Local time"
);
</script>
   </div>
  </div>

  <form class="actions" method="post">
<script runat="server">
Write(
    '<input type="hidden" name="automationName" value="'+
    esc(automationName)+
    '">'
);
Write(
    '<input type="hidden" name="startDate" value="'+
    esc(startDate)+
    '">'
);
Write(
    '<input type="hidden" name="endDate" value="'+
    esc(endDate)+
    '">'
);
</script>

<script runat="server">
Write(
    '<input class="control filter" id="tableFilter" name="resultFilter" type="search" '+
    'placeholder="Filter activities..." value="'+
    esc(resultFilter)+
    '">'
);
</script>

   <button
    class="btn2"
    name="action"
    value="filter"
    type="submit">
    Apply Filter
   </button>

<script runat="server">
if (csvDownloadHref) {
    Write(
        '<a class="btn2" href="'+
        esc(csvDownloadHref)+
        '" download="automation-activity.csv" '+
        'style="display:inline-flex;align-items:center;justify-content:center;text-decoration:none">'+
        'Export CSV</a>'
    );
}
</script>
  </form>

 </div>

<script runat="server">
if(!resultRows.length){
</script>

 <div class="empty">
  <b>0</b>
  <h3>No activity instances found</h3>
  <p>
   No execution history matched the selected local date range.
  </p>
 </div>

<script runat="server">
}else{
    var maxBase = maxDurationSec || 1;
</script>

 <div class="scroll">

  <table id="resultsTable">

   <thead>
    <tr>
     <th>Step</th>
     <th>Activity</th>
     <th>Type</th>
     <th>Start</th>
     <th>End</th>
     <th>Duration</th>
     <th>Status</th>
    </tr>
   </thead>

   <tbody>
<script runat="server">
for(var r=0;r<displayRows.length;r++){

    var row =
        displayRows[r];

    var pct =
        Math.max(
            3,
            Math.min(
                100,
                Math.round(
                    row.durationSec /
                    maxBase *
                    100
                )
            )
        );

    Write("<tr>");

    Write(
        '<td><span class="step">'+
        esc(row.step)+
        '</span></td>'
    );

    Write(
        '<td><span class="name">'+
        esc(row.activityName)+
        '</span></td>'
    );

    Write(
        '<td><span class="type-pill">'+
        esc(row.activityType)+
        '</span></td>'
    );

    Write(
        '<td class="muted">'+
        esc(row.createdLocal || "-")+
        '</td>'
    );

    Write(
        '<td class="muted">'+
        esc(row.modifiedLocal || "-")+
        '</td>'
    );

    Write(
        '<td><div class="dur">'+
        '<span class="dv">'+
        esc(row.duration)+
        '</span>'+
        '<span class="bar">'+
        '<i style="width:'+
        pct+
        '%"></i>'+
        '</span>'+
        '</div></td>'
    );

    Write(
        '<td><span class="badge '+
        esc(row.statusClass)+
        '">'+
        esc(row.status)+
        '</span></td>'
    );

    Write("</tr>");
}
</script>
   </tbody>

  </table>

 </div>

<script runat="server">
}
</script>

</section>


<script runat="server">
}
</script>

<footer style="display:flex;justify-content:space-between;align-items:flex-end;gap:20px;margin-top:20px;color:#98a2b3;font-size:11px;line-height:1.6">
 <div style="text-align:left;white-space:nowrap">Version 3.2 · No Login</div>
 <div style="margin-left:auto;text-align:right">
  <div>Automation Activity Viewer created by Nobuyuki Watanabe.</div>
 </div>
</footer>

</main>

<script>
(function(){

 var form =
    document.getElementById(
        "searchForm"
    );

 var auto =
    document.getElementById(
        "automationName"
    );

 var start =
    document.getElementById(
        "startDate"
    );

 var end =
    document.getElementById(
        "endDate"
    );

 var button =
    document.getElementById(
        "searchButton"
    );

 var postedAuto = "";
 var postedStart = "";
 var postedEnd = "";

<script runat="server">
Write('postedAuto="'+jsEsc(automationName)+'";');
Write('postedStart="'+jsEsc(startDate)+'";');
Write('postedEnd="'+jsEsc(endDate)+'";');
</script>

 if(auto && postedAuto){
    auto.value = postedAuto;
 }

 if(start && postedStart){
    start.value = postedStart;
 }

 if(end && postedEnd){
    end.value = postedEnd;
 }

 function isoLocal(d){

    var y =
        d.getFullYear();

    var m =
        String(
            d.getMonth()+1
        ).padStart(
            2,
            "0"
        );

    var day =
        String(
            d.getDate()
        ).padStart(
            2,
            "0"
        );

    return (
        y+"-"+m+"-"+day
    );
 }

 var accountToday = "";
 var accountYesterday = "";
 var accountOldest = "";

<script runat="server">
Write('accountToday="'+jsEsc(accountLocalToday)+'";');
Write('accountYesterday="'+jsEsc(accountLocalYesterday)+'";');
Write('accountOldest="'+jsEsc(accountLocalOldest)+'";');
</script>

 /*
   Prefer the Marketing Cloud account/user local dates computed server-side.
   Browser-local fallback is only used if the server-side conversion failed.
 */
 var fallbackToday =
    new Date();

 fallbackToday.setHours(
    0,0,0,0
 );

 var fallbackOldest =
    new Date(
        fallbackToday
    );

 fallbackOldest.setDate(
    fallbackOldest.getDate()-30
 );

 var maxDate =
    accountToday ||
    isoLocal(
        fallbackToday
    );

 var minDate =
    accountOldest ||
    isoLocal(
        fallbackOldest
    );

 var defaultDate =
    accountYesterday ||
    maxDate;

 if(start){
    start.min = minDate;
    start.max = maxDate;
 }

 if(end){
    end.min = minDate;
    end.max = maxDate;
 }

 if(!postedStart && start){
    start.value = defaultDate;
 }

 if(!postedEnd && end){
    end.value = defaultDate;
 }

 if(start && end){

    start.addEventListener(
        "change",
        function(){

            end.min =
                start.value ||
                minDate;

            if(
                end.value &&
                start.value &&
                end.value <
                start.value
            ){
                end.value =
                    start.value;
            }
        }
    );

    end.addEventListener(
        "change",
        function(){

            start.max =
                end.value ||
                maxDate;

            if(
                start.value &&
                end.value &&
                start.value >
                end.value
            ){
                start.value =
                    end.value;
            }
        }
    );
 }

 if(form && button){

    form.addEventListener(
        "submit",
        function(){

            button.disabled =
                true;

            button.textContent =
                "Loading...";
        }
    );
 }

 var table =
    document.getElementById(
        "resultsTable"
    );

 var filter =
    document.getElementById(
        "tableFilter"
    );

 if(table && filter){

    filter.addEventListener(
        "input",
        function(){

            var q =
                String(
                    filter.value || ""
                ).toLocaleLowerCase();

            var trs =
                table.tBodies[0].rows;

            for(
                var i=0;
                i<trs.length;
                i++
            ){
                trs[i].style.display =
                    String(
                        trs[i].innerText || ""
                    )
                    .toLocaleLowerCase()
                    .indexOf(q) >= 0
                    ? ""
                    : "none";
            }
        }
    );
 }

 var csv =
    document.getElementById(
        "csvButton"
    );

 if(table && csv){

    csv.addEventListener(
        "click",
        function(){

            var output = [];

            var trs =
                table.querySelectorAll(
                    "tr"
                );

            for(
                var i=0;
                i<trs.length;
                i++
            ){
                if(
                    trs[i].style.display ===
                    "none"
                ){
                    continue;
                }

                var cells =
                    trs[i].querySelectorAll(
                        "th,td"
                    );

                var values = [];

                for(
                    var j=0;
                    j<cells.length;
                    j++
                ){
                    var value =
                        String(
                            cells[j].innerText || ""
                        )
                        .replace(
                            /\r?\n|\r/g,
                            " "
                        )
                        .trim();

                    values.push(
                        '"'+
                        value.replace(
                            /"/g,
                            '""'
                        )+
                        '"'
                    );
                }

                output.push(
                    values.join(",")
                );
            }

            var csvText =
                "\uFEFF"+
                output.join("\r\n");

            try {

                var blob =
                    new Blob(
                        [csvText],
                        {
                            type:
                            "text/csv;charset=utf-8"
                        }
                    );

                if (
                    window.navigator &&
                    window.navigator.msSaveOrOpenBlob
                ) {
                    window.navigator.msSaveOrOpenBlob(
                        blob,
                        "automation-activity.csv"
                    );
                    return;
                }

                var url =
                    window.URL.createObjectURL(
                        blob
                    );

                var link =
                    document.createElement(
                        "a"
                    );

                link.style.display =
                    "none";

                link.href =
                    url;

                link.setAttribute(
                    "download",
                    "automation-activity.csv"
                );

                document.body.appendChild(
                    link
                );

                link.click();

                /*
                  Do not revoke immediately. Some browsers cancel the
                  download if the Blob URL is destroyed in the same tick.
                */
                window.setTimeout(
                    function(){
                        try {
                            document.body.removeChild(
                                link
                            );
                        } catch(ignoreRemove) {}

                        try {
                            window.URL.revokeObjectURL(
                                url
                            );
                        } catch(ignoreRevoke) {}
                    },
                    1500
                );

            } catch(exportError) {

                /*
                  Last-resort fallback that does not depend on Blob URLs.
                */
                var dataUri =
                    "data:text/csv;charset=utf-8,"+
                    encodeURIComponent(
                        csvText
                    );

                var fallback =
                    document.createElement(
                        "a"
                    );

                fallback.href =
                    dataUri;

                fallback.setAttribute(
                    "download",
                    "automation-activity.csv"
                );

                document.body.appendChild(
                    fallback
                );

                fallback.click();

                document.body.removeChild(
                    fallback
                );
            }
        }
    );
 }

})();
</script>

</body>
</html>

いかがでしたでしょうか。

気に入った方は、ログイン認証しないと見れないものを作ってみましょう。

アプリのログイン認証の技術は、ポーランドの Salesforce MVP である Mateusz Dąbrowski(マテウシュ・ドンブロフスキ)さん の技術をお借りします

Mateusz Dąbrowski

それでは、以下の手順で実装を開始してみましょう。


監査ログ用データエクステンションの作成

マテウシュ さんの記事では、まず 2 つの監査ログ用のデータエクステンションを作成しています。一つは「AUTHENTICATION_DATA_EXTENSION」で、もう一つが「ERROR_DATA_EXTENSION」です。

私の方でこれらのデータエクステンションを簡単に作成できるようにスクリプトを組みましたので、以下を Automation Studio のスクリプトアクティビティに挿入して、それぞれ一回実行して下さい。

AUTHENTICATION_DATA_EXTENSION

<script runat="server">
    Platform.Load("Core", "1");

    var dataExtensionConfig = {
        "CustomerKey": "",
        "Name": "AUTHENTICATION_DATA_EXTENSION",
        "Fields": [

{ "Name" : "session", "FieldType" : "Text", "MaxLength" : 50, "IsPrimaryKey" : true, "IsRequired" : true }, 
{ "Name" : "appName", "FieldType" : "Text", "MaxLength" : 100, "IsRequired" : false }, 
{ "Name" : "createdDate", "FieldType" : "Date", "IsRequired" : false }, 
{ "Name" : "token", "FieldType" : "Text", "MaxLength" : 520, "IsRequired" : false }, 
{ "Name" : "tokenExpire", "FieldType" : "Date", "IsRequired" : false }, 
{ "Name" : "userName", "FieldType" : "Text", "MaxLength" : 100, "IsRequired" : false }, 
{ "Name" : "userEmail", "FieldType" : "Text", "MaxLength" : 254, "IsRequired" : false }, 

        ]
    };

    var createdDataExtension = DataExtension.Add(dataExtensionConfig);
</script>

※ データエクステンション作成後に、「createdDate」のデフォルト値に「Current Date」を設定してください。

ERROR_DATA_EXTENSION

<script runat="server">
    Platform.Load("Core", "1");

    var dataExtensionConfig = {
        "CustomerKey": "",
        "Name": "ERROR_DATA_EXTENSION",
        "Fields": [

{ "Name" : "id", "FieldType" : "Text", "MaxLength" : 36, "IsPrimaryKey" : true, "IsRequired" : true }, 
{ "Name" : "errorSource", "FieldType" : "Text", "MaxLength" : 100, "IsRequired" : false }, 
{ "Name" : "errorMessage", "FieldType" : "Text", "MaxLength" : 2000, "IsRequired" : false }, 
{ "Name" : "errorDescription", "FieldType" : "Text", "MaxLength" : 2000, "IsRequired" : false }, 
{ "Name" : "errorDate", "FieldType" : "Date", "IsRequired" : false }, 

        ]
    };

    var createdDataExtension = DataExtension.Add(dataExtensionConfig);
</script>

※ データエクステンション作成後に、「errorDate」のデフォルト値に「Current Date」を設定してください。

これにより、トップのデータエクステンションフォルダに、2 つの監査ログ用データエクステンションが作成できたかと思います。


Cloudpages URL の発行

続いて、新規で Cloudpages URL を発行します。Cloudpages の作成画面を開いたら、特に何も配置せずに「保存」してしまって良いです。現時点では Cloudpages URL が欲しいだけです。

「保存」が完了したら、Cloudpages URL をコピー してください。


インストール済みパッケージの設定

続いて、Marketing Cloud セットアップの インストール済みパッケージ に移動して、新規作成を行います。

  • パッケージ名の例:Automation Activity Viewer

ここからコンポーネントを 2 つ設定します。「コンポーネントの追加」をクリックしてください。

まずは、「API Integration」を選択します。

続いて、「Web App」を選択します。

続いて、先ほどコピーした Cloudpages URL を入力して「保存」します。スコープの設定は不要です。

以前は、いつでもクライアントシークレット(WebApp 用)を確認できましたが、現在は、この時点でのみクライアントシークレット(WebApp 用)が表示されるので、しっかりとメモしてください。

クライアントシークレットに続き、クライアント ID(WebApp 用)もコピーしたら、続いて、2 つ目のコンポーネントを作成するために、再度「コンポーネントの追加」をクリックします。

次は、「Marketing Cloud App」を選択します。

アプリの名前を登録して、2 つの Endpoint に先ほどコピーした Cloudpages URL を入力してください。同じものを入力してもらって OK です。これで設定を「保存」します。

  • アプリ名の例:Automation Activity Viewer

これで、インストール済みパッケージの設定は完了です。


Cloudpages でのコード設定

それでは、最後に先ほど、URL だけ発行した Cloudpages へ以下のコードを貼り付けてください。その際、4 箇所ほど書き換えてください。

var APP_URL = "YOUR_CLOUDPAGE_URL";
var WEB_APP_CLIENT_ID = "YOUR_WEB_APP_CLIENT_ID";
var WEB_APP_CLIENT_SECRET = "YOUR_WEB_APP_CLIENT_SECRET";
var CLIENT_BASE = "YOUR_TSSD";
  • Cloudpages URL:メモした Cloudpages の URL

  • クライアント ID:インストール済みパッケージで生成

  • クライアントシークレット:インストール済みパッケージで生成

  • クライアントベース:(以下を確認)

※ あなたの URI が https:// mc123abc456def .auth.marketingcloudapis.com/
だとしたら、CLIENT_BASE は mc123abc456def の部分です。

<script runat="server">
/*
  ============================================================
  Marketing Cloud Login Protection
  ============================================================
  Authentication approach based on the technique by Mateusz Dąbrowski,
  as documented and introduced by Nobuyuki Watanabe.

  Configure the values below before publishing.
*/
Platform.Load("Core", "1");

var APP_NAME = "Automation Activity Viewer";
var APP_URL = "YOUR_CLOUDPAGE_URL";
var WEB_APP_CLIENT_ID = "YOUR_WEB_APP_CLIENT_ID";
var WEB_APP_CLIENT_SECRET = "YOUR_WEB_APP_CLIENT_SECRET";
var CLIENT_BASE = "YOUR_TSSD";
var AUTH_DE = "AUTHENTICATION_DATA_EXTENSION";
var ERROR_DE = "ERROR_DATA_EXTENSION";
var ERROR_URL = "YOUR_ERROR_URL";
var DEBUGGING = false;

var authState = String(Platform.Request.GetQueryStringParameter("state") || "");
var authCode = String(Platform.Request.GetQueryStringParameter("code") || "");
var authError = String(Platform.Request.GetQueryStringParameter("error") || "");
var authErrorDescription = String(Platform.Request.GetQueryStringParameter("error_description") || "");
var postedSession = String(Platform.Request.GetFormField("authSession") || "");
var authSession = postedSession || authState;
var signedInUserName = "";
var signedInUserEmail = "";
var authAllowed = false;

function authHtml(v) {
    if (v == null) return "";
    return String(v)
        .replace(/&/g,"&amp;")
        .replace(/</g,"&lt;")
        .replace(/>/g,"&gt;")
        .replace(/"/g,"&quot;")
        .replace(/'/g,"&#39;");
}

function authHandleError(message, description) {
    if (DEBUGGING) {
        Write(
            "<h3>Authentication Error</h3><p>"+
            authHtml(message)+
            "</p><p>"+
            authHtml(description)+
            "</p>"
        );
    } else {
        try {
            Platform.Function.InsertData(
                ERROR_DE,
                ["id","errorSource","errorMessage","errorDescription"],
                [GUID(),APP_NAME,message,description]
            );
        } catch(ignoreLog) {}

        if (ERROR_URL && ERROR_URL != "YOUR_ERROR_URL") {
            Platform.Response.Redirect(
                ERROR_URL+
                "?error="+
                encodeURIComponent(message)+
                "&error_description="+
                encodeURIComponent(description)
            );
        } else {
            Write("<h3>Authentication Error</h3>");
        }
    }
}

/*
  A session is accepted only when it exists in the authentication DE
  and has not expired. This keeps the article's DE-based session model,
  while ensuring Viewer POST requests cannot bypass the login gate.
*/
function loadValidSession(sessionId) {
    if (!sessionId) return false;

    try {
        var rows = Platform.Function.LookupRows(
            AUTH_DE,
            "session",
            sessionId
        );

        if (!rows || rows.length < 1) return false;

        var row = rows[0];
        var expire = row.tokenExpire;
        if (!expire) return false;

        var nowLocal = Platform.Function.SystemDateToLocalDate(
            Platform.Function.Now()
        );

        var expireDate = new Date(expire);
        var nowDate = new Date(nowLocal);

        if (
            isNaN(expireDate.getTime()) ||
            isNaN(nowDate.getTime()) ||
            expireDate.getTime() <= nowDate.getTime()
        ) {
            return false;
        }

        signedInUserName = String(row.userName || "");
        signedInUserEmail = String(row.userEmail || "");
        return true;
    } catch(e) {
        return false;
    }
}

if (authError) {
    authHandleError(
        authError,
        authErrorDescription
    );
} else if (postedSession && loadValidSession(postedSession)) {
    authAllowed = true;
} else if (authState && !authCode && loadValidSession(authState)) {
    authAllowed = true;
} else if (authState && authCode) {
    var payload = {
        grant_type:"authorization_code",
        code:authCode,
        client_id:WEB_APP_CLIENT_ID,
        client_secret:WEB_APP_CLIENT_SECRET,
        redirect_uri:APP_URL
    };

    var tokenResponse = HTTP.Post(
        "https://"+
        CLIENT_BASE+
        ".auth.marketingcloudapis.com/v2/token",
        "application/json",
        Stringify(payload)
    );

    if (tokenResponse.StatusCode == 200) {
        var parsedToken =
            Platform.Function.ParseJSON(
                tokenResponse.Response[0]
            );

        var accessToken =
            parsedToken.access_token;

        var tokenExpire =
            Platform.Function.SystemDateToLocalDate(
                Platform.Function.Now()
            );

        tokenExpire.setMinutes(
            tokenExpire.getMinutes()+18
        );

        var userInfoResponse = HTTP.Get(
            "https://"+
            CLIENT_BASE+
            ".auth.marketingcloudapis.com/v2/userinfo",
            ["Authorization"],
            ["Bearer "+accessToken]
        );

        /*
          Keep the same handling style as the supplied authentication article:
          HTTP.Get() is consumed from response.Content directly.
          In this CloudPages SSJS context, StatusCode is not reliably exposed
          on the HTTP.Get() response object and can be undefined.
        */
        try {
            var userInfo =
                Platform.Function.ParseJSON(
                    userInfoResponse.Content
                );

            signedInUserName =
                String(
                    userInfo.user &&
                    userInfo.user.name
                    ? userInfo.user.name
                    : ""
                );

            signedInUserEmail =
                String(
                    userInfo.user &&
                    userInfo.user.email
                    ? userInfo.user.email
                    : ""
                );

            Platform.Function.UpsertData(
                AUTH_DE,
                ["session"],
                [authState],
                [
                    "appName",
                    "token",
                    "tokenExpire",
                    "userName",
                    "userEmail"
                ],
                [
                    APP_NAME,
                    accessToken,
                    tokenExpire,
                    signedInUserName,
                    signedInUserEmail
                ]
            );

            authSession = authState;
            authAllowed = true;

        } catch(userInfoError) {
            authHandleError(
                "UserInfo Failed",
                userInfoError && userInfoError.message
                    ? String(userInfoError.message)
                    : String(userInfoError)
            );
        }
    } else {
        authHandleError(
            "Authentication Failed",
            "Status: "+tokenResponse.StatusCode
        );
    }
} else {
    authState = GUID();
    Platform.Response.Redirect(
        "https://"+
        CLIENT_BASE+
        ".auth.marketingcloudapis.com/v2/authorize"+
        "?response_type=code"+
        "&client_id="+
        encodeURIComponent(WEB_APP_CLIENT_ID)+
        "&redirect_uri="+
        encodeURIComponent(APP_URL)+
        "&state="+
        encodeURIComponent(authState)
    );
}
</script>

<script runat="server">
if (authAllowed) {
</script>
<script runat="server">
Platform.Load("Core","1.1.1");

/*
  Automation Activity Viewer 3.2
  ============================================================
  Production build for Salesforce Marketing Cloud Engagement.

  - Single CloudPage
  - No OAuth
  - No Data Extension
  - No Query Activity
  - Unicode automation names
  - Latest 31 days only
  - Input values persist after submit
  - Exact Automation Studio step reconstruction
  - Activity type shown only when reliably identified
  - Unresolved activity types are shown as "Unknown"
  - Marketing Cloud account/user local time conversion
  - Responsive UI and CSV export
*/

var MAX_ROWS = 10000;
var submitted = false;

var automationName = "";
var startDate = "";
var endDate = "";
var resultFilter = "";
var displayRows = [];

var errorMessage = "";
var infoMessage = "";

var automationNames = [];
var resultRows = [];
var attemptRows = [];

/*
  v14 join diagnostics.
  These arrays are populated only in memory for this page request.
  Nothing is written to a Data Extension.
*/
var diagStaticTasks = [];
var diagStaticActivities = [];
var diagTaskInstances = [];
var diagActivityInstances = [];

var accountLocalToday = "";
var accountLocalYesterday = "";
var accountLocalOldest = "";

var resultCount = 0;
var completedCount = 0;
var totalDurationSec = 0;
var avgDurationSec = 0;
var maxDurationSec = 0;

/*
  Automation-level duration KPIs.
  Each ProgramInstanceID represents one execution of the selected automation.
  Run duration = earliest activity start to latest activity end in that execution.
*/
var automationRunCount = 0;
var avgAutomationDurationSec = 0;
var maxAutomationDurationSec = 0;

var diag = {
    automationsLoaded: 0,
    validInstanceDates: 0,
    invalidInstanceDates: 0,
    candidateProgramInstanceIds: 0,
    staticTasks: 0,
    staticActivities: 0,
    taskInstances: 0,
    activityInstances: 0,
    mappedSteps: 0,
    unmappedSteps: 0
};

function esc(v) {
    if (v == null) return "";
    return String(v)
        .replace(/&/g,"&amp;")
        .replace(/</g,"&lt;")
        .replace(/>/g,"&gt;")
        .replace(/"/g,"&quot;")
        .replace(/'/g,"&#39;");
}

function jsEsc(v) {
    if (v == null) return "";
    return String(v)
        .replace(/\\/g,"\\\\")
        .replace(/"/g,'\\"')
        .replace(/\r/g,"\\r")
        .replace(/\n/g,"\\n")
        .replace(/</g,"\\u003c")
        .replace(/>/g,"\\u003e");
}

function pad2(n) {
    n = parseInt(n,10) || 0;
    return n < 10 ? "0"+n : String(n);
}

function monthNumber(mon) {
    var m = String(mon || "");

    if (m == "Jan") return 1;
    if (m == "Feb") return 2;
    if (m == "Mar") return 3;
    if (m == "Apr") return 4;
    if (m == "May") return 5;
    if (m == "Jun") return 6;
    if (m == "Jul") return 7;
    if (m == "Aug") return 8;
    if (m == "Sep") return 9;
    if (m == "Oct") return 10;
    if (m == "Nov") return 11;
    if (m == "Dec") return 12;

    return 0;
}

/*
  Parse an API date into its SOURCE CLOCK components.
  We intentionally do NOT convert GMT-06:00 to UTC here.

  Marketing Cloud SOAP dates are displayed in the Marketing Cloud
  system clock (commonly GMT-06:00). SystemDateToLocalDate expects
  a Marketing Cloud system time, so we preserve the clock portion
  and let MC perform the local conversion.
*/
function parseSystemDate(value) {
    if (!value) {
        return {
            valid:false,
            year:null,
            systemString:"",
            epoch:null
        };
    }

    var s = String(value);
    var m;

    /* ISO / SQL style */
    m = s.match(
        /(\d{4})-(\d{1,2})-(\d{1,2})[T\s](\d{1,2}):(\d{1,2}):(\d{1,2})/
    );

    if (m) {
        var y1 = parseInt(m[1],10);

        if (y1 <= 1) {
            return {valid:false,year:y1,systemString:"",epoch:null};
        }

        var sys1 =
            y1+"-"+
            pad2(m[2])+"-"+
            pad2(m[3])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y1,
            systemString:sys1,
            epoch:Date.UTC(
                y1,
                parseInt(m[2],10)-1,
                parseInt(m[3],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    /* US slash style */
    m = s.match(
        /(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})/
    );

    if (m) {
        var y2 = parseInt(m[3],10);

        if (y2 <= 1) {
            return {valid:false,year:y2,systemString:"",epoch:null};
        }

        var sys2 =
            y2+"-"+
            pad2(m[1])+"-"+
            pad2(m[2])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y2,
            systemString:sys2,
            epoch:Date.UTC(
                y2,
                parseInt(m[1],10)-1,
                parseInt(m[2],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    /* MC/.NET style: Thu, 03 Sep 2026 11:52:31 GMT-06:00 */
    m = s.match(
        /(?:[A-Za-z]{3},?\s+)?(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s+GMT[+-]\d{2}:?\d{2}/
    );

    if (m) {
        var mo3 = monthNumber(m[2]);
        var y3 = parseInt(m[3],10);

        if (y3 <= 1 || mo3 === 0) {
            return {valid:false,year:y3,systemString:"",epoch:null};
        }

        var sys3 =
            y3+"-"+
            pad2(mo3)+"-"+
            pad2(m[1])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y3,
            systemString:sys3,
            epoch:Date.UTC(
                y3,
                mo3-1,
                parseInt(m[1],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    /* JS Date style: Thu Sep 03 2026 11:52:31 GMT-0600 */
    m = s.match(
        /(?:[A-Za-z]{3}\s+)?([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\s+GMT[+-]\d{4}/
    );

    if (m) {
        var mo4 = monthNumber(m[1]);
        var y4 = parseInt(m[3],10);

        if (y4 <= 1 || mo4 === 0) {
            return {valid:false,year:y4,systemString:"",epoch:null};
        }

        var sys4 =
            y4+"-"+
            pad2(mo4)+"-"+
            pad2(m[2])+" "+
            pad2(m[4])+":"+
            pad2(m[5])+":"+
            pad2(m[6]);

        return {
            valid:true,
            year:y4,
            systemString:sys4,
            epoch:Date.UTC(
                y4,
                mo4-1,
                parseInt(m[2],10),
                parseInt(m[4],10),
                parseInt(m[5],10),
                parseInt(m[6],10)
            )
        };
    }

    return {
        valid:false,
        year:null,
        systemString:"",
        epoch:null
    };
}

function systemToLocalDisplay(systemString) {
    if (!systemString) return "";

    try {
        var localValue =
            Platform.Function.SystemDateToLocalDate(
                systemString
            );

        /*
          Keep the returned local value readable without trying to
          reinterpret its timezone again.
        */
        var parsed =
            parseSystemDate(
                localValue
            );

        if (parsed.valid) {
            return parsed.systemString;
        }

        return String(localValue);

    } catch(e) {
        return systemString;
    }
}

function localDateToSystemEpoch(localDate,endOfDay) {
    try {
        var localString =
            localDate+
            (endOfDay ? " 23:59:59" : " 00:00:00");

        var systemValue =
            Platform.Function.LocalDateToSystemDate(
                localString
            );

        var parsed =
            parseSystemDate(
                systemValue
            );

        return parsed.valid
            ? parsed.epoch
            : null;

    } catch(e) {
        return null;
    }
}

function isoDateFromParts(year,month,day) {
    return (
        String(year)+"-"+
        pad2(month)+"-"+
        pad2(day)
    );
}

function accountLocalDateOffset(dayOffset) {
    try {
        var localNow =
            Platform.Function.SystemDateToLocalDate(
                Platform.Function.Now()
            );

        var shifted =
            parseInt(dayOffset,10) === 0
            ? localNow
            : Platform.Function.DateAdd(
                localNow,
                parseInt(dayOffset,10),
                "D"
              );

        var formatted =
            Platform.Function.FormatDate(
                shifted,
                "yyyy-MM-dd"
            );

        if (
            formatted &&
            /^\d{4}-\d{2}-\d{2}$/.test(
                String(formatted)
            )
        ) {
            return String(formatted);
        }
    } catch(ignoreFormatDate) {}

    try {
        var fallbackLocal =
            Platform.Function.SystemDateToLocalDate(
                Platform.Function.Now()
            );

        var d =
            new Date(
                String(fallbackLocal)
            );

        if (!isNaN(d.getTime())) {
            d.setDate(
                d.getDate()+
                parseInt(dayOffset,10)
            );

            return (
                d.getFullYear()+"-"+
                pad2(d.getMonth()+1)+"-"+
                pad2(d.getDate())
            );
        }
    } catch(ignoreNativeDate) {}

    return "";
}

function fmtDuration(sec) {
    sec = parseInt(sec,10) || 0;

    var h = Math.floor(sec/3600);
    var m = Math.floor((sec%3600)/60);
    var s = sec%60;

    return h > 0
        ? pad2(h)+":"+pad2(m)+":"+pad2(s)
        : pad2(m)+":"+pad2(s);
}

function durationSeconds(aEpoch,bEpoch) {
    if (
        aEpoch == null ||
        bEpoch == null ||
        bEpoch < aEpoch
    ) {
        return 0;
    }

    return Math.floor(
        (bEpoch-aEpoch)/1000
    );
}

function daysBetween(a,b) {
    var x = String(a).split("-");
    var y = String(b).split("-");

    if (x.length != 3 || y.length != 3) {
        return 9999;
    }

    var d1 = Date.UTC(+x[0],+x[1]-1,+x[2]);
    var d2 = Date.UTC(+y[0],+y[1]-1,+y[2]);

    return Math.floor((d2-d1)/86400000);
}

function retrieveAll(api,type,cols,filter) {
    var out = [];
    var res = api.retrieve(type,cols,filter);

    if (res && res.Results) {
        for (
            var i=0;
            i<res.Results.length &&
            out.length<MAX_ROWS;
            i++
        ) {
            out.push(res.Results[i]);
        }
    }

    while (
        res &&
        res.HasMoreRows === true &&
        out.length < MAX_ROWS
    ) {
        res =
            api.getNextBatch(
                type,
                res.RequestID
            );

        if (res && res.Results) {
            for (
                var j=0;
                j<res.Results.length &&
                out.length<MAX_ROWS;
                j++
            ) {
                out.push(res.Results[j]);
            }
        }
    }

    return out;
}

function addAttempt(objectType,label,ok,count,detail) {
    attemptRows.push({
        objectType:objectType,
        label:label,
        ok:ok,
        count:count,
        detail:detail || ""
    });
}

function safeRetrieve(api,type,cols,filter,label) {
    try {
        var rows =
            retrieveAll(
                api,
                type,
                cols,
                filter
            );

        addAttempt(
            type,
            label,
            true,
            rows.length,
            ""
        );

        return rows;

    } catch(e) {
        addAttempt(
            type,
            label,
            false,
            0,
            e && e.message
                ? String(e.message)
                : String(e)
        );

        return [];
    }
}

function retrieveAutomationList(api) {
    var rows = [];
    var res =
        api.retrieve(
            "Program",
            [
                "Name",
                "ObjectID"
            ]
        );

    if (res && res.Results) {
        for (var i=0;i<res.Results.length;i++) {
            rows.push(res.Results[i]);
        }
    }

    while (res && res.HasMoreRows === true) {
        res =
            api.getNextBatch(
                "Program",
                res.RequestID
            );

        if (res && res.Results) {
            for (var j=0;j<res.Results.length;j++) {
                rows.push(res.Results[j]);
            }
        }
    }

    var all = [];
    var seen = {};

    for (var k=0;k<rows.length;k++) {
        var name =
            String(
                rows[k].Name || ""
            ).replace(/^\s+|\s+$/g,"");

        if (!name) continue;

        if (!seen[name]) {
            seen[name] = true;
            all.push({
                Name:name,
                ObjectID:rows[k].ObjectID || ""
            });
        }
    }

    return all;
}

function findAutomation(api,name,list) {
    for (var i=0;i<list.length;i++) {
        if (
            String(list[i].Name) ===
            String(name)
        ) {
            return list[i];
        }
    }

    var types = ["Automation","Program"];

    for (var t=0;t<types.length;t++) {
        var rows =
            safeRetrieve(
                api,
                types[t],
                [
                    "Name",
                    "ObjectID",
                    "ProgramID",
                    "CustomerKey",
                    "Status"
                ],
                {
                    Property:"Name",
                    SimpleOperator:"equals",
                    Value:name
                },
                "Name = "+name
            );

        if (rows.length) {
            return rows[0];
        }
    }

    return null;
}

function getStaticTasks(api,automationObjectID) {
    /*
      Important:
      Describe on this stack confirms these fields are retrievable.
      Do NOT request AutomationTaskType / TaskType here because a single
      non-retrievable property can make the whole retrieve fail.
    */
    return safeRetrieve(
        api,
        "Task",
        [
            "ObjectID",
            "Program.ObjectID",
            "Name",
            "Sequence"
        ],
        {
            Property:"Program.ObjectID",
            SimpleOperator:"equals",
            Value:automationObjectID
        },
        "Program.ObjectID = "+automationObjectID
    );
}

function getStaticActivities(api,automationObjectID) {
    /*
      v12 Describe proved that ObjectID, Task.ObjectID, Sequence,
      PartnerAPIObjectTypeID, Definition.ObjectID and Definition are
      retrievable on this stack.
    */
    return safeRetrieve(
        api,
        "Activity",
        [
            "ObjectID",
            "CustomerKey",
            "Program.ObjectID",
            "Task.ObjectID",
            "Name",
            "Sequence",
            "PartnerAPIObjectTypeID",
            "Definition.ObjectID"
        ],
        {
            Property:"Program.ObjectID",
            SimpleOperator:"equals",
            Value:automationObjectID
        },
        "Program.ObjectID = "+automationObjectID
    );
}

function getProgramInstances(api,automation) {
    var combined = [];
    var seen = {};

    var autoCols = [
        "ProgramInstanceID",
        "ObjectID",
        "ProgramID",
        "CustomerKey",
        "Name",
        "Status",
        "StatusMessage",
        "StatusLastUpdate",
        "StartTime",
        "CompletedTime",
        "ScheduledTime",
        "CreatedDate",
        "ModifiedDate"
    ];

    var progCols = [
        "ObjectID",
        "ProgramID",
        "CustomerKey",
        "Name",
        "StatusMessage",
        "StatusLastUpdate",
        "CreatedDate",
        "ModifiedDate"
    ];

    var probes = [];

    if (automation.ProgramID) {
        probes.push({
            type:"AutomationInstance",
            cols:autoCols,
            label:"ProgramID = "+automation.ProgramID,
            filter:{
                Property:"ProgramID",
                SimpleOperator:"equals",
                Value:automation.ProgramID
            }
        });

        probes.push({
            type:"ProgramInstance",
            cols:progCols,
            label:"ProgramID = "+automation.ProgramID,
            filter:{
                Property:"ProgramID",
                SimpleOperator:"equals",
                Value:automation.ProgramID
            }
        });
    }

    if (automation.CustomerKey) {
        probes.push({
            type:"AutomationInstance",
            cols:autoCols,
            label:"CustomerKey = "+automation.CustomerKey,
            filter:{
                Property:"CustomerKey",
                SimpleOperator:"equals",
                Value:automation.CustomerKey
            }
        });

        probes.push({
            type:"ProgramInstance",
            cols:progCols,
            label:"CustomerKey = "+automation.CustomerKey,
            filter:{
                Property:"CustomerKey",
                SimpleOperator:"equals",
                Value:automation.CustomerKey
            }
        });
    }

    if (automation.Name) {
        probes.push({
            type:"AutomationInstance",
            cols:autoCols,
            label:"Name = "+automation.Name,
            filter:{
                Property:"Name",
                SimpleOperator:"equals",
                Value:automation.Name
            }
        });

        probes.push({
            type:"ProgramInstance",
            cols:progCols,
            label:"Name = "+automation.Name,
            filter:{
                Property:"Name",
                SimpleOperator:"equals",
                Value:automation.Name
            }
        });
    }

    for (var p=0;p<probes.length;p++) {
        var rows =
            safeRetrieve(
                api,
                probes[p].type,
                probes[p].cols,
                probes[p].filter,
                probes[p].label
            );

        for (var r=0;r<rows.length;r++) {
            var item = rows[r];

            item.__source =
                probes[p].type;

            var key =
                [
                    item.__source,
                    item.ProgramInstanceID || "",
                    item.ObjectID || "",
                    item.ProgramID || "",
                    item.CustomerKey || "",
                    item.StatusLastUpdate || "",
                    item.CreatedDate || "",
                    item.ModifiedDate || ""
                ].join("|");

            if (!seen[key]) {
                seen[key] = true;
                combined.push(item);
            }
        }
    }

    return combined;
}

function effectiveSystemDate(row) {
    var candidates = [
        row.StartTime,
        row.ScheduledTime,
        row.StatusLastUpdate,
        row.CreatedDate,
        row.ModifiedDate
    ];

    for (var i=0;i<candidates.length;i++) {
        var parsed =
            parseSystemDate(
                candidates[i]
            );

        if (parsed.valid) {
            return parsed;
        }
    }

    return {
        valid:false,
        year:null,
        systemString:"",
        epoch:null
    };
}

function getTaskInstances(api,pid) {
    return safeRetrieve(
        api,
        "TaskInstance",
        [
            "ObjectID",
            "TaskDefinition.ObjectID",
            "Program.ObjectID",
            "ProgramInstance.ObjectID",
            "Name",
            "Sequence",
            "CreatedDate",
            "ModifiedDate"
        ],
        {
            Property:"ProgramInstance.ObjectID",
            SimpleOperator:"equals",
            Value:pid
        },
        "ProgramInstance.ObjectID = "+pid
    );
}

function getActivityInstances(api,pid) {
    return safeRetrieve(
        api,
        "ActivityInstance",
        [
            "ProgramID",
            "ObjectID",
            "CustomerKey",
            "ProgramInstance.ObjectID",
            "TaskInstance.ObjectID",
            "ActivityDefinition.ObjectID",
            "Name",
            "Status",
            "StatusMessage",
            "StatusLastUpdate",
            "SequenceID",
            "PartnerAPIObjectTypeID",
            "CreatedDate",
            "ModifiedDate"
        ],
        {
            Property:"ProgramInstance.ObjectID",
            SimpleOperator:"equals",
            Value:pid
        },
        "ProgramInstance.ObjectID = "+pid
    );
}

function statusLabel(instance) {
    if (instance.StatusMessage) {
        var message =
            String(
                instance.StatusMessage
            );

        var lower =
            message.toLowerCase();

        if (
            lower.indexOf("complete") >= 0 ||
            lower.indexOf("success") >= 0
        ) {
            return "Completed";
        }

        if (
            lower.indexOf("error") >= 0 ||
            lower.indexOf("fail") >= 0
        ) {
            return "Error";
        }

        return message;
    }

    var s =
        parseInt(
            instance.Status,
            10
        );

    if (s === 1) return "Completed";
    if (s < 0) return "Error";

    return "Status "+String(
        instance.Status == null
        ? ""
        : instance.Status
    );
}

function statusClass(label) {
    var l =
        String(
            label || ""
        ).toLowerCase();

    if (
        l.indexOf("complete") >= 0 ||
        l.indexOf("success") >= 0
    ) {
        return "completed";
    }

    if (
        l.indexOf("error") >= 0 ||
        l.indexOf("fail") >= 0
    ) {
        return "error";
    }

    if (
        l.indexOf("run") >= 0 ||
        l.indexOf("execut") >= 0
    ) {
        return "running";
    }

    return "other";
}


function normalizeTaskType(value) {
    var raw = String(value || "");
    var v = raw.toLowerCase();

    if (!raw) return "";

    if (v.indexOf("query") >= 0) return "SQL Query";
    if (v.indexOf("filter") >= 0) return "Filter";
    if (v.indexOf("script") >= 0 || v.indexOf("javascript") >= 0) return "Script";
    if (v.indexOf("extract") >= 0) return "Data Extract";
    if (v.indexOf("import") >= 0) return "Import File";
    if (v.indexOf("transfer") >= 0 || v.indexOf("ftp") >= 0) return "File Transfer";
    if (v.indexOf("email") >= 0 || v.indexOf("send") >= 0) return "Email Send";
    if (v.indexOf("report") >= 0) return "Report";
    if (v.indexOf("wait") >= 0) return "Wait";
    if (v.indexOf("verification") >= 0) return "Verification";
    if (v.indexOf("push") >= 0) return "Push";
    if (v.indexOf("sms") >= 0 || v.indexOf("mobileconnect") >= 0) return "SMS";
    if (v.indexOf("salesforce") >= 0) return "Salesforce Send";
    if (v.indexOf("event") >= 0) return "Event";

    return raw;
}

function activityTypeLabel(typeId) {
    var id = parseInt(typeId,10);

    if (id === 42) return "Email Send";
    if (id === 43) return "Import File";
    if (id === 45) return "Refresh Group";
    if (id === 53) return "File Transfer";
    if (id === 73) return "Data Extract";
    if (id === 84) return "Report";
    if (id === 300) return "SQL Query";
    if (id === 303) return "Filter";
    if (id === 423) return "Script";
    if (id === 425) return "Data Factory Utility";
    if (id === 427) return "Build Audience";
    if (id === 467) return "Wait";
    if (id === 724) return "Refresh Mobile Filtered List";
    if (id === 725) return "Send SMS";
    if (id === 726) return "Import Mobile Contacts";
    if (id === 733) return "Interaction Studio";
    if (id === 736) return "Send Push";
    if (id === 749) return "Fire Event";
    if (id === 756) return "Interaction Studio Date Event";
    if (id === 771) return "Salesforce Send";
    if (id === 783) return "GroupConnect";
    if (id === 1000) return "Verification";
    if (id === 1010) return "Thunderhead Transfer";
    if (id === 1101) return "Interaction Studio Decision";
    if (id === 1701) return "Predictive Intelligence Recommendation";

    return "Unknown";
}

function soapValue(obj,path) {
    if (!obj || !path) return null;

    try {
        if (
            typeof obj[path] != "undefined" &&
            obj[path] !== null &&
            obj[path] !== ""
        ) {
            return obj[path];
        }
    } catch(ignoreFlat) {}

    try {
        var parts =
            String(path).split(".");

        var current =
            obj;

        for (
            var i=0;
            i<parts.length;
            i++
        ) {
            if (
                current == null ||
                typeof current[parts[i]] == "undefined"
            ) {
                return null;
            }

            current =
                current[parts[i]];
        }

        return current;
    } catch(ignoreNested) {
        return null;
    }
}

function hasProperty(obj,key) {
    if (!obj) return false;

    try {
        return typeof obj[key] != "undefined";
    } catch(e) {
        return false;
    }
}

function hasOwnValue(obj,key) {
    if (!obj) return false;

    try {
        return typeof obj[key] != "undefined" &&
               obj[key] !== null;
    } catch(e) {
        return false;
    }
}

function inferActivityTypeFromDefinition(definition) {
    if (!definition) return "";

    var signature = "";

    try {
        signature =
            Stringify(
                definition
            );
    } catch(ignoreStringify) {
        try {
            signature =
                String(
                    definition
                );
        } catch(ignoreString) {
            signature = "";
        }
    }

    function sigHas(token) {
        return (
            String(signature)
                .indexOf(
                    '"'+token+'"'
                ) >= 0
        );
    }

    /*
      Signatures observed in the v12 raw SOAP payload.
    */
    if (
        sigHas("QueryText") ||
        sigHas("TargetUpdateType") ||
        sigHas("DataExtensionTarget")
    ) {
        return "SQL Query";
    }

    if (
        sigHas("SubscriberImportType") ||
        sigHas("FieldMappingType") ||
        sigHas("ControlColumnDefaultAction") ||
        sigHas("DestinationType")
    ) {
        return "Import File";
    }

    if (
        sigHas("ExtractType") ||
        sigHas("DataExtractTypeID") ||
        sigHas("DataFields")
    ) {
        return "Data Extract";
    }

    if (
        sigHas("TransferType") ||
        sigHas("FileTransferLocation") ||
        sigHas("IsUpload")
    ) {
        return "File Transfer";
    }

    if (
        sigHas("ScriptLanguage") ||
        sigHas("Script")
    ) {
        return "Script";
    }

    if (
        sigHas("EmailID") ||
        sigHas("SendDefinitionList")
    ) {
        return "Email Send";
    }

    if (
        sigHas("FilterDefinition") ||
        sigHas("FilterActivity")
    ) {
        return "Filter";
    }

    return "";
}

function probeDefinitionType(api,definitionObjectID) {
    /*
      Final OAuth-free fallback.

      A Definition.ObjectID belongs to a concrete SOAP definition object.
      When the generic Activity payload does not expose a usable type ID or
      a recognizable Definition shape, probe a small set of common
      Automation Studio definition objects by ObjectID.

      A failed/unsupported object probe is ignored. No OAuth or REST is used.
    */
    if (!definitionObjectID) return "";

    var probes = [
        { objectType:"QueryDefinition",          label:"SQL Query" },
        { objectType:"ImportDefinition",         label:"Import File" },
        { objectType:"DataExtractActivity",      label:"Data Extract" },
        { objectType:"DataExtractDefinition",    label:"Data Extract" },
        { objectType:"FileTransferActivity",     label:"File Transfer" },
        { objectType:"FileTransferDefinition",   label:"File Transfer" },
        { objectType:"ScriptActivity",           label:"Script" },
        { objectType:"ScriptActivityDefinition", label:"Script" },
        { objectType:"EmailSendDefinition",      label:"Email Send" },
        { objectType:"FilterActivity",           label:"Filter" },
        { objectType:"FilterDefinition",         label:"Filter" }
    ];

    for (var i=0;i<probes.length;i++) {
        try {
            var r = api.retrieve(
                probes[i].objectType,
                ["ObjectID"],
                {
                    Property:"ObjectID",
                    SimpleOperator:"equals",
                    Value:String(definitionObjectID)
                }
            );

            if (
                r &&
                r.Results &&
                r.Results.length > 0
            ) {
                return probes[i].label;
            }
        } catch(ignoreProbe) {}
    }

    return "";
}

function resolveActivityType(api,actDef,ainst) {
    var staticTypeId =
        actDef &&
        actDef.PartnerAPIObjectTypeID != null
        ? actDef.PartnerAPIObjectTypeID
        : "";

    var instanceTypeId =
        ainst &&
        ainst.PartnerAPIObjectTypeID != null
        ? ainst.PartnerAPIObjectTypeID
        : "";

    var typeId =
        staticTypeId !== ""
        ? staticTypeId
        : instanceTypeId;

    var byId =
        activityTypeLabel(
            typeId
        );

    if (
        byId &&
        byId != "Unknown"
    ) {
        return {
            label:byId,
            typeId:typeId,
            source:"PartnerAPIObjectTypeID"
        };
    }

    var byShape =
        inferActivityTypeFromDefinition(
            actDef
            ? actDef.Definition
            : null
        );

    if (byShape) {
        return {
            label:byShape,
            typeId:typeId,
            source:"Definition structure"
        };
    }

    /*
      Product behavior:
      Do not guess or perform speculative definition-object probes.
      If neither PartnerAPIObjectTypeID nor the returned Definition
      structure identifies the activity reliably, display "Unknown".
    */
    return {
        label:"Unknown",
        typeId:typeId,
        source:"Unresolved"
    };
}

/* ---------------------------------------------------------
   WSProxy
--------------------------------------------------------- */
var api =
    new Script.Util.WSProxy();

try {
    api.setClientId({
        ID:
        Platform.Function.AuthenticatedMemberID(),
        UserID:
        Platform.Function.AuthenticatedEmployeeID()
    });
} catch(ignoreClient) {}

/* ---------------------------------------------------------
   Suggestions
--------------------------------------------------------- */
var automationList = [];

try {
    automationList =
        retrieveAutomationList(
            api
        );

    diag.automationsLoaded =
        automationList.length;

    for (
        var li=0;
        li<automationList.length;
        li++
    ) {
        if (
            automationList[li].Name
        ) {
            automationNames.push(
                String(
                    automationList[li].Name
                )
            );
        }
    }
} catch(ignoreSuggestionFailure) {}

/*
  Date-input defaults and limits are based on the current Marketing Cloud
  account/user local date.
*/
accountLocalToday =
    accountLocalDateOffset(0);

accountLocalYesterday =
    accountLocalDateOffset(-1);

accountLocalOldest =
    accountLocalDateOffset(-30);

/* ---------------------------------------------------------
   Search
--------------------------------------------------------- */
var action =
    Platform.Request.GetFormField(
        "action"
    );

if (action === "search" || action === "filter") {
    submitted = true;

    automationName =
        String(
            Platform.Request.GetFormField(
                "automationName"
            ) || ""
        )
        .replace(
            /^\s+|\s+$/g,
            ""
        );

    startDate =
        String(
            Platform.Request.GetFormField(
                "startDate"
            ) || ""
        );

    endDate =
        String(
            Platform.Request.GetFormField(
                "endDate"
            ) || ""
        );

    resultFilter =
        String(
            Platform.Request.GetFormField(
                "resultFilter"
            ) || ""
        )
        .replace(
            /^\s+|\s+$/g,
            ""
        );

    try {
        if (
            !automationName ||
            !startDate ||
            !endDate
        ) {
            throw new Error(
                "Automation Name, Start Date, and End Date are required."
            );
        }

        var span =
            daysBetween(
                startDate,
                endDate
            );

        if (span < 0) {
            throw new Error(
                "End Date must be the same as or later than Start Date."
            );
        }

        if (span > 31) {
            throw new Error(
                "The maximum search range is 31 days."
            );
        }

        /*
          Convert the user's LOCAL date range back to Marketing Cloud
          SYSTEM time, then compare against the source system timestamps
          returned by SOAP.
        */
        var startSystemEpoch =
            localDateToSystemEpoch(
                startDate,
                false
            );

        var endSystemEpoch =
            localDateToSystemEpoch(
                endDate,
                true
            );

        if (
            startSystemEpoch == null ||
            endSystemEpoch == null
        ) {
            throw new Error(
                "The selected local date range could not be converted to Marketing Cloud system time."
            );
        }

        var automation =
            findAutomation(
                api,
                automationName,
                automationList
            );

        if (!automation) {
            throw new Error(
                "No automation was found with that exact name."
            );
        }

        var staticTasks =
            getStaticTasks(
                api,
                automation.ObjectID
            );

        var staticActivities =
            getStaticActivities(
                api,
                automation.ObjectID
            );

        diag.staticTasks =
            staticTasks.length;

        diag.staticActivities =
            staticActivities.length;

        diagStaticTasks =
            staticTasks;

        diagStaticActivities =
            staticActivities;

        var staticTaskSequence = {};

        for (
            var st=0;
            st<staticTasks.length;
            st++
        ) {
            var staticTaskId =
                String(
                    staticTasks[st].ObjectID
                );

            /*
              Task.Sequence is zero-based in the SOAP definition.
              Example from v12:
                Sequence 0 -> Step 1
                Sequence 1 -> Step 2
                Sequence 2 -> Step 3
                Sequence 3 -> Step 4
            */
            staticTaskSequence[
                staticTaskId
            ] =
                (
                    parseInt(
                        staticTasks[st].Sequence,
                        10
                    ) || 0
                ) + 1;
        }

        /*
          ActivityInstance.ActivityDefinition.ObjectID points to the static
          Activity.ObjectID, NOT to Activity.Definition.ObjectID.

          Keep all three maps because CustomerKey / Definition.ObjectID are
          still useful fallbacks on other stacks.
        */
        var activityByObjectID = {};
        var activityByDefinition = {};
        var activityByCustomerKey = {};

        for (
            var sa=0;
            sa<staticActivities.length;
            sa++
        ) {
            var act =
                staticActivities[sa];

            if (
                act.ObjectID
            ) {
                activityByObjectID[
                    String(
                        act.ObjectID
                    )
                ] = act;
            }

            var staticDefinitionObjectID =
                soapValue(
                    act,
                    "Definition.ObjectID"
                );

            if (
                staticDefinitionObjectID
            ) {
                activityByDefinition[
                    String(
                        staticDefinitionObjectID
                    )
                ] = act;
            }

            if (
                act.CustomerKey
            ) {
                activityByCustomerKey[
                    String(
                        act.CustomerKey
                    )
                ] = act;
            }
        }

        var instanceRows =
            getProgramInstances(
                api,
                automation
            );

        var candidateIds = [];
        var seenPid = {};

        for (
            var ir=0;
            ir<instanceRows.length;
            ir++
        ) {
            var instRow =
                instanceRows[ir];

            var effective =
                effectiveSystemDate(
                    instRow
                );

            if (
                effective.valid
            ) {
                diag.validInstanceDates++;
            } else {
                diag.invalidInstanceDates++;
                continue;
            }

            if (
                effective.epoch <
                startSystemEpoch ||
                effective.epoch >
                endSystemEpoch
            ) {
                continue;
            }

            var pid = "";

            if (
                instRow.ProgramInstanceID
            ) {
                pid =
                    String(
                        instRow.ProgramInstanceID
                    );
            } else if (
                instRow.__source ===
                "ProgramInstance" &&
                instRow.ObjectID
            ) {
                pid =
                    String(
                        instRow.ObjectID
                    );
            }

            if (
                pid &&
                !seenPid[pid]
            ) {
                seenPid[pid] = true;
                candidateIds.push(pid);
            }
        }

        diag.candidateProgramInstanceIds =
            candidateIds.length;

        for (
            var ci=0;
            ci<candidateIds.length;
            ci++
        ) {
            var pid =
                candidateIds[ci];

            var taskInstances =
                getTaskInstances(
                    api,
                    pid
                );

            var activityInstances =
                getActivityInstances(
                    api,
                    pid
                );

            diag.taskInstances +=
                taskInstances.length;

            diag.activityInstances +=
                activityInstances.length;

            for (
                var dti=0;
                dti<taskInstances.length;
                dti++
            ) {
                diagTaskInstances.push(
                    taskInstances[dti]
                );
            }

            for (
                var dai=0;
                dai<activityInstances.length;
                dai++
            ) {
                diagActivityInstances.push(
                    activityInstances[dai]
                );
            }

            var taskSequence = {};
            var taskInstanceMeta = {};

            for (
                var ti=0;
                ti<taskInstances.length;
                ti++
            ) {
                var taskInstanceId =
                    String(
                        taskInstances[ti].ObjectID
                    );

                var taskDefinitionRaw =
                    soapValue(
                        taskInstances[ti],
                        "TaskDefinition.ObjectID"
                    );

                var taskDefinitionId =
                    taskDefinitionRaw
                    ? String(
                        taskDefinitionRaw
                      )
                    : "";

                var mappedTaskNumber =
                    taskDefinitionId &&
                    staticTaskSequence[
                        taskDefinitionId
                    ] != null
                    ? staticTaskSequence[
                        taskDefinitionId
                      ]
                    : 0;

                var instanceTaskNumber =
                    (
                        parseInt(
                            taskInstances[ti].Sequence,
                            10
                        ) || 0
                    ) + 1;

                taskSequence[
                    taskInstanceId
                ] =
                    mappedTaskNumber > 0
                    ? mappedTaskNumber
                    : instanceTaskNumber;

                taskInstanceMeta[
                    taskInstanceId
                ] = {
                    taskDefinitionId:
                        taskDefinitionId,
                    taskNumber:
                        mappedTaskNumber > 0
                        ? mappedTaskNumber
                        : instanceTaskNumber,
                    activityType:""
                };
            }

            for (
                var ai=0;
                ai<activityInstances.length;
                ai++
            ) {
                var ainst =
                    activityInstances[ai];

                var actDef = null;

                var instanceActivityDefinitionID =
                    soapValue(
                        ainst,
                        "ActivityDefinition.ObjectID"
                    );

                var instanceDefinitionID =
                    soapValue(
                        ainst,
                        "Definition.ObjectID"
                    );

                /*
                  v14 diagnostics confirmed:
                  ActivityInstance.CustomerKey == Static Activity.CustomerKey.
                */
                if (
                    instanceActivityDefinitionID &&
                    activityByObjectID[
                        String(
                            instanceActivityDefinitionID
                        )
                    ]
                ) {
                    actDef =
                        activityByObjectID[
                            String(
                                instanceActivityDefinitionID
                            )
                        ];
                }
                else if (
                    instanceDefinitionID &&
                    activityByDefinition[
                        String(
                            instanceDefinitionID
                        )
                    ]
                ) {
                    actDef =
                        activityByDefinition[
                            String(
                                instanceDefinitionID
                            )
                        ];
                }
                else if (
                    ainst.CustomerKey &&
                    activityByCustomerKey[
                        String(
                            ainst.CustomerKey
                        )
                    ]
                ) {
                    actDef =
                        activityByCustomerKey[
                            String(
                                ainst.CustomerKey
                            )
                        ];
                }

                var taskNo = 0;
                var taskTypeFromInstance = "";

                var instanceTaskInstanceID =
                    soapValue(
                        ainst,
                        "TaskInstance.ObjectID"
                    );

                var staticActivityTaskID =
                    actDef
                    ? soapValue(
                        actDef,
                        "Task.ObjectID"
                      )
                    : null;

                if (
                    instanceTaskInstanceID &&
                    taskInstanceMeta[
                        String(
                            instanceTaskInstanceID
                        )
                    ]
                ) {
                    var taskMeta =
                        taskInstanceMeta[
                            String(
                                instanceTaskInstanceID
                            )
                        ];

                    taskNo =
                        taskMeta.taskNumber || 0;
                }

                if (
                    taskNo <= 0 &&
                    staticActivityTaskID &&
                    staticTaskSequence[
                        String(
                            staticActivityTaskID
                        )
                    ] != null
                ) {
                    taskNo =
                        staticTaskSequence[
                            String(
                                staticActivityTaskID
                            )
                        ];
                }

                if (
                    taskNo <= 0 &&
                    instanceTaskInstanceID &&
                    taskSequence[
                        String(
                            instanceTaskInstanceID
                        )
                    ] != null
                ) {
                    taskNo =
                        taskSequence[
                            String(
                                instanceTaskInstanceID
                            )
                        ];
                }

                var activityNo = 0;

                if (
                    actDef &&
                    actDef.Sequence != null
                ) {
                    activityNo =
                        (
                            parseInt(
                                actDef.Sequence,
                                10
                            ) || 0
                        ) + 1;
                }
                else if (
                    ainst.SequenceID != null
                ) {
                    var rawSequenceId =
                        parseInt(
                            ainst.SequenceID,
                            10
                        );

                    activityNo =
                        isNaN(
                            rawSequenceId
                        )
                        ? 1
                        : (
                            rawSequenceId <= 0
                            ? 1
                            : rawSequenceId
                          );
                }
                else if (
                    taskNo > 0
                ) {
                    activityNo = 1;
                }

                var step = "-";

                if (
                    taskNo > 0 &&
                    activityNo > 0
                ) {
                    step =
                        taskNo+"."+activityNo;

                    diag.mappedSteps++;
                } else if (
                    activityNo > 0
                ) {
                    step =
                        String(
                            activityNo
                        );

                    diag.unmappedSteps++;
                } else {
                    diag.unmappedSteps++;
                }

                var created =
                    parseSystemDate(
                        ainst.CreatedDate
                    );

                var modified =
                    parseSystemDate(
                        ainst.ModifiedDate ||
                        ainst.StatusLastUpdate
                    );

                var duration =
                    durationSeconds(
                        created.valid
                        ? created.epoch
                        : null,
                        modified.valid
                        ? modified.epoch
                        : (
                            created.valid
                            ? created.epoch
                            : null
                          )
                    );

                var label =
                    statusLabel(
                        ainst
                    );

                var css =
                    statusClass(
                        label
                    );

                var resolvedType =
                    resolveActivityType(
                        api,
                        actDef,
                        ainst
                    );

                resultRows.push({
                    programInstanceID:
                        pid,
                    step:
                        step,
                    taskNo:
                        taskNo,
                    activityNo:
                        activityNo,
                    activityName:
                        actDef &&
                        actDef.Name
                        ? actDef.Name
                        : (
                            ainst.Name ||
                            "(Unnamed activity)"
                          ),
                    activityTypeId:
                        resolvedType.typeId,
                    activityType:
                        resolvedType.label,
                    activityTypeSource:
                        resolvedType.source,
                    createdSystem:
                        created.valid
                        ? created.systemString
                        : "",
                    modifiedSystem:
                        modified.valid
                        ? modified.systemString
                        : "",
                    createdLocal:
                        created.valid
                        ? systemToLocalDisplay(
                            created.systemString
                          )
                        : "-",
                    modifiedLocal:
                        modified.valid
                        ? systemToLocalDisplay(
                            modified.systemString
                          )
                        : "-",
                    durationSec:
                        duration,
                    duration:
                        fmtDuration(
                            duration
                        ),
                    status:
                        label,
                    statusClass:
                        css,
                    statusMessage:
                        ainst.StatusMessage || ""
                });

                if (
                    css ===
                    "completed"
                ) {
                    completedCount++;
                }

                totalDurationSec +=
                    duration;

                if (
                    duration >
                    maxDurationSec
                ) {
                    maxDurationSec =
                        duration;
                }
            }
        }

        resultRows.sort(
            function(a,b) {
                if (
                    a.createdSystem <
                    b.createdSystem
                ) return -1;

                if (
                    a.createdSystem >
                    b.createdSystem
                ) return 1;

                if (
                    a.taskNo !==
                    b.taskNo
                ) {
                    return (
                        a.taskNo -
                        b.taskNo
                    );
                }

                if (
                    a.activityNo !==
                    b.activityNo
                ) {
                    return (
                        a.activityNo -
                        b.activityNo
                    );
                }

                if (
                    a.activityName <
                    b.activityName
                ) return -1;

                if (
                    a.activityName >
                    b.activityName
                ) return 1;

                return 0;
            }
        );

        resultCount =
            resultRows.length;

        /*
          Calculate automation-level durations by ProgramInstanceID.
          One automation execution duration is the SUM of the durations
          of all returned activity instances in that execution.
        */
        var runStats = {};

        for (var rs=0;rs<resultRows.length;rs++) {
            var rr =
                resultRows[rs];

            var runKey =
                String(
                    rr.programInstanceID || ""
                );

            if (!runKey) continue;

            if (!runStats[runKey]) {
                runStats[runKey] = 0;
            }

            runStats[runKey] +=
                Number(
                    rr.durationSec || 0
                );
        }

        var automationDurationTotalSec = 0;

        for (var runId in runStats) {
            if (!runStats.hasOwnProperty(runId)) continue;

            var runDurationSec =
                Math.max(
                    0,
                    Math.round(
                        runStats[runId]
                    )
                );

            automationRunCount++;
            automationDurationTotalSec +=
                runDurationSec;

            if (
                runDurationSec >
                maxAutomationDurationSec
            ) {
                maxAutomationDurationSec =
                    runDurationSec;
            }
        }

        avgAutomationDurationSec =
            automationRunCount
            ? Math.round(
                automationDurationTotalSec /
                automationRunCount
              )
            : 0;

        /*
          Keep the legacy activity average variable populated internally,
          although it is no longer shown as a KPI.
        */
        avgDurationSec =
            resultCount
            ? Math.round(
                totalDurationSec /
                resultCount
              )
            : 0;

        if (!candidateIds.length) {
            infoMessage =
                "No execution instances were found inside the selected local date range.";
        }

    } catch(e) {
        errorMessage =
            e && e.message
            ? String(
                e.message
              )
            : String(e);
    }

}

/*
  Results toolbar fallback.
  Filtering and CSV export are performed server-side so these controls
  do not depend on client-side JavaScript in the Marketing Cloud shell.
*/
displayRows = resultRows;

if (submitted && !errorMessage && resultFilter) {
    var filteredRows = [];
    var filterNeedle = String(resultFilter).toLowerCase();

    for (var fr=0; fr<resultRows.length; fr++) {
        var filterRow = resultRows[fr];

        var searchable =
            String(filterRow.step || "") + " " +
            String(filterRow.activityName || "") + " " +
            String(filterRow.activityType || "") + " " +
            String(filterRow.createdLocal || "") + " " +
            String(filterRow.modifiedLocal || "") + " " +
            String(filterRow.duration || "") + " " +
            String(filterRow.status || "") + " " +
            String(filterRow.statusMessage || "");

        if (searchable.toLowerCase().indexOf(filterNeedle) >= 0) {
            filteredRows.push(filterRow);
        }
    }

    displayRows = filteredRows;
}

function csvEscape(value) {
    var s = String(value == null ? "" : value);
    s = s.replace(/\r?\n|\r/g, " ");
    return '"' + s.replace(/"/g, '""') + '"';
}

var csvDownloadHref = "";

if (submitted && !errorMessage) {
    var csvLines = [];
    csvLines.push(
        [
            "Step",
            "Activity",
            "Type",
            "Start",
            "End",
            "Duration",
            "Status"
        ].join(",")
    );

    for (var cr=0; cr<displayRows.length; cr++) {
        var csvRow = displayRows[cr];

        csvLines.push(
            [
                csvEscape(csvRow.step),
                csvEscape(csvRow.activityName),
                csvEscape(csvRow.activityType),
                csvEscape(csvRow.createdLocal),
                csvEscape(csvRow.modifiedLocal),
                csvEscape(csvRow.duration),
                csvEscape(csvRow.status)
            ].join(",")
        );
    }

    /*
      Use a normal download link instead of changing the CloudPage response.
      A Content Builder CloudPage can add its own HTML wrapper to the HTTP
      response, which is why the previous "CSV" contained HTML.
    */
    /*
      In Marketing Cloud SSJS, encodeURIComponent can encode spaces as "+".
      A data: URI does not convert "+" back to a space, so normalize only
      literal "+" produced by the encoder to %20. Real plus signs are
      already encoded as %2B and remain intact.
    */
    var encodedCsv =
        encodeURIComponent(
            csvLines.join("\r\n")
        ).replace(/\+/g,"%20");

    csvDownloadHref =
        "data:text/csv;charset=utf-8,%EF%BB%BF" +
        encodedCsv;
}
</script>

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="color-scheme" content="light">
<title>Automation Activity Viewer</title>

<style>
:root{
 --page:#f4f7fb;--surface:#fff;--text:#182230;--muted:#667085;--line:#e4e7ec;
 --brand:#5b5ce2;--brand2:#7c3aed;--ok:#067647;--okbg:#ecfdf3;
 --info:#175cd3;--infobg:#eff8ff;--bad:#b42318;--badbg:#fef3f2;
 --warn:#b54708;--warnbg:#fffaeb;--shadow:0 22px 55px rgba(16,24,40,.08)
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
 margin:0;min-width:320px;color:var(--text);
 background:
 radial-gradient(circle at 8% 0,rgba(91,92,226,.14),transparent 30rem),
 radial-gradient(circle at 92% 3%,rgba(124,58,237,.10),transparent 28rem),
 var(--page);
 font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif
}
button,input{font:inherit}
.page{width:calc(100% - 40px);max-width:1440px;min-width:0;margin:auto;padding:32px 0 56px}
.hero{
 position:relative;overflow:hidden;padding:36px;color:#fff;border-radius:28px;
 background:linear-gradient(125deg,#24275f,#5558d9 52%,#7c3aed);
 box-shadow:0 26px 70px rgba(72,63,178,.22)
}
.hero:after{
 content:"";position:absolute;width:330px;height:330px;right:-90px;top:-190px;
 border-radius:50%;background:rgba(255,255,255,.09)
}
.eyebrow{
 display:flex;align-items:center;gap:9px;margin-bottom:14px;font-size:12px;font-weight:850;
 letter-spacing:.12em;text-transform:uppercase;opacity:.86
}
.dot{
 width:9px;height:9px;border-radius:50%;background:#9cf2bd;
 box-shadow:0 0 0 5px rgba(156,242,189,.16)
}
h1{
 position:relative;z-index:1;margin:0;font-size:clamp(32px,4.3vw,54px);
 line-height:1.04;letter-spacing:-.045em
}
.hero p{
 position:relative;z-index:1;max-width:860px;margin:16px 0 0;
 color:rgba(255,255,255,.8);font-size:15px;line-height:1.75
}
.chips{position:relative;z-index:1;display:flex;flex-wrap:wrap;gap:9px;margin-top:25px}
.chip{
 padding:8px 11px;border:1px solid rgba(255,255,255,.14);border-radius:999px;
 background:rgba(255,255,255,.11);font-size:12px;font-weight:780
}

.panel{
 min-width:0;margin-top:20px;border:1px solid var(--line);border-radius:20px;
 background:rgba(255,255,255,.96);box-shadow:var(--shadow);overflow:hidden
}
.search{padding:22px}
.title{margin:0;font-size:19px;letter-spacing:-.02em}
.sub{margin:6px 0 0;color:var(--muted);font-size:13px;line-height:1.55}
.grid{
 display:flex;flex-wrap:wrap;gap:14px;margin-top:19px;align-items:flex-end
}
.grid>div{min-width:0}
.grid .auto{flex:2 1 360px}
.grid>div:not(.auto):not(.searchbtn){flex:1 1 180px}
.grid .searchbtn{flex:0 1 auto}
.grid .searchbtn .btn{min-width:96px}
label{display:block;margin:0 0 7px;color:#344054;font-size:12px;font-weight:820}
.control{
 width:100%;height:46px;padding:0 13px;color:var(--text);border:1px solid #d0d5dd;
 border-radius:12px;outline:0;background:#fff
}
.control:focus{border-color:#8587eb;box-shadow:0 0 0 4px rgba(91,92,226,.11)}
.btn{
 display:inline-flex;align-items:center;justify-content:center;height:46px;padding:0 19px;color:#fff;
 border:0;border-radius:12px;background:linear-gradient(135deg,var(--brand),var(--brand2));
 font-size:13px;font-weight:850;cursor:pointer;box-shadow:0 11px 24px rgba(91,92,226,.24)
}
.btn:disabled{opacity:.6;cursor:wait}
.btn2{
 height:38px;padding:0 14px;color:#344054;border:1px solid #d0d5dd;border-radius:11px;
 background:#fff;font-size:12px;font-weight:800;cursor:pointer
}

.notice{
 margin-top:16px;padding:13px 15px;border:1px solid;border-radius:12px;
 font-size:13px;line-height:1.55
}
.err{color:var(--bad);border-color:#fecdca;background:var(--badbg)}
.info{color:var(--info);border-color:#b2ddff;background:var(--infobg)}

.kpis{
 display:grid;
 grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));
 gap:14px;margin-top:18px
}
.kpi{
 padding:19px;border:1px solid var(--line);border-radius:17px;background:#fff;
 box-shadow:0 8px 24px rgba(16,24,40,.04)
}
.kl{color:var(--muted);font-size:12px;font-weight:780}
.kv{margin-top:8px;font-size:27px;font-weight:900;line-height:1;letter-spacing:-.04em}
.kc{margin-top:8px;color:#98a2b3;font-size:11px}

.toolbar{
 display:flex;align-items:center;justify-content:space-between;gap:18px;padding:18px 20px;
 border-bottom:1px solid var(--line)
}
.toolbar-left{min-width:0}
.toolbar-left h2{
 overflow:hidden;margin:0;font-size:18px;text-overflow:ellipsis;white-space:nowrap
}
.meta{margin-top:5px;color:var(--muted);font-size:12px}
.actions{display:flex;gap:8px}
.filter{width:250px;height:38px}

.scroll{width:100%;max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}
table{width:100%;min-width:980px;border-collapse:separate;border-spacing:0}
th{
 position:sticky;top:0;z-index:2;padding:12px 16px;color:#667085;background:#f8fafc;
 border-bottom:1px solid var(--line);font-size:11px;font-weight:850;letter-spacing:.055em;
 text-align:left;text-transform:uppercase;white-space:nowrap
}
td{padding:14px 16px;border-bottom:1px solid #eef1f5;font-size:13px;vertical-align:middle}
tbody tr:hover{background:#fbfbfe}
.step{
 display:inline-flex;min-width:54px;justify-content:center;padding:5px 9px;color:#4d4fc6;
 border-radius:9px;background:#f0f1ff;font-weight:850
}
.name{font-weight:780}
.type-pill{
 display:inline-flex;padding:5px 9px;border-radius:999px;background:#f2f4f7;
 color:#475467;font-size:11px;font-weight:800;white-space:nowrap
}
.muted{color:var(--muted);white-space:nowrap}
.dur{display:flex;min-width:155px;align-items:center;gap:10px}
.dv{min-width:50px;font-variant-numeric:tabular-nums;font-weight:820}
.bar{width:78px;height:7px;overflow:hidden;border-radius:999px;background:#eceef3}
.bar i{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#6668e8,#9a62e8)}
.badge{display:inline-flex;padding:5px 9px;border-radius:999px;font-size:11px;font-weight:850;white-space:nowrap}
.completed{color:var(--ok);background:var(--okbg)}
.running{color:var(--info);background:var(--infobg)}
.error{color:var(--bad);background:var(--badbg)}
.other{color:#475467;background:#f2f4f7}
.msg{max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted)}

.empty{padding:64px 24px;text-align:center}
.empty b{
 display:grid;width:62px;height:62px;margin:0 auto 16px;place-items:center;color:#5658cf;
 border-radius:19px;background:#f0f1ff;font-size:22px
}
.empty h3{margin:0;font-size:17px}
.empty p{max-width:540px;margin:8px auto 0;color:var(--muted);font-size:13px;line-height:1.6}

.diag{padding:18px 20px}
.diag summary{cursor:pointer;font-weight:850;font-size:13px}
.diag-grid{
 display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px
}
.diag-card{
 padding:12px;border:1px solid var(--line);border-radius:12px;background:#fafbfc
}
.diag-label{color:var(--muted);font-size:11px;font-weight:780}
.diag-value{margin-top:4px;font-size:18px;font-weight:900}

.footer{
 display:flex;justify-content:space-between;gap:18px;margin-top:18px;color:#98a2b3;font-size:11px
}

@media(max-width:1280px){
 .grid .auto{flex-basis:100%}
 .grid .searchbtn{flex:1 1 100%}
 .grid .searchbtn .btn{width:100%}
 .toolbar{align-items:stretch;flex-direction:column}
 .actions{width:100%}
 .filter{width:100%}
}

@media(max-width:760px){
 .page{width:calc(100% - 24px);padding-top:14px}
 .hero{padding:25px 20px;border-radius:22px}
 h1{font-size:34px}
 .search{padding:16px}
 .grid{display:block}
 .grid>div{width:100%;margin-top:14px}
 .grid>div:first-child{margin-top:0}
 .grid .searchbtn .btn{width:100%}
 .actions{flex-direction:column}
 .btn2{width:100%}
 .toolbar{padding:16px}
 .toolbar-left h2{white-space:normal}
 .meta{line-height:1.5}
 table{min-width:860px}
}

@media(max-width:480px){
 .page{width:calc(100% - 16px)}
 .hero{padding:22px 16px}
 h1{font-size:30px}
 .chips{gap:6px}
 .chip{padding:7px 9px;font-size:11px}
 .kpis,.diag-grid{grid-template-columns:1fr}
 .kpi{padding:16px}
}

/* Results toolbar release fix */
.actions{
  display:flex;
  align-items:center;
  justify-content:flex-end;
  gap:10px;
  flex-wrap:wrap;
}
.actions .filter{
  flex:1 1 280px;
  min-width:220px;
  max-width:420px;
}
.actions .btn2{
  flex:0 0 auto;
  white-space:nowrap;
}
@media(max-width:760px){
  .actions{
    width:100%;
    justify-content:stretch;
  }
  .actions .filter{
    flex:1 1 100%;
    max-width:none;
    min-width:0;
  }
  .actions .btn2{
    flex:1 1 140px;
  }
}

/* Final search-form alignment */
.grid{
  align-items:flex-start;
}
.grid > div{
  align-self:flex-start;
}
.grid label{
  display:block;
  min-height:18px;
  margin-bottom:7px;
}
.grid .control,
.grid input[type="date"]{
  margin-top:0;
}

/* Final Search button alignment */
.searchbtn{
  align-self:flex-start;
  padding-top:25px;
}
@media(max-width:760px){
  .searchbtn{
    padding-top:0;
  }
}
</style>
</head>

<body>
<main class="page">

<section class="hero">
 <div class="eyebrow"><span class="dot"></span>Marketing Cloud Engagement</div>
 <h1>Automation Activity Viewer</h1>
 <p>
  Inspect recent Automation Studio activity execution history directly from
  CloudPages. Start time, end time, duration, status, step, and identifiable
  activity type are shown in your current Marketing Cloud account/user local time.
 </p>
 <div class="chips">
  <span class="chip">Single CloudPage</span>
  <span class="chip">Marketing Cloud Login Required</span>
  <span class="chip">Authenticated Access</span>
  <span class="chip">Local Time Aware</span>
  <span class="chip">Unicode Ready</span>
  <span class="chip">CSV Export</span>
 </div>
<script runat="server">
if(signedInUserName || signedInUserEmail){
    Write(
        '<div style="margin-top:12px;font-size:12px;opacity:.82">'+
        'Signed in as <strong>'+
        authHtml(signedInUserName || signedInUserEmail)+
        '</strong>'+
        (
            signedInUserName && signedInUserEmail
            ? ' &middot; '+authHtml(signedInUserEmail)
            : ''
        )+
        '</div>'
    );
}
</script>
</section>

<section class="panel search">
 <h2 class="title">Search activity history</h2>
 <p class="sub">
  Select an Automation Studio automation by typing to filter the available names. The selectable date window
  is limited to the latest 31 days, and dates are interpreted in your current
  Marketing Cloud account/user local time.
 </p>

 <form method="post" id="searchForm">
<script runat="server">
Write(
    '<input type="hidden" name="authSession" value="'+
    authHtml(authSession)+
    '">'
);
</script>
  <input type="hidden" name="action" value="search">

  <div class="grid">

   <div class="auto">
    <label for="automationName">Automation</label>
<script runat="server">
var selectedAutomationValue =
    automationName
        ? String(automationName)
        : "";

Write(
    '<input class="control" id="automationName" name="automationName" type="text" '+
    'list="automationSuggestions" autocomplete="off" '+
    'placeholder="Start typing an automation name..." value="'+
    esc(selectedAutomationValue)+
    '" required>'
);
</script>

    <datalist id="automationSuggestions">
<script runat="server">
for(var n=0;n<automationNames.length;n++){
    Write(
        '<option value="'+
        esc(automationNames[n])+
        '"></option>'
    );
}
</script>
    </datalist>

    <div class="hint">
<script runat="server">
Write(
    String(automationNames.length)+
    " automations available in this Business Unit"
);
</script>
    </div>
   </div>

   <div>
    <label for="startDate">Start Date</label>
<script runat="server">
var initialStartDate =
    startDate ||
    accountLocalYesterday ||
    accountLocalToday;

Write(
    '<input class="control" id="startDate" name="startDate" type="date" value="'+
    esc(initialStartDate)+
    '" min="'+
    esc(accountLocalOldest)+
    '" max="'+
    esc(accountLocalToday)+
    '" required>'
);
</script>
   </div>

   <div>
    <label for="endDate">End Date</label>
<script runat="server">
var initialEndDate =
    endDate ||
    accountLocalYesterday ||
    accountLocalToday;

Write(
    '<input class="control" id="endDate" name="endDate" type="date" value="'+
    esc(initialEndDate)+
    '" min="'+
    esc(accountLocalOldest)+
    '" max="'+
    esc(accountLocalToday)+
    '" required>'
);
</script>
   </div>

   <div class="searchbtn">
    <button class="btn" id="searchButton" type="submit">Search</button>
   </div>

  </div>
 </form>

 <p class="sub" style="margin-top:12px">
  Activity types are displayed only when they can be identified reliably from
  the available SOAP metadata. Otherwise, the type is shown as <strong>Unknown</strong>.
 </p>

<script runat="server">
if(errorMessage){
    Write(
        '<div class="notice err"><strong>Unable to load results.</strong><br>'+
        esc(errorMessage)+
        '</div>'
    );
}

if(infoMessage){
    Write(
        '<div class="notice info">'+
        esc(infoMessage)+
        '</div>'
    );
}
</script>
</section>

<script runat="server">
if(submitted && !errorMessage){
</script>

<section class="kpis">

 <div class="kpi">
  <div class="kl">Activities</div>
  <div class="kv"><script runat="server">Write(resultCount);</script></div>
  <div class="kc">Activity instances returned</div>
 </div>

 <div class="kpi">
  <div class="kl">Longest Activity Duration</div>
  <div class="kv"><script runat="server">Write(fmtDuration(maxDurationSec));</script></div>
  <div class="kc">Longest returned activity instance</div>
 </div>

 <div class="kpi">
  <div class="kl">Longest Automation Run</div>
  <div class="kv"><script runat="server">Write(fmtDuration(maxAutomationDurationSec));</script></div>
  <div class="kc">Longest summed activity duration in one automation execution</div>
 </div>

 <div class="kpi">
  <div class="kl">Average Automation Duration</div>
  <div class="kv"><script runat="server">Write(fmtDuration(avgAutomationDurationSec));</script></div>
  <div class="kc">Average of summed activity durations across automation executions</div>
 </div>

</section>

<section class="panel">

 <div class="toolbar">

  <div class="toolbar-left">
   <h2><script runat="server">Write(esc(automationName));</script></h2>

   <div class="meta">
<script runat="server">
Write(
    esc(startDate)+
    " to "+
    esc(endDate)+
    " &middot; "+
    (resultFilter ? (displayRows.length+" of "+resultCount) : resultCount)+
    " activity instances &middot; Local time"
);
</script>
   </div>
  </div>

  <form class="actions" method="post">
<script runat="server">
Write(
    '<input type="hidden" name="authSession" value="'+
    authHtml(authSession)+
    '">'
);
Write(
    '<input type="hidden" name="automationName" value="'+
    authHtml(automationName)+
    '">'
);
Write(
    '<input type="hidden" name="startDate" value="'+
    authHtml(startDate)+
    '">'
);
Write(
    '<input type="hidden" name="endDate" value="'+
    authHtml(endDate)+
    '">'
);
</script>

<script runat="server">
Write(
    '<input class="control filter" id="tableFilter" name="resultFilter" type="search" '+
    'placeholder="Filter activities..." value="'+
    authHtml(resultFilter)+
    '">'
);
</script>

   <button
    class="btn2"
    name="action"
    value="filter"
    type="submit">
    Apply Filter
   </button>

<script runat="server">
if (csvDownloadHref) {
    Write(
        '<a class="btn2" href="'+
        authHtml(csvDownloadHref)+
        '" download="automation-activity.csv" '+
        'style="display:inline-flex;align-items:center;justify-content:center;text-decoration:none">'+
        'Export CSV</a>'
    );
}
</script>
  </form>

 </div>

<script runat="server">
if(!resultRows.length){
</script>

 <div class="empty">
  <b>0</b>
  <h3>No activity instances found</h3>
  <p>
   No execution history matched the selected local date range.
  </p>
 </div>

<script runat="server">
}else{
    var maxBase = maxDurationSec || 1;
</script>

 <div class="scroll">

  <table id="resultsTable">

   <thead>
    <tr>
     <th>Step</th>
     <th>Activity</th>
     <th>Type</th>
     <th>Start</th>
     <th>End</th>
     <th>Duration</th>
     <th>Status</th>
    </tr>
   </thead>

   <tbody>
<script runat="server">
for(var r=0;r<displayRows.length;r++){

    var row =
        displayRows[r];

    var pct =
        Math.max(
            3,
            Math.min(
                100,
                Math.round(
                    row.durationSec /
                    maxBase *
                    100
                )
            )
        );

    Write("<tr>");

    Write(
        '<td><span class="step">'+
        esc(row.step)+
        '</span></td>'
    );

    Write(
        '<td><span class="name">'+
        esc(row.activityName)+
        '</span></td>'
    );

    Write(
        '<td><span class="type-pill">'+
        esc(row.activityType)+
        '</span></td>'
    );

    Write(
        '<td class="muted">'+
        esc(row.createdLocal || "-")+
        '</td>'
    );

    Write(
        '<td class="muted">'+
        esc(row.modifiedLocal || "-")+
        '</td>'
    );

    Write(
        '<td><div class="dur">'+
        '<span class="dv">'+
        esc(row.duration)+
        '</span>'+
        '<span class="bar">'+
        '<i style="width:'+
        pct+
        '%"></i>'+
        '</span>'+
        '</div></td>'
    );

    Write(
        '<td><span class="badge '+
        esc(row.statusClass)+
        '">'+
        esc(row.status)+
        '</span></td>'
    );

    Write("</tr>");
}
</script>
   </tbody>

  </table>

 </div>

<script runat="server">
}
</script>

</section>


<script runat="server">
}
</script>

<footer style="display:flex;justify-content:space-between;align-items:flex-end;gap:20px;margin-top:20px;color:#98a2b3;font-size:11px;line-height:1.6">
 <div style="text-align:left;white-space:nowrap">Version 3.2</div>
 <div style="margin-left:auto;text-align:right">
  <div>Automation Activity Viewer created by Nobuyuki Watanabe.</div>
  <div>Authentication approach based on the technique by Mateusz Dąbrowski.</div>
 </div>
</footer>

</main>

<script>
(function(){

 var form =
    document.getElementById(
        "searchForm"
    );

 var auto =
    document.getElementById(
        "automationName"
    );

 var start =
    document.getElementById(
        "startDate"
    );

 var end =
    document.getElementById(
        "endDate"
    );

 var button =
    document.getElementById(
        "searchButton"
    );

 var postedAuto = "";
 var postedStart = "";
 var postedEnd = "";

<script runat="server">
Write('postedAuto="'+jsEsc(automationName)+'";');
Write('postedStart="'+jsEsc(startDate)+'";');
Write('postedEnd="'+jsEsc(endDate)+'";');
</script>

 if(auto && postedAuto){
    auto.value = postedAuto;
 }

 if(start && postedStart){
    start.value = postedStart;
 }

 if(end && postedEnd){
    end.value = postedEnd;
 }

 function isoLocal(d){

    var y =
        d.getFullYear();

    var m =
        String(
            d.getMonth()+1
        ).padStart(
            2,
            "0"
        );

    var day =
        String(
            d.getDate()
        ).padStart(
            2,
            "0"
        );

    return (
        y+"-"+m+"-"+day
    );
 }

 var accountToday = "";
 var accountYesterday = "";
 var accountOldest = "";

<script runat="server">
Write('accountToday="'+jsEsc(accountLocalToday)+'";');
Write('accountYesterday="'+jsEsc(accountLocalYesterday)+'";');
Write('accountOldest="'+jsEsc(accountLocalOldest)+'";');
</script>

 /*
   Prefer the Marketing Cloud account/user local dates computed server-side.
   Browser-local fallback is only used if the server-side conversion failed.
 */
 var fallbackToday =
    new Date();

 fallbackToday.setHours(
    0,0,0,0
 );

 var fallbackOldest =
    new Date(
        fallbackToday
    );

 fallbackOldest.setDate(
    fallbackOldest.getDate()-30
 );

 var maxDate =
    accountToday ||
    isoLocal(
        fallbackToday
    );

 var minDate =
    accountOldest ||
    isoLocal(
        fallbackOldest
    );

 var defaultDate =
    accountYesterday ||
    maxDate;

 if(start){
    start.min = minDate;
    start.max = maxDate;
 }

 if(end){
    end.min = minDate;
    end.max = maxDate;
 }

 if(!postedStart && start){
    start.value = defaultDate;
 }

 if(!postedEnd && end){
    end.value = defaultDate;
 }

 if(start && end){

    start.addEventListener(
        "change",
        function(){

            end.min =
                start.value ||
                minDate;

            if(
                end.value &&
                start.value &&
                end.value <
                start.value
            ){
                end.value =
                    start.value;
            }
        }
    );

    end.addEventListener(
        "change",
        function(){

            start.max =
                end.value ||
                maxDate;

            if(
                start.value &&
                end.value &&
                start.value >
                end.value
            ){
                start.value =
                    end.value;
            }
        }
    );
 }

 if(form && button){

    form.addEventListener(
        "submit",
        function(){

            button.disabled =
                true;

            button.textContent =
                "Loading...";
        }
    );
 }

 var table =
    document.getElementById(
        "resultsTable"
    );

 var filter =
    document.getElementById(
        "tableFilter"
    );

 if(table && filter){

    filter.addEventListener(
        "input",
        function(){

            var q =
                String(
                    filter.value || ""
                ).toLocaleLowerCase();

            var trs =
                table.tBodies[0].rows;

            for(
                var i=0;
                i<trs.length;
                i++
            ){
                trs[i].style.display =
                    String(
                        trs[i].innerText || ""
                    )
                    .toLocaleLowerCase()
                    .indexOf(q) >= 0
                    ? ""
                    : "none";
            }
        }
    );
 }

 var csv =
    document.getElementById(
        "csvButton"
    );

 if(table && csv){

    csv.addEventListener(
        "click",
        function(){

            var output = [];

            var trs =
                table.querySelectorAll(
                    "tr"
                );

            for(
                var i=0;
                i<trs.length;
                i++
            ){
                if(
                    trs[i].style.display ===
                    "none"
                ){
                    continue;
                }

                var cells =
                    trs[i].querySelectorAll(
                        "th,td"
                    );

                var values = [];

                for(
                    var j=0;
                    j<cells.length;
                    j++
                ){
                    var value =
                        String(
                            cells[j].innerText || ""
                        )
                        .replace(
                            /\r?\n|\r/g,
                            " "
                        )
                        .trim();

                    values.push(
                        '"'+
                        value.replace(
                            /"/g,
                            '""'
                        )+
                        '"'
                    );
                }

                output.push(
                    values.join(",")
                );
            }

            var csvText =
                "\uFEFF"+
                output.join("\r\n");

            try {

                var blob =
                    new Blob(
                        [csvText],
                        {
                            type:
                            "text/csv;charset=utf-8"
                        }
                    );

                if (
                    window.navigator &&
                    window.navigator.msSaveOrOpenBlob
                ) {
                    window.navigator.msSaveOrOpenBlob(
                        blob,
                        "automation-activity.csv"
                    );
                    return;
                }

                var url =
                    window.URL.createObjectURL(
                        blob
                    );

                var link =
                    document.createElement(
                        "a"
                    );

                link.style.display =
                    "none";

                link.href =
                    url;

                link.setAttribute(
                    "download",
                    "automation-activity.csv"
                );

                document.body.appendChild(
                    link
                );

                link.click();

                /*
                  Do not revoke immediately. Some browsers cancel the
                  download if the Blob URL is destroyed in the same tick.
                */
                window.setTimeout(
                    function(){
                        try {
                            document.body.removeChild(
                                link
                            );
                        } catch(ignoreRemove) {}

                        try {
                            window.URL.revokeObjectURL(
                                url
                            );
                        } catch(ignoreRevoke) {}
                    },
                    1500
                );

            } catch(exportError) {

                /*
                  Last-resort fallback that does not depend on Blob URLs.
                */
                var dataUri =
                    "data:text/csv;charset=utf-8,"+
                    encodeURIComponent(
                        csvText
                    );

                var fallback =
                    document.createElement(
                        "a"
                    );

                fallback.href =
                    dataUri;

                fallback.setAttribute(
                    "download",
                    "automation-activity.csv"
                );

                document.body.appendChild(
                    fallback
                );

                fallback.click();

                document.body.removeChild(
                    fallback
                );
            }
        }
    );
 }

})();
</script>

</body>
</html>

<script runat="server">
}
</script>

これで保存して公開すれば完了です。


アプリを確認する

このアプリは AppExchange のタブの中に出来上がりますので、探してクリックしてください。

以下のような画面になったら成功です。

もしエラーが発生した場合は、手動で入れた 以下の内容が正しく入れられているか確認してください。

var APP_URL = "YOUR_CLOUDPAGE_URL";
var WEB_APP_CLIENT_ID = "YOUR_WEB_APP_CLIENT_ID";
var WEB_APP_CLIENT_SECRET = "YOUR_WEB_APP_CLIENT_SECRET";
var CLIENT_BASE = "YOUR_TSSD";
  • Cloudpages URL:メモした Cloudpages の URL

  • クライアント ID:インストール済みパッケージで生成

  • クライアントシークレット:インストール済みパッケージで生成

  • クライアントベース:(以下を確認)

※ あなたの URI が https:// mc123abc456def .auth.marketingcloudapis.com/ だとしたら、CLIENT_BASE は mc123abc456def の部分です。


API コール数について

このようなアプリを作ると必ず質問されるのが API コール数です。

今回の実装では、ページングが発生しない場合の目安として、1 回の検索に必要な SOAP API(WSProxy)の呼び出し回数は、

  • 約 5 +(対象期間内の Automation 実行回数 × 2)

となります。

例えば、検索期間内にその Automation の実行が 1 回なら、

  • 約 7 API calls

5 回(5 日分まとめて検索)なら、

  • 約 15 API calls

30 回なら、

  • 約 65 API calls

が目安となります。

なお、結果件数が多く WSProxy の Retrieve でページングが発生した場合などは、これより API コール数が増える可能性があります。

重要なのがフィルター機能です。現在の実装では、Apply Filter を実行すると画面が再 POST され、検索処理そのものを再実行します。

そのため、例えば 30 回の実行履歴を取得する検索であれば、

  • 最初の Search:約 65 calls

  • Apply Filter:約 65 calls

が目安となります。

一方、Export CSV では追加の SOAP API コールは発生しません。CSV データはページ生成時に作成されており、Export CSV では生成済みデータをダウンロードします。

認証版では、SOAP API とは別に初回ログイン時に、

  • /v2/token → 1 HTTP API call

  • /v2/userinfo → 1 HTTP API call

が発生します。これは認証時の処理であり、通常の Search ごとに発生するものではありません。


いかがでしたでしょうか。

本ツールは、仮に運用を誤ったとしても、すぐに大きな問題につながるようなものではありません。ただし、ログイン認証を設定していない CloudPages を公開したまま放置しないよう、十分にご注意ください。

特に実際の環境で利用する場合は、セキュリティや運用方法についても考慮したうえで、ご自身の環境や用途に合わせて導入をご検討ください。

今回は以上です。


前回の記事はこちら

私の note のトップページはこちら