0.2.27-0.2.32: LP portal polish, brand palette, default investor logins

Cumulative checkpoint since 0.2.26:
- 0.2.27/28: entity valuation-history table; investor gain/loss = NAV +
  distributions vs paid-in
- 0.2.29: Reset Fund Partners (endpoint, Partners-tab button, CLI, action)
- 0.2.30: "Current Capital Balance" label, %-only gain/loss
- 0.2.31: Management Entities rename, Carry Vehicle type, chart
  distributions-line gate
- 0.2.32: LP-facing polish pass
  * Ten31 brand palette from the logo (navy/mint); orange retired
  * portfolio summary card across funds; gain labeled "net of paid-in"
  * whole-dollar headline figures; "History · N quarters" toggle
  * documents grouped by year with a "New" badge (users.docs_seen_at)
  * eNAV-created members start on default password with login enabled;
    enable-investor-logins CLI + StartOS action for existing accounts
  * password minimum raised to 8 chars; login help line (Portal@ten31.xyz)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-03 08:40:40 -05:00
co-authored by Claude Opus 4.8
parent 69f12b0519
commit 4215d4478f
47 changed files with 935 additions and 111 deletions
+72
View File
@@ -87,6 +87,42 @@ def reset_password(args: argparse.Namespace) -> None:
print(f"Password reset for {user.name} ({user.username}).")
def enable_investor_logins(args: argparse.Namespace) -> None:
"""Give every no-login investor account the shared default password and enable sign-in.
Targets investor-role accounts with login_enabled=False that log in on their own
(linked secondary names are skipped — they sign in under their primary). Accounts that
already have a working login are never touched.
"""
run_migrations()
from ten31portal.models import UserRole
with Session(engine) as session:
users = session.exec(
select(User).where(
User.role == UserRole.investor,
User.login_enabled == False, # noqa: E712 — SQL expression
User.primary_account_id == None, # noqa: E711
).order_by(User.name) # type: ignore[arg-type]
).all()
if not users:
print("Nothing to do — every investor account already has a login.")
return
for u in users:
u.password_hash = hash_password(config.DEFAULT_INVESTOR_PASSWORD)
u.login_enabled = True
session.add(u)
session.commit()
for u in users:
print(f"Enabled login for {u.name} ({u.username})")
print(
f"\n{len(users)} investor account(s) set to the default password "
f"'{config.DEFAULT_INVESTOR_PASSWORD}'. Each investor should change it in the "
"portal (Change password)."
)
def show_admin_password(args: argparse.Namespace) -> None:
"""Print the randomly-generated initial admin password recorded on first boot."""
path = config.ADMIN_PASSWORD_FILE
@@ -234,6 +270,29 @@ def reset_holdings(args: argparse.Namespace) -> None:
f"Re-import the fund's NAV to repopulate it.")
def reset_partners(args: argparse.Namespace) -> None:
"""Remove all partners (capital-account statements + access grants) from one fund."""
run_migrations()
from ten31portal.models import Entity
from ten31portal.routers.capital_import_router import reset_entity_partners
with Session(engine) as session:
entity = None
if args.entity_id:
entity = session.get(Entity, args.entity_id)
elif args.name:
entity = session.exec(select(Entity).where(Entity.name == args.name)).first()
if entity is None:
print(f"Error: no fund found for '{args.name or args.entity_id}'. "
f"Check the exact name with list-funds.", file=sys.stderr)
sys.exit(1)
res = reset_entity_partners(entity.id, session)
session.commit()
print(f"Cleared partners from {entity.name}: removed {res['statements']} capital "
f"statement(s) and {res['access_grants']} access grant(s). "
f"Investor accounts were kept. Re-import the correct roster to repopulate.")
def list_funds(args: argparse.Namespace) -> None:
"""Print every entity's id and name (so the exact name is known for reset-holdings)."""
run_migrations()
@@ -268,6 +327,11 @@ def main() -> None:
sub.add_parser("list-users", help="List all user accounts")
sub.add_parser(
"enable-investor-logins",
help="Set every no-login investor account to the default password and enable sign-in",
)
sub.add_parser("show-admin-password", help="Show the initial admin password from first boot")
delete = sub.add_parser("delete-user", help="Delete a user (not the Service Admin)")
@@ -284,6 +348,10 @@ def main() -> None:
reset.add_argument("--name", required=False, default=None, help="Exact fund name")
reset.add_argument("--entity-id", type=int, required=False, default=None)
rparts = sub.add_parser("reset-partners", help="Remove all partners (capital accounts + access) from a fund")
rparts.add_argument("--name", required=False, default=None, help="Exact fund name")
rparts.add_argument("--entity-id", type=int, required=False, default=None)
args = parser.parse_args()
if args.command == "create-user":
create_user(args)
@@ -291,6 +359,8 @@ def main() -> None:
reset_password(args)
elif args.command == "list-users":
list_users(args)
elif args.command == "enable-investor-logins":
enable_investor_logins(args)
elif args.command == "show-admin-password":
show_admin_password(args)
elif args.command == "delete-user":
@@ -301,6 +371,8 @@ def main() -> None:
list_funds(args)
elif args.command == "reset-holdings":
reset_holdings(args)
elif args.command == "reset-partners":
reset_partners(args)
else:
parser.print_help()