{
  "name": "nadprofteh prof.mos.ru",
  "code": "(() => {\n  // ====== settings ======\n  const DEFAULT_ENDPOINT = 'https://docflow.ks54.pro/api/bearer-set';\n  const DEFAULT_SERVICE = 'AIS_PT';\n  const FETCH_TIMEOUT_MS = 15_000;\n\n  const TOKEN_KEYS = ['saved_token', 'access_token', 'accessToken', 'token', 'auth_token'];\n\n  const LS_ENDPOINT = 'ks54_npt_prof.endpoint';\n  const LS_ADMIN = 'ks54_npt_prof.admin_token';\n  const LS_STATUS = 'ks54_npt_prof.last_status';\n\n  const SUPER_FLAG = 'nadprofteh_supermode';\n  const MENU_TITLE = 'Надпрофтехник';\n  const MENU_MARKER = 'data-nadprofteh-menu';\n  const LOG = '[NPT]';\n\n  if (window.__KS54_NPT_PROF__) return;\n  window.__KS54_NPT_PROF__ = true;\n\n  if (window.top !== window.self) return;\n\n  // ====== utils ======\n  function nowStr() {\n    return new Date().toLocaleString();\n  }\n\n  function normalizeBearer(v) {\n    if (v == null) return null;\n    let s = String(v).trim();\n    if (!s) return null;\n    for (let i = 0; i < 2; i++) {\n      if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n        s = s.slice(1, -1).trim();\n      }\n    }\n    if (s.toLowerCase().startsWith('bearer ')) s = s.slice(7).trim();\n    return s || null;\n  }\n\n  function looksLikeJwt(v) {\n    const s = String(v || '').trim();\n    if (s.length < 20) return false;\n    return /^[A-Za-z0-9\\-_]+\\.[A-Za-z0-9\\-_]+\\.[A-Za-z0-9\\-_]+$/.test(s);\n  }\n\n  function loadSettings() {\n    return {\n      endpoint: localStorage.getItem(LS_ENDPOINT) || DEFAULT_ENDPOINT,\n      admin: localStorage.getItem(LS_ADMIN) || '',\n      service: DEFAULT_SERVICE,\n    };\n  }\n\n  function setStatus(text) {\n    localStorage.setItem(LS_STATUS, text);\n  }\n\n  function getStatus() {\n    return localStorage.getItem(LS_STATUS) || '';\n  }\n\n  async function fetchWithTimeout(url, opts, timeoutMs) {\n    const ctrl = new AbortController();\n    const t = setTimeout(() => ctrl.abort(), timeoutMs);\n    try {\n      return await fetch(url, { ...opts, signal: ctrl.signal });\n    } finally {\n      clearTimeout(t);\n    }\n  }\n\n  // ====== bearer (prof: sniffer + storage) ======\n  const liveAuth = { token: null, source: null };\n\n  function headerGet(headers, name) {\n    if (!headers) return null;\n    if (typeof Headers !== 'undefined' && headers instanceof Headers) return headers.get(name);\n    const lower = name.toLowerCase();\n    for (const k of Object.keys(headers)) {\n      if (k.toLowerCase() === lower) return headers[k];\n    }\n    return null;\n  }\n\n  function rememberBearer(token, source) {\n    const t = normalizeBearer(token);\n    if (!t || !looksLikeJwt(t)) return;\n    liveAuth.token = t;\n    liveAuth.source = source;\n  }\n\n  function installAuthSniffer() {\n    if (window.__NPT_authSniffer) return;\n\n    const origFetch = window.fetch;\n    window.fetch = function (input, init) {\n      try {\n        const url = typeof input === 'string' ? input : input && input.url ? input.url : '';\n        if (url.includes('prof.mos.ru/back/api')) {\n          const auth = headerGet(init && init.headers, 'authorization');\n          if (auth) rememberBearer(auth.replace(/^Bearer\\s+/i, ''), 'fetch');\n        }\n      } catch (_) {\n        /* ignore */\n      }\n      return origFetch.apply(this, arguments);\n    };\n\n    if (window.XMLHttpRequest?.prototype) {\n      const origOpen = XMLHttpRequest.prototype.open;\n      const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;\n      XMLHttpRequest.prototype.open = function (method, url) {\n        this.__nptApiUrl = url;\n        return origOpen.apply(this, arguments);\n      };\n      XMLHttpRequest.prototype.setRequestHeader = function (name, value) {\n        try {\n          if (this.__nptApiUrl && String(this.__nptApiUrl).includes('prof.mos.ru/back/api')) {\n            if (String(name).toLowerCase() === 'authorization') {\n              rememberBearer(String(value).replace(/^Bearer\\s+/i, ''), 'xhr');\n            }\n          }\n        } catch (_) {\n          /* ignore */\n        }\n        return origSetHeader.apply(this, arguments);\n      };\n    }\n\n    window.__NPT_authSniffer = true;\n  }\n\n  installAuthSniffer();\n\n  function findTokenInStorage(storage) {\n    const out = [];\n    function add(key, value, src) {\n      const b = normalizeBearer(value);\n      if (b && looksLikeJwt(b)) out.push({ token: b, src: `${src}:${key}` });\n    }\n    for (const k of TOKEN_KEYS) {\n      try {\n        add(k, storage.getItem(k), 'storage');\n      } catch (_) {\n        /* ignore */\n      }\n    }\n    try {\n      for (let i = 0; i < storage.length; i++) {\n        const k = storage.key(i);\n        if (!k || !/token/i.test(k)) continue;\n        add(k, storage.getItem(k), 'scan');\n      }\n    } catch (_) {\n      /* ignore */\n    }\n    return out;\n  }\n\n  function getBearer() {\n    if (liveAuth.token) return liveAuth.token;\n\n    const candidates = [\n      ...findTokenInStorage(localStorage),\n      ...(() => {\n        try {\n          return findTokenInStorage(sessionStorage);\n        } catch (_) {\n          return [];\n        }\n      })(),\n    ];\n\n    if (!candidates.length) return null;\n    candidates.sort((a, b) => (b.token?.length || 0) - (a.token?.length || 0));\n    liveAuth.source = candidates[0].src;\n    return (liveAuth.token = candidates[0].token);\n  }\n\n  async function pushNow() {\n    const s = loadSettings();\n    const bearer = getBearer();\n\n    if (!bearer) {\n      const msg = `[${nowStr()}] Bearer не найден (${TOKEN_KEYS.join(', ')})`;\n      setStatus(msg);\n      showToast('Токен не найден — F5 и дождитесь загрузки', false);\n      return { ok: false };\n    }\n\n    const payload = {\n      service: s.service,\n      bearer: normalizeBearer(bearer),\n      cookie: (document.cookie || '').slice(0, 8000),\n      meta: {\n        origin: location.origin,\n        href: location.href,\n        ts: Date.now(),\n        ua: navigator.userAgent,\n        tokenSource: liveAuth.source,\n      },\n    };\n\n    const headers = { 'Content-Type': 'application/json' };\n    if (s.admin) headers['X-Admin-Token'] = s.admin;\n\n    showToast('Отправляю токен…', true, 2000);\n\n    try {\n      const res = await fetchWithTimeout(\n        s.endpoint,\n        {\n          method: 'POST',\n          mode: 'cors',\n          credentials: 'omit',\n          headers,\n          cache: 'no-store',\n          body: JSON.stringify(payload),\n        },\n        FETCH_TIMEOUT_MS\n      );\n\n      const txt = await res.text();\n      const msg = `[${nowStr()}] ${res.status} ${res.ok ? 'OK' : 'ERR'}: ${txt.slice(0, 500)}`;\n      setStatus(msg);\n      showToast(res.ok ? 'Токен отправлен' : `Ошибка ${res.status}`, res.ok, 4000);\n      return { ok: res.ok, status: res.status, text: txt };\n    } catch (e) {\n      const msg = `[${nowStr()}] FETCH: ${e?.name === 'AbortError' ? 'timeout' : e?.message || e}`;\n      setStatus(msg);\n      showToast('Не удалось отправить токен', false, 4000);\n      return { ok: false };\n    }\n  }\n\n  // ====== prof API + copy JSON ======\n  function buildApiHeaders(extra, hasBody) {\n    const token = getBearer();\n    if (!token) return null;\n    return Object.assign(\n      {\n        Accept: 'application/json, text/plain, */*',\n        'Accept-Language': 'ru-RU,ru;q=0.9',\n        'Content-Language': 'ru-RU',\n        'x-mes-subsystem': 'proftechw_app',\n        Authorization: 'Bearer ' + token,\n      },\n      hasBody ? { 'Content-Type': 'application/json' } : {},\n      extra || {}\n    );\n  }\n\n  function api(path, method = 'GET', body, extra = {}) {\n    const headers = buildApiHeaders(extra, body != null);\n    if (!headers) {\n      return Promise.reject(\n        new Error('Нет токена — дождитесь загрузки (запросы к /back/api) или F5')\n      );\n    }\n    return fetch('https://prof.mos.ru/back/api' + path, {\n      method,\n      credentials: 'include',\n      headers,\n      body: body != null ? JSON.stringify(body) : undefined,\n    }).then((res) => {\n      if (!res.ok) {\n        const err = new Error(String(res.status));\n        if (res.status === 401) {\n          err.message =\n            '401: токен не принят. F5 и повторите. Источник: ' + (liveAuth.source || 'нет');\n        }\n        throw err;\n      }\n      return res.json().catch(() => null);\n    });\n  }\n\n  function copyText(text) {\n    if (navigator.clipboard?.writeText) {\n      return navigator.clipboard.writeText(text).catch(() => copyTextFallback(text));\n    }\n    return copyTextFallback(text);\n  }\n\n  function copyTextFallback(text) {\n    return new Promise((resolve, reject) => {\n      try {\n        const ta = document.createElement('textarea');\n        ta.value = text;\n        ta.setAttribute('readonly', '');\n        ta.style.cssText = 'position:fixed;left:-9999px;top:0';\n        document.body.appendChild(ta);\n        ta.focus();\n        ta.select();\n        const ok = document.execCommand('copy');\n        ta.remove();\n        ok ? resolve() : reject(new Error('copy failed'));\n      } catch (e) {\n        reject(e);\n      }\n    });\n  }\n\n  let toastRoot = null;\n\n  function ensureToastRoot() {\n    if (toastRoot?.isConnected) return toastRoot;\n    toastRoot = document.createElement('div');\n    Object.assign(toastRoot.style, {\n      position: 'fixed',\n      top: '16px',\n      right: '16px',\n      display: 'flex',\n      flexDirection: 'column',\n      gap: '6px',\n      zIndex: '99999',\n    });\n    document.body.appendChild(toastRoot);\n    return toastRoot;\n  }\n\n  function showToast(msg, ok = true, ms = 3000) {\n    if (!document.body) return;\n    const root = ensureToastRoot();\n    const t = document.createElement('div');\n    Object.assign(t.style, {\n      maxWidth: '320px',\n      padding: '16px 24px',\n      borderRadius: '8px',\n      background: ok ? 'rgba(0,150,136,0.95)' : 'rgba(244,67,54,0.95)',\n      color: '#fff',\n      fontSize: '16px',\n      fontWeight: '500',\n      textAlign: 'center',\n      boxShadow: '0 4px 12px rgba(0,0,0,0.3)',\n      opacity: '0',\n      transform: 'translateY(-10px)',\n      transition: 'opacity .3s, transform .3s',\n    });\n    t.textContent = msg;\n    root.appendChild(t);\n    requestAnimationFrame(() => {\n      t.style.opacity = '1';\n      t.style.transform = 'translateY(0)';\n    });\n    setTimeout(() => {\n      t.style.opacity = '0';\n      t.style.transform = 'translateY(-10px)';\n      t.addEventListener('transitionend', () => t.remove(), { once: true });\n    }, ms);\n  }\n\n  function isSuperMode() {\n    return localStorage.getItem(SUPER_FLAG) === '1';\n  }\n\n  function toggleSuperMode() {\n    if (isSuperMode()) localStorage.removeItem(SUPER_FLAG);\n    else localStorage.setItem(SUPER_FLAG, '1');\n    showToast('Супер-режим ' + (isSuperMode() ? 'ON' : 'OFF'), true, 2000);\n    setTimeout(() => location.reload(), 500);\n  }\n\n  function normalizePath(pathname) {\n    return pathname.replace(/\\/+$/, '') || '/';\n  }\n\n  function getApplicationId(pathname) {\n    const p = normalizePath(pathname);\n    const m =\n      p.match(/\\/application\\/(\\d+)$/) ||\n      p.match(/\\/application\\/[^/]+\\/(\\d+)$/) ||\n      p.match(/\\/(\\d+)$/);\n    return m ? m[1] : null;\n  }\n\n  function getActionsForPath(pathname) {\n    const p = normalizePath(pathname);\n    if (/\\/application\\/\\d+$/.test(p) || /\\/application\\/[^/]+\\/\\d+$/.test(p)) return 'detailed';\n    if (p === '/application') return 'minimal';\n    if (/^\\/orders(\\/create)?$/.test(p)) return 'orders';\n    return null;\n  }\n\n  function convertDate(iso) {\n    if (!iso) return '';\n    return String(iso).split('-').reverse().join('.');\n  }\n\n  function parseAddress(full, apt) {\n    const parts = String(full || '')\n      .split(',')\n      .map((s) => s.trim());\n    const keysStreet = [\n      'улица', 'ул.', 'проспект', 'пр-кт', 'пр.', 'бульвар', 'аллея', 'шоссе',\n      'переулок', 'пл.', 'площадь', 'проезд',\n    ];\n    let city = '',\n      street = '',\n      building = '',\n      flat = '';\n    for (const part of parts) {\n      const L = part.toLowerCase();\n      if (!city && L.includes('город ')) {\n        city = part.replace(/.*?город\\s+/i, '').trim();\n        continue;\n      }\n      if (!flat && (L.startsWith('кв.') || L.startsWith('квартира'))) {\n        flat = part.replace(/^(кв\\.?|квартира)\\s*/i, '');\n        continue;\n      }\n      if (!street && keysStreet.some((k) => L.includes(k)) && L.includes('дом')) {\n        const stHm = part.split(/дом\\s*/i);\n        street = stHm[0].trim();\n        const hm = stHm[1] || '';\n        const corp = hm.match(/(.+?)\\s+корпус\\s+(\\S+)/i);\n        building = corp ? corp[1].trim() + ' корп. ' + corp[2].trim() : hm.trim();\n        continue;\n      }\n      if (!street && keysStreet.some((k) => L.startsWith(k))) {\n        street = part;\n        continue;\n      }\n      if (!building && L.startsWith('дом ')) {\n        building = part.replace(/^дом\\s*/i, '').trim();\n        continue;\n      }\n      if (L.startsWith('корпус ')) {\n        building += ' корп. ' + part.replace(/^корпус\\s*/i, '').trim();\n        continue;\n      }\n      if (L.startsWith('строение ')) {\n        building += ' стр. ' + part.replace(/^строение\\s*/i, '').trim();\n        continue;\n      }\n    }\n    if (!flat) flat = apt || '';\n    return { city, address: street, building, flat };\n  }\n\n  function extractFormJSON(id) {\n    return Promise.all([\n      api('/applications/' + id + '/header'),\n      api('/applications/' + id + '/education'),\n      api('/applications/' + id + '/applicant'),\n    ]).then(([h, e, a]) => {\n      const nameParts = (h.fullName || '').split(' ');\n      const addr = parseAddress(\n        a.address?.address?.fullAddress || '',\n        a.address?.apartment\n      );\n      const rawOther = (a.arrival?.docEducationOtherPlace || '').trim();\n      const rawWho = (a.arrival?.docEducationWhoIssued || '').trim();\n      const pristFrom = rawOther || (/[^\\d]/.test(rawWho) ? rawWho : '');\n      const docEduType = a.arrival?.docEducationType?.label || '';\n      const attType = /аттестат/i.test(docEduType)\n        ? 'Аттестат'\n        : /диплом/i.test(docEduType)\n          ? 'Диплом'\n          : '';\n\n      return {\n        status: 'pguonly',\n        gender: h.gender?.label === 'Мужской' ? 'м' : 'ж',\n        address_register_type: a.address.registrationType.label,\n        address_register_city: addr.city,\n        address_register: addr.address,\n        address_register_bld: addr.building,\n        address_register_flat: addr.flat,\n        address_live_city: addr.city,\n        address_live: addr.address,\n        address_live_bld: addr.building,\n        address_live_flat: addr.flat,\n        surname: nameParts[0] || '',\n        name: nameParts[1] || '',\n        father: nameParts[2] || '',\n        date_birth: convertDate(h.birthday),\n        citizenship:\n          a.docIdentity?.nationality?.label ||\n          a.arrival?.docEducationOtherPlace ||\n          a.address?.country?.label ||\n          '',\n        pasp_type: a.docIdentity.type.label,\n        pasp_code: a.docIdentity.issuerCode,\n        pasp_ser: a.docIdentity.series,\n        pasp_no: a.docIdentity.number,\n        pasp_date: convertDate(a.docIdentity.issueDate),\n        pasp_vydan: a.docIdentity.issuer,\n        phone_mob: a.contact.phone,\n        email: a.contact.email,\n        snils: a.snils,\n        ed_level: a.arrival?.basicEducation?.label || '',\n        att_type: attType,\n        att_no: a.arrival.docEducationNumber,\n        att_date: convertDate(a.arrival.docEducationIssueDate),\n        '9_prib_from': pristFrom,\n        school_year: a.arrival.graduationYear,\n        request: {\n          spo_spec: h.type?.code === 'ADMISSION_SPECIAL' ? 'С' + e.code : e.code,\n          pay_level: e.financing.label,\n          ed_type: e.studyForm.label,\n          date_gosuslugi: convertDate(h.registrationDate),\n          gosuslugi: '«' + h.status.label + '»',\n          id_gosuslugi: h.registrationNumber,\n          comment_gosuslugi: '',\n        },\n        comment_zayav: h.personId || '',\n      };\n    });\n  }\n\n  function onCopy() {\n    const id = getApplicationId(location.pathname);\n    if (!id) return showToast('ID не найден', false);\n    if (!getBearer()) return showToast('Нет токена — F5 и дождитесь загрузки', false);\n    showToast('Копирую…', true, 2000);\n    extractFormJSON(id)\n      .then((data) => copyText(JSON.stringify(data, null, 2)))\n      .then(() => showToast('JSON скопирован', true))\n      .catch((err) => {\n        console.error(LOG, err);\n        showToast('Ошибка: ' + (err?.message || err), false, 4000);\n      });\n  }\n\n  // ====== sidebar menu ======\n  function getSidebarContainer() {\n    const sider = document.querySelector('.layout__sider');\n    if (!sider) return null;\n    return (\n      sider.querySelector('.sider-scroll__container > div') ||\n      sider.querySelector('.sider-scroll > div > div') ||\n      sider.querySelector('.sider-scroll .sc-dmlrTW') ||\n      null\n    );\n  }\n\n  function hasInjectedMenu() {\n    return !!document.querySelector('.layout__sider [' + MENU_MARKER + '=\"1\"]');\n  }\n\n  function createAction(cfg) {\n    const btn = document.createElement('button');\n    btn.type = 'button';\n    btn.className = 'sider-ul__li';\n    if (cfg.action) btn.setAttribute('data-npt-action', cfg.action);\n    const icon = cfg.icon != null ? cfg.icon : '⚙️';\n    btn.innerHTML =\n      '<div class=\"sider-ul__icon\">' +\n      icon +\n      '</div><div class=\"sider-ul__text\">' +\n      cfg.title +\n      '</div>';\n    return btn;\n  }\n\n  function onMenuClick(e) {\n    const btn = e.target?.closest?.('[data-npt-action]');\n    if (!btn) return;\n    e.preventDefault();\n    e.stopPropagation();\n    const action = btn.getAttribute('data-npt-action');\n    if (action === 'copy-json') onCopy();\n    else if (action === 'push-token') pushNow();\n    else if (action === 'super') toggleSuperMode();\n  }\n\n  function createCategory(title, actions) {\n    const c = getSidebarContainer();\n    if (!c) return false;\n    const h = document.createElement('div');\n    h.className = 'sider-title';\n    h.setAttribute(MENU_MARKER, '1');\n    const hText = document.createElement('div');\n    hText.className = 'sider-title__text';\n    hText.textContent = title;\n    h.appendChild(hText);\n    const l = document.createElement('div');\n    l.className = 'sider-ul';\n    l.setAttribute(MENU_MARKER, '1');\n    actions.forEach((cfg) => l.appendChild(createAction(cfg)));\n    c.prepend(h, l);\n    return true;\n  }\n\n  function clearMenu() {\n    const c = getSidebarContainer();\n    if (!c) return;\n    c.querySelectorAll('[' + MENU_MARKER + '=\"1\"]').forEach((el) => el.remove());\n  }\n\n  function buildActionLists() {\n    const superToggle = {\n      title: isSuperMode() ? 'Выключить супер-режим' : 'Включить супер-режим',\n      icon: '🚀',\n      action: 'super',\n    };\n    const pushToken = {\n      title: 'Направить токен',\n      icon: '📤',\n      action: 'push-token',\n    };\n    const copyJson = {\n      title: 'Скопировать JSON',\n      icon: '📋',\n      action: 'copy-json',\n    };\n    return {\n      detailed: [superToggle, copyJson, pushToken],\n      minimal: [superToggle, pushToken],\n      orders: [superToggle, pushToken],\n    };\n  }\n\n  function tryInjectMenu() {\n    const kind = getActionsForPath(location.pathname);\n    if (!kind || !getSidebarContainer() || hasInjectedMenu()) return !!hasInjectedMenu();\n    const configs = buildActionLists()[kind];\n    if (!configs?.length) return false;\n    clearMenu();\n    return createCategory(MENU_TITLE, configs);\n  }\n\n  function scheduleInject() {\n    tryInjectMenu();\n  }\n\n  let booted = false;\n\n  function boot() {\n    if (booted) return;\n    if (!document.body) {\n      document.addEventListener('DOMContentLoaded', boot, { once: true });\n      return;\n    }\n    booted = true;\n    document.documentElement.setAttribute('data-npt', '1');\n    console.log(LOG, 'loaded', location.pathname, 'token:', getBearer() ? liveAuth.source : 'нет');\n    showToast('Надпрофтехник загружен', true, 1500);\n    if (!getBearer()) {\n      showToast('Токен не подхвачен — F5 и дождитесь загрузки', false, 5000);\n    }\n    scheduleInject();\n    document.addEventListener('click', onMenuClick, true);\n    const observer = new MutationObserver(() => {\n      if (!getActionsForPath(location.pathname)) {\n        if (hasInjectedMenu()) clearMenu();\n        return;\n      }\n      if (!hasInjectedMenu()) scheduleInject();\n    });\n    observer.observe(document.body, { childList: true, subtree: true });\n    window.addEventListener('popstate', scheduleInject);\n    const { pushState, replaceState } = history;\n    history.pushState = function () {\n      pushState.apply(history, arguments);\n      scheduleInject();\n    };\n    history.replaceState = function () {\n      replaceState.apply(history, arguments);\n      scheduleInject();\n    };\n  }\n\n  window.__NPT = {\n    __loaded: true,\n    api,\n    showToast,\n    getBearer,\n    profToken: getBearer,\n    pushNow,\n    extractFormJSON,\n    copyText,\n    onCopy,\n    settings: loadSettings,\n    lastStatus: getStatus,\n    tryInjectMenu,\n    debugSidebar() {\n      const t = getBearer();\n      console.log(LOG, {\n        pathname: location.pathname,\n        kind: getActionsForPath(location.pathname),\n        container: getSidebarContainer(),\n        hasMenu: hasInjectedMenu(),\n        token: !!t,\n        tokenSource: liveAuth.source,\n        endpoint: loadSettings().endpoint,\n      });\n    },\n  };\n\n  boot();\n})();",
  "runCondition": "url",
  "matchType": "contains",
  "conditionKey": "https://prof.mos.ru/",
  "trigger": "automatic",
  "autoTiming": "onLoad",
  "enabled": true
}
