Connector Open-Source Code
Browse connector files locally. Exchange API keys stay on your device.
ui/common.php
<?php
// /opt/nuxvision_connector/ui/common.php
declare(strict_types=1);
if (session_status() !== PHP_SESSION_ACTIVE) {
$sessionLifetime = 172800; // 2 days
ini_set('session.gc_maxlifetime', (string)$sessionLifetime);
session_set_cookie_params([
'lifetime' => $sessionLifetime,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
/* =========================================================
PHP ERROR DISPLAY (DEV MODE)
========================================================= */
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
/* -------------------- paths / constants -------------------- */
$UI_DIR = __DIR__;
$BASE_DIR = realpath(__DIR__ . '/..') ?: (__DIR__ . '/..');
$INSTANCES_DIR = $BASE_DIR . '/instances';
/* -------------------- local auth -------------------- */
function nv_ui_auth_default_password(): string {
return 'nuxvision';
}
function nv_ui_auth_file_path(): string {
global $INSTANCES_DIR;
return rtrim((string)$INSTANCES_DIR, '/') . '/_ui_auth.php';
}
function nv_ui_auth_load_hash(): string {
$path = nv_ui_auth_file_path();
if (!is_file($path)) return '';
$data = @require $path;
if (!is_array($data)) return '';
$hash = trim((string)($data['password_hash'] ?? ''));
if ($hash === '') return '';
$info = password_get_info($hash);
if (empty($info['algo'])) return '';
return $hash;
}
function nv_ui_auth_verify(string $password): bool {
$hash = nv_ui_auth_load_hash();
if ($hash !== '') {
return password_verify($password, $hash);
}
return hash_equals(nv_ui_auth_default_password(), $password);
}
function nv_ui_auth_save_password(string $password, ?string &$error = null): bool {
$password = trim($password);
if ($password === '') {
$error = 'Password cannot be empty.';
return false;
}
$hash = password_hash($password, PASSWORD_DEFAULT);
if (!is_string($hash) || $hash === '') {
$error = 'Failed to generate password hash.';
return false;
}
$authFile = nv_ui_auth_file_path();
$authDir = dirname($authFile);
if (!is_dir($authDir) && !@mkdir($authDir, 0775, true)) {
$error = 'Failed to create auth directory.';
return false;
}
$payload = "<?php\n";
$payload .= "// generated by ui/common.php\n";
$payload .= "return [\n";
$payload .= " 'password_hash' => " . var_export($hash, true) . ",\n";
$payload .= " 'updated_at' => " . var_export(date('c'), true) . ",\n";
$payload .= "];\n";
$tmp = $authFile . '.tmp.' . getmypid() . '.' . mt_rand(1000, 9999);
if (@file_put_contents($tmp, $payload, LOCK_EX) === false) {
$error = 'Failed to write auth file.';
return false;
}
if (!@rename($tmp, $authFile)) {
@unlink($tmp);
$error = 'Failed to replace auth file.';
return false;
}
@chmod($authFile, 0640);
$error = null;
return true;
}
function nv_ui_is_logged_in(): bool {
return !empty($_SESSION['nv_ui_auth_ok']);
}
function nv_ui_mark_logged_in(): void {
@session_regenerate_id(true);
$_SESSION['nv_ui_auth_ok'] = 1;
}
function nv_ui_logout(): void {
unset($_SESSION['nv_ui_auth_ok']);
}
function nv_ui_require_login(): void {
$self = strtolower((string)basename((string)($_SERVER['SCRIPT_NAME'] ?? '')));
if (in_array($self, ['login.php', 'forgot_password.php'], true)) return;
if (nv_ui_is_logged_in()) return;
header('Location: /login.php');
exit;
}
nv_ui_require_login();
/* -------------------- HTML escaping -------------------- */
function h($s): string { return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function esc($s): string { return h($s); }
/* -------------------- misc helpers -------------------- */
function to_int($v, int $fallback = 0): int {
if (!is_numeric($v)) return $fallback;
return (int)$v;
}
function to_float($v, float $fallback = 0.0): float {
if (!is_numeric($v)) return $fallback;
return (float)$v;
}
function clamp_int($v, int $min, int $max, int $fallback): int {
if (!is_numeric($v)) return $fallback;
$n = (int)$v;
if ($n < $min) return $min;
if ($n > $max) return $max;
return $n;
}
function clamp_float($v, float $min, float $max, float $fallback): float {
if (!is_numeric($v)) return $fallback;
$n = (float)$v;
if ($n < $min) return $min;
if ($n > $max) return $max;
return $n;
}
function is_valid_url(string $url): bool {
$url = trim($url);
if ($url === '') return false;
return (bool)filter_var($url, FILTER_VALIDATE_URL);
}
function v(array $arr, string $path, $default = null) {
$cur = $arr;
foreach (explode('.', $path) as $k) {
if (!is_array($cur) || !array_key_exists($k, $cur)) return $default;
$cur = $cur[$k];
}
return $cur;
}
/* -------------------- wizard session -------------------- */
function wiz_get(): array {
$w = $_SESSION['wiz'] ?? [];
return is_array($w) ? $w : [];
}
function wiz_set(array $w): void {
$_SESSION['wiz'] = $w;
}
/* expose $wiz as a convenience for pages that expect it */
$wiz = wiz_get();
/* -------------------- instance selection in session -------------------- */
function nv_instance_id_from_session(): int {
$id = $_SESSION['instance_id'] ?? 0;
$id = is_numeric($id) ? (int)$id : 0;
return $id > 0 ? $id : 0;
}
function nv_set_instance_in_session(int $instanceId): void {
$_SESSION['instance_id'] = $instanceId > 0 ? $instanceId : 0;
}
/* -------------------- flash -------------------- */
function nv_flash_set(string $type, string $msg): void {
nv_toast_set($type, $msg);
}
function nv_flash_get(): ?array {
return null;
}
function nv_toast_normalize_type(string $type): string {
$t = strtolower(trim($type));
if (in_array($t, ['ok', 'success'], true)) return 'success';
if (in_array($t, ['err', 'error', 'danger'], true)) return 'danger';
if (in_array($t, ['warn', 'warning'], true)) return 'warning';
return 'info';
}
function nv_toast_set(string $type, string $msg, int $durationMs = 5000): void {
$msg = trim($msg);
if ($msg === '') return;
$durationMs = max(1500, min(12000, $durationMs));
$toast = [
'type' => nv_toast_normalize_type($type),
'msg' => $msg,
'duration' => $durationMs,
];
if (!isset($_SESSION['nv_toasts']) || !is_array($_SESSION['nv_toasts'])) {
$_SESSION['nv_toasts'] = [];
}
$_SESSION['nv_toasts'][] = $toast;
}
function nv_toast_take_all(): array {
$toasts = $_SESSION['nv_toasts'] ?? [];
unset($_SESSION['nv_toasts']);
return is_array($toasts) ? array_values($toasts) : [];
}
function nv_toast_collect_legacy(): void {
$legacy = $_SESSION['inst_toast'] ?? null;
if (!is_array($legacy)) return;
$msg = trim((string)($legacy['msg'] ?? ''));
if ($msg !== '') {
nv_toast_set((string)($legacy['type'] ?? 'info'), $msg, 5000);
}
unset($_SESSION['inst_toast']);
}
function render_flash(): void {
// Kept for backward compatibility: flashes now render as toasts in footer.
}
/* -------------------- UI chrome -------------------- */
function nv_current_step_from_subtitle(string $subtitle): int {
if (preg_match('~Step\s+(\d+)~i', $subtitle, $m)) {
return max(1, min(3, (int)$m[1]));
}
return 1;
}
function render_stepper(int $activeStep): void {
$steps = [
['label' => 'NuxVision', 'href' => './nuxvision.php'],
['label' => 'Exchange', 'href' => './exchange.php'],
['label' => 'Finish', 'href' => './finish.php'],
];
?>
<div class="nv-card">
<div class="nv-titlebar">
<strong>Setup steps</strong>
<span class="nv-muted">Step <?=h((string)$activeStep)?>/3</span>
</div>
<div class="nv-pad">
<div class="row g-2">
<?php foreach ($steps as $i => $st):
$n = $i + 1;
$cls = 'nv-step';
if ($n < $activeStep) $cls .= ' done';
if ($n === $activeStep) $cls .= ' active';
?>
<div class="col-12 col-md-4">
<div class="<?=h($cls)?>">
<span class="dot"><?=h((string)$n)?></span>
<div>
<?php if ($n <= $activeStep): ?>
<a href="<?=h($st['href'])?>"><?=h($st['label'])?></a>
<?php else: ?>
<span class="nv-muted2"><?=h($st['label'])?></span>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="nv-muted small mt-2">
Trading settings are managed on NuxVision. The connector fetches settings via API at runtime.
</div>
</div>
</div>
<?php
}
function render_header(string $title, string $subtitle = '', bool $showStepper = true, bool $fluid = false): void {
nv_toast_collect_legacy();
$step = nv_current_step_from_subtitle($subtitle);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?=h($title)?></title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
<style>
:root{
--bg:#070a16;
--bg-2:#0e152c;
--card:#11182d;
--card-2:#0f1629;
--text:#e9eefc;
--muted:#a8b4d4;
--muted-2:#95a1c0;
--line:rgba(156,182,255,.22);
--accent:#2f7bff;
--accent-2:#1f5cd3;
--ok:#22b07d;
--warn:#cf9c45;
--danger:#cf4d62;
--field-bg:rgba(8,14,30,.72);
--field-bd:rgba(156,182,255,.22);
--field-bd-focus:rgba(75,134,255,.78);
--ring:rgba(47,123,255,.30);
}
*{box-sizing:border-box}
body{
background:
radial-gradient(1200px 680px at 5% -10%, rgba(69,108,255,.22), transparent 60%),
radial-gradient(980px 540px at 100% 20%, rgba(35,95,220,.24), transparent 58%),
linear-gradient(180deg, #070a16 0%, #050813 100%);
color:var(--text);
min-height:100vh;
font-family:"Avenir Next","Segoe UI Variable","Segoe UI",sans-serif;
letter-spacing:.01em;
}
.nv-card{
background:linear-gradient(130deg, rgba(16,26,50,.86) 0%, rgba(13,19,36,.92) 100%);
border:1px solid var(--line);
border-radius:16px;
box-shadow:0 14px 40px rgba(0,0,0,.36), inset 0 1px 0 rgba(255,255,255,.05);
}
.nv-muted{color:var(--muted)!important}
.nv-muted2{color:var(--muted-2)!important}
.nv-titlebar{
display:flex;
align-items:center;
justify-content:space-between;
border-bottom:1px solid var(--line);
padding:.78rem 1rem
}
.nv-pad{padding:1rem}
.form-control,.form-select{
background:var(--field-bg)!important;
color:#eef3ff!important;
border:1px solid var(--field-bd)!important;
border-radius:12px
}
.form-control::placeholder{color:#92a1c5!important}
.form-control:focus,.form-select:focus{
border-color:var(--field-bd-focus)!important;
box-shadow:0 0 0 .2rem var(--ring)!important
}
.btn{
border-radius:12px;
border-width:1px;
transition:all .2s ease;
}
.btn-soft{
background:rgba(17,28,54,.84);
border:1px solid var(--line);
color:#eff3ff
}
.btn-soft:hover{
background:rgba(30,46,84,.96);
color:#fff;
border-color:rgba(145,171,245,.45)
}
.btn-accent{
background:linear-gradient(180deg,var(--accent),var(--accent-2));
color:#fff;
font-weight:700;
border:1px solid rgba(125,170,255,.45);
padding:.58rem 1.1rem;
box-shadow:0 10px 24px rgba(41,102,231,.35), inset 0 -1px 0 rgba(0,0,0,.28);
}
.btn-accent:hover{filter:brightness(1.08);transform:translateY(-1px)}
.nv-badge{
display:inline-flex;
align-items:center;
gap:.45rem;
background:rgba(45,99,210,.2);
border:1px solid rgba(110,151,247,.42);
color:#d8e7ff;
border-radius:999px;
padding:.28rem .62rem;
font-size:.85rem
}
.nv-step{
display:flex;
align-items:center;
gap:.62rem;
padding:.58rem .75rem;
border-radius:14px;
border:1px solid var(--line);
background:rgba(255,255,255,.02)
}
.nv-step .dot{
width:29px;
height:29px;
border-radius:999px;
display:inline-flex;
align-items:center;
justify-content:center;
border:1px solid rgba(157,185,255,.35);
background:rgba(10,16,34,.9);
color:#f4f7ff;
font-weight:700
}
.nv-step.active{
border-color:rgba(91,148,255,.55);
background:rgba(34,75,170,.18)
}
.nv-step.done{
border-color:rgba(59,181,129,.35);
background:rgba(41,147,104,.14)
}
.nv-step a{color:inherit;text-decoration:none}
.nv-step a:hover{text-decoration:underline}
hr.nv-soft{border-color:var(--line)}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;}
.tile{
border:1px solid var(--line);
border-radius:16px;
background:linear-gradient(140deg, rgba(18,26,49,.8), rgba(13,19,36,.9));
padding:16px
}
.smallhint{font-size:.9rem;color:var(--muted)}
.badge-soft{
background:rgba(38,50,84,.76)!important;
border:1px solid rgba(146,170,240,.26)!important
}
.howitworks-box{
border:1px solid var(--line);
border-radius:14px;
background:rgba(13,22,44,.58);
padding:14px
}
.howitworks-link{color:#d9e6ff;text-decoration:none}
.howitworks-link:hover{text-decoration:underline}
details summary{list-style:none}
details summary::-webkit-details-marker{display:none}
.nv-account-actions{
display:flex;
justify-content:flex-end;
gap:.5rem;
margin-bottom:.8rem;
}
.nv-account-actions .btn{
padding:.35rem .72rem;
font-size:.86rem;
}
#nv-toast-stack{
position:fixed;
right:20px;
top:20px;
z-index:1080;
display:flex;
flex-direction:column;
gap:12px;
pointer-events:none;
max-width:min(520px, calc(100vw - 24px));
}
.nv-toast{
pointer-events:auto;
border-radius:16px;
border:2px solid rgba(170,200,255,.55);
background:linear-gradient(155deg, rgba(26,44,83,.98), rgba(16,28,56,.98));
color:#f6f9ff;
box-shadow:0 18px 42px rgba(0,0,0,.48), inset 0 1px 0 rgba(255,255,255,.14);
padding:.9rem 1rem;
transform:translateY(-10px) scale(.98);
opacity:0;
transition:opacity .24s ease, transform .24s ease;
backdrop-filter:blur(2px);
}
.nv-toast.show{opacity:1;transform:translateY(0) scale(1)}
.nv-toast .rowx{display:flex;gap:.7rem;align-items:flex-start}
.nv-toast .icon{
width:28px;height:28px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
background:rgba(255,255,255,.16);flex:0 0 28px;font-size:1rem;
}
.nv-toast .msg{font-size:1rem;line-height:1.35;font-weight:600}
.nv-toast.success{
border-color:rgba(100,231,172,.75);
box-shadow:0 18px 42px rgba(0,0,0,.48), 0 0 0 1px rgba(104,235,176,.22), inset 0 1px 0 rgba(255,255,255,.14);
}
.nv-toast.success .icon{background:rgba(55,198,131,.3);color:#8ff1c4}
.nv-toast.danger{
border-color:rgba(255,130,151,.82);
box-shadow:0 18px 42px rgba(0,0,0,.48), 0 0 0 1px rgba(255,122,145,.22), inset 0 1px 0 rgba(255,255,255,.14);
}
.nv-toast.danger .icon{background:rgba(234,92,118,.33);color:#ffc0cc}
.nv-toast.warning{
border-color:rgba(255,198,101,.82);
box-shadow:0 18px 42px rgba(0,0,0,.48), 0 0 0 1px rgba(255,198,101,.2), inset 0 1px 0 rgba(255,255,255,.14);
}
.nv-toast.warning .icon{background:rgba(221,166,74,.33);color:#ffe1a8}
</style>
</head>
<body>
<main class="<?= $fluid ? 'container-fluid' : 'container' ?> my-4">
<?php if (nv_ui_is_logged_in()): ?>
<div class="nv-account-actions">
<a class="btn btn-soft btn-sm" href="/account.php#security"><i class="bi bi-shield-lock me-1"></i>Change password</a>
<a class="btn btn-soft btn-sm" href="/login.php?action=logout"><i class="bi bi-box-arrow-right me-1"></i>Log out</a>
</div>
<?php endif; ?>
<?php if ($showStepper): ?>
<?php render_stepper($step); ?>
<div class="mt-3">
<?php else: ?>
<div class="mt-2">
<?php endif; ?>
<?php
}
function render_footer(): void {
$toasts = nv_toast_take_all();
?>
</div>
<div class="mt-4 nv-muted small">
<hr class="nv-soft">
<div>NuxVision Connector UI - https://nuxvision.com/</div>
</div>
</main>
<div id="nv-toast-stack"></div>
<?php if (is_file(__DIR__ . '/js/bootstrap.bundle.min.js')): ?>
<script src="/js/bootstrap.bundle.min.js"></script>
<?php endif; ?>
<script>
(function () {
var initialToasts = <?=json_encode($toasts, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)?>;
var stack = document.getElementById('nv-toast-stack');
if (!stack) return;
var iconByType = {
success: 'bi-check2-circle',
danger: 'bi-exclamation-triangle',
warning: 'bi-exclamation-circle',
info: 'bi-info-circle'
};
function normalizeType(type) {
if (type === 'success' || type === 'danger' || type === 'warning' || type === 'info') return type;
return 'info';
}
function showToast(type, message, duration) {
var msg = (message || '').toString().trim();
if (!msg) return;
var t = normalizeType((type || '').toString());
var ms = parseInt(duration, 10);
if (!Number.isFinite(ms)) ms = 5000;
if (ms < 1500) ms = 1500;
if (ms > 12000) ms = 12000;
var toast = document.createElement('div');
toast.className = 'nv-toast ' + t;
toast.innerHTML = '<div class="rowx"><span class="icon"><i class="bi ' + iconByType[t] + '"></i></span><div class="msg"></div></div>';
toast.querySelector('.msg').textContent = msg;
stack.appendChild(toast);
requestAnimationFrame(function () { toast.classList.add('show'); });
window.setTimeout(function () {
toast.classList.remove('show');
window.setTimeout(function () {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 260);
}, ms);
}
window.nvShowToast = showToast;
if (Array.isArray(initialToasts)) {
initialToasts.forEach(function (t) {
if (!t || typeof t !== 'object') return;
showToast((t.type || 'info').toString(), (t.msg || '').toString(), t.duration || 5000);
});
}
})();
</script>
</body>
</html>
<?php
}
/* -------------------- instances helpers -------------------- */
function nv_instance_dir(int $instanceId): string {
global $INSTANCES_DIR;
return rtrim($INSTANCES_DIR, '/') . '/' . $instanceId;
}
function nv_instance_config_path(int $instanceId): string {
return nv_instance_dir($instanceId) . '/config.php';
}
function nv_instance_default_log_path(int $instanceId): string {
// runner.php default: instances/<id>/runner.log
return nv_instance_dir($instanceId) . '/runner.log';
}
function nv_delete_instance_dir(int $instanceId): array {
$dir = nv_instance_dir($instanceId);
if (!is_dir($dir)) return ['ok'=>true];
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $f) {
$p = $f->getPathname();
if ($f->isDir()) @rmdir($p);
else @unlink($p);
}
$ok = @rmdir($dir);
return $ok ? ['ok'=>true] : ['ok'=>false,'error'=>'rmdir_failed'];
}
/* -------------------- HTTP / curl -------------------- */
function curl_json(string $url, array $headers = [], int $timeout = 6): array {
$ch = curl_init();
if (!$ch) return [0, 'curl_init_failed', null, null];
$hdr = [];
foreach ($headers as $k => $v) {
if (is_int($k)) $hdr[] = (string)$v;
else $hdr[] = (string)$k . ': ' . (string)$v;
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => min(3, $timeout),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_HTTPHEADER => $hdr,
CURLOPT_USERAGENT => 'nuxvision-connector-ui/1.0',
]);
$body = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = null;
if (is_string($body)) {
$tmp = json_decode($body, true);
if (is_array($tmp)) $json = $tmp;
}
return [$code, $err ?: '', $body, $json];
}
/* -------------------- NuxVision API (UI usage only) -------------------- */
function nv_auth_headers(string $apiKey): array {
// adapte si ton backend attend un header différent
return [
'Accept' => 'application/json',
'X-API-KEY' => $apiKey,
];
}
function nv_fetch_instances(string $baseUrl, string $apiKey, int $timeout = 6): array {
$baseUrl = rtrim(trim($baseUrl), '/');
$url = $baseUrl . '/api/nuxvision/v1/instance_list.php';
[$code, $err, $body, $json] = curl_json($url, nv_auth_headers($apiKey), $timeout);
if ($err !== '' || $code < 200 || $code >= 400 || !is_array($json)) {
return ['ok'=>false,'http'=>$code,'curl_error'=>$err,'raw'=>is_string($body)?substr($body,0,200):null];
}
// attend { ok: true, instances: [...] } ou { instances: [...] }
$instances = [];
if (isset($json['instances']) && is_array($json['instances'])) $instances = $json['instances'];
elseif (isset($json['data']) && is_array($json['data'])) $instances = $json['data'];
return ['ok'=>true,'http'=>$code,'instances'=>$instances,'raw'=>null];
}
function nv_heartbeat(string $baseUrl, string $apiKey, int $instanceId, int $timeout = 6): array {
$baseUrl = rtrim(trim($baseUrl), '/');
$url = $baseUrl . '/api/nuxvision/v1/heartbeat.php?instance_id=' . urlencode((string)$instanceId);
[$code, $err, $body, $json] = curl_json($url, nv_auth_headers($apiKey), $timeout);
if ($err !== '' || $code < 200 || $code >= 400 || !is_array($json)) {
return ['ok'=>false,'http'=>$code,'curl_error'=>$err,'raw'=>is_string($body)?substr($body,0,200):null];
}
$ok = !empty($json['ok']) || (isset($json['status']) && $json['status'] === 'ok');
return ['ok'=>$ok,'http'=>$code,'json'=>$json,'raw'=>null];
}