1<!DOCTYPE html><html lang="ru" data-multify-language="ru" data-multify-region="RU"><head><meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta name="robots" content="noindex"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>â</title><!--favicons--> <link rel="icon" type="image/png" sizes="32x32" href="https://static.tildacdn.com/tild3333-3736-4639-b538-376461323536/Coat_of_arms_of_Sain.png" media="(prefers-color-scheme: light)"> <link rel="icon" type="image/png" sizes="32x32" href="https://static.tildacdn.com/tild3066-6635-4938-b934-633339333539/Coat_of_arms_of_Sain.png" media="(prefers-color-scheme: dark)"> <link rel="icon" type="image/svg+xml" sizes="any" href="https://static.tildacdn.com/tild3532-6563-4362-b234-613237373832/Coat_of_arms_of_Sain.svg"> <link rel="apple-touch-icon" type="image/png" href="https://static.tildacdn.com/tild3163-6138-4561-b065-383061623238/Frame_354.png"> <link rel="icon" type="image/png" sizes="192x192" href="https://static.tildacdn.com/tild3163-6138-4561-b065-383061623238/Frame_354.png"> <!--/favicons--><link href="https://members.tildaapi.com/frontend/css/tilda-members-base.min.css?t=1791466972" rel="stylesheet">
1<script src="https://spbtech.ru/proxy-5c1448ff47?url=https%3A%2F%2Fmembers.tildaapi.com%2Ffrontend%2Fjs%2Ftilda-members-init.min.js%3Ft%3D1791466972" id="tilda-membersarea-js"></script>
1<link rel="alternate" hreflang="x-default" href="https://spbtech.ru/members/login"><link rel="alternate" hreflang="ru-RU" href="https://spbtech.ru/members/login"><link rel="alternate" hreflang="ru" href="https://spbtech.ru/members/login"><link rel="alternate" hreflang="en-US" href="https://spbtech.ru/en/members/login"><link rel="alternate" hreflang="en" href="https://spbtech.ru/en/members/login"><link rel="alternate" hreflang="zh-CN" href="https://spbtech.ru/cn/members/login"><link rel="alternate" hreflang="zh" href="https://spbtech.ru/cn/members/login"></head><body><div id="allrecords" class="t-records" data-tilda-project-id="12180255" data-tilda-root-zone="com" data-tilda-project-country="RU"></div>
1<script>window.multify = { 2 locale: 'ru_RU', 3 language: 'RU', 4 region: 'RU', 5 prefix: '', 6 root: 'spbtech.ru', 7 variants: [{"locale":"en_US","language":"EN","region":"US","prefix":"/en","root":"spbtech.ru/en"},{"locale":"zh_CN","language":"ZH","region":"CN","prefix":"/cn","root":"spbtech.ru/cn"}], 8} 9 10function watchAndProxyGlobalFunction(name, handler) { 11 function tryProxy() { 12 const original = window[name] 13 if (typeof original === 'function' && !original.__isProxied) { 14 const wrapped = function (...args) { 15 return handler.call(this, original, ...args) 16 } 17 wrapped.__isProxied = true 18 window[name] = wrapped 19 console.debug(`[Proxy] ${name} hooked`) 20 } 21 } 22 23 // Immediate attempt (e.g., inline scripts) 24 tryProxy() 25 26 // Observe dynamically added scripts 27 const observer = new MutationObserver(() => { 28 setTimeout(tryProxy, 0) 29 }) 30 31 observer.observe(document.documentElement, { 32 childList: true, 33 subtree: true, 34 }) 35} 36 37function proxyDictionary(dict) { 38 dict = dict || {} 39 40 const originalDict = {} 41 42 return new Proxy(originalDict, { 43 get(target, prop) { 44 // Return override value if it exists, otherwise return original 45 if (dict.hasOwnProperty(prop)) { 46 return dict[prop] 47 } 48 return target[prop] 49 }, 50 set(target, prop, value) { 51 // Store the original value 52 target[prop] = value 53 return true 54 }, 55 ownKeys(target) { 56 // Return all keys from both original and override 57 return [...new Set([...Object.keys(target), ...Object.keys(dict)])] 58 }, 59 has(target, prop) { 60 return prop in target || prop in dict 61 }, 62 }) 63} 64</script>
64<script>;(function () { 65 // Generic function to check and rewrite URLs 66 function rewriteUrlIfNeeded(url) { 67 if (!url) return url 68 69 // Avoid double-wrapping already proxied URLs or generic ?url= wrappers 70 if ( 71 typeof url === 'string' && 72 (url.includes('/proxy-5c1448ff47') || /[?&]url=/.test(url)) 73 ) { 74 return url 75 } 76 77 // Skip data/blob URLs 78 if ( 79 typeof url === 'string' && 80 (url.startsWith('data:') || url.startsWith('blob:')) 81 ) { 82 return url 83 } 84 85 // if ( 86 // ['tildaapi.pro', 'tildacdn.pro'].some((domain) => url.includes(domain)) 87 // ) { 88 // url = url.replace('.pro', '.com') 89 // } 90 91 // if ( 92 // ['tildaapi.one', 'tildacdn.one'].some((domain) => url.includes(domain)) 93 // ) { 94 // url = url.replace('.one', '.com') 95 // } 96 97 const excludePatterns = [ 98 'geoserv.tildacdn.*/api/detect-city', 99 'geo.tildaapi.*/geo', 100 // 'feeds.tildacdn.*/api/setpostviewed', 101 ] 102 103 const matchesPattern = (url, pattern) => { 104 if (pattern.includes('*')) { 105 const regexPattern = pattern.replace(/\*/g, '.*?') 106 const regex = new RegExp(regexPattern, 'i') 107 return regex.test(url) 108 } 109 return url.includes(pattern) 110 } 111 112 if (excludePatterns.some((pattern) => matchesPattern(url, pattern))) { 113 return url 114 } 115 116 // Check if URL contains our target domain 117 if ( 118 ['tildaapi', 'tildacdn', 'remarked.ru'].some((domain) => 119 url.includes(domain), 120 ) 121 ) { 122 // Create new URL with current host + /api-proxy/ + encoded original URL 123 const newUrl = `/proxy-5c1448ff47?url=${encodeURIComponent(url)}` 124 console.log(`URL rewritten: ${url} -> ${newUrl}`) 125 return newUrl 126 } 127 128 return url 129 } 130 131 // Store the original XMLHttpRequest open method 132 const originalOpen = XMLHttpRequest.prototype.open 133 134 // Override the XMLHttpRequest open method 135 XMLHttpRequest.prototype.open = function ( 136 method, 137 url, 138 async = true, 139 user, 140 password, 141 ) { 142 url = rewriteUrlIfNeeded(url) 143 144 // Call the original open method with the potentially modified URL 145 return originalOpen.call(this, method, url, async, user, password) 146 } 147 148 // Store the original fetch function 149 const originalFetch = window.fetch 150 151 // Override the fetch function 152 window.fetch = function (input, init) { 153 let url = input 154 155 // Handle Request objects 156 if (input instanceof Request) { 157 url = input.url 158 } 159 160 const rewrittenUrl = rewriteUrlIfNeeded(url) 161 162 // Only modify input if URL was rewritten 163 if (rewrittenUrl !== url) { 164 // If input was a Request object, create a new one with the new URL 165 if (input instanceof Request) { 166 input = new Request(rewrittenUrl, input) 167 } else { 168 input = rewrittenUrl 169 } 170 } 171 172 // Call the original fetch with the potentially modified input 173 return originalFetch.call(this, input, init) 174 } 175 176 // Fix for timeout and responseType issues with synchronous requests 177 const originalSetTimeout = Object.getOwnPropertyDescriptor( 178 XMLHttpRequest.prototype, 179 'timeout', 180 ).set 181 const originalSetResponseType = Object.getOwnPropertyDescriptor( 182 XMLHttpRequest.prototype, 183 'responseType', 184 ).set 185 186 // Override the timeout setter 187 Object.defineProperty(XMLHttpRequest.prototype, 'timeout', { 188 set: function (value) { 189 try { 190 originalSetTimeout.call(this, value) 191 } catch (e) { 192 // Silently ignore timeout errors for synchronous requests 193 if ( 194 !e.message.includes( 195 'synchronous XMLHttpRequests do not support timeout', 196 ) 197 ) { 198 throw e 199 } 200 } 201 }, 202 get: Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'timeout') 203 .get, 204 }) 205 206 // Override the responseType setter 207 Object.defineProperty(XMLHttpRequest.prototype, 'responseType', { 208 set: function (value) { 209 try { 210 originalSetResponseType.call(this, value) 211 } catch (e) { 212 // Silently ignore responseType errors for synchronous requests 213 if ( 214 !e.message.includes( 215 'synchronous XMLHttpRequests do not support responseType', 216 ) 217 ) { 218 throw e 219 } 220 } 221 }, 222 get: Object.getOwnPropertyDescriptor( 223 XMLHttpRequest.prototype, 224 'responseType', 225 ).get, 226 }) 227})() 228</script>
228<script>;(async function () { 229 const LOCALE_KEY = 'multify_locale' 230 231 const clearCart = () => { 232 sessionStorage.removeItem('tCityFromGeo') 233 localStorage.removeItem('iso_code') 234 localStorage.removeItem('tcart') 235 localStorage.removeItem('tcart_countries') 236 localStorage.removeItem('tcart_discounts') 237 } 238 239 const clearWishlist = () => { 240 sessionStorage.removeItem('tCityFromGeo') 241 localStorage.removeItem('iso_code') 242 localStorage.removeItem('twishlist') 243 } 244 245 const validate = (options = { reload: false, updateLocale: true }) => { 246 const cartElement = document.querySelector('.t706[data-project-currency]') 247 248 if (!cartElement) { 249 return 250 } 251 252 const currency = cartElement.getAttribute('data-project-currency') 253 254 if (!currency) { 255 return 256 } 257 258 const currentLocale = window.multify?.locale 259 const storedLocale = localStorage.getItem(LOCALE_KEY) 260 const tcart = JSON.parse(localStorage.getItem('tcart')) 261 const twishlist = JSON.parse(localStorage.getItem('twishlist')) 262 263 // Update stored locale (only on initial load, not from storage events) 264 if ( 265 options.updateLocale && 266 currentLocale && 267 currentLocale !== storedLocale 268 ) { 269 localStorage.setItem(LOCALE_KEY, currentLocale) 270 } 271 272 // Check locale mismatch (if we had a stored locale and it changed) 273 if (storedLocale && currentLocale && storedLocale !== currentLocale) { 274 console.warn( 275 `[Multify] Locale mismatch, clearing cart & wishlist (expected: ${currentLocale}, got: ${storedLocale})`, 276 ) 277 278 clearCart() 279 clearWishlist() 280 281 if (options.reload && tcart) { 282 location.reload() 283 } 284 return 285 } 286 287 // Check currency mismatch (fallback for non-multify or same-locale scenarios) 288 if (tcart && currency !== tcart.currency) { 289 console.warn( 290 `[Multify] Currency mismatch, clearing cart (expected: ${currency}, got: ${tcart.currency})`, 291 ) 292 293 clearCart() 294 295 if (options.reload) { 296 location.reload() 297 } 298 } else if (twishlist && currency !== twishlist.currency) { 299 console.warn( 300 `[Multify] Currency mismatch, clearing wishlist (expected: ${currency}, got: ${twishlist.currency})`, 301 ) 302 303 clearWishlist() 304 305 if (options.reload) { 306 location.reload() 307 } 308 } 309 } 310 311 window.addEventListener('storage', function (e) { 312 // Ignore locale key changes to prevent infinite loop between tabs 313 if (e.storageArea === localStorage && e.key !== LOCALE_KEY) { 314 console.debug('[Multify] LocalStorage changed, validating cart') 315 validate({ reload: false, updateLocale: false }) 316 } 317 }) 318 319 document.addEventListener('DOMContentLoaded', () => { 320 validate({ reload: true, updateLocale: true }) 321 }) 322})() 323</script>
323<style>.t744__price.force-right, 324.t744__price.force-left { 325 display: inline-flex; 326 margin-right: 0 !important; 327} 328 329.t744__price.force-right .t744__price-value { 330 order: 1; 331} 332 333.t744__price.force-right .t744__price-currency { 334 order: 2; 335 margin-left: 5px; 336} 337 338t-store__prod__price-portion 339 340.t754__price, 341.t754__price_old { 342 display: inline-flex; 343} 344 345.t754__price.force-right .t754__price-value, 346.t754__price_old.force-right .t754__price-value { 347 order: 1; 348} 349 350.t754__price.force-right .t754__price-currency, 351.t754__price_old.force-right .t754__price-currency { 352 order: 2; 353} 354 355.t754__price.force-left .t754__price-value, 356.t754__price_old.force-left .t754__price-value { 357 order: 2; 358} 359 360.t754__price.force-left .t754__price-currency, 361.t754__price_old.force-left .t754__price-currency { 362 order: 1; 363} 364 365.t754__price.force-left .t754__price-currency::before, 366.t754__price_old.force-left .t754__price-currency::before, 367.js-catalog-prod-price.force-left .t-catalog__prod-popup__price-currency::before, 368.js-catalog-prod-price-old.force-left .t-catalog__prod-popup__price-currency::before { 369 display: none !important; 370} 371 372.t754__price.force-left .t754__price-currency::after, 373.t754__price_old.force-left .t754__price-currency::after, 374.js-catalog-prod-price.force-left .t-catalog__prod-popup__price-currency::after, 375.js-catalog-prod-price-old.force-left .t-catalog__prod-popup__price-currency::after { 376 content: ' '; 377 white-space: pre; 378} 379 380.js-store-prod-price.force-right, 381.js-store-prod-price.force-left, 382.js-store-prod-price-old.force-right, 383.js-store-prod-price-old.force-left, 384.js-catalog-prod-price.force-right, 385.js-catalog-prod-price.force-left { 386 display: inline-flex; 387} 388 389.js-store-prod-price.force-right .t-store__prod-popup__price-value, 390.js-store-prod-price-old.force-right .t-store__prod-popup__price-value 391.js-catalog-prod-price.force-right .js-catalog-prod-price-val, 392.js-catalog-prod-price-old.force-right .js-catalog-prod-price-old-val { 393 order: 1; 394} 395 396.js-store-prod-price.force-right .t-store__prod-popup__price-currency, 397.js-store-prod-price-old.force-right .t-store__prod-popup__price-currency, 398.js-catalog-prod-price.force-right .t-catalog__prod-popup__price-currency, 399.js-catalog-prod-price-old.force-right .t-catalog__prod-popup__price-currency { 400 order: 2; 401} 402 403.js-store-prod-price.force-right .t-store__prod__price-portion, 404.js-store-prod-price-old.force-right .t-store__prod__price-portion { 405 order: 3; 406} 407 408.js-store-prod-price.force-left .t-store__prod-popup__price-value, 409.js-store-prod-price-old.force-left .t-store__prod-popup__price-value, 410.js-catalog-prod-price.force-left .js-catalog-prod-price-val, 411.js-catalog-prod-price-old.force-left .js-catalog-prod-price-old-val { 412 order: 2; 413} 414 415.js-store-prod-price.force-left .t-store__prod-popup__price-currency, 416.js-store-prod-price-old.force-left .t-store__prod-popup__price-currency, 417.js-catalog-prod-price.force-left .t-catalog__prod-popup__price-currency, 418.js-catalog-prod-price-old.force-left .t-catalog__prod-popup__price-currency { 419 order: 1; 420} 421 422.js-store-prod-price.force-left .t-store__prod-popup__price-currency::before, 423.js-store-prod-price-old.force-left .t-store__prod-popup__price-currency::before { 424 display: none !important; 425} 426 427.t762__price.force-right, 428.t762__price.force-left, 429.t762__price_old.force-right, 430.t762__price_old.force-left { 431 display: inline-flex; 432} 433 434.t762__price.force-right .t762__price-value, 435.t762__price_old.force-right .t762__price-value { 436 order: 1; 437} 438 439.t762__price.force-right .t762__price-currency, 440.t762__price_old.force-right .t762__price-currency { 441 order: 2; 442} 443 444.t762__price.force-left .t762__price-value, 445.t762__price_old.force-left .t762__price-value { 446 order: 2; 447} 448 449.t762__price.force-left .t762__price-currency, 450.t762__price_old.force-left .t762__price-currency, 451.t762__price.force-left .js-product-price-currency, 452.t762__price_old.force-left .js-product-price-currency { 453 order: 1; 454} 455 456.t762__price.force-left .t762__price-currency::before, 457.t762__price_old.force-left .t762__price-currency::before, 458.t762__price.force-left .js-product-price-currency::before, 459.t762__price_old.force-left .js-product-price-currency::before { 460 display: none !important; 461} 462 463.t762__price.force-left .t762__price-currency::after, 464.t762__price_old.force-left .t762__price-currency::after, 465.t762__price.force-left .js-product-price-currency::after, 466.t762__price_old.force-left .js-product-price-currency::after {
467 content: ' '; 468 white-space: pre; 469}</style>
469<script>; (async function () { 470 const upstream = 'https://spbtech.mtfy.xyz' 471 472 // Check if this is a Tilda members area authentication page 473 if ([ 474 '/members/login', 475 '/members/signup', 476 '/members/recover-password', 477 ].some(p => location.pathname.endsWith(p)) == false) { 478 return 479 } 480 481 function patchMembersLinks() { 482 // Strip protocol from upstream if present and use current protocol 483 const upstreamDomain = upstream.replace(/^https?:\/\//, '') 484 const protocol = window.location.protocol 485 const upstreamUrl = `${protocol}//${upstreamDomain}` 486 487 // if (!window.multify?.root) { 488 // return 489 // } 490 491 // const base = window.location.protocol + '//' + window.multify.root 492 493 // const prefix = window.multify?.prefix || '' 494 495 // Find all links to signup and recover-password pages with the prefix 496 const links = document.querySelectorAll( 497 [ 498 `a[href*="/members/signup"]`, 499 `a[href*="/members/recover-password"]`, 500 ].join(',') 501 ) 502 503 links.forEach((link) => { 504 if ( 505 !link.hasAttribute('href') || 506 link.getAttribute('href').includes(upstreamUrl) 507 ) { 508 return 509 } 510 511 const href = link.getAttribute('href') 512 // Extract just the page name (signup or recover-password) 513 const page = href.split('/members/')[1] 514 515 // Update href to absolute upstream URL 516 link.setAttribute('href', `${upstreamUrl}/members/${page}`) 517 518 // Remove click event listeners by cloning the node 519 const newLink = link.cloneNode(true) 520 link.parentNode.replaceChild(newLink, link) 521 522 // Remove data-route attribute (used by Tilda's router) 523 newLink.removeAttribute('data-route') 524 }) 525 } 526 527 // Patch links on initial load 528 document.addEventListener('DOMContentLoaded', () => { 529 patchMembersLinks() 530 }) 531 532 // Also patch links after a delay in case they're added dynamically 533 setTimeout(patchMembersLinks, 500) 534 535 // Watch for DOM changes and patch new links 536 const observer = new MutationObserver(() => { 537 patchMembersLinks() 538 }) 539 540 observer.observe(document.body, { 541 childList: true, 542 subtree: true, 543 }) 544})() 545</script>
545</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.