OpenChart
Free Desktop charting with Claude or Codex. Describe alerts, research chart moves, and choose your data providers.
მთელი შენი ქონება ერთ დაფაზე — უძრავი ქონება, გაქირავება, ავტოპარკი, შემოსავალი და ციფრული აქტივები (კრიპტო და აქციები).
Translation pending
FinanceTypeScript · PythonGitHub @giorgidoborjginidzeOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Property-operations tool for short-term-rental (STR) and aparthotel operators in Georgia (Tbilisi & Batumi). One dashboard for the whole portfolio: units, a unified booking calendar synced from Airbnb / Booking.com iCal feeds, occupancy and revenue analytics, rule-based price suggestions against district benchmarks, and proactive alerts for vacancy gaps and expiring leases.
This app is also the permissioned supply source for a separate future guest/renter concierge (Phase 2) — the data model is kept clean and exportable.
@prisma/adapter-better-sqlite3;
schema kept Postgres-compatible for later)ANTHROPIC_API_KEY the rationales come from a
deterministic local stub and the chat answers from its keyword FAQnpm install
cp .env.example .env # ANTHROPIC_API_KEY is optional
npx prisma migrate dev # creates prisma/dev.db and applies migrations
npx prisma db seed # loads the sample portfolio (12 units, ~450 bookings)
npm run dev # http://localhost:3000
Windows note: Windows PowerShell 5.1 doesn't support
&&— run the commands above one per line (useCopy-Item .env.example .envfor the copy), or upgrade to PowerShell 7+.
Useful scripts: npm run db:migrate, npm run db:seed, npm run db:studio,
npm test (vitest), npm run scheduler (local stand-in for the production
cron: iCal sync every SYNC_INTERVAL_MINUTES, and once a day from 08:00
Tbilisi time the full daily job — see Automatic jobs below).
The app is multi-tenant: each operator registers with email + password
(/register), signs in at /login, and sees only their own units,
bookings, assets, alerts, and income. Passwords are hashed with Node's
built-in scrypt (async, so hashing never blocks other requests); sessions
are 30-day httpOnly cookies backed by a Session table storing only the
token's SHA-256. Every page, server action, and API route is scoped to the
signed-in operator — cross-tenant record access returns 404, links to
another account's unit or asset are refused, and the manual scan/sync
routes (POST only) return 401 without a session. The only route that works
for every operator is /api/cron, and it requires CRON_SECRET.
AuthAttempt table, so it
holds across serverless instances). An unknown email costs one scrypt
against a dummy hash, so response times do not reveal which emails have
accounts; registering a taken email gets a neutral message./forgot sends a single-use link to /reset/<token>
that works for one hour (only the token's SHA-256 is stored, in
PasswordReset; 3 requests per email an hour). Email goes through
Resend when RESEND_API_KEY and EMAIL_FROM are set; without them the
page says reset by email is not on yet and shows the support address.
Setting a new password signs out every device.Operator.isDemo. Every server
action goes through requireWriter() (lib/auth/session.ts), which refuses
the demo and sends the visitor back with a "changes aren't saved —
register free" note; its WhatsApp outbox never sends over the Cloud API,
and its trackers' pings are refused. scripts/ensure-demo.ts sets the
flag on every deploy.The build is database-portable: scripts/prepare-db.mjs runs before
next build and follows DATABASE_URL — a file: URL generates the
SQLite client (local default), a postgres… URL derives
prisma/schema.postgres.prisma from the single source schema, generates
the Postgres client, and (on Vercel) runs prisma db push against the
database. To deploy: import the GitHub repo into Vercel, create a free
Postgres database (e.g. Neon), and set two environment variables —
DATABASE_URL (the Neon connection string) and optionally
ANTHROPIC_API_KEY. The runtime picks the matching driver adapter
automatically (@prisma/adapter-pg vs better-sqlite3). Note: the seed
script is for local SQLite demo data; production starts empty and users
register their own accounts.
The schema push never passes --accept-data-loss: a change that would
drop or rewrite data (a removed or renamed column, a new unique constraint
over existing rows) makes prisma db push refuse and the deploy fail
before anything is lost. Keep schema changes additive (new tables, new
columns with defaults).
Other production settings: FLITT_MERCHANT_ID / FLITT_SECRET_KEY
(without them checkout is switched off on production, never the free
sandbox), RESEND_API_KEY / EMAIL_FROM (password-reset email) — see
.env.example. Server errors are logged as one JSON line each
(instrumentation.ts, onRequestError: route, kind, message, digest —
never headers, cookies, query strings or bodies), visible in Vercel → Logs
or any log drain. next.config.ts sends security headers on every
response (X-Frame-Options DENY, a report-only CSP with
frame-ancestors 'none', nosniff, strict-origin-when-cross-origin, a
minimal Permissions-Policy, HSTS on production) and drops X-Powered-By.
vercel.json schedules GET /api/cron once a day at 04:00 UTC (08:00 in
Tbilisi — the Hobby plan allows one cron run a day, fired within that
hour). Set CRON_SECRET (a random string of 16+ characters) in the
Vercel project: Vercel then sends it as Authorization: Bearer …, and the
route refuses every call without it (503 while the variable is missing).
Each run, for every workspace in turn: pulls the iCal calendars, runs the
alert scan (vacancies, double bookings, expiring and finished contracts,
silent trackers, late rent — which also queues the WhatsApp reminders),
then delivers that workspace's own outbox (sent through the WhatsApp Cloud
API when WHATSAPP_TOKEN / WHATSAPP_PHONE_NUMBER_ID are set; otherwise
the messages wait with a one-tap send link). Every run is recorded in the
SystemRun table and /alerts shows "last automatic check", warning when
it failed for that account or has not run for over a day.
For fresher calendars, an external scheduler (cron-job.org, GitHub
Actions, Upstash QStash…) can call GET /api/cron?only=sync hourly with the
same header — that pulls the calendars only and sends nothing.
Every new account gets a 30-day free trial with top-tier limits. A plan
is bought one month at a time on /billing through Flitt's hosted
checkout; only the verified server callback
(/api/payments/flitt/callback) activates it and sets paidUntil. The
plan applies while it is paid plus 3 grace days, then the account falls
back to the trial (if still running) or the bottom tier — nothing is
deleted, only adding past the limits stops. Paying again extends from the
current paid-through date while it is still ahead (renewing early loses no
days), with calendar months clamped in UTC (31 Jan → 28/29 Feb). After the
payment page the buyer lands on /billing/return, which forwards to
/billing?order=<id> and shows that order's real status (approved,
waiting — re-checked every 3 s for a minute — or declined), plus a payment
history. Personal tiers:
Starter 15₾ (5 assets / 3 units), Standard 29₾ (20 / 10), Pro 49₾
(50 / 30). Company tiers add team seats: Business S 99₾ (100 assets /
60 units / 5 seats), Business M 199₾ (300 / 200 / 15). Business owners
invite employees from /billing — the invite link
(/register?invite=<token>) joins the new account to the company, the
employee enters the listings they manage, and all limits are counted
company-wide. Creating assets/units past the plan limit is blocked with
an upgrade hint; over-limit records from a downgrade stay visible.
The Assets section tracks the operator's own property portfolio beyond STR: real estate (apartments, houses, commercial, land, garages) and movable assets (vehicles, equipment). Each asset carries a status (rented / vacant / personal use / listed — or "On STR" when linked to an STR unit), rental contracts (tenant, term, monthly rent, deposit) with expiry alerts, a market-rent estimate from mock per-district GEL/m² benchmarks (with a below-market flag), and estimated value. Each asset stores its own listing URLs per platform — myhome.ge / ss.ge / Airbnb / Booking.com for real estate, myauto.ge for vehicles — plus a long-term vs daily rental mode. One-click Rented/Vacant buttons update the status here and open the asset's first listing in a new tab (the Georgian portals have no public third-party APIs); assets in personal use show no listings or buttons at all, and the buttons hide while an active contract governs the status. Rentable real-estate assets also carry a digital door key: a one-click 6-digit code generator with a WhatsApp deep link that prefills the code and address to the tenant's phone (from the contract) — no WhatsApp Business API required. The page consolidates monthly income: long-term rent from active contracts + STR revenue + manual entries (salary, business, dividends).
/invest/pro (Pro and Business plans; unlocked during the trial) ports
the operator's underwriting spreadsheet as a hidden-algorithm engine
(lib/invest/worthiness.ts, 18 tests): full loan amortization (PMT +
monthly walk), a 5-year projection with rent growth, vacancy, seven
operating expense lines, depreciation, points amortization and income
tax, producing NOI, cap rate, BT/AT cash flow, cash-on-cash, equity
build-up, payback period and a weighted worth-it verdict. The UI shows
only a short deal form (extras collapsed) and the results — none of the
internals. Three source-sheet formula bugs were corrected on port
(multiplicative vacancy, points input instead of a hard-coded 20%,
single-currency return ratios).
The scan job (the daily cron, the Scan now button on /alerts,
POST /api/alerts/scan — POST only, or the local scheduler) creates alerts with a
suggested action: double bookings (two stays, or two contracts on one
asset, sharing nights in the next 90 days), free windows of 2+ nights
starting within 14 days, leases and contracts expiring within 30 days,
finished contracts (and any rent left unpaid under them), silent GPS
trackers, late rent and the repossession right, and units priced below
their district benchmark. Alerts dedupe on a stable key — a free window is
identified by the stays around it, so the same vacancy is one alert until
it is booked or over. Open alerts whose situation has ended close
themselves (the reason shows under Completed); alerts the owner closed
never reappear. /alerts and the dashboard's Market Advice list the most
severe first.
WhatsApp messages are limited per recipient: one account sends any number (a renter's or the owner's own — it is free text and not verified) at most 3 a day, one number receives at most 10 a day from all accounts together, and each red line announces each event kind once a day; texts are kept on one line and under 600 characters (templates: 500). The shared demo account never sends over the Cloud API (its outbox only offers wa.me links).
Each unit stores its channels' iCal export URLs (/units → edit). Sync runs
on the Automatic jobs above, the Sync calendars button on the units page
(it reports what changed and what failed), and POST /api/sync for the
signed-in workspace. Feeds are parsed with a dependency-free RFC 5545 parser
(lib/ical/parse.ts) and deduped by unitId + source + externalId (feed UID,
or the stay window when a feed has no UIDs).
cancelReason "missing"); explicit STATUS:CANCELLED is kept as "channel"; a stay the
owner cancelled in Activo ("owner") is never revived by a feed.UnitFeed stores lastSyncedAt, lastError (a short
code), lastStatus, lastCount, lastCancelled; /units and the unit edit
page show it.lib/ical/fetch.ts): https only, default port, no
credentials; every resolved address must be public (checked inside the
socket's DNS lookup — no rebinding gap); redirects are followed by hand and
re-checked (max 3); 5 MB cap; the body must be a VCALENDAR; errors are
short codes, never raw network errors./bookings (edit: amount, guest name,
cancel/restore; dates only for manual bookings). ADR counts priced nights
only and revenue is flagged partial while any sold night has no price./bookings/new) and date edits are refused when
they overlap a non-cancelled booking, a lease, or a rental contract on the
linked asset.?s=demo, …-demo) are never
fetched and are labelled as demo links.One sample operator (Kolkheti Stays) with 12 units across Tbilisi districts (Vake, Vera, Saburtalo, Old Town, Mtatsminda) and Batumi. Each unit has bilingual (EN/KA) names, channel links with iCal URLs, several months of mixed Airbnb / Booking / direct bookings (Feb–Oct 2026), a few long-stay leases, and mock per-district market benchmarks. The seed is deterministic (seeded PRNG) and idempotent — re-running it resets the data.
app/ Next.js App Router pages + API routes
app/generated/prisma Generated Prisma client (gitignored)
lib/db.ts Prisma client singleton (SQLite adapter)
lib/i18n/ EN default / KA toggle string map
lib/calendar/ Vacancy-gap + overlap interval math (pure, tested)
lib/ical/ iCal parse + sync → Bookings (pure core, tested)
scripts/scheduler.ts Local stand-in for the cron (lib/automation/run.ts)
lib/analytics/ Occupancy, ADR, RevPAR, revenue math (pure, tested)
lib/pricing/ Rule-based pricing engine (pure, tested)
lib/market/ MarketDataSource interface + Db/Mock sources
lib/alerts/ Vacancy-gap / lease-expiry / underpriced scan job
lib/ai/ Claude rationale writer + stub, support-chat answers
prisma/ Schema, migrations, seed
The iCal sync, analytics, and pricing modules are kept framework-free and independently unit-testable.
Operator → Unit (district, type, capacity, base rate, amenities, channel
iCal URLs) → Booking (source, dates, amount; deduped on
unitId + source + externalId) and Lease (long stays). Plus
PricingSuggestion, MarketBenchmark (district × month ADR/occupancy, mock
for the MVP), and Alert (vacancy_gap | lease_expiry | underpriced,
each carrying a suggested action). Personal assets add Asset, RentalContract, IncomeRecord, and RentBenchmark (district × month GEL/m², mock).
Guest PII is minimal by design (Georgian Personal Data Protection Law /
GDPR-aligned): guestName is optional; a booking needs only source, dates,
and amount. Registration asks only for an email and a password — the
operator's name is optional. Tenant name/phone are optional and stored
only for contact. Income sources store an amount only (no employer or
payer). /terms (Terms of Service) and /privacy (Privacy Policy, written
against the Law of Georgia on Personal Data Protection) are linked in the
site footer, on sign-up and on billing; their text lives in lib/legal/
(Georgian first, English). Both are drafts for a lawyer's review — see
docs/legal-review.md for the facts they rest on, the open questions and
the placeholders (company details, support phone) that need real values.
Market benchmark data is mock behind a pluggable MarketDataSource —
no scraping/republishing of third-party listings.
Assets also include an income-source category (salary, dividend, business, pension, interest, royalty) that records a recurring monthly amount and folds into the monthly-income total alongside rent and STR revenue — no location, listing links or market-rent estimate.
.github/workflows/ci.yml runs on every push and pull request: npm ci,
prisma generate, tsc --noEmit, npm run lint and vitest run. Run the
same four locally before committing.
Before a release:
npm run build passes and the changed pages were checked on a phone
(390 px) and a desktop (1440 px), in Georgian./terms and /privacy still describe what the code does (new
data, new processors, new retention) — update lib/legal/ and
docs/legal-review.md together./learn
(public/tutorials/<slug>.mp4) must be re-recorded on the new build; until
then remove the old file and keep the lesson's slug in OUTDATED_VIDEOS
in lib/learn/lessons.ts (the page then says once that the videos are
being re-recorded). The 2026-08-24 recordings were removed for this reason.Out of scope for the MVP (designed for, not built): two-way channel APIs, auto-posting to portals, payments/invoicing, ML pricing, and the Phase 2 guest-facing concierge.