Обновить
256K+

JavaScript *

Прототипно-ориентированный язык программирования

120,2
Рейтинг
Сначала показывать
Порог рейтинга

Bases Board | GitHub

Недавно в Obsidian появился встроенный канбан для Bases, но мой плагин лучше :)

На скриншоте пример доски с исследовательскими экспериментами. Колонки соответствуют стадиям работы, строки разделяют эксперименты по направлениям. Таким же образом можно разложить свои проекты по статусам и областям деятельности.

Основное удобство в том, что заметки можно перемещать между колонками и строками, сразу меняя их свойства. Эдакая очень структурированная доска.

Из основных возможностей:

  • обложки, иконки и цвета

  • заголовки колонок и подгрупп остаются видны при прокрутке

  • несколько размеров карточек

  • копируемые по клику ID, чтобы скармливать их агентам

  • отдельные шаблоны новых заметок для каждой доски

В моём хранилище плагин используется почти во всех базах, но больше всего помогает в работе с проектными задачами.

Забавный факт. Изначально это был форк другого плагина. Я отправил автору 6 пулл-реквестов, чтобы исправить ошибки и добавить новые возможности. Но, как это нередко случается, автор забросил плагин.

Решил, что грех такому сокровищу пропадать, и продолжил развивать его как отдельный плагин.

Плагин доступен в Community store по названию Bases Board.

Теги:
+3
Комментарии0

Почему setTimeout в React видит старое значение state

Введите в поле «Первый», нажмите кнопку, а затем, пока таймер не сработал, замените текст на «Второй»:

import { useState } from "react";

export default function Demo() {
  const [text, setText] = useState("");

  function handleClick() {
    setTimeout(() => {
      console.log(text);
    }, 3000);
  }

  return (
    <>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button onClick={handleClick}>
        Вывести через 3 секунды
      </button>
    </>
  );
}

В консоли появится «Первый», хотя в поле уже «Второй».

React здесь успел обновить состояние. Но при каждом рендере функция компонента вызывается заново — со своим значением text и новыми обработчиками. Колбэк уже запущенного таймера сохраняет через замыкание доступ к text из предыдущего рендера.

Новый рендер обновит поле на экране, но не заменит колбэк внутри этого таймера.

А какое значение нам нужно?

Если пользователь зафиксировал текст для отложенной отправки, значение на момент клика может быть правильным. Последующие правки не обязательно должны менять уже запланированное сообщение.

Если же нужно прочитать последнее введённое значение на момент срабатывания таймера, один из вариантов — useRef.

Добавим useRef в импорт:

import { useRef, useState } from "react";

Внутри компонента создадим ref:

const latestText = useRef("");

Обновим обработчик поля:

onChange={(e) => {
  const value = e.target.value;
  latestText.current = value;
  setText(value);
}}

А внутри колбэка таймера заменим чтение text:

console.log(latestText.current);

Теперь колбэк обращается к объекту ref, который сохраняется между рендерами, и читает его текущее свойство current.

Сам ref за состоянием не следит: в этом примере мы обновляем его при вводе. text по-прежнему нужен для отображения значения в поле — изменение ref.current не вызывает рендер.

Получается, «старое значение» не всегда означает ошибку. Сначала стоит определить, что должно использовать отложенное действие: текст на момент клика или текст на момент выполнения.

Поделитесь, должны ли последующие правки в вашей форме «Отправить позже» менять уже запланированное сообщение?

Теги:
0
Комментарии0

Tabbed | GitHub

Пора бы уже рассказать о своих новых плагинах для Obsidian. Начну с любимого Tabbed.

Основная его мощь в том, что он позволяет уплотнить информацию внутри одной заметки с помощью вкладок. Это очень помогает, когда нужно на одном экране сразу разместить и задачи, и детали, и заметки, и оглавление.

На скриншоте вымышленная археологическая экспедиция. Обратите внимание на то, как можно интересно скомпоновать контент внутри заметки с помощью вкладок и колонок.

Работает это просто. Делаете блок кода и помещаете внутрь контент:

~~~tabs
tab: Вкладка 1
Контент

tab: Вкладка 2
Контент
~~~

Можно размещать контент в колонках и сочетать их с вкладками:

~~~columns
column: Колонка 1
Контент

column: Колонка 2
Контент
~~~

Особое внимание я уделил производительности. Cодержимое вкладки загружается при открытии и выгружается, когда вы переключаетесь на другую. Это особенно полезно для сложных Dataview-запросов и таблиц Bases.

Устанавливается из Community plugins по названию Tabbed.

Сам я очень интенсивно использую Tabbed в Abyss Vault. Кто пользуется хранилищем, уже видел, сколько всего можно собрать на одной странице :)

Теги:
+2
Комментарии0

React 19: почему return { error } не предотвращает очистку формы

Форма показывает ошибку сохранения, но введённый текст уже исчез. Такое возможно при использовании функции в action: после её нормального завершения React сбрасывает неконтролируемые поля.

Минимальный пример без сервера — обработчик намеренно всегда возвращает ошибку:

import { useActionState } from "react";

