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:
co-authored by
Claude Opus 4.8
parent
69f12b0519
commit
4215d4478f
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user