FediScanner

#react

← Back

Как я спроектировал офлайн‑приложение для заметок под будущую синхронизацию — и собрал лендинг без единой сборки

Полгода назад я в одиночку начал делать Nookly — локальное приложение для заметок на Flutter: страницы, блочный редактор, таблицы с канбан-видом, полнотекстовый поиск. Ключевое требование с самого начала: всё работает офлайн, данные не покидают устройство. В этом посте — не про сам продукт, а про два инженерных решения, которые могут быть полезны: как спроектировать схему БД так, чтобы синхронизация потом не потребовала переписывания половины кода, и как собрать лендинг вообще без шага сборки.

habr.com/ru/articles/1085646/

#flutter #sqlite #sqlite3 #react #tailwindcss #локальное_хранение_данных #petпроект

Show Original PostReport

Хотел свою систему, которую можно менять когда угодно, и сделал три десктопных AI-приложения

Я всегда хотел иметь свою систему: свои приложения, в которых можно изменить что угодно, а не что-то чужое, что завтра могут поменять. Так появился Zeqou — три десктопных приложения для работы с языковыми моделями: чат ZeqouXChat на Tauri 2 и React 19, рабочее пространство для агентов Zeqou Harness со списком задач и памятью и студия дообучения моделей ZeqouXTraining с LoRA, QLoRA и режимом Thinking. Рассказываю, как всё устроено, с какими проблемами столкнулся (спойлер: установщики) и почему выбрал закрытую лицензию.

habr.com/ru/articles/1085650/

#Ai #aiагенты #tauri #electron #ollama #локальные_llm #finetuning #lora #десктопные_приложения #react

Show Original PostReport

🧭 How to structure a Next.js project for scale:

```
app/ → Routes
components/ → UI components (dumb)
features/ → Feature modules (smart)
lib/ → Shared utils
server/ → Server actions + API
types/ → Global TypeScript types
```

Feature-first > type-first folder structure.
Co-locate tests with components.

Consistency > perfection.

Show Original PostReport

Most job boards are glorified spreadsheets with a search bar. GiggleGigs isn't. It's a two-sided marketplace — "Find Work. Hire Talent." — built to close the gap between "I posted a job" and "I hired someone good," using AI at exactly the three points where it saves real time: writing the posting, writing the application, and screening the results.

Here's what's actually under the hood, and why it's worth a look.

The Problem It Solves


Job boards fail in a predictable way: employers post vague listings because writing a good one is tedious, candidates send generic applications because tailoring one is tedious, and then the employer drowns in a stack of applications that all look the same. Nobody's time gets saved — the tedium just moves downstream.

GiggleGigs attacks all three steps at once:

  • Companies write a two-sentence prompt and GiggleGigs' AI job-description writer expands it into a full posting with requirements, budget framing, and structure.
  • Seekers get an AI-drafted, job-specific cover letter instead of copy-pasting the same one everywhere — and can even upload a resume during onboarding and have Gemini auto-fill their entire profile (name, bio, skills, headline, links, experience) in seconds.
  • Companies then use an AI application screener that match-scores and summarizes every applicant against the job's actual requirements, so reviewing 40 applications doesn't mean reading 40 applications.

That's the pitch: less busywork on both sides of the transaction, and a marketplace that gets more useful the more people use it.

What's Actually Built


This isn't a prototype — it's a full production-shaped Django + React application with a genuinely deep feature set:

For job seekers

  • Skill/location/budget/timeline-filtered job search
  • One-click applications with optional resume upload (PDF/DOC/DOCX, validated server-side)
  • AI cover letter generation and AI resume-parsing onboarding
  • Real-time application tracking across a five-stage pipeline (Pending → Reviewing → Interviewed → Hired/Rejected/Withdrawn)
  • Application withdrawal, favorited jobs, and a daily digest email of new jobs matching your skills

For companies

  • Full job posting management (hourly or fixed price, detailed requirements)
  • A filterable, sortable applicant dashboard with live hiring-pipeline stats
  • AI-powered candidate search and AI match-scoring per applicant
  • Star-rating feedback to candidates, resume downloads, and team invitations for multi-seat company accounts
  • Credit-based billing via PayPal to unlock posting and candidate contact

Platform-wide

  • Real-time notifications over WebSockets (Django Channels + Daphne ASGI) — no polling, no refresh
  • JWT auth plus django-allauth for email/social login
  • Celery + Redis background processing with Celery Beat and Flower for monitoring
  • Content moderation tooling for jobs, companies, and users
  • A full analytics dashboard for staff on jobs and user activity
  • An API-first backend (Django REST Framework, OpenAPI schema via drf-spectacular) behind a React 18 + Vite SPA

Every one of these shipped incrementally and is live in the codebase today — this is a v1.7 product with a real changelog, not a feature wishlist.

The Business Model Is Already Wired In


GiggleGigs isn't just feature-complete — it's monetization-complete. Companies post their first two jobs a month for free (liquidity first, revenue second), and then convert through one of:

  • $29 single-post unlock — the low-commitment first purchase, for a company that just got applicants and wants to actually contact them
  • Starter ($49/mo) — for companies hiring regularly
  • Growth ($99/mo) — adds full candidate search
  • Annual plans at two months free

