Properties: ${(PROPS||[]).length} across ${(LLCS||[]).length} LLCs Β· Hours logged: ${((D.participation?.hours)||[]).length}
Metrics generated ${new Date(stats.generated_at).toLocaleString()} Β· Auto-refreshes on each page visit
`;
// Store customer list for the detail modal
window._founderCustomers=custs;
}
function showCustomerDetail(id){
const c=(window._founderCustomers||[]).find(x=>x.id===id);
if(!c)return;
ensureCustModal();
const days=Math.floor(c.days_on||0);
const mrr=c.free_forever?0:(TIER_MRR[c.tier]||0);
const html=`
${escHtml(c.email)}
${escHtml(c.id)}
Joined: ${new Date(c.created_at).toLocaleDateString()} Β· ${days} ${days===1?'day':'days'} on RentReel
Last activity: ${c.last_updated?new Date(c.last_updated).toLocaleString():'never'}
π Grant Promo Code
Updates this customer's plan + promo status immediately. They'll see it next time they refresh the app.
`;
document.getElementById('cust-modal').innerHTML=html;
document.getElementById('cust-modal').style.display='flex';
}
async function grantPromo(userId){
const sel=document.getElementById('grant-code-'+userId);
const code=sel?.value;
if(!code)return;
const info=KNOWN_PROMO_CODES[code];
if(!info){toast('Invalid code');return;}
if(!confirm(`Grant "${code}" to this customer?\n\n${info.msg}`))return;
try{
const {data,error}=await SB.rpc('founder_grant_promo',{
target_user_id:userId,
code:code,
tier:info.tier,
beta_lifetime:info.betaLifetime,
free_forever:!!info.freeForever,
days_valid:info.days
});
if(error){toast('Failed: '+error.message);return;}
if(data?.error){toast('Failed: '+data.error);return;}
toast(`β Granted ${code} to ${data.target_email}`);
closeCustModal();
rFounder(); // refresh the console
}catch(e){toast('Error: '+e.message);}
}
function ensureCustModal(){
if(document.getElementById('cust-modal'))return;
const m=document.createElement('div');m.id='cust-modal';
m.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);backdrop-filter:blur(8px);z-index:1000;align-items:center;justify-content:center;padding:20px';
m.onclick=(e)=>{if(e.target.id==='cust-modal')closeCustModal();};
document.body.appendChild(m);
}
function closeCustModal(){const m=document.getElementById('cust-modal');if(m)m.style.display='none';}
// βββ CPA READ-ONLY SHARE βββ
// Encodes the current portfolio state into a compressed URL fragment so a CPA can open a
// read-only view of the client's Schedule E without needing to sign up. Cursor-safe: the fragment
// stays local (never sent to server, never logged in server access logs).
function generateCpaShareUrl(year){
const Y=year||activeYear();
const relevantTxns=[...D.transactions,...(D.manual||[])]
.filter(t=>new Date(t.date).getFullYear()===Y)
.map(t=>({d:t.date,p:t.payee||t.desc,c:t.c,s:t.s,a:t.amount,t:t.t,pid:t.pid}));
const payload={
v:1,year:Y,generatedAt:new Date().toISOString(),
bizName:bizName()||'',
props:PROPS.map(p=>({id:p.id,name:p.name,llc:p.llc,short:p.short||'',partner:p.partner||null,type:propType(p)})),
llcs:LLCS.map(l=>({id:l.id,name:l.name,props:l.props,partner:!!l.partner})),
txns:relevantTxns,
miles:D.miles.filter(m=>(m.date||'').slice(0,4)===String(Y)).map(m=>({d:m.date,mi:m.miles,f:m.from,to:m.to,p:m.purpose})),
mileRate:mileRate?.()||0.725
};
const json=JSON.stringify(payload);
const encoded=btoa(unescape(encodeURIComponent(json))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
return `${window.location.origin}${window.location.pathname}#cpa=${encoded}`;
}
// Write both text/plain (raw URL) and text/html (hyperlinked anchor) to clipboard.
// When pasted into Gmail/Apple Mail/rich editors β nice clickable link.
// When pasted into plain text (Terminal, Notes plain) β raw URL.
async function _clipboardWriteRich(plainText, htmlText, successMsg){
try{
if(navigator.clipboard&&navigator.clipboard.write&&typeof ClipboardItem!=='undefined'){
const item=new ClipboardItem({
'text/plain':new Blob([plainText],{type:'text/plain'}),
'text/html':new Blob([htmlText],{type:'text/html'})
});
await navigator.clipboard.write([item]);
toast(successMsg);
return;
}
if(navigator.clipboard&&navigator.clipboard.writeText){
await navigator.clipboard.writeText(plainText);
toast(successMsg+' (plain text)');
return;
}
}catch(e){/* fall through */}
prompt('Copy this:',plainText);
}
function copyCpaShareUrl(year){
const url=generateCpaShareUrl(year);
const Y=year||activeYear();
const linkText=`π Read-only view of my Schedule E ${Y} prep β`;
const html=`
`;
_clipboardWriteRich(url, html, 'Link copied β paste into email β auto-hyperlinks');
}
function copyCpaEmailTemplate(year){
const url=generateCpaShareUrl(year);
const Y=year||activeYear();
const biz=bizName()||'my rental portfolio';
const linkText=`π Read-only view of my Schedule E ${Y} prep β`;
const plainBody=`Hi,\n\nHere's a read-only link to review my ${Y} Schedule E prep. No login needed β just click to open in your browser:\n\n${linkText}\n${url}\n\nThe link contains a snapshot of my books as of today. Nothing is stored on any server β the data lives in the URL itself. Please review at your convenience and reply with any questions.\n\nAlso β I have a free lifetime Pro+ Tax account waiting for you if you want to explore RentReel yourself. Use promo code CPAREVIEW at app.rentreel.co.\n\nThanks!`;
const htmlBody=`
Hi,
Here's a read-only link to review my ${escHtml(Y)} Schedule E prep for ${escHtml(biz)}. No login needed β just click to open in your browser:
The link contains a snapshot of my books as of today. Nothing is stored on any server β the data lives in the URL itself. Please review at your convenience and reply with any questions.
Also β I have a free lifetime Pro+ Tax account waiting for you if you want to explore RentReel yourself. Use promo code CPAREVIEW at app.rentreel.co.
Thanks!
`;
_clipboardWriteRich(plainBody, htmlBody, 'Email template copied β paste into Gmail/Mail compose');
}
function cpaShareCardHtml(){
return `
π€ Share with your CPA
Generate a read-only link your CPA can open in their browser β no signup required. Perfect for tax-season review.
How it works: the link contains a snapshot of your ${activeYear()} Schedule E β properties, LLCs, transactions, mileage. Your CPA opens it in any browser and sees a clean read-only view. Nothing hits any server; the data lives entirely in the URL fragment (never logged, never tracked).
π‘ Pre-hyperlinked: both buttons copy in rich format β pastes into Gmail/Apple Mail as a clean clickable "Read-only view β" instead of the raw 30KB URL.
Also send them: the promo code CPAREVIEW β free lifetime Pro+ Tax account so they can explore RentReel themselves.
`;
}
// Handle ?cpa-demo=1 URL param on load β seed demo data into memory, then render the CPA read-only view.
// Purpose: prospects (and Jordan) can click a single URL to see exactly what a CPA sees when a client shares their books.
// Does NOT persist to storage β no cloud pull, no localStorage write. Pure in-memory preview.
function checkCpaDemo(){
try{
const params=new URLSearchParams(window.location.search);
if(params.get('cpa-demo')!=='1')return false;
}catch(e){return false;}
// CRITICAL: set global flag BEFORE any D mutation. This blocks save_local() + cloud.push()
// so demo data never touches localStorage or Supabase. Real workspace stays intact.
window.__CPA_DEMO_MODE__=true;
// Seed demo data in-memory ONLY
D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.lastUp=null;
D.props=DEFAULT_PROPS.map(p=>({...p}));
D.llcs=DEFAULT_LLCS.map(l=>({...l,props:[...l.props]}));
D.settings=D.settings||{};D.settings.workspaceName='Demo Portfolio (Sample Client)';
D.demoSeeded=false;
loadPortfolio();
seedDemo(true);
// Build the CPA read-only payload from the seeded workspace
const Y=new Date().getFullYear();
const payload={
v:1,year:Y,generatedAt:new Date().toISOString(),
bizName:'Demo Portfolio β Sample Client',
props:(D.props||[]).map(p=>({id:p.id,name:p.name,llc:p.llc,short:p.short||'',partner:p.partner||null,type:p.type||'str'})),
llcs:(D.llcs||[]).map(l=>({id:l.id,name:l.name,props:l.props,partner:!!l.partner})),
txns:[...D.transactions,...(D.manual||[])].filter(t=>new Date(t.date).getFullYear()===Y).map(t=>({d:t.date,p:t.payee||t.desc,c:t.c,s:t.s,a:t.amount,t:t.t,pid:t.pid})),
miles:(D.miles||[]).filter(m=>(m.date||'').slice(0,4)===String(Y)).map(m=>({d:m.date,mi:m.miles,f:m.from,to:m.to,p:m.purpose})),
mileRate:0.725
};
renderCpaView(payload);
// Add a banner explaining this is a demo view
setTimeout(()=>{
if(document.getElementById('cpa-demo-banner'))return;
const b=document.createElement('div');b.id='cpa-demo-banner';
b.style.cssText='position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,var(--yellow),var(--a4));color:#fff;padding:10px 16px;text-align:center;font-size:13px;font-weight:700;z-index:9999;box-shadow:0 4px 14px rgba(0,0,0,.15)';
b.innerHTML='π CPA VIEW DEMO β This is exactly what a CPA sees when a RentReel client shares their books. Sample data only. β Back to app';
document.body.appendChild(b);
document.body.style.paddingTop='44px';
},100);
return true;
}
// Handle #cpa= URL fragment on load β render read-only CPA view instead of the auth landing.
function checkCpaHash(){
const hash=window.location.hash||'';
const m=hash.match(/^#cpa=([A-Za-z0-9_-]+)$/);
if(!m)return false;
try{
const b64=m[1].replace(/-/g,'+').replace(/_/g,'/');
const json=decodeURIComponent(escape(atob(b64+'='.repeat((4-b64.length%4)%4))));
const p=JSON.parse(json);
renderCpaView(p);return true;
}catch(e){toast('Could not read shared link');return false;}
}
function cpaExportXlsx(payload){
// Build a lightweight multi-tab CSV bundle downloaded as a single .csv (CPAs can open in Excel).
// Full .xlsx would require the SheetJS bundle in read-only mode β CSV works everywhere.
const Y=payload.year;
const rows=[['Schedule E Β· '+(payload.bizName||'Client')+' Β· Tax Year '+Y+' Β· Snapshot '+new Date(payload.generatedAt).toLocaleDateString()]];
rows.push([]);
rows.push(['Line','Category','Property','LLC','Amount']);
const sched={Cleaning:'Line 7',Repairs:'Line 14',Supplies:'Line 15',Utilities:'Line 17',Insurance:'Line 9',Professional:'Line 10',Management:'Line 11',Mortgage:'Line 12',Travel:'Line 6',Meals:'Line 19',Other:'Line 19',Taxes:'Line 16',Licenses:'Line 16'};
const propMap={};payload.props.forEach(p=>{propMap[p.id]={name:p.name,llc:p.llc};});
payload.txns.forEach(t=>{
const line=t.t==='income'?'Line 3':(sched[t.c]||'Line 19');
const prop=propMap[t.pid]||{name:'(unassigned)',llc:''};
rows.push([line,t.c,prop.name,prop.llc,t.a.toFixed(2)]);
});
rows.push([]);
rows.push(['PROPERTY TOTALS']);
rows.push(['Property','LLC','Revenue','Expenses','Net']);
payload.props.forEach(p=>{
const ts=payload.txns.filter(t=>t.pid===p.id);
let r=0,e=0;ts.forEach(t=>{if(t.t==='income')r+=t.a;else if(t.t==='expense')e+=Math.abs(t.a);});
rows.push([p.name,p.llc,r.toFixed(2),e.toFixed(2),(r-e).toFixed(2)]);
});
const csv=rows.map(r=>r.map(c=>{const s=String(c==null?'':c);return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;}).join(',')).join('\n');
const blob=new Blob([csv],{type:'text/csv'});const url=URL.createObjectURL(blob);
const a=document.createElement('a');a.href=url;a.download=`schedule-e-${Y}-${(payload.bizName||'client').replace(/[^a-z0-9]+/gi,'-').toLowerCase()}.csv`;
document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);
}
function renderCpaView(payload){
// Hide the main app UI + auth landing
const sb=document.getElementById('sidebar');if(sb)sb.style.display='none';
const mob=document.querySelector('.mob-nav');if(mob)mob.style.display='none';
const main=document.querySelector('.mn-w');if(main)main.style.display='block';
const auth=document.getElementById('auth-landing');if(auth)auth.style.display='none';
const banner=document.getElementById('demo-banner');if(banner)banner.style.display='none';
// Only pg-cpa visible
document.querySelectorAll('[id^="pg-"]').forEach(p=>p.style.display='none');
const el=document.getElementById('pg-cpa');if(!el)return;el.style.display='block';
const gen=new Date(payload.generatedAt).toLocaleDateString('en-US',{dateStyle:'long'});
const totals={rev:0,exp:0};
payload.txns.forEach(t=>{if(t.t==='income')totals.rev+=t.a;else if(t.t==='expense')totals.exp+=Math.abs(t.a);});
const net=totals.rev-totals.exp;
const perProp=payload.props.map(p=>{
const ts=payload.txns.filter(t=>t.pid===p.id);
let r=0,e=0;ts.forEach(t=>{if(t.t==='income')r+=t.a;else if(t.t==='expense')e+=Math.abs(t.a);});
return{p,r,e,n:r-e,ts};
});
const sched=[
{line:'Line 3',label:'Rents received',cats:null,income:true},
{line:'Line 6',label:'Auto & travel',cats:['Travel']},
{line:'Line 7',label:'Cleaning & maintenance',cats:['Cleaning']},
{line:'Line 9',label:'Insurance',cats:['Insurance']},
{line:'Line 10',label:'Legal & professional',cats:['Professional']},
{line:'Line 11',label:'Management fees',cats:['Management']},
{line:'Line 12',label:'Mortgage interest',cats:['Mortgage']},
{line:'Line 14',label:'Repairs',cats:['Repairs']},
{line:'Line 15',label:'Supplies',cats:['Supplies']},
{line:'Line 16',label:'Taxes',cats:['Taxes','Licenses']},
{line:'Line 17',label:'Utilities',cats:['Utilities']},
{line:'Line 19',label:'Other (incl. meals)',cats:['Other','Meals']}
];
const totalMi=(payload.miles||[]).reduce((s,m)=>s+(+m.mi||0),0);
const mileDed=totalMi*(payload.mileRate||0.725);
el.innerHTML=`
${escHtml(payload.bizName||'Client')} Β· Schedule E Prep
Tax year ${payload.year} Β· Snapshot generated ${escHtml(gen)}
RentReel is tax-optimized bookkeeping for STR + LTR operators. This snapshot is one property tax year β the app also handles REPS + Material Participation tracking, cost segregation estimation, and multi-tab Excel exports.
`;
}
async function acctWipeData(){
if(!confirm('Wipe ALL your data?\n\nDeletes every transaction, upload, learned rule, hour log, asset/liability β keeps your account so you can start fresh. Cannot be undone.'))return;
D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.lastUp=null;
D.learned={};D.learnedProp={};D.ambiguousPayees={};D.excluded=[];
D.participation={hours:[],otherParticipants:[],nonReHours:0,costBasis:{},marginalRate:0.32};
D.demoSeeded=true;
save();rDash();rSettings();toast('All data wiped β');
}
async function acctDeleteAccount(){
if(!confirm('Delete your account?\n\nThis permanently deletes:\n β’ All your transactions, properties, hours, mileage, uploaded documents\n β’ Your authentication record (you cannot recover it)\n β’ Any data stored in RentReel about you\n\nGDPR/CCPA compliant hard-delete. Cannot be undone.\n\nType YES on the next prompt to confirm.'))return;
const confirmText=prompt('Type YES (all caps) to permanently delete your account:');
if(confirmText!=='YES'){toast('Deletion cancelled β nothing changed');return;}
if(!cloud.user||!SB){toast('Not signed in');return;}
toast('ποΈ Deleting accountβ¦');
// RPC handles workspace + personal_workspace + auth.users atomically (SECURITY DEFINER).
const {data,error}=await SB.rpc('delete_my_account');
if(error){
console.error('delete_my_account error:',error);
toast(`β οΈ Delete failed: ${error.message}. Email support@rentreel.co for manual removal.`);
return;
}
// Successful hard-delete β clean up local state before signing out.
D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.props=undefined;D.llcs=undefined;D.lastUp=null;
D.learned={};D.learnedProp={};D.ambiguousPayees={};D.excluded=[];D.participation=undefined;D.demoSeeded=true;
save_local();
try{await cloud.signOut();}catch(e){}
loadPortfolio();
localStorage.removeItem('strfinance_skip_auth');
rDash();
toast('β Account permanently deleted. All your data has been erased.');
showAuthLanding('signin');
}
async function csSignOut(){
if(!confirm('Sign out? Your local data stays on this device.'))return;
await cloud.signOut();
// Clear local data on sign-out so cousin/Dad can sign in cleanly on the same device
D.transactions=[];D.miles=[];D.manual=[];D.uploads=[];D.props=undefined;D.llcs=undefined;D.lastUp=null;D.demoSeeded=true;
save_local();
loadPortfolio();
localStorage.removeItem('strfinance_skip_auth');
rDash();rSettings();
toast('Signed out');
showAuthLanding('signin');
}
// βββ HELP / FAQ βββ
function rHelp(){
const el=document.getElementById('pg-help');
const sections=[
{title:'π Getting Started',body:`
Set up your portfolio in Settings β Properties section. Add each STR with its bank account, LLC, and color.
Upload your CSVs on the Upload page: reservations from any PMS (Hospitable, Guesty, Hostaway, OwnerRez, Lodgify) or Airbnb-direct, bank statements, credit cards, cleaning platform log, mileage tracker.
No PMS? Skip that β just upload banks. RentReel treats Airbnb bank deposits as revenue automatically. Add reservations manually if you want STR β€7-day qualification tracking.
Review uncategorized on the Transactions page. Click any red badge (or bulk-select) to assign a category β the app learns.
Export Schedule E when tax season hits. Excel file with one tab per property, ready for your CPA.
`},
{title:'π Uploading CSVs β what gets auto-detected',body:`
Any PMS reservations CSV (Hospitable, Guesty, Hostaway, OwnerRez, Lodgify) or Airbnb-direct earnings β Revenue + auto-booked host service fees
Bank statements (Relay, Chase, BofA, Capital One, Wells Fargo) β Expenses (mortgage, utilities, cleaning). Platform deposits become Transfers (no double-count).
The app ships with 70+ built-in rules for common STR/LTR vendors β major mortgage servicers (Rocket, Mr. Cooper, Wells Fargo, Selene, NewRez), utilities (Duke Energy, PG&E, Xcel, Comcast, Spectrum), insurers (State Farm, Allstate, GEICO, Proper, Obie), cleaning brands (Merry Maids, Molly Maid, generic housekeeping), contractors (plumbing, HVAC, roofing, landscaping), platforms (Airbnb, VRBO, Booking.com, Hospitable), and PMS/software (Guesty, Hostaway, OwnerRez, Beyond Pricing, QuickBooks).
When you reassign a category for an unrecognized vendor, the app learns: future imports of that same payee auto-categorize, AND existing Uncategorized matches get back-applied.
Property learning works the same way β assign a payee to a property once, future ones route automatically. If you assign a payee to 2+ different properties, the app marks it ambiguous and stops auto-applying (forces manual assignment instead of silent mis-routing).
`},
{title:'π Smart Dedup β why bank Airbnb deposits are Transfers',body:`
If you upload a PMS reservations file AND bank statements, your books would double-count revenue without smart dedup. Here's why:
What the app does: your reservations import is the revenue source of truth. Bank Airbnb/VRBO/Booking/Hospitable Inc deposits get auto-tagged as "Platform Payout Β· Transfer" so they show on bank statements but don't double-count revenue. The host service fee from the reservations file gets auto-booked as a Management expense (Schedule E Line 11), keeping the math clean.
Hand the file to your accountant β no reformatting needed on their end.
`},
{title:'π― Tax Loophole Tracker (REPS + Material Participation)',body:`
If you're a real estate professional (or your spouse is W-2 and you offset their income via the STR loophole), this is the money page.
Real Estate Professional Status (REPS Β· IRC Β§469(c)(7)) requires:
750+ hours/year in real property trades
More than 50% of all your personal services in RE
If you qualify, rental losses become non-passive β they can offset W-2 income on a joint return.
STR loophole alternative (IRC Β§469 + Reg Β§1.469-1T): Material Participation per property with average customer use β€ 7 days. Easiest test: 100+ hours AND more than anyone else (you vs. cleaner, contractor, co-host).
Cost Segregation can add significant Year 1 deductions on top β but any actual filing requires an engineered study by a qualified firm.
All numbers on this page are estimates. Log hours as you go (IRS audits look for contemporaneous records). Always consult a licensed CPA before filing.
Schedule E page auto-flags any non-corporate vendor you paid $600+ this year. Excludes utilities, mortgages, platforms (Airbnb/VRBO), and obvious corps.
Collect W-9s from flagged vendors before year-end. File 1099-NECs by Jan 31. The Excel export includes a "1099 Vendors" tab with blank W-9/TIN/Address columns for your CPA to fill.
`},
{title:'π¨ Common Issues & Fixes',body:`
Revenue looks too high? Check Transactions for bank "AIRBNB" or "Hospitable Inc" entries showing as Revenue (not Transfer). Reload β the on-load reconcile fixes them automatically when reservations data is present.
Bank file mapped to wrong property? Likely the filename had a year in it that got mistaken for the account number. Delete the bad upload (Upload History β β), rename the CSV to include only the account number (e.g. bank-1234-may.csv), and re-upload.
Personal charge on business account? Click the category badge β set to Personal. Keeps it on record for bank reconciliation but excludes it from P&L and tax.
Scam charge + refund? Click the Γ on both to delete. Use the Undo toast if you delete something by mistake. They won't come back even if you re-upload the CSV.
Forgot password? Sign-in screen β "Forgot password?" link β enter email β check inbox β click link.
`},
{title:'π§ Support & Feedback',body:`
Found a bug, have a feature idea, or need help with your books? Email support@rentreel.co or use the Send Feedback button in the sidebar.
For tax-specific questions, talk to your CPA. RentReel organizes your books and surfaces the loopholes β it doesn't give tax advice. If you need help finding an STR-savvy CPA, email hello@rentreel.co and we can share a shortlist.
`},
];
el.innerHTML=`
Help & FAQ
How to use RentReel, troubleshoot common issues, and reach support
Shown in the sidebar, dashboard subtitle, Schedule E exports, and CPA workbook.
Used for YTD filters and Schedule E
2026 IRS standard: $0.725/mi
π API Key
Stored locally in your browser. Reserved for future AI-assisted categorization.
π Your data & security
Row-Level Security (RLS) at the database. Every user gets an isolated row keyed to their auth ID. Postgres refuses to return one user's data to another β enforced at the DB level, not just in the app code.
HTTPS everywhere. All traffic encrypted in transit via Cloudflare TLS. Passwords are bcrypt-hashed by Supabase β we never see or store your plaintext password.
Where your data lives. Supabase Postgres, hosted on AWS US-East. SOC 2 Type 2 compliant. We don't sell, share, or use your data for advertising or AI training β ever.
CSV parsing runs in your browser. Only the parsed transaction rows leave your device. The original CSV file never touches our servers.
You own your data. Download a full JSON backup anytime via the button below. Wipe or delete your account permanently via the Account section β no support ticket required.
No property rules yet. Only needed if your bank account isn't dedicated to one property.
`}
π Help
${Object.keys(D.helpDismissed||{}).length?``:''}
${Object.keys(D.helpDismissed||{}).length?`${Object.keys(D.helpDismissed).length} help banners hidden β click reset to show again.`:`Help banners appear on Transactions, Upload, and Schedule E pages explaining the app's smart features.`}
π About RentReel
Business Name: ${bizName()||'(not set β add it in Preferences above)'}
Works with: any PMS (Hospitable, Guesty, Hostaway, OwnerRez, Lodgify, Airbnb-direct) Β· any bank Β· any cleaning platform Β· any mileage tracker
Exports: Schedule E .xlsx, per-property tabs, 1099 vendor summary β ready for any CPA
RentReel BETA Β· Single-file web app Β· Cloud-synced via Supabase
`;
}
function saveSettings(){
D.settings.year=parseInt(document.getElementById('st-year').value)||2026;
D.settings.mileRate=parseFloat(document.getElementById('st-mile').value)||0.725;
D.settings.workspaceName=document.getElementById('st-bizname').value.trim();
// Re-run categorization on Uncategorized transactions so newly added rules
// (Related Entities, updated bizName, etc.) apply retroactively without a re-upload.
let recatCount=0;
(D.transactions||[]).forEach(t=>{
if(t.c!=='Uncategorized')return;
const c=cat(t.payee,t.desc,t.amount);
if(c.c!=='Uncategorized'){t.c=c.c;t.s=c.s;t.t=c.t;t.se=c.se;t.ok=true;recatCount++;}
});
D.miles.forEach(m=>{m.ded=parseFloat((m.miles*D.settings.mileRate).toFixed(2));});
save();applyBizName();
toast(recatCount>0?`Preferences saved β Β· ${recatCount} transaction${recatCount===1?'':'s'} auto-categorized`:'Preferences saved β');
rSettings();rDash();
}
function saveApiKey(){
D.settings.apiKey=document.getElementById('st-key').value.trim();
save();toast('API key saved β');
}
function exportBackup(){
if(!guardDemoAction('export your data'))return;
const json=JSON.stringify(D,null,2);
dl(json,`rentreel-backup-${new Date().toISOString().slice(0,10)}.json`,'application/json');
toast('Backup exported β');
}
function importBackup(file){
if(!file)return;
const r=new FileReader();
r.onload=e=>{
try{
const obj=JSON.parse(e.target.result);
if(!obj||typeof obj!=='object'||!Array.isArray(obj.transactions)){toast('Invalid backup file');return;}
if(!confirm(`Replace local data with ${obj.transactions.length} txns from backup?`))return;
D=Object.assign({transactions:[],miles:[],manual:[],uploads:[],lastUp:null,settings:{year:2026,mileRate:0.725,apiKey:''}},obj);
if(!D.settings)D.settings={year:2026,mileRate:0.725,apiKey:''};
save();rSettings();rDash();toast('Backup imported β');
}catch(err){toast('Failed: '+err.message);}
};
r.readAsText(file);
}
// βββ PORTFOLIO MANAGER βββ
const PROP_COLORS=['#00ffb3','#4d94ff','#ff8c42','#b84dff','#ffb830','#4dffea','#ff4d7a','#00d4ff','#ff6b9d','#9d6bff'];
function ensurePropModal(){
if(document.getElementById('prop-modal'))return;
const m=document.createElement('div');
m.id='prop-modal';
m.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);backdrop-filter:blur(8px);z-index:1000;align-items:center;justify-content:center;padding:20px';
m.innerHTML='
'
+'
Add Property
'
+'
Properties are the unit of P&L. Each one rolls up to an LLC.
'
+''
+'
'
+'
'
+''
+'
Determines which tax loopholes apply. STR gets Β§469 material participation; LTR falls under passive rules but REPS + Cost Seg still apply to both.
Estimate only. The interest/principal split shown is calculated from these fields using standard declining-balance amortization. Your servicer\'s Form 1098 is the tax-authoritative number for Schedule E Line 12 β reconcile at year-end.
'
+'
'
+'
'
+'
From your servicer app
'
+'
Principal + interest only
'
+'
Taxes + insurance held by servicer. Leave blank if you pay separately.