export default function Form() {
  const [state, action] = useActionState(
    async () => ({ error: "Сохранение отклонено" }),
    { error: "" }
  );

  return (
    <form action={action}>
      <label>
        Сообщение
        <textarea name="message" defaultValue="" />
      </label>
      <button type="submit">Сохранить</button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

Введите текст и отправьте форму. Сообщение об ошибке появится, а поле вернётся к пустому defaultValue. Функция завершилась без исключения; свойство error для React — обычные данные, а не указание сохранить содержимое формы.

Один из способов оставить текст для исправления — контролируемое поле с value и onChange. Его состояние можно очищать только после подтверждённого сохранения. Наличие state.error само по себе значениями полей не управляет.

Бросать исключение только ради предотвращения сброса — другая крайность: оно передаёт обработку ближайшему Error Boundary и не заменяет управление черновиком.
Показать ошибку — только половина задачи. Пользователь должен получить возможность исправить уже написанное, поэтому поведение полей при отказе стоит проверять отдельно от сообщения об ошибке.

Итак, вопрос: в ваших тестах формы проверяется, что введённый текст остаётся на месте после неудачного сохранения?

Теги:
0
Комментарии0

Удалили строку в React, а её заметка осталась у другого пользователя

Представим: в списке два человека, у каждого поле для заметки. Вводим «Позвонить вечером» рядом с Анной, удаляем Анну — и видим ту же заметку у Бориса.

Данные людей не перепутаны. Проблема в том, как React сопоставляет строки между рендерами.

Вот компонент, в котором ошибку можно воспроизвести:

import { useState } from "react";

function Row({ person }) {
  const [note, setNote] = useState("");

  return (
    <label>
      {person.name}
      <input
        value={note}
        onChange={event => setNote(event.target.value)}
      />
    </label>
  );
}

export default function App() {
  const [people, setPeople] = useState([
    { id: "anna", name: "Анна" },
    { id: "boris", name: "Борис" }
  ]);

  return (
    <div>
      <button onClick={() => setPeople(p => p.slice(1))}>
        Удалить первого
      </button>
      {people.map((person, index) => (
        <Row key={index} person={person} />
      ))}
    </div>
  );
}

До удаления ключ 0 принадлежит строке Анны. После удаления ключ 0 получает строка Бориса. Для React это тот же компонент на прежнем месте с тем же ключом, поэтому его состояние note сохраняется. При этом person уже содержит Бориса.

Для этого списка исправление выглядит так:

{people.map(person => (
  <Row key={person.id} person={person} />
))}

Теперь у Бориса остаётся собственный ключ boris. При удалении Анны её компонент размонтируется, а состояние строки Бориса сохранится.

Ключ должен быть стабильным для одной записи и уникальным среди соседних элементов. Случайный ключ на каждом рендере тоже создаёт проблему: React будет пересоздавать компоненты, сбрасывая их состояние. Индекс не приводит к ошибке в любом списке, но для списка с удалением, вставкой или перестановкой записей он не сохраняет связь состояния с записью.

Проверить исправление можно в обе стороны: заметка Анны исчезает вместе с Анной, а заметка Бориса остаётся у Бориса после удаления предыдущей строки. Затем стоит повторить проверку с сортировкой списка.

Есть и отдельное ограничение: правильный key не хранит черновик после размонтирования. Если фильтр скрывает Бориса, удаляя его Row из дерева, при возвращении строки локальная заметка начнётся заново. Для сохранения таких черновиков состояние нужно поднять выше и хранить по person.id.

Если при сортировке или удалении элементы будто обмениваются введёнными значениями, стоит проверить ключи до того, как искать ошибку в обработчиках формы.

Теги:
+2
Комментарии0

Таймер работы и отдыха кажется простой задачей, пока не начинаешь хранить его состояние. Я сделал для себя Android-приложение на React Native + Expo и TypeScript. Оно делит рабочий день, например с 9:00 до 18:00, на отрезки работы и отдыха и напоминает о перерывах. Режимов пять: настройка, ожидание начала дня, работа, отдых и окончание дня. На его примере покажу три способа описания состояния, их сильные и слабые стороны. Полный код — на GitHub.

1. Флаги

const [isRunning, setIsRunning] = useState(false);
const [isResting, setIsResting] = useState(false);
const [isFinished, setIsFinished] = useState(false);
  • Три флага дают 8 комбинаций, половина из которых не имеет смысла. Что значит isRunning и isResting одновременно?

  • Правила переходов размазаны по обработчикам: уходя на отдых, нужно не забыть сбросить isRunning.

  • Экран выбирается цепочкой if, и спорную комбинацию решает порядок проверок.

2. Конечный автомат на поле status

type TState = {
  status: 'setup' | 'pending' | 'work' | 'rest' | 'finished';
  dayEndsAt?: number;
  restStartedAt?: number;
  restType?: 'short' | 'long';
};

function reduce(state: TState, event: TEvent): TState {
  switch (state.status) {
    // case 'setup': ... case 'finished': ...
    default: {
      const _exhaustive: never = state.status;
      return state;
    }
  }
}
  • Все режимы перечислены в типе: опечатку 'wrok' или режим, которого нет в списке, компилятор не пропустит.

  • Проверка never в default: если добавить в тип новый режим и забыть обработать его в редьюсере, проект не соберётся.

  • Переходы собраны в одном месте, а всё состояние — один объект, который легко сохранить и восстановить после выгрузки приложения.

Но данные всех режимов лежат в этом объекте необязательными полями.

  • Внутри case 'rest' вид отдыха всё равно может быть undefined, хотя у отдыха он обязан быть всегда, и код обрастает ! и ?..

  • Компилятор пропустит { status: 'work', restType: 'long' }, хотя у работы нет вида отдыха: данные никак не привязаны к режиму.

3. Конечный автомат + размеченное объединение

type TSetup = { state: 'setup' };

type TWork = {
  state: 'work';
  dayEndsAt: number;
  workStartedAt: number;
};

type TRest = {
  state: 'rest';
  dayEndsAt: number;
  restStartedAt: number;
  restType: 'short' | 'long';
};

// TPending и TFinished — аналогично
type TSnapshot = TSetup | TPending | TWork | TRest | TFinished;

Каждый режим описан своим типом со своими полями и общей меткой state. В настройке нет ни конца дня, ни отдыха, а restType и restStartedAt есть только у отдыха.

  • После проверки state TypeScript точно знает, какие поля доступны, и ! с ?. не нужны.

  • «Отдых без времени начала» невозможно даже собрать.

  • Если при переходе забыть перенести поле, это будет ошибка компиляции, а не undefined в рантайме.

  • Цена — многословность: каждый переход заново собирает объект со всеми полями нового режима.

Способы применения

  • Флаги подходят, когда они независимы, например, звук и полноэкранный режим в плеере друг другу не мешают.

  • Поля status без объединения хватит, когда у режимов нет собственных данных.

  • Размеченное объединение нужно, когда у каждого режима свои поля.

XState или свой редьюсер

Для пяти плоских режимов хватает своего редьюсера без зависимостей. Если появятся вложенные или параллельные состояния, отложенные переходы или понадобится визуализация, стоит смотреть на XState.

Технически в приложении расширенный конечный автомат (EFSM). Кроме режимов, он хранит переменные: границы дня, начало текущего отрезка и счётчик рабочих отрезков. На переходах у него есть условия (начать работу можно, только если день ещё не закончился), а сами переходы обновляют переменные. Без переменных пришлось бы плодить состояния WORK_1, WORK_2…

Подробнее:

Теги:
+1
Комментарии0

Поиск в React показывает старый результат. Запросы отправлены правильно, ответы пришли не по порядку

Представьте: пользователь ищет «кот», затем дописывает «котёл». Первый запрос выполняется 1,5 секунды, второй — 100 мс. Сначала появляются результаты для «котёл», затем запоздавший ответ заменяет их результатами для «кот».

Оба запроса успешны. Ошибка в том, что любой ответ получает право обновить экран.

Debounce уменьшает число запросов, но не гарантирует порядок ответов. Уже отправленный медленный запрос всё равно может завершиться последним. Для простого компонента можно совместить отмену запроса с проверкой актуальности его результата.
Ниже хук: query — строка поиска, /api/search — ваш endpoint, возвращающий JSON.

import { useEffect, useState } from "react";

export function useSearch(query) {
  const [state, setState] = useState({ status: "loading" });

  useEffect(() => {
    let active = true;
    const controller = new AbortController();
    setState({ status: "loading" });

    async function load() {
      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(query)}`,
          { signal: controller.signal }
        );
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const items = await response.json();
        if (active) setState({ status: "ready", items });
      } catch (error) {
        if (active && error.name !== "AbortError") {
          setState({ status: "error", message: error.message });
        }
      }
    }

    load();
    return () => {
      active = false;
      controller.abort();
    };
  }, [query]);

  return state;
}

У каждого запуска эффекта свой active. При изменении query React очищает предыдущий эффект: старый результат больше не должен менять состояние. Та же проверка стоит в обработке ошибок, иначе запоздавшая ошибка первого запроса может закрыть уже загруженные результаты второго.

AbortController позволяет прекратить ненужный fetch и чтение ответа. Флаг дополнительно защищает обновление состояния. При этом отмена на клиенте не откатывает работу, которую сервер уже успел выполнить.

Для проверки стоит специально задержать первый ответ, быстро изменить запрос и убедиться, что на экране остаются данные второго. Отдельный сценарий — первый запрос завершается ошибкой уже после успешного второго. Механика очистки разобрана в документации React, отмена запроса — в MDN.

Теги:
+2
Комментарии0

OpenAI выпустила скилл PHP Version Checker для Codex, который помогает оперативно проверять сайты на уязвимости PHP2Shell в PHP (CVE-2026-109618 и CVE-2026-109619), которые потенциально могли привести к несанкционированному RCE:

  • нужно Codex ссылку на ресурс, ИИ прогонит скрипт, проверит версию языка PHP и подскажет, требуется ли срочное обновление.

  • также ИИ-агент может сразу поставить патч и защитить сайт на PHP от угрозы взлома.

Уязвимости CVE-2026-109618 и CVE-2026-109619 связаны с разными проблемами при работе с памятью. Первая связана с нехваткой памяти: при очистке запроса программа может обратиться к некорректным данным, что способно привести к удалённому выполнению кода. При обработке последующих запросов программа может обратиться к устаревшим данным до полной инициализации состояния, что тоже грозит удалённым выполнением кода.

Команда безопасности PHP выпустила корректирующие выпуски PHP 8.5.12, 8.4.27, 8.3.36 и 8.2.35, а также патчи для уже не поддерживаемых веток PHP 7.4, PHP 8.0 и PHP 8.1, в которых исправлены критические уязвимости. В команде PHP призвали администраторов установить обновления. Заявлено, что полное блокирование уязвимостей возможно только через установку обновления.

Теги:
0
Комментарии0

Делюсь опытом вайбкодинга Работа в Antigravity, Open Code, Windsurf

Сразу небольшая ремарка -

Всем ребятам которые когда-либо создавали сайт или приложение с помощью ИИ или агента, хочу сказать что опыт у всех разный , разные модели, взаимодействие, а также склад ума, мышления, понимая конечной цели и желаний, также различен, поэтому прошу не судить строго :)

Моё знакомство с искусственным интеллектом началось примерно в 2022 году, во время пандемии, когда много предприятий закрывалось и было скучно, особо не куда не выйдешь погулять или на природу, появился ChatGPT , я кинулся регаться в OpenAi , используя сервис виртуальных номеров той страны , какой включаешь ВПН, это получилось у меня только на 3 Гугл аккаунте Первые вопросы которые я ему задавал были простыми, но его ответы уже на тот момент мне казались пугающими. Тогда ещё можно было настроить прямо в чате температуру, где больше это значило модель может лгать, меньше или 0 модель всегда отвечала как есть.

В то время мне был очень интересен язык программирования javascript , и я кинулся его изучать, прошёл один бесплатный курс на стёпике и один платный, небольшой, на одной из известных площадок, по видео с Ютуба попробовал сделать мини-апп, но до конца это у меня не вышло, потому что замах был слишком большим, тогда я ещё не понимал что мини-апп этот - целая CRM, работающая на Sanity а не просто приложение. Всё что у меня получилось это 4 работающих страницы и возможность через crm добавлять блоки контента, понимая что наполнять их в одиночку, у меня не будет ни сил не времени, а также не было материальной мотивации, я это дело забросил на три года. У меня родился старший сын, были обычные бытовые заботы, в это время я мало следил за развитием ИИ , так как на тот момент мне казалось что нужно всё таки нужно больше гуглить, серфить stackoverflow, а хотелось максимум автоматизации из-за чувства что не хочу проводить свою жизнь за компьютером.

На тот момент я работал менеджером на ПВЗ Wildberries, по деньгам у меня выходило 5-6 тыс р. а в хорошие дни и 8-9, для моего небольшого города это отлично, с учётом графика 2/2

Но, я как любой другой отец задумался о будущем своего, тогда ещё одного ребёнка, хотел увеличить свой доход

А так как ИИ модели очень продвинулись, я недолго думаю кинулся создавать своё приложение 🐱.

Показывать свой гитхаб я не буду, так как там одни вайбкодинг приложения и нет ничего ценного, но хочу поделиться опытом и ошибками которые я совершал

Вообще ИИ уже очень сильно продвинулся, он мог предложить модули регистрации/авторизации через Гугл/firebase , Яндекс или ВК, или простую почту, также сделать простую метрику кто откуда зашёл, какой тариф купил и так далее, это было всё организованно через ТГ айди пользователя, которое синхронизировалось и при обычном создании аккаунта при регистрации через почту. Что я тогда понял при создании этого приложения?

А проблем была куча :

  • он мог постоянно генерить новые или менять куски функций, или блоки

  • если ему не объяснить про безопасность , sql, аудит и gitignore, а также МД файлы, он тупо пушил их кучей в гитхаб те инструкции которые ты ему писал

  • он хардкодил css

  • не правильно поставленная задача или промт часто приводила к переделке какого-либо участка кода

  • переключение на другую модель могло усугубить и добавить абракадабры в код

Итого: ИИ до сих пор не совершенен, и если даже даже мастера сеньоры ошибаются, то что говорить об ИИ?

Кому интересно - Продолжение будет в ч.2

Теги:
-2
Комментарии9

🏦 Портфель альткоины/золото с автоматической балансировкой

Идея следующая: когда растет Bitcoin, растут альты. Золото растет независимо от Bitcoin.

Исходя от дней роста Bitcoin подряд, смещать распределение портфеля из золота в криптовалюты. Криптовалюты покупать в равной доле независимо от объема торгов, так как все коррелируют с биткоином. При этом не держать в USDT ничего совсем, чтобы избежать инфляции

Закрытие кассы и ребалансировку делаем раз в неделю

import {
  addLauncherSchema,
  addStrategySchema,
  Cache,
  getCandles,
  listenDoneBacktest,
  Position,
  setConfig,
} from "backtest-kit";

setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100, // иначе валидатор не пропустит технический стоп -50%
});

// Депозит делится поровну между тикерами корзины; доля депозита вне корзины паркуется в золото
const DEPOSIT_USD = 1000;
const PARKING_SYMBOL = "PAXGUSDT";

// Позиция живёт ровно неделю и закрывается по времени; перезаход на первом тике после закрытия
const HOLD_MINUTES = 7 * 24 * 60;

// Трендовый гейт по BTC: доля окон 20..45 дней с положительной доходностью BTC. Это экспозиция
// корзины от 0 до 1; остаток уходит в PARKING_SYMBOL. Ансамбль окон вместо одного N: одиночные
// окна за пределами 25..45 дней заметно хуже, внутри плато результат от выбора N не зависит.
const GATE_SYMBOL = "BTCUSDT";
const GATE_WINDOWS_DAYS = [20, 25, 30, 35, 40, 45];

// Технические уровни: за неделю на споте без плеча недостижимы, штатный выход только по времени
const STOP_LOSS_PERCENT = 50;
const TAKE_PROFIT_PERCENT = 500;

// BASKET:BEGIN
// Сентябрь 2020: топ-20 USDT-пар спота Binance по обороту за август 2020 (известен до начала месяца),
// листинг не моложе 90 дней, без стейблкоинов, плечевых токенов и обёрток BTC/ETH.
const BASKET = [
  "BTCUSDT",
  "ETHUSDT",
  "LINKUSDT",
  "XRPUSDT",
  "BNBUSDT",
  "EOSUSDT",
  "LTCUSDT",
  "TRXUSDT",
  "BCHUSDT",
  "BANDUSDT",
  "OMGUSDT",
  "XTZUSDT",
  "ADAUSDT",
  "ATOMUSDT",
  "VETUSDT",
  "LENDUSDT",
  "ZECUSDT",
  "WAVESUSDT",
  "QTUMUSDT",
  "NEOUSDT",
];
// BASKET:END

const SYMBOL_LIST = [...BASKET, PARKING_SYMBOL];
const PER_SYMBOL_USD = DEPOSIT_USD / BASKET.length;

// getCandles внутри getSignal обрезан по времени движка: последняя дневная свеча - последний
// закрытый день, заглядывания вперёд нет. Cache.fn с интервалом 1d считает гейт один раз
// в день на все тикеры и сам инвалидируется при переходе движка на следующий день.
const getGate = Cache.fn(async (): Promise<number> => {
  const candles = await getCandles(GATE_SYMBOL, "1d", Math.max(...GATE_WINDOWS_DAYS) + 1);
  const closes = candles.map(({ close }) => close);
  const last = closes[closes.length - 1];
  const up = GATE_WINDOWS_DAYS.filter((days) => last > closes[closes.length - 1 - days]).length;
  return up / GATE_WINDOWS_DAYS.length;
}, { interval: "1d" });

addStrategySchema({
  strategyName: "index_fund",
  interval: "1h",
  getSignal: async (symbol, when, currentPrice) => {

    const gate = await getGate();

    const cost = symbol === PARKING_SYMBOL
      ? DEPOSIT_USD * (1 - gate)
      : PER_SYMBOL_USD * gate;

    // cost: 0 движок молча заменяет на CC_POSITION_ENTRY_COST, поэтому нулевая доля - это отсутствие сигнала
    if (cost < 1) {
      return null;
    }

    return {
      id: `${symbol.toLowerCase()}-${when.toISOString().slice(0, 10)}`,
      symbol,
      ...Position.bracket({
        position: "long",
        currentPrice,
        percentStopLoss: STOP_LOSS_PERCENT,
        percentTakeProfit: TAKE_PROFIT_PERCENT,
      }),
      cost: Math.round(cost * 100) / 100,
      minuteEstimatedTime: HOLD_MINUTES,
      payload: { gate, weight: cost / DEPOSIT_USD, basketSize: BASKET.length },
      note: `${symbol} gate=${gate.toFixed(2)} cost=$${cost.toFixed(2)}`,
    };
  },
});

addLauncherSchema({
  launcherName: "main_launcher",
  backtest: true,
  symbolList: SYMBOL_LIST,
  cache: false,
})
Теги:
+3
Комментарии3

Сделал промо-ролик для своего инструмента — и собрал его в Remotion.

Была идея не рисовать интерфейс, а взять уже готовый в Viaduct. Headless Chrome открывает приложение и снимает каждый экран тремя слоями — пустой холст, только связи, полный кадр — и заодно выгружает координаты всех элементов в JSON. Дальше карточки вырезаются из скриншота по этим координатам и выезжают по одной, камера наезжает на нужный элемент, уровни перетекают друг в друга.

Что это даёт: поменялся интерфейс — npm run capture && npm run render, и ролик снова актуален. Нужна локализация — перевести один файл с текстами. Нужна вертикальная версия для Shorts — та же композиция другого размера.

76 секунд, рендер около четырёх минут.

Ролик про Viaduct — браузерный редактор архитектуры по модели C4 с MCP-сервером для кодовых агентов: https://c4.quietgridlabs.com

Если интересно, как устроено внутри — нарезка озвучки по паузам, приглушение музыки под голосом, грабли с headless Chrome — тогда напишу статью с примерами и с готовыми функциями.

Теги:
+2
Комментарии2

polluSensWeb теперь умеет I²C и 1-Wire

Если коротко: проект, который раньше читал только UART-датчики, теперь работает ещё и с I²C и 1-Wire. Заодно в списке уже 39 датчиков, которые подключаются без написания кода.

Для тех, кто не в курсе: polluSensWeb это страница в браузере, которая через Web Serial API читает данные с датчиков качества воздуха и рисует графики. Ничего ставить не нужно, драйверы тоже. Код открытый, лицензия MIT. Работает в Chrome, Edge, Opera и Brave, а Firefox и Safari Web Serial не поддерживают.

UART

Тут всё как раньше. Берёте любой USB-UART адаптер (CP2102, CH340, FT232), TX с одной стороны идёт на RX с другой и наоборот, плюс питание и земля. Грабли только одни: многим PM-датчикам вроде Plantower PMS5003 нужно 5 В питания, а логика у них 3.3 В, так что уровни адаптера надо проверить.

I²C

К UART-адаптеру I²C-датчик не подключишь, нужен мост USB-I²C, который определяется как COM-порт. Примеры в sensors.json написаны под модуль KEL USB-I2C, и на нём проверeны SCD41, SEN63C и SEN66. У модуля KEL нет подтягивающих резисторов, и у голых датчиков Sensirion их тоже нет. Если ничего не добавить, датчик просто молчит и каждый пакет прилетает с ошибкой. Нужно по одному резистору на SDA и SCL к питанию датчика, от 2.2 до 10 кОм, я ставлю 2.4k. Если плата-переходник уже с подтяжками, свои не нужны. Провода лучше держать короче 30 см, а на длинных кабелях брать резисторы поменьше. Опрашиваются такие датчики через режим multi-command. В JSON описывается последовательность команд: что отправить один раз при старте, что повторять в цикле, какой формат ответа ждать и как проверять контрольную сумму.

1-Wire

Здесь без отдельного мастера шины. 1-Wire гоняется через обычный USB-UART адаптер: сброс идёт на 9600 бод, дальше на 115200 каждый бит шины кодируется одним байтом UART. Скорость порта можно задавать для каждой команды отдельно, поэтому вся последовательность для DS18B20 (reset, Skip ROM, Convert T, чтение scratchpad) описывается тем же JSON, что и остальные датчики. Есть два пути. Самый простой это готовый USB-термометр с usbtemp.com. Он сам определяется как COM-порт, паять ничего не нужно, в списке выбираете «usbtemp.com USB thermometer (DS18B20)». Второй путь: собрать самому из адаптера, диода и резистора. TX и RX объединяются в одну линию данных, TX подключается через диод катодом к себе (чтобы он мог только тянуть линию вниз), на линии данных стоит подтяжка к питанию около 4.7 кОм.

Не подойдут адаптеры вроде DS9490R: они не создают COM-порт, и браузер их не видит. Мосты на DS2480B (тот же DS9097U) порт создают, но говорят на своём протоколе, так что для них нужно отдельное описание. Пока этого нет.

Про сами датчики

В списке сейчас 39 штук: Plantower, SenseAir S8, Sensirion (SPS30, SEN55 по UART, несколько I²C-моделей), DS18B20 и другие. Чтобы не копировать конфиги, есть наследование. В новом описании указываешь, от какого датчика наследуешься, и переопределяешь только отличия. Например, SenseAir S8 004-0-0017 отличается от 004-0-0053 только множителем в формуле CO₂, и в конфиге это одна строка. Если вашего датчика нет, можно загрузить свой JSON прямо в интерфейсе, он появится в списке с пометкой. Или напишите в issue модель, посмотрим вместе.

Остальное вкратце: несколько графиков одновременно, лог сырых пакетов в hex с отметкой, прошла ли контрольная сумма, экспорт в CSV и вебхуки для Home Assistant, Node-RED и всего, что умеет принимать HTTP. Вебхуки идут через CORS-прокси, и по умолчанию он внешний. Если данные не хочется отдавать наружу, поднимите свой.

Как подключать - https://github.com/WeSpeakEnglish/polluSensWeb/wiki#how-to-connect

Попробовать можно здесь: https://wespeakenglish.github.io/polluSensWeb/

Код и вики: https://github.com/WeSpeakEnglish/polluSensWeb

Теги:
0
Комментарии0

Представлен открытый проект Papermorph — скилл для Claude, чтобы усилить пользовательский опыт чтения, например, с помощью преобразования PDF в интерактивную веб‑книгу:

  • можно загрузить PDF, а дальше Claude сам создаст план книги, сториборды, озвучку, анимации и даже квизы для лучшего запоминания

  • в результате будет полноценный интерактивный учебный проект.

Теги:
+6
Комментарии0

Ближайшие события

Статический сайт для блогов, создания статей из RSS-каналов

Сайт egaxegax.github.io можно запустить и использовать локально:

  • Выгрузить файлы из репозитория

  • Запустить веб-сервер HTTP/1.1 с поддержкой Requested Ranges из корневого каталога сайта

      python3 -m http.server 88 (если HTTP/1.1 поддерживается версией Python)
      python3 srv.py 88 (доработанный веб-сервер с HTTP/1.1)
    

    и набрать в браузере localhost:88

Добавить пост в раздел /posts:

  • Создать файл с расширением .md с текстовым содержимым, html-, markdown-разметкой в папке с названием подтемы в папке с названием темы или создать новые.

  • Первой строкой добавить html-комментарий в формате с датой создания файла, затем добавить текст поста. Комментарий с датой используется для сортировки страниц в индексном файле раздела index.js.

  • Перейти в каталог раздела и обновить индексный файл, выполнив скрипт:

      python3 ../update.py
    

Добавление постов в раздел /posts через RSS-каналы:

  • Перейти в каталог раздела и запустить скрипт чтения RSS-канала:

      python3 readrss.py <RSSlist-id> (ключ источника из списка RSSlist в скрипте)
      python3 readsites.py <RSSlist-id> (ключ источника из RSSlist)
      python3 readrufeed.py (обновления каналов из Рутуб из списка RSSlist)
      python3 readyfeed.py (обновления каналов из Ютуб из списка RSSlist)
    

Добавить текст в раздел /songs:

  • Создать текстовый файл .txt с названием песни с аккордами в markdown-разметке или без в папке с названием музыкального жанра и подпапке с именем исполнителя. Он будет переименован в .md скриптом update.py.

  • Первой строкой добавить html-комментарий в формате с датой создания файла.

  • Создать файл about.md с информацией о группе, исполнителе и картинку <Исполнитель.jpg>. После индексации появится описание группы и кавер-картинка при просмотре списка песен исполнителя.

  • Перейти в каталог раздела и обновить индексный файл, выполнив скрипт:

      python3 ../update.py
    

Добавить описание книг в /books:

  • Из папки books запустить скрипт

      python books2fb2.py <fb2_dir> [<out_dir>]
    
  • Обновить страницу в браузере.

Добавить картинку в раздел /foto:

  • Выгрузить файлы из репозитория в папку foto.

  • Добавить картинку с расширением .jpg в папку с темой.

  • Обновить индекс и превью (в каталоге th) командой из папки foto

      python3 ../update.py
    
  • Обновить страницу в браузере.

Редактировать/удалить пост

  • Распаковать упакованный файл README.md в .md файлы командой:

      python3 updatelist.py <раздел/папка_темы>
    
  • Удалить или отредактировать пост и упаковать архив README.md обратно:

      python3 update.py <раздел>
    

Ссылки:

Основной обновляемый сайт: egax.ru
Сайт репозитория github.com: egaxegax.github.io
Сайт репозитория gitverse.ru: egaxegax.gitverse.site

Теги:
Всего голосов 2: ↑2 и ↓0+4
Комментарии0

🤔 Как не терять requestId в логах не пробрасывая его через параметры методов?

Классическая проблема NodeJS разработки: логи не читаемы. При дебаге когда пользователь один, разобрать что происходит ещё можно. Но после запуска в прод в логах сборная солянка из запросов и восстановить трейс вызова композиции методов нельзя

import { scoped } from "di-scoped";

export interface IRequestContext {
  userId: string;
  requestId: string;
  serviceName: "mobile" | "desktop";
  version: number;
}

export const RequestContextService = scoped(
  class {
    constructor(readonly context: IRequestContext) {}
  }
);

export type TRequestContextService = InstanceType<
  typeof RequestContextService
>;

export default RequestContextService;

Согласно MVC, делается два слоя: View и Controller. На уровне view кладем requestId в контекст исполнения через RequestContextService.runInContext

import { inject } from "di-kit";

export class SocketViewService {
  readonly loggerService = inject<LoggerService>(TYPES.loggerService);
  readonly socketControllerService = inject<SocketControllerService>(TYPES.socketControllerService);

  public sendNewOrder = async (
    request: TRequest<{ room: string; orderId: string }>,
  ): Promise<TResponse<SendNewOrder>> => {
    this.loggerService.log("socketViewService sendNewOrder", { request });
    try {
      const data = await RequestContextService.runInContext(
//                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        async () =>
          await this.socketControllerService.sendNewOrder(request.data),
        request,
      );
      return { status: "ok", serviceName: request.serviceName, version: request.version, userId: request.userId, requestId: request.requestId, data };
    } catch (error: any) {
      this.loggerService.log("socketViewService sendNewOrder error", { request, error: errorData(error) });
      return { status: "error", error: VerificationError.getErrorMessage(error), errorCode: VerificationError.getErrorCode(error), serviceName: request.serviceName, version: request.version, userId: request.userId, requestId: request.requestId };
    }
  };
}

Voila! Во всех вложенных сервисах к записи в лог будет приложен идентификатор requestId. Дополнительно, этот код позволяет сериализовать ошибки так, чтобы передавать их по шине gRPC

import { inject } from "di-kit";
import { createLogger } from 'pinolog';

const logger = createLogger("socket.log");

export class LoggerService {

  readonly requestContextService = inject<TRequestContextService>(TYPES.requestContextService);

  private get context() {
    if (RequestContextService.hasContext()) {
      // в лог - только конверт IRequestContext; scoped-контекст. но дебаггеру виден весь запрос
      const { serviceName, version, userId, requestId } = this.contextService.context;
      return { serviceName, version, userId, requestId };
    }
    return {};
  }

  public log = (topic: string, ...args: any[]) => {
    logger.log(topic, ...args, this.context);
  }

}
Теги:
Всего голосов 4: ↑4 и ↓0+4
Комментарии0

Представлен открытый проект Free Router, автоматически переключает ИИ‑агента между 25 бесплатными провайдерами для значительной экономии токенов, включая бесплатные. На выбор есть любые актуальные модели: DeepSeek, GLM-5.3, Qwen-3.8.

Теги:
Всего голосов 3: ↑3 и ↓0+5
Комментарии0

🤡 Новый фреймворк на NodeJS

Это не draft, а работающий код из прода. Посмотрите код сервиса, код http контроллера и код декларации

Шутки шутками, а существующие реализации DI на ноде либо перегружены, либо используют декораторы, для которых нет единого стандарта у старого и нового кода.

import { provide, inject } from "di-kit";

export class WalkerLogicPublicService {

  private readonly loggerService = inject<LoggerService>("loggerService");

  ...

}

...

provide("loggerService", () => new LoggerService());

Вот почему нельзя просто вот так? Никаких минусов нет, выносите типы в enum, создавайте изолированные пространства имен.

import { createActivator } from "di-kit";

export const { init, inject, provide } = createActivator("separate-scope");
Теги:
Всего голосов 6: ↑6 и ↓0+8
Комментарии0

Регуляторы скоро спросят «где у вас RSA?». Я написал сканер, который отвечает за секунды

  1. Завязка. Три дедлайна: США — PQC для новых госзакупок нацбезопасности с 2027 (EO 14412 / CNSA 2.0, там прямо назван CBOM), Великобритания — полная криптографическая инвентаризация и план миграции к 2028 (NCSC), ЕС — переход с конца 2026. Первый вопрос аудитора одинаковый: «где именно у вас используется RSA?» Ответа нет почти ни у кого.

  2. Что такое CBOM и почему это не то же самое, что SBOM.

  3. «Harvest now, decrypt later» — почему это не про далёкое будущее: трафик записывают сегодня, расшифруют потом. Если у секрета срок жизни больше нескольких лет — проблема уже наступила.

  4. Как работает сканер. Одна команда, три артефакта: CycloneDX 1.6 CBOM, отчёт с планом миграции на ML-KEM/ML-DSA, SARIF для GitHub code scanning. Плюс scan-tls — одно рукопожатие и вердикт по живому эндпоинту.

  5. Байка про баг (Хабр это любит): первый же регрессионный тест поймал, как сканер находит «криптографию» в собственных регулярках. Лечилось маскировкой литералов RSA_generate_ke[y], и теперь есть вечный тест, что репозиторий подсвечивает только свои фикстуры.

  6. Честный бенчмарк против PQCA CBOMkit. Не «мы лучше», а «мы разные»: CBOMkit глубже на Java/Python (знает размеры ключей, симметрику), мы шире (10+ языков, конфиги, сертификаты) и быстрее (0,05–0,12 с против минуты). Прямая ссылка на docs/BENCHMARK.md — воспроизводимо кнопкой в CI.

  7. Ограничения честно: v0 на паттернах, динамические вызовы пропустит, отсутствие находок ≠ отсутствие проблем. AST-движок в планах.

  8. Финал: вопрос к читателям — у кого уже спрашивали CBOM и в каком виде.

Теги:
Всего голосов 1: ↑1 и ↓0+3
Комментарии0

Chrome изменил работу window.open с несколькими мониторами

Раньше в Chrome можно было открыть окно на втором мониторе обычным window.open, передав его координаты:

window.open(url, '_blank', 'left=1920,top=0');

Теперь Chrome может ограничить позицию окна текущим экраном, если сайту не выдано разрешение «Управление окнами» (window-management). При этом в Firefox старый вариант продолжает работать.

Современное решение — Window Management API:

const details = await window.getScreenDetails();

const screen = details.screens.find(
  x => x !== details.currentScreen
);

window.open(
  url,
  '_blank',
  `left=${screen.availLeft},top=${screen.availTop}`
);

Главный нюанс: вызов getScreenDetails() запрашивает разрешение на управление окнами. После его выдачи Chrome снова позволяет корректно размещать окно на другом мониторе.

P.S. это изменение уже упоминалось в версии 135.0.7049.116 - ссылка

Теги:
Всего голосов 3: ↑3 и ↓0+5
Комментарии0

У коллег увидел интересный HTML трюк для значений в SELECT, который раньше не встречал.

Задача: сделать мульти-селект. Значения, которые влезут на текущем экране отображаем, а остальные указываем цифрой.

Первая мысль: нужно смотреть размеры элемента и родителя. Динамический высчитывать размеры и пытаться понять, влезет еще одно на экран или нет.

Решение коллег:

  • Выводим сразу штук 10 значений;

  • Каждому значению сразу пишем цифру, сколько элементов осталось после него;

  • Делаем всем отрицательный margin-left, чтобы они своим контейнером перекрыли цифру прошлого значения.

Никаких вычислений и перерасчетов при масштабировании и изменении размера экрана.

Теги:
Всего голосов 9: ↑9 и ↓0+11
Комментарии1
1
23 ...