That progression — free listing → paid contact unlock → subscription — is a deliberate sequencing choice: liquidity (seekers applying) has to exist before employers will ever pay, so the free tier isn't a loss leader, it's the engine that makes the paid tier valuable at all.

Why the Engineering Choices Matter


A few decisions stand out as the kind that pay off later rather than look impressive now:

  • Services layer, not fat views — business logic lives outside the DRF views, which keeps the API surface thin and testable.
  • Proxy user model (Company / Seeker / Employee subtypes) instead of bolting role flags onto a single User table — cleaner permissions, cleaner queries.
  • Consistent { data, message, errors } API envelope across every endpoint, which makes the frontend's error handling and loading states uniform instead of ad hoc per-page.
  • Docker-only local workflow — Postgres, Redis, Django, Celery, Channels, and MailHog all come up together with one command, so "works on my machine" isn't a category of bug here.
  • Real-time built on Channels from day one, not retrofitted — notifications, status changes, and messaging all ride the same WebSocket infrastructure.

None of this shows up in a demo GIF, but it's the difference between a project that can take on its next ten features and one that needs a rewrite to get there.

Try It


GiggleGigs is a working, self-hostable job board and marketplace — clone it, docker compose -f local.yml up, and you have Postgres, Redis, Celery, Channels, and MailHog running together in one shot. If you're evaluating job-board platforms, building a niche talent marketplace, or just want to see AI applied to hiring in a way that removes tedium instead of adding a chatbot nobody asked for, it's worth a look.

Find work. Hire talent. Skip the busywork.#webdev #django #react #showdev #software #coding #development #engineering #inclusive #community
GiggleGigs: A Job Board That Actually Uses AI Where It Helps

Show Original PostReport

Публикуйте инструменты, а не пиксели: собираем WebMCP-магазин на React и считаем грабли

Представим обычный интернет-магазин. Пользователь открывает каталог, вводит «наушники» в поиск, включает фильтр наличия, задаёт цену до 10 000 рублей, открывает карточку и добавляет товар в корзину. Шесть вполне понятных действий. Если их выполняет человек, всё хорошо. Если за дело берётся AI-агент, начинается небольшой квест: прочитать страницу, найти поле, угадать назначение переключателя, нажать кнопку, проверить результат и не купить случайно кофеварку вместо наушников. А что, если сайт сам скажет агенту: «Я умею искать товары, показывать карточку и добавлять позицию в корзину. Вот названия функций, вот параметры, вот ограничения»? Эту задачу решает WebMCP — экспериментальный браузерный API, который позволяет странице объявлять инструменты для AI-агентов прямо из JavaScript или HTML-формы. Черновик и обсуждение стандарта открыты в репозитории Web Machine Learning Community Group . Меня зовут Денис Смирнов, я ведущий .NET-разработчик, в профессии больше пяти лет. Это не пересказ новости. Соберу небольшой магазин электроники на React, добавлю три инструмента, прогоню проверки и покажу четверо граблей, на которые наступил. Грабли оказались интереснее рабочего примера.

habr.com/ru/companies/first/ar

#webmcp #model_context_protocol #aiагенты #react #javascript #google_chrome #вайбкодинг #браузерный_api #origin_trial

Show Original PostReport

Introduction


“I added an item in aisle 5, closed the app, and it synced automatically on the bus ride home.” That one sentence describes the behavior users notice and remember: writes that survive crashes, restarts, and flaky networks.

This article shows a practical, production-tested 3-step recipe to get durable, replayable optimistic writes using TanStack Query (React Query), Dexie (IndexedDB), and a small outbox. The pattern centers on treating TanStack Query as a reactive cache over your local DB and making writes durable before they leave the device.

Primary keyword: tanstack query offline persistence

The big idea in three pieces


1) Persist the reactive cache to IndexedDB (Dexie)
2) Register mutation defaults so paused mutations can be rebuilt after a restart
3) Use client-generated UUIDs + an outbox replay queue for idempotency and ordering

We'll walk each piece and end with concrete code showing how they fit together.

1) Persist the reactive cache to IndexedDB


Treat TanStack Query as a reactive cache that mirrors your local DB. Persisting the query client (queries + paused mutations) to durable storage lets your app restore a consistent local view on launch and replay paused work.

Use the PersistQueryClientProvider or persistQueryClient utilities from @tanstack/react-query-persist-client and an async persister backed by IndexedDB/Dexie (or idb-keyval). Important options:

  • Provide a persister that implements getItem/setItem/removeItem (or use the provided helpers).
  • Set gcTime on your QueryClient to at least the persister maxAge.
  • Use shouldDehydrateMutation to only persist paused mutations you know how to resume.

Example wiring (conceptual):

// app/query-persist.ts
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
// or implement AsyncStorage using Dexie get/set/remove

const persister = createAsyncStoragePersister({ storage: myDexieStorage })

// In your App root
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister, maxAge: 1000*60*60*24 }}
onSuccess={() => queryClient.resumePausedMutations()}
>
<App />
</PersistQueryClientProvider>

