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

# Power BI

> Conectați Power BI la Living Security Public API cu Power Query, construiți un model de tip star-schema pe /v1 și alegeți între seturile de date operaționale și cele de raportare pentru phishing.

Acest ghid este un exemplu complet de citire a `/v1` în Power BI. Acesta acoperă
modelul de conectivitate Power Query (M), un model semantic de tip star-schema construit
pe `/v1/activities`, reîmprospătarea incrementală și situațiile în care se utilizează seturile de date
**reporting** în variantă preliminară pentru rezultatele de phishing la nivel de persoană.

Exemplele utilizează gazda de producție din SUA
(`https://api.us-east-1.livingsecurity.ai/v1`). Pentru o organizație din UE, utilizați
`https://api.eu-west-1.livingsecurity.ai/v1` atât în `Fn_FetchAll`, cât și în
`Fn_GetJson`, apoi asociați credențialul sursei de date cu această gazdă. Consultați
[Regiuni](/ro/developers/v1/regions).

Acest ghid utilizează credențiale Basic stocate, cu o cheie API. Nu aveți nevoie de un client
OAuth. Consultați [Autentificare](/ro/developers/v1/authentication) pentru
opțiunile de credențiale și [Developer Hub](/ro/developers/developer-hub/overview)
pentru acces și configurare.

## Scopul acestui exemplu

Utilizați-l atunci când trebuie să:

* Încărcați endpoint-urile de listare `/v1` într-un set de date Power BI, cu
  paginare corectă prin cursor și reîncercare în caz de depășire a limitei de rată.