Why IndexedDB? It stores larger payloads, runs off the main thread in well-implemented adapters, and survives quotas typical web storage can't. Dexie is an excellent high-level IndexedDB wrapper and makes outbox tables and reads simple.

2) Register mutation defaults so paused mutations can be rebuilt


A paused mutation persists variables and metadata, but not the closure that implements mutationFn. When your app restarts, TanStack Query attempts to resume paused mutations by looking up a default mutationFn registered via queryClient.setMutationDefaults(mutationKey, { mutationFn, ... }).

Best practices:

  • Use stable, declarative mutation keys (arrays or strings) — e.g. ["items", "create"].
  • Register defaults at module load so they exist before persisted mutations are resumed. If you call resumePausedMutations before the defaults are registered, rehydrated pauses become unreplayable zombies.
  • Persist only mutations you know how to rebuild (use shouldDehydrateMutation to filter).

Example:

// lib/item-mutations.ts — side-effect import at app boot
export const MK = { createItem: ['items', 'create'] as const }

queryClient.setMutationDefaults(MK.createItem, {
mutationFn: async (vars: { id: string; title: string}) =>
api.items.create(vars),
onMutate: ({ id, title }) => {
// optimistic update applied to local DB/query cache
},
});

Ensure this module is imported before PersistQueryClientProvider calls resumePausedMutations.

3) Use client-generated UUIDs + a Dexie outbox replay queue


Idempotency and ordering are the two hardest problems in offline writes. A simple and reliable approach:

  • In onMutate stamp a client-generated UUID (crypto.randomUUID() or uuidv4) into the new item.
  • Persist the optimistic change into your Dexie tables immediately (this makes your local DB the single source of truth).
  • Enqueue a durable outbox record in Dexie that contains: id (uuid), action (create/update/delete), variables, mutationKey, metadata, createdAt.
  • Surface a visible sync state in the UI (queued / syncing / failed).
  • On reconnect or app launch, replay outbox entries in order (or per-key sequentially), call the server, and remove succeeded items from the outbox.

Minimal Dexie sketch + mutation flow:

import Dexie from 'dexie'
import { v4 as uuidv4 } from 'uuid'

class AppDB extends Dexie {
items: Dexie.Table<any, string>
outbox: Dexie.Table<any, string>
constructor() {
super('appDB')
this.version(1).stores({ items: 'id, listId, title', outbox: 'id, createdAt' })
}
}
const db = new AppDB()

// user action: add item
const id = uuidv4()
await db.items.add({ id, title })
await db.outbox.add({ id, action: 'create', mutationKey: ['items','create'], variables: { id, title }, createdAt: Date.now() })
queryClient.invalidateQueries(['items'])

Outbox replay worker (run at launch, on online, or via Background Sync if available):

async function drainOutbox() {
const pending = await db.outbox.toArray()
for (const op of pending) {
try {
// Either call queryClient.mutate with defaults already registered
await apiCallFor(op) // send idempotency header: Idempotency-Key: op.id
await db.outbox.delete(op.id)
} catch (err) {
// handle retry/backoff, or classify as non-retriable and surface error
}
}
}

Server-side: honor Idempotency-Key or sequence numbers so a request replayed twice won't create duplicates.

Practical tips and traps


  • Use Dexie as the single source of truth: writes go to Dexie first; TanStack Query reacts to DB changes. This keeps the UI consistent across reloads.
  • Persist only resumable paused mutations. Persisting everything without defaults or handlers resurrects non-actionable zombies.
  • Ordering: give related mutations a shared scope/id or process outbox in-order for the same entity so a create arrives before its updates.
  • Background sync: Chrome supports the Background Sync API and service workers, but iOS Safari does not. Trigger replay on online/visibility and at app launch for broad coverage.
  • Retry/backoff and non-retriable errors: implement exponential backoff with jitter for transient failures and remove failing transactions classified as permanent (validation errors, 4xx).
  • Leader election: if you support multi-tab, use a single-leader approach so only one tab processes the outbox (BroadcastChannel/Web Locks). Dexie + BroadcastChannel are common.


Why this works


The pattern combines three durable guarantees:

  • Persistence: the optimistic state and outbox survive restarts because they’re in IndexedDB (Dexie).
  • Rebuildability: register mutation defaults so persisted paused mutations can be rehydrated and replayed.
  • Idempotency: client UUIDs + server dedupe prevent duplicate-side effects during retries.

Taken together, flaky networks become a feature: users keep working, see visible sync state, and their writes land eventually — even if the app was backgrounded, killed, or the network bounces.

Conclusion


If you build offline-capable UIs, aim for writes that survive app death. Persist TanStack Query’s cache (queries + paused mutations) to IndexedDB, register stable mutation defaults, and use a durable Dexie outbox with client-generated UUIDs. That combination creates reliable, observable sync: the item is visible instantly, shows a syncing state, and gets delivered when connectivity allows.

How have you handled replay and idempotency in your apps? What edge case gave you the most grief?#tanstack #dexie #offline #react #software #coding #development #engineering #inclusive #community
Offline-First React: TanStack Query + IndexedDB Patterns

Show Original PostReport