* Construiți un model semantic în care măsurile („entități active", „activități per
  entitate activă") sunt dimensionale, nu calculate dintr-un tabel plat.
* Reduceți o reîmprospătare programată Power BI de la parcurgerea completă a istoricului la
  o fereastră zilnică mică, folosind reîmprospătarea incrementală.
* Alegeți granularitatea corectă pentru phishing: totaluri la nivel de campanie (operațional) față de
  rezultate la nivel de persoană și evenimente tipizate (reporting, variantă preliminară).

## Cerințe preliminare

* O cheie API cu permisiunile necesare interogărilor dumneavoastră:
  * Exemplul operațional: `activity:read`, `entity:read` și
    `simulation:read`.
  * Phishing la nivel de persoană prin seturi de date de raportare: `reporting:read`.
    Consultați [Chei API](/ro/developers/developer-hub/api-keys).
* Power BI Desktop sau un spațiu de lucru Power BI / Fabric în care puteți crea un
  model semantic și programa reîmprospătarea.

## Alegeți granularitatea potrivită

`/v1` expune două suprafețe conexe pentru phishing. Acestea nu sunt interschimbabile.

| Necesitate | Utilizați | Permisiune | Granularitate |
| - | - | - | - |
| Totaluri ale fluxului campaniei | `GET /v1/simulations/{id}/results` | `simulation:read` | Un rând per simulare (livrate, deschise, accesate prin clic, …) |
| Rezultate la nivel de persoană (operațional) | `GET /v1/simulation_results` | `simulation:read` | Un rând per entitate × simulare |
| Rezultate la nivel de persoană (BI / raportare) | `GET /v1/reporting/simulation_results` | `reporting:read` | Aceeași granularitate, nume de câmpuri BI (`entity_id`, `simulation_id`), marcaj de filigran `updated_after` |
| Fapte de interacțiune tipizate | `GET /v1/reporting/simulation_events` | `reporting:read` | Un fapt per interacțiune (`simulation.link_clicked`, …), `occurred_after` |
| Dimensiunea curentă a Forței de muncă | `GET /v1/reporting/entities` | `reporting:read` | Un rând curent per entitate. Asociați prin `reporting.simulation_results.entity_id = reporting.entities.id` |
| Versiunile curente ale entităților | `GET /v1/reporting/entity_versions` | `reporting:read` | Un interval curent deschis per entitate. Nu există un tabel cu istoricul versiunilor |
| Apartenențele curente la Cohortă | `GET /v1/reporting/cohort_memberships` | `reporting:read` | Un fapt curent per `(cohort_id, entity_id)`. Nu există un tabel cu istoricul apartenențelor |
| Coloana vertebrală de asociere pentru toate tipurile de activitate | `GET /v1/activities` | `activity:read` | Un eveniment per activitate, cu `participants[]` imbricat |

Interogările de exemplu Power BI de mai jos apelează rutele **operaționale**
(`/v1/simulations`, `/v1/simulations/{id}/results`, `/v1/activities`,
`/v1/entities`). Pentru raportarea phishing la nivel de persoană, preferați familia
**reporting** în variantă preliminară de mai jos. O cheie căreia i s-a acordat doar `reporting:read` nu poate
apela `/v1/simulation_results` operațional (API-ul returnează `403`).

### Scala de dificultate

Dificultatea simulării este un număr întreg de la 1 la 5 pe `/v1/simulations` (`difficulty_level`),
`/v1/simulation_results` (`difficulty_level`), proprietățile `/v1/activities`
(`difficulty_tier`) și `/v1/reporting/simulation_events` (`difficulty_tier`).
`1` este cel mai ușor nivel, iar `5` este cel mai dificil. Simulările adaptive și progresive nu au
o valoare unică la nivel de campanie, deci `/v1/simulations.difficulty_level` este `null`,
în timp ce `difficulty_level` per destinatar din rezultate poate fi totuși setat. Pentru a face corespondența cu
exporturile vechi de tip scăzut/mediu/ridicat, tratați `1–2` ca scăzut, `3` ca mediu și `4–5` ca
ridicat.

<Warning>
  Nu construiți integrări BI noi pe baza apelurilor de rețea nedocumentate ale Tabloului de bord
  sau a prezentării generale vechi, curate, a Public API `/api/v1`. Autentificați-vă la gazda
  `/v1` regională a organizației cu o cheie API din Developer Hub. Consultați
  [Regiuni](/ro/developers/v1/regions).
</Warning>

## Model de conectivitate (Power Query)

Patru reguli stau la baza fiecărei interogări din exemplu. Fiecare dintre ele este impusă de
serviciul Power BI, nu de preferințe de stil.

<AccordionGroup>
  <Accordion title="Păstrați URL-ul de bază Web.Contents static și încheiați-l la /v1">
    Puneți tot ce este dinamic în `RelativePath` și `Query`. Primul argument al
    `Web.Contents` trebuie să fie un șir literal care se încheie la rădăcina `/v1`
    regională a organizației:

    * SUA: `https://api.us-east-1.livingsecurity.ai/v1`
    * UE: `https://api.eu-west-1.livingsecurity.ai/v1`

    Apelanții transmit apoi doar resursa (`"simulations"`, nu
    `"v1/simulations"`). Nu lipiți o gazdă din SUA pentru o organizație din UE.

    Dacă primul argument este construit prin concatenare de șiruri, serviciul Power BI
    nu poate identifica static sursa de date, iar reîmprospătarea programată eșuează cu
    o eroare de „sursă de date dinamică", chiar dacă interogarea funcționează în Power BI
    Desktop.
  </Accordion>

  <Accordion title="Furnizați cheia API ca un credențial Basic stocat, nu în textul interogării">
    Când Power BI solicită credențiale pentru sursa de date `/v1`, alegeți
    **Basic** și lipiți cheia API ca **nume de utilizator**. Introduceți un substituent
    precum `x` în câmpul **Parolă**. Serviciul Power BI respinge o
    parolă necompletată, chiar dacă API-ul ignoră acest câmp. Cheia ca parolă, cu un nume
    de utilizator substituent, funcționează de asemenea.

    Nu adăugați un antet `Authorization` în M. Funcționează pe desktopul dumneavoastră, apoi
    încorporează cheia în fișierul interogării și intră în conflict cu reîmprospătarea programată.
  </Accordion>

  <Accordion title="Lăsați să ruleze testul de conexiune din caseta de dialog a credențialelor">
    `GET /v1` este autentificat: returnează `200` cu o cheie validă și `401`
    fără una. Indicați sursa de date către `…/v1` și **nu** omiteți testul —
    o cheie bună se salvează, una revocată sau malformată eșuează imediat.

    Exemplele mai vechi utilizau gazda simplă ca sursă de date și indicau omiterea testului
    deoarece `GET /` este întotdeauna `404`. Reorientați acele modele către `…/v1`.
  </Accordion>

  <Accordion title="Setați explicit limit=500 pentru fiecare interogare de listare">
    Dimensiunea implicită a paginii pentru fiecare endpoint de listare `/v1` este 25. Lăsarea valorii
    implicite multiplică numărul de solicitări pentru aceleași date. Valorile interogării trebuie să fie text —
    `limit = "500"` în M, nu `limit = 500`.
  </Accordion>
</AccordionGroup>

### Funcția comună de paginare

Fiecare interogare din exemplu apelează o singură `Fn_FetchAll` care parcurge
`next_page_token` până când acesta este `null` (consultați
[paginare](/ro/developers/v1/pagination)). Proprietățile sale notabile:

* **URL de bază `/v1` static, `RelativePath` și `Query` dinamice.** Necesar pentru
  reîmprospătarea programată și pentru testul credențialelor.
* **`ManualStatusHandling = {429}` cu reîncercare limitată.** Funcția citește
  `Retry-After` când Power Query o expune, altfel așteaptă 30 de secunde și
  renunță după 5 răspunsuri `429` consecutive, cu o eroare explicită, în loc să
  intre în buclă până la expirarea reîmprospătării. `5xx` nu este reîncercat în mod deliberat
  — o eroare de server trebuie să fie vizibilă, nu să producă în tăcere un set de date
  parțial.
* **`Timeout` de 60 de secunde.** Paginile răspund de obicei în mult mai puțin de o secundă, deci
  un minut de tăcere înseamnă că solicitarea este moartă.
* **`Binary.Buffer` înainte de `Json.Document`.** Fără buffering, motorul mashup
  poate solicita din nou fluxul de răspuns dacă îl citește de mai multe ori.
* **`baseQuery` este transmis neschimbat la fiecare iterație.** Token-urile de pagină sunt
  cursoare opace, semnate, legate de organizație, credențial, versiunea contractului
  și amprenta filtrelor și a sortării. Modificarea unui filtru în timpul parcurgerii este respinsă ca token nevalid,
  în loc să fie reluată de la prima pagină.

```m Fn_FetchAll.pq — base and call shape theme={null}
let
    // Literal /v1 root. Callers pass the resource alone ("simulations").
    // US: "https://api.us-east-1.livingsecurity.ai/v1"
    BaseUrl = "https://api.eu-west-1.livingsecurity.ai/v1",

    Fn_FetchAll = (relativePath as text, baseQuery as record) as list =>
        // Walk next_page_token until null. Full pagination + 429 retry lives
        // in examples/powerbi/queries/Fn_FetchAll.pq.
        ...
in
    Fn_FetchAll
```

### Interogări tipizate per endpoint

Fiecare consumator al `Fn_FetchAll` urmează aceeași structură: apelează endpoint-ul,
proiectează o listă fixă de coloane, elimină discriminatorul `object` și tipizează
marcajele temporale ca `datetimezone`, astfel încât măsurile de dată să nu se deplaseze în tăcere în funcție de
fusul orar al capacității de reîmprospătare.

<CodeGroup>
  ```m Simulations.pq (GET /v1/simulations) theme={null}
  let
      Source = Fn_FetchAll("simulations", [ limit = "500" ]),

      AsTable = Table.FromRecords(
          Source,
          {
              "id", "object", "name", "description", "status", "vectors",
              "difficulty_level", "difficulty_mode",
              "started_at", "completed_at", "created_at", "updated_at"
          },
          MissingField.UseNull),

      Dropped = Table.RemoveColumns(AsTable, { "object" }),

      Flattened = Table.TransformColumns(
          Dropped,
          {
              {
                  "vectors",
                  each if _ = null then null
                       else Text.Combine(List.Transform(_, Text.From), ", "),
                  type text
              }
          }),

      Typed = Table.TransformColumnTypes(
          Flattened,
          {
              { "id",           type text },
              { "name",         type text },
              { "description",  type text },
              { "status",       type text },
              { "started_at",   type datetimezone },
              { "completed_at", type datetimezone },
              { "created_at",   type datetimezone },
              { "updated_at",   type datetimezone }
          })
  in
      Typed
  ```

  ```m Entities.pq (GET /v1/entities) theme={null}
  let
      Source = Fn_FetchAll("entities", [ limit = "500" ]),

      AsTable = Table.FromRecords(
          Source,
          { "id", "object", "type", "status", "display_name", "email",
            "created_at", "updated_at" },
          MissingField.UseNull),

      Dropped = Table.RemoveColumns(AsTable, { "object" }),

      Typed = Table.TransformColumnTypes(
          Dropped,
          {
              { "id",           type text },
              { "type",         type text },
              { "status",       type text },
              { "display_name", type text },
              { "email",        type text },
              { "created_at",   type datetimezone },
              { "updated_at",   type datetimezone }
          })
  in
      Typed
  ```
</CodeGroup>

<Tip>
  O listă explicită de coloane pe `Table.FromRecords` menține schema stabilă atunci când un
  endpoint returnează zero rânduri. Un `Table.FromRecords({})` simplu produce un
  tabel fără coloane, iar toți pașii ulteriori eșuează din cauza câmpurilor lipsă.
</Tip>

## Excel

Același Power Query (M) rulează în Get & Transform din Excel — este același
motor ca în Power BI Desktop. Utilizați această variantă când doriți un tabel într-o foaie de lucru
în loc de un model semantic.

În Excel pentru Windows (Microsoft 365, Excel 2024, 2021, 2019 sau 2016):

1. **Data → Get Data → From Other Sources → Blank Query.**
2. În Power Query Editor, **Home → Editor avansat**. Lipiți mai întâi
   `Fn_FetchAll`, apoi redenumiți interogarea respectivă `Fn_FetchAll` (Query
   Settings → Name sau clic dreapta în panoul Queries → **Rename**).
3. Repetați pentru `Fn_GetJson`, apoi pentru fiecare interogare de raport.
4. **Home → Close & Load → Close & Load To…** deschide caseta de dialog **Import Data**.
   Alegeți **Only Create Connection** pentru `Fn_FetchAll` și
   `Fn_GetJson` (sunt funcții, nu tabele). Alegeți **Table** pentru
   fiecare interogare de raport pe care o doriți într-o foaie de lucru.
5. La prima reîmprospătare, alegeți **Basic**: cheia API ca **User name**, un
   substituent precum `x` ca **Password**. Excel stochează un singur credențial
   per URL de sursă de date; interogările ulterioare care accesează aceeași gazdă `/v1` îl
   reutilizează.

<Note>
  **Close & Load To…** poate fi inactiv prima dată când creați o interogare într-un
  registru de lucru. Utilizați **Close & Load**, apoi în Excel selectați **Data →
  Queries & Connections**, faceți clic dreapta pe interogare și alegeți **Load To**.
  Această cale este documentată de Microsoft pentru acest caz.
</Note>

Nu puteți trimite `Living-Security-Version` din Power Query pe o sursă
autentificată — aceeași restricție ca în Power BI. Fixați contul de serviciu sau transmiteți `api_version` în înregistrarea interogării. Consultați
[Versiunile contractului și credențialele stocate](#versiunile-contractului-și-credențialele-stocate).

Interogări de raport gata de utilizare (corespondențe de coloane și diferențe față de un
export al unui furnizor anterior):

<CardGroup cols={3}>
  <Card title="Conformitate per persoană" href="/ro/developers/v1/reports/per-person-compliance">
    Înscrieri la Instruire la granularitate persoană × atribuire.
  </Card>

  <Card title="Jurnal phishing prin e-mail" href="/ro/developers/v1/reports/email-phishing-log">
    Interacțiuni de phishing la nivel de eveniment, ultimele 12 luni.
  </Card>

  <Card title="Cei mai vulnerabili utilizatori" href="/ro/developers/v1/reports/most-susceptible-users">
    Sinteză per persoană pe 12 luni a clicurilor, credențialelor și raportărilor.
  </Card>
</CardGroup>

## Seturi de date de raportare în variantă preliminară (phishing per persoană)

Familia de raportare în variantă preliminară oferă seturi de date pentru phishing per persoană.
Necesită `reporting:read` și acces la raportare pentru organizația dumneavoastră.
Utilizați catalogul de seturi de date de mai jos pentru a inspecta schemele și politicile de date.

Descoperiți catalogul live:

```text theme={null}
GET /v1/reporting/datasets
GET /v1/reporting/datasets/simulation_results
GET /v1/reporting/datasets/simulation_events
GET /v1/reporting/datasets/entities
GET /v1/reporting/datasets/entity_versions
GET /v1/reporting/datasets/cohort_memberships
```

Fiecare set de date declară granularitatea, cheia primară, asocierile, filigranul, politica de corecție și
politica de ștergere.

### simulation\_results

* **Cale:** `GET /v1/reporting/simulation_results`
* **Granularitate:** entitate × simulare
* **Filtre:** `simulation`, `entity`, `updated_after`, plus `limit` /
  `page_token`
* **Obiect wire:** `reporting.simulation_result`
* **Chei de asociere:** `entity_id`, `simulation_id`

```m ReportingSimulationResults.pq (sketch) theme={null}
let
    Source = Fn_FetchAll(
        "reporting/simulation_results",
        [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "simulation_id", "status",
            "targeted_at", "delivered_at", "opened_at", "clicked_at",
            "credential_submitted_at", "reported_at",
            "email_subject", "sender_email", "difficulty_level",
            "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "entity_id",     type text },
            { "simulation_id", type text },
            { "status",        type text },
            { "targeted_at",   type datetimezone },
            { "updated_at",    type datetimezone }
        })
in
    Typed
```

Utilizați `updated_after` ca limită inferioară inclusivă a filigranului când reîmprospătați
partițiile recente. Dimensiunile modificabile ale rezultatelor se actualizează pe loc sub același `id`.

### simulation\_events

* **Cale:** `GET /v1/reporting/simulation_events`
* **Granularitate:** un fapt tipizat per interacțiune observată
* **Filtre:** `simulation`, `entity`, `event_type`, `occurred_after`, plus
  `limit` / `page_token`
* **Obiect wire:** `reporting.simulation_event`
* **Tipuri de evenimente:** token-uri publice închise, precum `simulation.delivered`,
  `simulation.opened`, `simulation.link_clicked`,
  `simulation.credentials_submitted`, `simulation.reported` — nu tipuri brute de activitate
  ClickHouse `phishing.simulation.*`
* **Dispozitiv:** `device_type` este clasa derivată (token-urile observate includ
  `desktop`, `mobile`, `tablet`, `unknown`). `user_agent` brut nu se află pe
  `/v1`. IP-ul brut nu se află niciodată pe `/v1` (nu este niciodată persistat; doar `ip_hash` cu salt).

```m ReportingSimulationEvents.pq (sketch) theme={null}
let
    Source = Fn_FetchAll(
        "reporting/simulation_events",
        [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "simulation_id",
            "event_type", "occurred_at",
            "email_subject", "sender_email", "sender_domain", "difficulty_tier",
            "device_type",
            "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "entity_id",     type text },
            { "simulation_id", type text },
            { "event_type",    type text },
            { "occurred_at",   type datetimezone }
        })
in
    Typed
```

Utilizați `occurred_after` pentru încărcări incrementale de evenimente. Faptele sunt doar de tip append;
sosirile întârziate sunt rânduri noi.

### entities

* **Cale:** `GET /v1/reporting/entities`
* **Granularitate:** un rând curent per entitate
* **Filtre:** `type`, `updated_after`, plus `limit` / `page_token`
* **Obiect wire:** `reporting.entity`
* **Cheie de asociere:** `id` / `entity_id` (aceeași valoare)

Asociați faptele de phishing cu persoanele prin
`reporting.simulation_results.entity_id = reporting.entities.id`.
`GET /v1/entities` operațional rămâne pe `entity:read` și reprezintă o
permisiune diferită.

```m ReportingEntities.pq theme={null}
let
    Source = Fn_FetchAll("reporting/entities", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "type", "status",
            "display_name", "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",          type text },
            { "entity_id",   type text },
            { "type",        type text },
            { "status",      type text },
            { "display_name", type text },
            { "created_at",  type datetimezone },
            { "updated_at",  type datetimezone }
        })
in
    Typed
```

### entity\_versions și cohort\_memberships

Aceste două seturi de date au o excepție denumită, limitată la starea curentă:

> Nu există un tabel autoritar cu istoricul versiunilor sau al apartenențelor. Acest set de date proiectează rândul curent ca un singur interval deschis (`valid_to` este null, `is_current` este true). `valid_from` este `created_at` sau, în caz contrar, `updated_at` pentru versiunile entităților și momentul observării solicitării pentru apartenențele la Cohortă. Intervalele istorice închise nu sunt fabricate.

* **entity\_versions:** `GET /v1/reporting/entity_versions`. Aceleași filtre ca
  entities. `valid_from` este `created_at` sau, în caz contrar, `updated_at`. O entitate
  cu ambele marcaje temporale null este omisă.
* **cohort\_memberships:** `GET /v1/reporting/cohort_memberships`. Filtre:
  `cohort`, `entity`, `updated_after`. `updated_at` este ultima actualizare a Cohortei,
  astfel încât reîmprospătarea incrementală să rămână corectă. `valid_from` este momentul
  observării solicitării. Asociați `entity_id` cu `reporting.entities.id`.

```m ReportingEntityVersions.pq theme={null}
let
    Source = Fn_FetchAll("reporting/entity_versions", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "entity_id", "type", "status", "display_name",
            "valid_from", "valid_to", "is_current",
            "created_at", "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",         type text },
            { "entity_id",  type text },
            { "valid_from", type datetimezone },
            { "is_current", type logical }
        })
in
    Typed
```

```m ReportingCohortMemberships.pq theme={null}
let
    Source = Fn_FetchAll("reporting/cohort_memberships", [ limit = "500" ]),

    AsTable = Table.FromRecords(
        Source,
        {
            "id", "object", "cohort_id", "entity_id",
            "valid_from", "valid_to", "is_current",
            "updated_at", "deleted_at"
        },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",         type text },
            { "cohort_id",  type text },
            { "entity_id",  type text },
            { "valid_from", type datetimezone },
            { "is_current", type logical },
            { "updated_at", type datetimezone }
        })
in
    Typed
```

## Model semantic star-schema pe /v1/activities

`/v1/activities` este fluxul de evenimente cu marcaj temporal al organizației. Nu are
măsuri numerice — fiecare cantitate dintr-un raport este un număr de rânduri — ceea ce face ca
o schemă stea să fie o potrivire naturală.

```text theme={null}
Dates (dimension)
      │  1
      ▼  ∗
  Activities (fact)   1  ∗  ActivityParticipants (bridge)   ∗  1   Entities (dimension)
```

* **`Activities`** este faptul de eveniment la granularitatea reală. Un rând per activitate.
  Participanții sunt eliminați de aici și reconstruiți ca tabel propriu.
* **`ActivityParticipants`** există deoarece participanții sunt cu adevărat
  de tip unu-la-mulți, cu roluri precum `actor` și `target`. Aplatizarea pe
  `Activities` ar duplica rândurile de activitate (umflând fiecare număr)
  sau ar elimina participanții suplimentari.
  Relația cu `Activities` este singura relație bidirecțională a modelului,
  astfel încât un slicer de entitate ajunge la tabelul de fapte.
* **`Entities`** este dimensiunea care denumește participanții. Rândurile de participanți
  dintr-o activitate conțin `entity_id` și un rol, dar nu un nume afișat.
* **`Dates`** este generat în M (zero apeluri API) și marcat ca tabel de date al modelului.
  Aceasta face legale funcțiile DAX de inteligență temporală
  (`DATESINPERIOD`, `DATEADD`). Coloanele cu numele lunii și al zilei săptămânii declară
  `sortByColumn` pe omoloagele lor numerice, astfel încât vizualizările să nu sorteze lunile
  alfabetic.

Toate măsurile sunt explicite (modelul setează `discourageImplicitMeasures`): total
activități, entități active, activități per entitate activă, zile cu activitate
și o medie zilnică glisantă pe 7 zile.

### Structura tabelului de legătură

`ActivityParticipants` parcurge `/v1/activities` și expandează matricea `participants`
la câte un rând per participant:

```m ActivityParticipants.pq theme={null}
let
    WindowStart = DateTime.AddZone(RangeStart, 0),
    WindowEnd   = DateTime.AddZone(RangeEnd, 0),
    ToIso = (value as datetimezone) as text =>
        DateTimeZone.ToText(value,
            [ Format = "yyyy-MM-dd'T'HH:mm:ss'Z'", Culture = "en-US" ]),

    Source = Fn_FetchAll(
        "activities",
        [
            limit      = "100",
            start_date = ToIso(WindowStart - #duration(0, 0, 1, 0)),
            end_date   = ToIso(WindowEnd + #duration(0, 0, 1, 0))
        ]),

    AsTable = Table.FromRecords(
        Source,
        { "id", "object", "activity_type", "event_ts",
          "service_id", "participants" },
        MissingField.UseNull),

    Selected = Table.SelectColumns(AsTable, { "id", "event_ts", "participants" }),
    Renamed  = Table.RenameColumns(Selected, { { "id", "activity_id" } }),
    Expanded = Table.ExpandListColumn(Renamed, "participants"),
    NonNull  = Table.SelectRows(Expanded, each [participants] <> null),
    Fields   = Table.ExpandRecordColumn(
        NonNull, "participants",
        { "role", "entity_id", "display_name" }),

    Typed = Table.TransformColumnTypes(
        Fields,
        {
            { "activity_id",  type text },
            { "event_ts",     type datetimezone },
            { "role",         type text },
            { "entity_id",    type text },
            { "display_name", type text }
        }),

    Filtered = Table.SelectRows(
        Typed,
        each [event_ts] >= WindowStart and [event_ts] < WindowEnd)
in
    Filtered
```

`ActivityParticipants` parcurge `/v1/activities` independent, în loc să
facă referire la interogarea `Activities`. În Power BI, două interogări încărcate ca două
tabele își aduc fiecare datele independent la momentul reîmprospătării. Menținerea lor independente
permite serviciului să reîmprospăteze în paralel partițiile ambelor tabele.

## Reîmprospătare incrementală

Ambele tabele la granularitate de fapt (`Activities` și `ActivityParticipants`) declară o
politică de reîmprospătare incrementală Power BI: păstrarea a 5 ani de partiții anuale,
reimportarea ultimelor 14 zile la fiecare reîmprospătare.

Mecanismul este o pereche de parametri DateTime, `RangeStart` și `RangeEnd`,
pe care serviciul Power BI îi reatribuie per partiție la momentul reîmprospătării. Fiecare interogare de fapte
integrează acești parametri în filtrele `start_date` și `end_date`
ale API-ului, astfel încât volumul de solicitări la nivel de partiție este proporțional cu partiția,
nu cu întregul istoric. Interogarea lărgește fereastra API cu un minut pe fiecare
parte și reaplică local limita exactă semideschisă `[RangeStart, RangeEnd)`,
astfel încât corectitudinea partiției nu depinde de faptul că serverul
tratează limitele ca inclusive sau exclusive.

```m Activities.pq — the fold that makes incremental refresh work theme={null}
let
    WindowStart = DateTime.AddZone(RangeStart, 0),
    WindowEnd   = DateTime.AddZone(RangeEnd, 0),
    ToIso = (value as datetimezone) as text =>
        DateTimeZone.ToText(value,
            [ Format = "yyyy-MM-dd'T'HH:mm:ss'Z'", Culture = "en-US" ]),

    Source = Fn_FetchAll(
        "activities",
        [
            limit      = "100",
            start_date = ToIso(WindowStart - #duration(0, 0, 1, 0)),
            end_date   = ToIso(WindowEnd + #duration(0, 0, 1, 0))
        ]),

    AsTable = Table.FromRecords(
        Source,
        { "id", "object", "activity_type", "event_ts",
          "service_id", "participants" },
        MissingField.UseNull),

    Dropped = Table.RemoveColumns(AsTable, { "object", "participants" }),

    Typed = Table.TransformColumnTypes(
        Dropped,
        {
            { "id",            type text },
            { "activity_type", type text },
            { "event_ts",      type datetimezone },
            { "service_id",    type text }
        }),

    Filtered = Table.SelectRows(
        Typed,
        each [event_ts] >= WindowStart and [event_ts] < WindowEnd),

    WithDate = Table.AddColumn(
        Filtered, "event_date", each Date.From([event_ts]), type date)
in
    WithDate
```

Prima reîmprospătare după o implementare aplică politica: o încărcare istorică completă,
lentă. Fiecare reîmprospătare ulterioară atinge doar fereastra incrementală.

Măsurat pe un set de date de \~158.000 de activități și \~2.300 de entități:

| Reîmprospătare | Timp total |
| - | - |
| Prima (aplicarea politicii, încărcare istorică completă) | \~31 min |
| Fiecare reîmprospătare ulterioară | **\~15 s** |

<Warning>
  Endpoint-ul REST clasic de reîmprospătare Power BI (`POST .../refreshes` cu doar
  `notifyOption`) ignoră în tăcere politicile de reîmprospătare și parcurge totul
  de fiecare dată. Pentru a aplica politica prin API-ul REST, utilizați reîmprospătarea îmbunătățită: trimiteți
  `type`, `commitMode` și `applyRefreshPolicy` în corpul solicitării. Reîmprospătările
  declanșate de planificatorul serviciului Power BI aplică întotdeauna politicile — această
  particularitate este specifică apelului REST.
</Warning>

<Note>
  Fereastra de 14 zile este generoasă în mod deliberat. Ingestia activităților nu este
  de tip append-only-la-momentul-curent: completările retroactive ale conectorilor aduc evenimente cu marcaje temporale din trecut.
  O fereastră de 1-2 zile le-ar pierde definitiv.
</Note>

## Extinderea ferestrei de reîmprospătare în API

`GET /v1/activities` acceptă filtrele `start_date` și `end_date` în care
se integrează parametrii Power BI, filtrând `event_ts` între limite.
`Entities` este o dimensiune, așa că acest exemplu o reîmprospătează integral de fiecare dată,
nu incremental.

Seturile de date de raportare utilizează propriile filigrane (`updated_after`,
`occurred_after`) în locul ferestrei de date a activităților.

## Limite de rată în timpul reîmprospătării

`/v1` aplică bugete separate per cheie și per IP (consultați
[limite de rată](/ro/developers/v1/rate-limits)). Respectați `429` + `Retry-After`; nu
codificați un buget de solicitări direct în model. Două proprietăți ale reîmprospătărilor Power BI
sunt importante:

* **Parcurgerea unui singur tabel este secvențială.** Fiecare solicitare de pagină necesită
  token-ul paginii anterioare, deci un tabel este limitat de latență indiferent de dimensiunea
  paginii.
* **Reîmprospătările concurente ale tabelelor se multiplică.** Mai multe tabele mari reîmprospătate
  în paralel reprezintă cazul care atinge plafonul, motiv pentru care calea de reîncercare
  429 din `Fn_FetchAll` contează. Preferați mai puține tabele în paralel, în loc să
  presupuneți o concurență nelimitată.

Reîncercarea a fost testată față de un limitator real saturat. O reîmprospătare
rulată în timp ce o sarcină paralelă menținea deliberat cheia la plafon
s-a finalizat totuși și a produs aceleași numere de rânduri ca o referință fără contenție —
reîncercările au returnat pagini complete, fără goluri.

## Versiunile contractului și credențialele stocate

`/v1` acceptă un antet de solicitare `Living-Security-Version` pentru a fixa o versiune publicată a
contractului (consultați [Ghid rapid API](/ro/developers/v1/quickstart)). Power
Query nu poate trimite acel antet când sursa de date utilizează credențiale stocate —
doar sursele de date anonime permit antete personalizate. Adăugarea lui face ca o reîmprospătare să eșueze cu:

```text theme={null}
Expression.Error: The 'Living-Security-Version' header is only supported when
connecting anonymously.
Microsoft.Data.Mashup.ErrorCode = 10348.
```

Păstrați cheia în credențialele stocate ale Power BI și omiteți antetul personalizat.
Fără o versiune explicită în solicitare, API-ul utilizează `contract_version` fixată a contului de serviciu
sau cea mai recentă versiune publicată dacă nu este setată nicio fixare.
Confirmați versiunea contractului contului cu administratorul API înainte de
a publica o reîmprospătare. Consultați [jurnalul de modificări API](/ro/developers/v1/changelog) pentru
versiunile acceptate.

Dacă o reîmprospătare începe să se comporte neașteptat, inspectați antetul de răspuns al API-ului
`Living-Security-Version` pentru a vedea ce contract a
servit efectiv solicitarea.

## Reîmprospătați și verificați modelul

Publicați modelul semantic în spațiul de lucru Power BI, asociați credențialul său stocat
cu sursa regională `/v1` și rulați o reîmprospătare înainte de a activa o
programare. Examinați istoricul reîmprospătărilor pentru eșecuri sau citiri incomplete.

Verificați numărul de rânduri față de o citire API independentă, folosind aceleași filtre.
[Ghidul de paginare](/ro/developers/v1/pagination) include exemple Python și cURL
pentru citirea unui set complet de rezultate. O reîmprospătare care citește doar
prima pagină poate părea reușită pe un set de date mic; comparați numerele înainte de
a vă baza pe raport.

## Alegeri de modelare

* **Totalurile campaniei și rezultatele per persoană au granularități diferite.**
  `SimulationResults` apelează `GET /v1/simulations/{id}/results`
  (un GET per rulare). Pentru phishing per persoană, adăugați interogări pentru
  `GET /v1/reporting/simulation_results` și
  `GET /v1/reporting/simulation_events` (sau `GET /v1/simulation_results`
  operațional, dacă dețineți deja `simulation:read`).
* **`vectors` pe `/v1/simulations` este aplatizat într-un șir separat prin virgulă.**
  O coloană de tip listă nu poate fi utilizată ca dimensiune reală într-un slicer. Dacă o pagină de raport
  trebuie să filtreze după vector, adăugați un tabel de legătură simulare-vector.

## Pașii următori

<CardGroup cols={2}>
  <Card title="Developer Hub" href="/ro/developers/developer-hub/overview">
    Creați un cont de serviciu și o cheie API pentru Power BI.
  </Card>

  <Card title="Ghid rapid API" href="/ro/developers/v1/quickstart">
    Autentificați solicitările `/v1` cu autentificare Basic.
  </Card>

  <Card title="Paginare" href="/ro/developers/v1/pagination">
    Envelope-ul listei, token-urile cursor și stabilitatea filtrelor pe durata unei parcurgeri.
  </Card>

  <Card title="Limite de rată" href="/ro/developers/v1/rate-limits">
    Bugete per cheie și per IP, contractul 429 și recomandări de reîncercare.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.